Module: ClaudeAgentSDK
- Defined in:
- lib/claude_agent_sdk.rb,
lib/claude_agent_sdk/query.rb,
lib/claude_agent_sdk/types.rb,
lib/claude_agent_sdk/errors.rb,
lib/claude_agent_sdk/version.rb,
lib/claude_agent_sdk/observer.rb,
lib/claude_agent_sdk/sessions.rb,
lib/claude_agent_sdk/streaming.rb,
lib/claude_agent_sdk/transport.rb,
lib/claude_agent_sdk/configuration.rb,
lib/claude_agent_sdk/session_store.rb,
lib/claude_agent_sdk/fiber_boundary.rb,
lib/claude_agent_sdk/message_parser.rb,
lib/claude_agent_sdk/sdk_mcp_server.rb,
lib/claude_agent_sdk/session_resume.rb,
lib/claude_agent_sdk/command_builder.rb,
lib/claude_agent_sdk/session_summary.rb,
lib/claude_agent_sdk/session_mutations.rb,
lib/claude_agent_sdk/instrumentation/otel.rb,
lib/claude_agent_sdk/subprocess_cli_transport.rb,
lib/claude_agent_sdk/transcript_mirror_batcher.rb,
lib/claude_agent_sdk/testing/session_store_conformance.rb
Overview
Claude Agent SDK for Ruby
Defined Under Namespace
Modules: FiberBoundary, Instrumentation, Observer, SessionMutations, SessionResume, SessionStores, SessionSummary, Sessions, Streaming, Testing Classes: APIRetryMessage, AgentDefinition, AssistantMessage, AsyncHookJSONOutput, AuthStatusMessage, BaseHookInput, CLIConnectionError, CLIJSONDecodeError, CLINotFoundError, ClaudeAgentOptions, ClaudeSDKError, Client, CommandBuilder, CompactBoundaryMessage, CompactMetadata, ConfigChangeHookInput, Configuration, ControlRequestTimeoutError, CwdChangedHookInput, CwdChangedHookSpecificOutput, DeferredToolUse, ElicitationCompleteMessage, ElicitationHookInput, ElicitationResultHookInput, FileChangedHookInput, FileChangedHookSpecificOutput, FilesPersistedMessage, ForkSessionResult, HookContext, HookMatcher, HookProgressMessage, HookResponseMessage, HookStartedMessage, InMemorySessionStore, InitMessage, InstructionsLoadedHookInput, LocalCommandOutputMessage, MaterializedResume, McpClaudeAIProxyServerConfig, McpHttpServerConfig, McpSSEServerConfig, McpSdkServerConfig, McpSdkServerConfigStatus, McpServerInfo, McpServerStatus, McpStatusResponse, McpStdioServerConfig, McpToolAnnotations, McpToolInfo, MessageParseError, MessageParser, MirrorErrorMessage, NotificationHookInput, NotificationHookSpecificOutput, PermissionDeniedHookInput, PermissionDeniedHookSpecificOutput, PermissionRequestHookInput, PermissionRequestHookSpecificOutput, PermissionResultAllow, PermissionResultDeny, PermissionRuleValue, PermissionUpdate, PostCompactHookInput, PostToolUseFailureHookInput, PostToolUseFailureHookSpecificOutput, PostToolUseHookInput, PostToolUseHookSpecificOutput, PreCompactHookInput, PreToolUseHookInput, PreToolUseHookSpecificOutput, ProcessError, PromptSuggestionMessage, Query, RateLimitEvent, RateLimitInfo, ResultMessage, SDKSessionInfo, SandboxFilesystemConfig, SandboxNetworkConfig, SandboxSettings, SdkMcpPrompt, SdkMcpResource, SdkMcpServer, SdkMcpTool, SdkPluginConfig, ServerToolResultBlock, ServerToolUseBlock, SessionEndHookInput, SessionMessage, SessionStartHookInput, SessionStartHookSpecificOutput, SessionStateChangedMessage, SessionStore, SetupHookInput, SetupHookSpecificOutput, StatusMessage, StopFailureHookInput, StopHookInput, StreamEvent, SubagentStartHookInput, SubagentStartHookSpecificOutput, SubagentStopHookInput, SubprocessCLITransport, SyncHookJSONOutput, SystemMessage, SystemPromptFile, SystemPromptPreset, TaskBudget, TaskCompletedHookInput, TaskCreatedHookInput, TaskNotificationMessage, TaskProgressMessage, TaskStartedMessage, TaskUpdatedMessage, TaskUsage, TeammateIdleHookInput, TextBlock, ThinkingBlock, ThinkingConfigAdaptive, ThinkingConfigDisabled, ThinkingConfigEnabled, ToolPermissionContext, ToolProgressMessage, ToolResultBlock, ToolUseBlock, ToolUseSummaryMessage, ToolsPreset, TranscriptMirrorBatcher, Transport, Type, UnknownBlock, UnknownHookInput, UserMessage, UserPromptSubmitHookInput, UserPromptSubmitHookSpecificOutput, WorktreeCreateHookInput, WorktreeRemoveHookInput
Constant Summary collapse
- OBSERVER_INTERFACE =
The duck-typed observer surface probed by resolve_observers — implementing any one of these counts as an observer (see Observer's no-op defaults).
%i[on_user_prompt on_message on_error on_close].freeze
- PERMISSION_MODES =
Type constants for permission modes
%w[default acceptEdits plan bypassPermissions dontAsk auto].freeze
- SETTING_SOURCES =
Type constants for setting sources
%w[user project local].freeze
- EFFORT_LEVELS =
Effort levels for
ClaudeAgentOptions#effort. The CLI (Claude Code 2.1.111+) accepts these values; the set of supported levels is model-dependent (e.g.xhighis only supported on Opus 4.7 and falls back tohighon Opus 4.6 / Sonnet 4.6). An Integer is also accepted and forwarded verbatim. %w[low medium high xhigh max].freeze
- PERMISSION_UPDATE_DESTINATIONS =
Type constants for permission update destinations
%w[userSettings projectSettings localSettings session].freeze
- PERMISSION_BEHAVIORS =
Type constants for permission behaviors
%w[allow deny ask].freeze
- HOOK_EVENTS =
Type constants for hook events
%w[ PreToolUse PostToolUse PostToolUseFailure Notification UserPromptSubmit SessionStart SessionEnd Stop StopFailure SubagentStart SubagentStop PreCompact PostCompact PermissionRequest PermissionDenied Setup TeammateIdle TaskCreated TaskCompleted Elicitation ElicitationResult ConfigChange WorktreeCreate WorktreeRemove InstructionsLoaded CwdChanged FileChanged ].freeze
- ASSISTANT_MESSAGE_ERRORS =
Type constants for assistant message errors
%w[authentication_failed billing_error rate_limit invalid_request server_error max_output_tokens unknown].freeze
- SDK_BETAS =
Type constants for SDK beta features Available beta features that can be enabled via the betas option
%w[context-1m-2025-08-07].freeze
- TASK_NOTIFICATION_STATUSES =
Task lifecycle notification statuses
%w[completed failed stopped].freeze
- TASK_UPDATED_STATUSES =
Possible status values reported inside a
task_updatedpatch. pending/running/paused are non-terminal; completed/failed/killed are terminal. Note: task_updated reports the raw "killed"; the CLI maps that to "stopped" only when it emits a task_notification. %w[pending running paused completed failed killed].freeze
- TERMINAL_TASK_STATUSES =
Task statuses that mean the task has finished and should be cleared from any "active task" tracking. Spans both lifecycle vocabularies: task_notification reports "stopped" (the CLI's mapped form of a killed task) while task_updated reports the raw "killed". Treat the status of a TaskNotificationMessage and a TaskUpdatedMessage the same way.
%w[completed failed stopped killed].freeze
- RATE_LIMIT_STATUSES =
Type constants for rate limit statuses
%w[allowed allowed_warning rejected].freeze
- RATE_LIMIT_TYPES =
Type constants for rate limit types
%w[five_hour seven_day seven_day_opus seven_day_sonnet overage].freeze
- THINKING_DISPLAY_VALUES =
Thinking configuration types
displaycontrols how thinking content appears in responses. Valid values are"summarized"(plaintext summary) and"omitted"(empty thinking field, signature only). Defaults are model-dependent: Opus 4.6/Sonnet 4.6 default to"summarized"; Opus 4.7 and Mythos Preview default to"omitted". Passdisplay: "summarized"explicitly on Opus 4.7 to get visible thinking text. Not supported withThinkingConfigDisabled. %w[summarized omitted].freeze
- MCP_SERVER_CONNECTION_STATUSES =
MCP server connection status values
%w[connected failed needs-auth pending disabled].freeze
- VERSION =
'0.22.0'- SESSION_STORE_FLUSH_MODES =
Controls when transcript-mirror entries are flushed to a SessionStore.
- "batched" (default): buffer entries and flush once per turn (on the
resultmessage) or when the pending buffer exceeds 500 entries / 1 MiB. - "eager": trigger a background flush after every transcript_mirror frame so SessionStore#append sees entries in near real time.
- "batched" (default): buffer entries and flush once per turn (on the
%w[batched eager].freeze
Class Method Summary collapse
-
.configuration ⇒ Configuration
Get the configuration object.
-
.configure {|Configuration| ... } ⇒ Object
Configure the SDK with default options.
-
.create_prompt(name:, description: nil, arguments: nil, &generator) ⇒ SdkMcpPrompt
Helper function to create a prompt definition.
-
.create_resource(uri:, name:, description: nil, mime_type: nil, &reader) ⇒ SdkMcpResource
Helper function to create a resource definition.
-
.create_sdk_mcp_server(name:, version: '1.0.0', tools: [], resources: [], prompts: []) ⇒ Hash
Create an SDK MCP server.
-
.create_tool(name, description, input_schema, annotations: nil, meta: nil, &handler) ⇒ SdkMcpTool
Helper function to create a tool definition.
-
.deep_normalize_schema(obj) ⇒ Object
Like deep_symbolize_keys, but also converts Symbol VALUES to strings so a prebuilt schema written with symbols ({ type: :object, ... }) emits clean wire-format JSON Schema.
-
.deep_symbolize_keys(obj) ⇒ Object
Recursively convert all hash keys to symbols.
-
.default_options ⇒ Hash
Get merged default options for use with ClaudeAgentOptions.
-
.delete_session(session_id:, directory: nil) ⇒ Object
Delete a session by removing its JSONL file (hard delete).
-
.delete_session_via_store(session_store:, session_id:, directory: nil) ⇒ Object
Delete a session from a SessionStore (store-backed counterpart to delete_session).
-
.extract_exclude_dynamic_sections(system_prompt) ⇒ Object
Internal: pull exclude_dynamic_sections out of a preset system prompt for the initialize request (older CLIs ignore unknown initialize fields).
-
.extract_sdk_mcp_servers(mcp_servers) ⇒ Object
Internal: pull live SDK MCP server instances out of an mcp_servers Hash.
-
.extract_user_prompt_text(message) ⇒ Object
Extract the user-visible prompt text from a streamed input item, or nil when there is none (non-user messages, tool_result-only content, …).
-
.flexible_fetch(hash, camel_key, snake_key) ⇒ Object
Look up a value in a hash that may use symbol or string keys in camelCase or snake_case.
-
.fold_session_summary(prev, key, entries) ⇒ Hash
Fold a batch of appended transcript entries into a running session summary.
-
.fork_session(session_id:, directory: nil, up_to_message_id: nil, title: nil) ⇒ ForkSessionResult
Fork a session into a new branch with fresh UUIDs.
-
.fork_session_via_store(session_store:, session_id:, directory: nil, up_to_message_id: nil, title: nil) ⇒ ForkSessionResult
Fork a session in a SessionStore into a new branch with fresh UUIDs (store-backed counterpart to fork_session).
-
.get_session_info(session_id:, directory: nil) ⇒ SDKSessionInfo?
Read metadata for a single session by ID (no full directory scan).
-
.get_session_info_from_store(session_store:, session_id:, directory: nil) ⇒ SDKSessionInfo?
Read metadata for a single session from a SessionStore.
-
.get_session_messages(session_id:, directory: nil, limit: nil, offset: 0) ⇒ Array<SessionMessage>
Get messages from a session transcript.
-
.get_session_messages_from_store(session_store:, session_id:, directory: nil, limit: nil, offset: 0) ⇒ Array<SessionMessage>
Read a session's conversation messages from a SessionStore.
-
.get_subagent_messages(session_id:, agent_id:, directory: nil, limit: nil, offset: 0) ⇒ Array<SessionMessage>
Read a subagent's conversation messages from local disk.
-
.get_subagent_messages_from_store(session_store:, session_id:, agent_id:, directory: nil, limit: nil, offset: 0) ⇒ Array<SessionMessage>
Read a subagent's conversation messages from a SessionStore.
-
.import_session_to_store(session_id:, session_store:, directory: nil, include_subagents: true, batch_size: TranscriptMirrorBatcher::MAX_PENDING_ENTRIES) ⇒ Object
Replay a local on-disk session transcript into a SessionStore (migration / gap-backfill).
-
.list_sessions(directory: nil, limit: nil, offset: 0, include_worktrees: true) ⇒ Array<SDKSessionInfo>
List sessions for a directory (or all sessions).
-
.list_sessions_from_store(session_store:, directory: nil, limit: nil, offset: 0) ⇒ Array<SDKSessionInfo>
List sessions from a SessionStore (store-backed counterpart to list_sessions).
-
.list_subagents(session_id:, directory: nil) ⇒ Array<String>
List subagent IDs recorded for a session on local disk.
-
.list_subagents_from_store(session_store:, session_id:, directory: nil) ⇒ Array<String>
List subagent IDs for a session from a SessionStore (requires list_subkeys).
-
.normalize_tool_schema(schema) ⇒ Object
Single source of truth for tool input schemas: prebuilt schemas are normalized (symbol keys, string values); simple { name: :type } hashes become a full JSON Schema with every param required (string keys).
-
.notify_observers(observers, method, *args) ⇒ Object
Safely call a method on each observer, suppressing any errors.
-
.observing_prompt_stream(prompt, observers) ⇒ Object
Wrap a streaming-input enumerable so observers get on_user_prompt for each user message before it is written to stdin.
-
.prebuilt_json_schema?(schema) ⇒ Boolean
A prebuilt JSON Schema is detected by type == 'object' (String or Symbol) with properties either ABSENT or a Hash.
-
.project_key_for_directory(directory = nil) ⇒ String
Derive the SessionStore
project_keyfor a directory (default: cwd). -
.prompt_text_from_content(content) ⇒ Object
Text from a user-message content payload: the string itself, or the newline-joined non-empty top-level text blocks.
-
.query(prompt:, options: nil, transport: nil) {|Message| ... } ⇒ Enumerator
Query Claude Code for one-shot or unidirectional streaming interactions.
-
.rename_session(session_id:, title:, directory: nil) ⇒ Object
Rename a session by appending a custom-title entry.
-
.rename_session_via_store(session_store:, session_id:, title:, directory: nil) ⇒ Object
Rename a session in a SessionStore (store-backed counterpart to rename_session).
-
.reset_configuration ⇒ Object
Reset configuration to defaults (useful for testing).
-
.resolve_observers(observers) ⇒ Object
Resolve observers array: callables (Proc/lambda) are invoked to produce a fresh instance per query/session (thread-safe); plain objects are used as-is.
- .ruby_type_to_json_schema(type) ⇒ Object
-
.tag_session(session_id:, tag:, directory: nil) ⇒ Object
Tag a session.
-
.tag_session_via_store(session_store:, session_id:, tag:, directory: nil) ⇒ Object
Tag a session in a SessionStore (store-backed counterpart to tag_session).
Class Method Details
.configuration ⇒ Configuration
Get the configuration object
58 59 60 |
# File 'lib/claude_agent_sdk/configuration.rb', line 58 def configuration @configuration ||= Configuration.new end |
.configure {|Configuration| ... } ⇒ Object
Configure the SDK with default options
51 52 53 |
# File 'lib/claude_agent_sdk/configuration.rb', line 51 def configure yield(configuration) end |
.create_prompt(name:, description: nil, arguments: nil, &generator) ⇒ SdkMcpPrompt
Helper function to create a prompt definition
594 595 596 597 598 599 600 601 602 603 |
# File 'lib/claude_agent_sdk/sdk_mcp_server.rb', line 594 def self.create_prompt(name:, description: nil, arguments: nil, &generator) raise ArgumentError, 'Block required for prompt generator' unless generator SdkMcpPrompt.new( name: name, description: description, arguments: arguments, generator: generator ) end |
.create_resource(uri:, name:, description: nil, mime_type: nil, &reader) ⇒ SdkMcpResource
Helper function to create a resource definition
536 537 538 539 540 541 542 543 544 545 546 |
# File 'lib/claude_agent_sdk/sdk_mcp_server.rb', line 536 def self.create_resource(uri:, name:, description: nil, mime_type: nil, &reader) raise ArgumentError, 'Block required for resource reader' unless reader SdkMcpResource.new( uri: uri, name: name, description: description, mime_type: mime_type, reader: reader ) end |
.create_sdk_mcp_server(name:, version: '1.0.0', tools: [], resources: [], prompts: []) ⇒ Hash
Create an SDK MCP server
646 647 648 649 650 651 652 653 654 655 656 657 658 659 660 661 |
# File 'lib/claude_agent_sdk/sdk_mcp_server.rb', line 646 def self.create_sdk_mcp_server(name:, version: '1.0.0', tools: [], resources: [], prompts: []) server = SdkMcpServer.new( name: name, version: version, tools: tools, resources: resources, prompts: prompts ) # Return configuration for ClaudeAgentOptions { type: 'sdk', name: name, instance: server } end |
.create_tool(name, description, input_schema, annotations: nil, meta: nil, &handler) ⇒ SdkMcpTool
Helper function to create a tool definition
470 471 472 473 474 475 476 477 478 479 480 481 482 483 484 485 486 487 488 489 490 491 492 493 |
# File 'lib/claude_agent_sdk/sdk_mcp_server.rb', line 470 def self.create_tool(name, description, input_schema, annotations: nil, meta: nil, &handler) raise ArgumentError, 'Block required for tool handler' unless handler # Auto-populate _meta with maxResultSizeChars from annotations if present = if .nil? && annotations max_chars = annotations[:maxResultSizeChars] || annotations['maxResultSizeChars'] = { 'anthropic/maxResultSizeChars' => max_chars } if max_chars end # tools/call arrives from the CLI with the name as a JSON String; the mcp # gem keys its lookup on the value stored here, so a Symbol name would be # advertised in tools/list yet miss on every invocation ('Tool not found'). name = name.to_s if name.is_a?(Symbol) SdkMcpTool.new( name: name, description: description, input_schema: input_schema, handler: handler, annotations: annotations, meta: ) end |
.deep_normalize_schema(obj) ⇒ Object
Like deep_symbolize_keys, but also converts Symbol VALUES to strings so a prebuilt schema written with symbols ({ type: :object, ... }) emits clean wire-format JSON Schema.
18 19 20 21 22 23 24 25 |
# File 'lib/claude_agent_sdk/sdk_mcp_server.rb', line 18 def self.deep_normalize_schema(obj) case obj when Hash then obj.transform_keys(&:to_sym).transform_values { |v| deep_normalize_schema(v) } when Array then obj.map { |v| deep_normalize_schema(v) } when Symbol then obj.to_s else obj end end |
.deep_symbolize_keys(obj) ⇒ Object
Recursively convert all hash keys to symbols
7 8 9 10 11 12 13 |
# File 'lib/claude_agent_sdk/sdk_mcp_server.rb', line 7 def self.deep_symbolize_keys(obj) case obj when Hash then obj.transform_keys(&:to_sym).transform_values { |v| deep_symbolize_keys(v) } when Array then obj.map { |v| deep_symbolize_keys(v) } else obj end end |
.default_options ⇒ Hash
Get merged default options for use with ClaudeAgentOptions
70 71 72 |
# File 'lib/claude_agent_sdk/configuration.rb', line 70 def configuration. || {} end |
.delete_session(session_id:, directory: nil) ⇒ Object
Delete a session by removing its JSONL file (hard delete).
239 240 241 |
# File 'lib/claude_agent_sdk.rb', line 239 def self.delete_session(session_id:, directory: nil) SessionMutations.delete_session(session_id: session_id, directory: directory) end |
.delete_session_via_store(session_store:, session_id:, directory: nil) ⇒ Object
Delete a session from a SessionStore (store-backed counterpart to delete_session). No-op when the store does not implement #delete (WORM/append-only backends).
329 330 331 332 |
# File 'lib/claude_agent_sdk.rb', line 329 def self.delete_session_via_store(session_store:, session_id:, directory: nil) SessionMutations.delete_session_via_store(session_store: session_store, session_id: session_id, directory: directory) end |
.extract_exclude_dynamic_sections(system_prompt) ⇒ Object
Internal: pull exclude_dynamic_sections out of a preset system prompt for the initialize request (older CLIs ignore unknown initialize fields). Shared by Client#connect and the one-shot query() path.
68 69 70 71 72 73 74 75 76 77 78 79 80 |
# File 'lib/claude_agent_sdk.rb', line 68 def self.extract_exclude_dynamic_sections(system_prompt) if system_prompt.is_a?(SystemPromptPreset) eds = system_prompt.exclude_dynamic_sections return eds if [true, false].include?(eds) elsif system_prompt.is_a?(Hash) type = system_prompt[:type] || system_prompt['type'] if type == 'preset' eds = system_prompt.fetch(:exclude_dynamic_sections) { system_prompt['exclude_dynamic_sections'] } return eds if [true, false].include?(eds) end end nil end |
.extract_sdk_mcp_servers(mcp_servers) ⇒ Object
Internal: pull live SDK MCP server instances out of an mcp_servers Hash. Accepts both raw Hash configs and typed Mcp*ServerConfig objects — a McpSdkServerConfig passed without .to_h previously failed the Hash-only guard, so its in-process server was silently never registered.
54 55 56 57 58 59 60 61 62 63 |
# File 'lib/claude_agent_sdk.rb', line 54 def self.extract_sdk_mcp_servers(mcp_servers) return {} unless mcp_servers.is_a?(Hash) servers = {} mcp_servers.each do |name, config| config = config.to_h if config.is_a?(Type) servers[name] = config[:instance] if config.is_a?(Hash) && config[:type] == 'sdk' end servers end |
.extract_user_prompt_text(message) ⇒ Object
Extract the user-visible prompt text from a streamed input item, or nil when there is none (non-user messages, tool_result-only content, …). Only Hash and JSON-string items are inspected; arbitrary objects written via to_s are never notified.
101 102 103 104 105 106 107 108 109 110 111 112 113 114 115 116 117 118 119 120 121 122 123 124 |
# File 'lib/claude_agent_sdk.rb', line 101 def self.extract_user_prompt_text() data = case when Hash then when String # Cheap prefilter: skip the full parse for items that cannot be # user messages (e.g. multi-MB tool_result frames) — parsing # would block the reactor fiber for the duration. False # positives just cost one parse; correctness is unchanged. return nil unless .include?('user') begin JSON.parse() rescue JSON::ParserError nil end end return nil unless data.is_a?(Hash) return nil unless (data[:type] || data['type']) == 'user' inner = data[:message] || data['message'] return nil unless inner.is_a?(Hash) prompt_text_from_content(inner[:content] || inner['content']) end |
.flexible_fetch(hash, camel_key, snake_key) ⇒ Object
Look up a value in a hash that may use symbol or string keys in camelCase or snake_case. Returns the first non-nil value found, preserving false as a meaningful value.
164 165 166 167 168 169 170 |
# File 'lib/claude_agent_sdk.rb', line 164 def self.flexible_fetch(hash, camel_key, snake_key) val = hash[camel_key.to_sym] val = hash[camel_key.to_s] if val.nil? val = hash[snake_key.to_sym] if val.nil? val = hash[snake_key.to_s] if val.nil? val end |
.fold_session_summary(prev, key, entries) ⇒ Hash
Fold a batch of appended transcript entries into a running session summary. SessionStore adapters call this inside #append to maintain a summary sidecar incrementally (see SessionStore#list_session_summaries).
270 271 272 |
# File 'lib/claude_agent_sdk.rb', line 270 def self.fold_session_summary(prev, key, entries) SessionSummary.fold_session_summary(prev, key, entries) end |
.fork_session(session_id:, directory: nil, up_to_message_id: nil, title: nil) ⇒ ForkSessionResult
Fork a session into a new branch with fresh UUIDs.
249 250 251 252 |
# File 'lib/claude_agent_sdk.rb', line 249 def self.fork_session(session_id:, directory: nil, up_to_message_id: nil, title: nil) SessionMutations.fork_session(session_id: session_id, directory: directory, up_to_message_id: , title: title) end |
.fork_session_via_store(session_store:, session_id:, directory: nil, up_to_message_id: nil, title: nil) ⇒ ForkSessionResult
Fork a session in a SessionStore into a new branch with fresh UUIDs (store-backed counterpart to fork_session).
339 340 341 342 |
# File 'lib/claude_agent_sdk.rb', line 339 def self.fork_session_via_store(session_store:, session_id:, directory: nil, up_to_message_id: nil, title: nil) SessionMutations.fork_session_via_store(session_store: session_store, session_id: session_id, directory: directory, up_to_message_id: , title: title) end |
.get_session_info(session_id:, directory: nil) ⇒ SDKSessionInfo?
Read metadata for a single session by ID (no full directory scan)
186 187 188 |
# File 'lib/claude_agent_sdk.rb', line 186 def self.get_session_info(session_id:, directory: nil) Sessions.get_session_info(session_id: session_id, directory: directory) end |
.get_session_info_from_store(session_store:, session_id:, directory: nil) ⇒ SDKSessionInfo?
Read metadata for a single session from a SessionStore.
283 284 285 |
# File 'lib/claude_agent_sdk.rb', line 283 def self.get_session_info_from_store(session_store:, session_id:, directory: nil) Sessions.get_session_info_from_store(session_store: session_store, session_id: session_id, directory: directory) end |
.get_session_messages(session_id:, directory: nil, limit: nil, offset: 0) ⇒ Array<SessionMessage>
Get messages from a session transcript
196 197 198 |
# File 'lib/claude_agent_sdk.rb', line 196 def self.(session_id:, directory: nil, limit: nil, offset: 0) Sessions.(session_id: session_id, directory: directory, limit: limit, offset: offset) end |
.get_session_messages_from_store(session_store:, session_id:, directory: nil, limit: nil, offset: 0) ⇒ Array<SessionMessage>
Read a session's conversation messages from a SessionStore.
289 290 291 292 |
# File 'lib/claude_agent_sdk.rb', line 289 def self.(session_store:, session_id:, directory: nil, limit: nil, offset: 0) Sessions.(session_store: session_store, session_id: session_id, directory: directory, limit: limit, offset: offset) end |
.get_subagent_messages(session_id:, agent_id:, directory: nil, limit: nil, offset: 0) ⇒ Array<SessionMessage>
Read a subagent's conversation messages from local disk
215 216 217 218 |
# File 'lib/claude_agent_sdk.rb', line 215 def self.(session_id:, agent_id:, directory: nil, limit: nil, offset: 0) Sessions.(session_id: session_id, agent_id: agent_id, directory: directory, limit: limit, offset: offset) end |
.get_subagent_messages_from_store(session_store:, session_id:, agent_id:, directory: nil, limit: nil, offset: 0) ⇒ Array<SessionMessage>
Read a subagent's conversation messages from a SessionStore.
302 303 304 305 306 |
# File 'lib/claude_agent_sdk.rb', line 302 def self.(session_store:, session_id:, agent_id:, directory: nil, limit: nil, offset: 0) Sessions.(session_store: session_store, session_id: session_id, agent_id: agent_id, directory: directory, limit: limit, offset: offset) end |
.import_session_to_store(session_id:, session_store:, directory: nil, include_subagents: true, batch_size: TranscriptMirrorBatcher::MAX_PENDING_ENTRIES) ⇒ Object
Replay a local on-disk session transcript into a SessionStore (migration / gap-backfill). Keys under the on-disk project dir so the imported session is resumable via session_store + resume from the original cwd.
349 350 351 352 353 |
# File 'lib/claude_agent_sdk.rb', line 349 def self.import_session_to_store(session_id:, session_store:, directory: nil, include_subagents: true, batch_size: TranscriptMirrorBatcher::MAX_PENDING_ENTRIES) Sessions.import_session_to_store(session_id: session_id, session_store: session_store, directory: directory, include_subagents: include_subagents, batch_size: batch_size) end |
.list_sessions(directory: nil, limit: nil, offset: 0, include_worktrees: true) ⇒ Array<SDKSessionInfo>
List sessions for a directory (or all sessions)
178 179 180 |
# File 'lib/claude_agent_sdk.rb', line 178 def self.list_sessions(directory: nil, limit: nil, offset: 0, include_worktrees: true) Sessions.list_sessions(directory: directory, limit: limit, offset: offset, include_worktrees: include_worktrees) end |
.list_sessions_from_store(session_store:, directory: nil, limit: nil, offset: 0) ⇒ Array<SDKSessionInfo>
List sessions from a SessionStore (store-backed counterpart to list_sessions).
277 278 279 |
# File 'lib/claude_agent_sdk.rb', line 277 def self.list_sessions_from_store(session_store:, directory: nil, limit: nil, offset: 0) Sessions.list_sessions_from_store(session_store: session_store, directory: directory, limit: limit, offset: offset) end |
.list_subagents(session_id:, directory: nil) ⇒ Array<String>
List subagent IDs recorded for a session on local disk
204 205 206 |
# File 'lib/claude_agent_sdk.rb', line 204 def self.list_subagents(session_id:, directory: nil) Sessions.list_subagents(session_id: session_id, directory: directory) end |
.list_subagents_from_store(session_store:, session_id:, directory: nil) ⇒ Array<String>
List subagent IDs for a session from a SessionStore (requires list_subkeys).
296 297 298 |
# File 'lib/claude_agent_sdk.rb', line 296 def self.list_subagents_from_store(session_store:, session_id:, directory: nil) Sessions.list_subagents_from_store(session_store: session_store, session_id: session_id, directory: directory) end |
.normalize_tool_schema(schema) ⇒ Object
Single source of truth for tool input schemas: prebuilt schemas are normalized (symbol keys, string values); simple { name: :type } hashes become a full JSON Schema with every param required (string keys).
50 51 52 53 54 55 56 57 58 59 60 61 |
# File 'lib/claude_agent_sdk/sdk_mcp_server.rb', line 50 def self.normalize_tool_schema(schema) return deep_normalize_schema(schema) if prebuilt_json_schema?(schema) if schema.is_a?(Hash) properties = schema.to_h { |param, type| [param.to_sym, ruby_type_to_json_schema(type)] } result = { type: 'object', properties: properties } result[:required] = properties.keys.map(&:to_s) unless properties.empty? return result end { type: 'object', properties: {} } end |
.notify_observers(observers, method, *args) ⇒ Object
Safely call a method on each observer, suppressing any errors. Each observer is invoked through FiberBoundary so that user code runs on a plain thread (no Fiber scheduler) even when called from inside the SDK's Async reactor.
86 87 88 89 90 91 92 93 94 95 |
# File 'lib/claude_agent_sdk.rb', line 86 def self.notify_observers(observers, method, *args) observers.each do |obs| FiberBoundary.invoke { obs.send(method, *args) } rescue StandardError, ScriptError # ScriptError too: NotImplementedError < ScriptError (not # StandardError), and a stubbed observer must never mask the original # error being notified or abort connect/teardown cleanup. nil end end |
.observing_prompt_stream(prompt, observers) ⇒ Object
Wrap a streaming-input enumerable so observers get on_user_prompt for each user message before it is written to stdin. Identity when no observers are configured.
150 151 152 153 154 155 156 157 158 159 160 |
# File 'lib/claude_agent_sdk.rb', line 150 def self.observing_prompt_stream(prompt, observers) return prompt if observers.empty? Enumerator.new do |yielder| prompt.each do || text = extract_user_prompt_text() notify_observers(observers, :on_user_prompt, text) if text yielder << end end end |
.prebuilt_json_schema?(schema) ⇒ Boolean
A prebuilt JSON Schema is detected by type == 'object' (String or Symbol) with properties either ABSENT or a Hash. Properties-present-but-not-Hash falls through to the simple idiom, which uses Symbols as type VALUES — { type: :string, properties: :string } is a legal simple schema with params literally named type/properties. Propertyless object schemas ({ type: 'object', additionalProperties: ... } / oneOf / empty-object accept-anything forms) previously fell into the simple branch and were mangled into nonsense parameter lists ("additionalProperties" as a required string param). A $ref-only schema without type: 'object' remains indistinguishable from a params hash — declare the type alongside $ref.
37 38 39 40 41 42 43 44 45 |
# File 'lib/claude_agent_sdk/sdk_mcp_server.rb', line 37 def self.prebuilt_json_schema?(schema) return false unless schema.is_a?(Hash) type_val = schema[:type] || schema['type'] return false unless (type_val.is_a?(String) || type_val.is_a?(Symbol)) && type_val.to_s == 'object' return true unless schema.key?(:properties) || schema.key?('properties') (schema[:properties] || schema['properties']).is_a?(Hash) end |
.project_key_for_directory(directory = nil) ⇒ String
Derive the SessionStore project_key for a directory (default: cwd).
Matches the CLI's project-directory naming so keys align between local-disk
and store-mirrored transcripts.
259 260 261 |
# File 'lib/claude_agent_sdk.rb', line 259 def self.project_key_for_directory(directory = nil) Sessions.project_key_for_directory(directory) end |
.prompt_text_from_content(content) ⇒ Object
Text from a user-message content payload: the string itself, or the newline-joined non-empty top-level text blocks. Returns nil (never '') when there is no extractable text — on_user_prompt('') would latch OTelObserver's first-prompt buffer while never setting the attribute, permanently suppressing later real prompts.
131 132 133 134 135 136 137 138 139 140 141 142 143 144 145 |
# File 'lib/claude_agent_sdk.rb', line 131 def self.prompt_text_from_content(content) case content when String content.empty? ? nil : content when Array texts = content.filter_map do |block| next unless block.is_a?(Hash) next unless (block[:type] || block['type']) == 'text' text = block[:text] || block['text'] text unless text.to_s.empty? end texts.empty? ? nil : texts.join("\n") end end |
.query(prompt:, options: nil, transport: nil) {|Message| ... } ⇒ Enumerator
An attempted #next may still spawn the CLI subprocess before failing and leaves the query unusable.
Query Claude Code for one-shot or unidirectional streaming interactions
This function is ideal for simple, stateless queries where you don't need bidirectional communication or conversation management.
390 391 392 393 394 395 396 397 398 399 400 401 402 403 404 405 406 407 408 409 410 411 412 413 414 415 416 417 418 419 420 421 422 423 424 425 426 427 428 429 430 431 432 433 434 435 436 437 438 439 440 441 442 443 444 445 446 447 448 449 450 451 452 453 454 455 456 457 458 459 460 461 462 463 464 465 466 467 468 469 470 471 472 473 474 475 476 477 478 479 480 481 482 483 484 485 486 487 488 489 490 491 492 493 494 495 496 497 498 499 500 501 502 503 504 505 506 507 508 509 510 511 512 513 514 515 516 517 518 519 520 521 522 523 524 525 526 527 528 529 530 531 532 533 534 535 536 537 538 539 540 541 542 543 544 545 546 547 548 549 550 551 552 553 554 555 556 557 558 559 560 561 562 563 564 565 566 567 568 |
# File 'lib/claude_agent_sdk.rb', line 390 def self.query(prompt:, options: nil, transport: nil, &block) # Validate BEFORE the block-less enum_for return so a bad prompt fails at # the call site, not on first iteration. Mirrors Client#query: a bare Hash # responds to #each and would stream [key, value] pairs' to_s garbage to # the CLI; nil/Integer would hang forever waiting for input. raise ArgumentError, 'prompt must be a String or an Enumerable of message Hashes/JSONL Strings (got Hash)' if prompt.is_a?(Hash) raise ArgumentError, "prompt must be a String or respond to #each (got #{prompt.class})" unless prompt.is_a?(String) || prompt.respond_to?(:each) return enum_for(:query, prompt: prompt, options: , transport: transport) unless block ||= ClaudeAgentOptions.new = if .can_use_tool if prompt.is_a?(String) raise ArgumentError, 'can_use_tool callback requires streaming mode. Please provide prompt as an Enumerator instead of a String.' end raise ArgumentError, 'can_use_tool callback cannot be used with permission_prompt_tool_name' if . = .dup_with(permission_prompt_tool_name: 'stdio') end # Fail fast on invalid session_store combinations before spawning the CLI. SessionStores.() # Resolve callable observers into fresh instances (thread-safe for global defaults) resolved_observers = ClaudeAgentSDK.resolve_observers(.observers) raise ArgumentError, 'transport must respond to #connect (see ClaudeAgentSDK::Transport)' if transport && !transport.respond_to?(:connect) Async do materialized = nil query_handler = nil begin if transport.nil? # Resume-from-store: when a session_store is set and resume/continue # is requested, load the session into a temp CLAUDE_CONFIG_DIR and # repoint options at it (env + --resume) BEFORE spawning. Returns # options unchanged when no materialization applies. Skipped # entirely for an injected transport — the materialized # env/--resume only apply to the CLI subprocess (Python parity: # client.py skips materialization when a transport is supplied). materialized = SessionResume.materialize_resume_session() = SessionResume.(, materialized) if materialized # Always use streaming mode with control protocol (matches Python # SDK). This sends agents via initialize request instead of CLI # args, avoiding OS ARG_MAX limits. transport = SubprocessCLITransport.new() end # Deliberate deviation from Python: the ensure below also closes an # injected transport whose #connect raised (Python leaves it # unclosed); Transport#close must be idempotent. transport.connect # Extract SDK MCP servers sdk_mcp_servers = extract_sdk_mcp_servers(.mcp_servers) hooks = nil if .hooks hooks = {} .hooks.each do |event, matchers| next if matchers.nil? || matchers.empty? entries = [] matchers.each do |matcher| config = { matcher: matcher.matcher, hooks: matcher.hooks } config[:timeout] = matcher.timeout if matcher.timeout entries << config end hooks[event.to_s] = entries unless entries.empty? end hooks = nil if hooks.empty? end # Create Query handler for control protocol query_handler = Query.new( transport: transport, is_streaming_mode: true, can_use_tool: .can_use_tool, hooks: hooks, agents: .agents, sdk_mcp_servers: sdk_mcp_servers, exclude_dynamic_sections: ClaudeAgentSDK.extract_exclude_dynamic_sections(.system_prompt), skills: .skills ) # Mirror transcripts to the session_store, if configured. Installed # before #start so the read loop captures transcript_mirror frames. if .session_store query_handler.set_transcript_mirror_batcher( SessionResume.build_mirror_batcher( store: .session_store, env: .env, on_error: ->(key, ) { query_handler.report_mirror_error(key, ) }, eager: .session_store_flush.to_s == 'eager' ) ) end # Start reading messages in background query_handler.start # Initialize the control protocol (sends agents) query_handler.initialize_protocol # Send prompt(s) as user messages, then close stdin if prompt.is_a?(String) ClaudeAgentSDK.notify_observers(resolved_observers, :on_user_prompt, prompt) = { type: 'user', message: { role: 'user', content: prompt }, parent_tool_use_id: nil, session_id: '' } transport.write(JSON.generate() + "\n") # Background-spawn so messages stream to the user block while stdin # close waits (without timeout) for the first result; a synchronous # call would defer all delivery until the turn completes (mirrors # Python's query.spawn_task(query.wait_for_result_and_end_input())). query_handler.spawn_task { query_handler.wait_for_result_and_end_input } elsif prompt.is_a?(Enumerator) || prompt.respond_to?(:each) # Tracked on the Query so close() stops it; an untracked Async task # here kept the root reactor alive forever when the read loop died # while the user enumerator was still blocked (matches Python's # query.spawn_task(query.stream_input(prompt))). observed_prompt = ClaudeAgentSDK.observing_prompt_stream(prompt, resolved_observers) query_handler.spawn_task { query_handler.stream_input(observed_prompt) } end # Read and yield messages from the query handler (filters out control messages). # User block is invoked through FiberBoundary so ActiveRecord / PG calls # inside it don't see the async gem's Fiber scheduler. query_handler. do |data| = MessageParser.parse(data) next unless ClaudeAgentSDK.notify_observers(resolved_observers, :on_message, ) signal = FiberBoundary.invoke_iteration(block, ) break signal.value if signal.is_a?(FiberBoundary::Break) end rescue StandardError => e # One notify point for every error surfacing from query() — transport # connect, initialize, stream errors re-raised from the message queue, # parse errors, and user-block errors. StandardError only: Async::Stop # is cancellation, not an error. Bare raise preserves the backtrace; # the ensure below still fires on_close after on_error. ClaudeAgentSDK.notify_observers(resolved_observers, :on_error, e) raise ensure ClaudeAgentSDK.notify_observers(resolved_observers, :on_close) # query_handler.close stops the background read task and closes the # transport (flushing the mirror batcher first). Fall back to a bare # transport close when the handler was never built. begin if query_handler query_handler.close elsif transport transport.close end ensure # Remove the materialized resume temp dir (which holds a redacted # .credentials.json copy) AFTER the subprocess has exited, even when # close itself raises — unless the mirror dropped batches: the store # copy is then incomplete and the temp dir holds the only copy of # the dropped turns, so it is preserved (scrubbed of credentials) # with a warning instead of deleted. if materialized query_handler&.mirror_batches_dropped? ? materialized.preserve_transcripts : materialized.cleanup end end end end.wait end |
.rename_session(session_id:, title:, directory: nil) ⇒ Object
Rename a session by appending a custom-title entry
224 225 226 |
# File 'lib/claude_agent_sdk.rb', line 224 def self.rename_session(session_id:, title:, directory: nil) SessionMutations.rename_session(session_id: session_id, title: title, directory: directory) end |
.rename_session_via_store(session_store:, session_id:, title:, directory: nil) ⇒ Object
Rename a session in a SessionStore (store-backed counterpart to rename_session). Appends a custom-title entry carrying a fresh uuid + timestamp via SessionStore#append.
312 313 314 315 |
# File 'lib/claude_agent_sdk.rb', line 312 def self.rename_session_via_store(session_store:, session_id:, title:, directory: nil) SessionMutations.rename_session_via_store(session_store: session_store, session_id: session_id, title: title, directory: directory) end |
.reset_configuration ⇒ Object
Reset configuration to defaults (useful for testing)
63 64 65 |
# File 'lib/claude_agent_sdk/configuration.rb', line 63 def reset_configuration @configuration = Configuration.new end |
.resolve_observers(observers) ⇒ Object
Resolve observers array: callables (Proc/lambda) are invoked to produce a fresh instance per query/session (thread-safe); plain objects are used as-is. Array() guards against nil (e.g., when observers: nil is passed explicitly). Anything implementing none of the observer methods is warned about and skipped — most commonly a Class passed instead of an instance, which previously produced silent zero instrumentation (every notify raised NoMethodError, swallowed by notify_observers' error containment).
37 38 39 40 41 42 43 44 45 46 47 48 |
# File 'lib/claude_agent_sdk.rb', line 37 def self.resolve_observers(observers) Array(observers).filter_map do |obs| resolved = obs.respond_to?(:call) ? obs.call : obs if OBSERVER_INTERFACE.none? { |m| resolved.respond_to?(m) } label = resolved.is_a?(Module) ? resolved : resolved.class hint = resolved.is_a?(Module) ? " — pass an instance (#{resolved}.new) or a factory lambda" : '' warn "ClaudeAgentSDK: ignoring observer #{label}: it implements none of #{OBSERVER_INTERFACE.join('/')}#{hint}" next nil end resolved end end |
.ruby_type_to_json_schema(type) ⇒ Object
63 64 65 66 67 68 69 70 71 |
# File 'lib/claude_agent_sdk/sdk_mcp_server.rb', line 63 def self.ruby_type_to_json_schema(type) case type when :string, String then { type: 'string' } when :integer, Integer then { type: 'integer' } when :float, Float, :number then { type: 'number' } when :boolean, TrueClass, FalseClass then { type: 'boolean' } else { type: 'string' } # Default fallback end end |
.tag_session(session_id:, tag:, directory: nil) ⇒ Object
Tag a session. Pass nil to clear the tag.
232 233 234 |
# File 'lib/claude_agent_sdk.rb', line 232 def self.tag_session(session_id:, tag:, directory: nil) SessionMutations.tag_session(session_id: session_id, tag: tag, directory: directory) end |
.tag_session_via_store(session_store:, session_id:, tag:, directory: nil) ⇒ Object
Tag a session in a SessionStore (store-backed counterpart to tag_session). Pass nil to clear the tag.
320 321 322 323 |
# File 'lib/claude_agent_sdk.rb', line 320 def self.tag_session_via_store(session_store:, session_id:, tag:, directory: nil) SessionMutations.tag_session_via_store(session_store: session_store, session_id: session_id, tag: tag, directory: directory) end |