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/option_warnings.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, OptionWarnings, 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.28.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
-
.check_inline_isolation(scheduling) ⇒ Object
Internal: warn once per process when :inline callback scheduling is enabled while ActiveSupport reports thread isolation — the host then almost certainly violates inline mode's fiber-isolation precondition (solid_queue fiber workers require isolation_level = :fiber).
-
.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, scheduling: :thread, wrapper: nil) ⇒ Object
Safely call a method on each observer, suppressing any errors.
-
.observing_prompt_stream(prompt, observers, scheduling: :thread, wrapper: nil) ⇒ Object
Wrap a streaming-input enumerable so observers get on_user_prompt for each user message before it is written to stdin.
-
.offload(&block) ⇒ Object
Public escape hatch for hosts running with callback_scheduling: :inline: run a heavy piece of a callback on a plain thread instead of the shared reactor fiber.
-
.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
.check_inline_isolation(scheduling) ⇒ Object
Internal: warn once per process when :inline callback scheduling is enabled while ActiveSupport reports thread isolation — the host then almost certainly violates inline mode's fiber-isolation precondition (solid_queue fiber workers require isolation_level = :fiber). defined? probing only; the SDK never loads ActiveSupport itself.
127 128 129 130 131 132 133 134 135 136 137 138 139 140 141 142 143 |
# File 'lib/claude_agent_sdk.rb', line 127 def self.check_inline_isolation(scheduling) return unless scheduling == :inline return unless defined?(ActiveSupport::IsolatedExecutionState) return unless ActiveSupport::IsolatedExecutionState.isolation_level == :thread INLINE_ISOLATION_WARN_LOCK.synchronize do return if @inline_isolation_warned @inline_isolation_warned = true end warn 'ClaudeAgentSDK: callback_scheduling: :inline is enabled but ' \ 'ActiveSupport::IsolatedExecutionState.isolation_level is :thread. ' \ 'Inline callbacks run on reactor fibers that share one thread, so ' \ 'thread-keyed Rails state will leak across fibers. Set ' \ 'isolation_level = :fiber (as solid_queue fiber workers require) ' \ 'or use the default callback_scheduling: :thread.' end |
.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
693 694 695 696 697 698 699 700 701 702 |
# File 'lib/claude_agent_sdk/sdk_mcp_server.rb', line 693 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
635 636 637 638 639 640 641 642 643 644 645 |
# File 'lib/claude_agent_sdk/sdk_mcp_server.rb', line 635 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
745 746 747 748 749 750 751 752 753 754 755 756 757 758 759 760 |
# File 'lib/claude_agent_sdk/sdk_mcp_server.rb', line 745 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
569 570 571 572 573 574 575 576 577 578 579 580 581 582 583 584 585 586 587 588 589 590 591 592 |
# File 'lib/claude_agent_sdk/sdk_mcp_server.rb', line 569 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).
287 288 289 |
# File 'lib/claude_agent_sdk.rb', line 287 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).
377 378 379 380 |
# File 'lib/claude_agent_sdk.rb', line 377 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.
69 70 71 72 73 74 75 76 77 78 79 80 81 |
# File 'lib/claude_agent_sdk.rb', line 69 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.
55 56 57 58 59 60 61 62 63 64 |
# File 'lib/claude_agent_sdk.rb', line 55 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.
149 150 151 152 153 154 155 156 157 158 159 160 161 162 163 164 165 166 167 168 169 170 171 172 |
# File 'lib/claude_agent_sdk.rb', line 149 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.
212 213 214 215 216 217 218 |
# File 'lib/claude_agent_sdk.rb', line 212 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).
318 319 320 |
# File 'lib/claude_agent_sdk.rb', line 318 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.
297 298 299 300 |
# File 'lib/claude_agent_sdk.rb', line 297 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).
387 388 389 390 |
# File 'lib/claude_agent_sdk.rb', line 387 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)
234 235 236 |
# File 'lib/claude_agent_sdk.rb', line 234 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.
331 332 333 |
# File 'lib/claude_agent_sdk.rb', line 331 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
244 245 246 |
# File 'lib/claude_agent_sdk.rb', line 244 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.
337 338 339 340 |
# File 'lib/claude_agent_sdk.rb', line 337 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
263 264 265 266 |
# File 'lib/claude_agent_sdk.rb', line 263 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.
350 351 352 353 354 |
# File 'lib/claude_agent_sdk.rb', line 350 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.
397 398 399 400 401 |
# File 'lib/claude_agent_sdk.rb', line 397 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)
226 227 228 |
# File 'lib/claude_agent_sdk.rb', line 226 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).
325 326 327 |
# File 'lib/claude_agent_sdk.rb', line 325 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
252 253 254 |
# File 'lib/claude_agent_sdk.rb', line 252 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).
344 345 346 |
# File 'lib/claude_agent_sdk.rb', line 344 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, scheduling: :thread, wrapper: nil) ⇒ 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 — or in place when scheduling is :inline.
87 88 89 90 91 92 93 94 95 96 |
# File 'lib/claude_agent_sdk.rb', line 87 def self.notify_observers(observers, method, *args, scheduling: :thread, wrapper: nil) observers.each do |obs| FiberBoundary.invoke(scheduling: scheduling, wrapper: wrapper) { 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, scheduling: :thread, wrapper: nil) ⇒ 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.
198 199 200 201 202 203 204 205 206 207 208 |
# File 'lib/claude_agent_sdk.rb', line 198 def self.observing_prompt_stream(prompt, observers, scheduling: :thread, wrapper: nil) return prompt if observers.empty? Enumerator.new do |yielder| prompt.each do || text = extract_user_prompt_text() notify_observers(observers, :on_user_prompt, text, scheduling: scheduling, wrapper: wrapper) if text yielder << end end end |
.offload(&block) ⇒ Object
Public escape hatch for hosts running with callback_scheduling: :inline: run a heavy piece of a callback on a plain thread instead of the shared reactor fiber. What that buys, precisely:
- scheduler-opaque BLOCKING that releases the GVL (native DB drivers, file/socket calls the scheduler can't see): the reactor keeps running.
- pure-Ruby CPU-bound work: degrades a hard reactor stall into GVL time-slicing — added latency for other fibers, not starvation.
- a C extension that HOLDS the GVL for the whole computation: no help; nothing in-process can protect the reactor from that — move such work to a subprocess. No-op outside a Fiber scheduler, so it is safe to call unconditionally. Returns the block's value; exceptions propagate.
113 114 115 |
# File 'lib/claude_agent_sdk.rb', line 113 def self.offload(&block) FiberBoundary.invoke(&block) 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.
307 308 309 |
# File 'lib/claude_agent_sdk.rb', line 307 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.
179 180 181 182 183 184 185 186 187 188 189 190 191 192 193 |
# File 'lib/claude_agent_sdk.rb', line 179 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.
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 569 570 571 572 573 574 575 576 577 578 579 580 581 582 583 584 585 586 587 588 589 590 591 592 593 594 595 596 597 598 599 600 601 602 603 604 605 606 607 608 609 610 611 612 613 614 615 616 617 618 619 620 621 622 623 624 625 626 627 628 629 630 631 632 633 634 635 636 |
# File 'lib/claude_agent_sdk.rb', line 438 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 # Advisory: warn if other options shadow the can_use_tool callback. # After the ArgumentError validations so invalid configs raise, not warn. OptionWarnings.warn_if_can_use_tool_shadowed() # 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) # Where user callbacks run (see ClaudeAgentOptions#callback_scheduling) # and the middleware wrapped around them (#callback_wrapper). callback_scheduling = .callback_scheduling || :thread callback_wrapper = .callback_wrapper ClaudeAgentSDK.check_inline_isolation(callback_scheduling) 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, callback_scheduling: callback_scheduling, callback_wrapper: callback_wrapper ) # 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', callback_wrapper: callback_wrapper ) ) 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, scheduling: callback_scheduling, wrapper: callback_wrapper) = { 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, scheduling: callback_scheduling, wrapper: callback_wrapper) 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 (default :thread # mode; :inline runs it in place on the reactor fiber). query_handler. do |data| = MessageParser.parse(data) next unless ClaudeAgentSDK.notify_observers(resolved_observers, :on_message, , scheduling: callback_scheduling, wrapper: callback_wrapper) signal = FiberBoundary.invoke_iteration(block, , scheduling: callback_scheduling, wrapper: callback_wrapper) 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, scheduling: callback_scheduling, wrapper: callback_wrapper) raise ensure ClaudeAgentSDK.notify_observers(resolved_observers, :on_close, scheduling: callback_scheduling, wrapper: callback_wrapper) # 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
272 273 274 |
# File 'lib/claude_agent_sdk.rb', line 272 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.
360 361 362 363 |
# File 'lib/claude_agent_sdk.rb', line 360 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).
38 39 40 41 42 43 44 45 46 47 48 49 |
# File 'lib/claude_agent_sdk.rb', line 38 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.
280 281 282 |
# File 'lib/claude_agent_sdk.rb', line 280 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.
368 369 370 371 |
# File 'lib/claude_agent_sdk.rb', line 368 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 |