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. xhigh is only supported on Opus 4.7 and falls back to high on 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_updated patch. 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

display controls 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". Pass display: "summarized" explicitly on Opus 4.7 to get visible thinking text. Not supported with ThinkingConfigDisabled.

%w[summarized omitted].freeze
MCP_SERVER_CONNECTION_STATUSES =

MCP server connection status values

%w[connected failed needs-auth pending disabled].freeze
VERSION =
'0.19.1'
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 result message) 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.
%w[batched eager].freeze

Class Method Summary collapse

Class Method Details

.configurationConfiguration

Get the configuration object

Returns:



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

Examples:

Set default env and other options

ClaudeAgentSDK.configure do |config|
  config.default_options = {
    env: { 'API_KEY' => 'xxx' },
    permission_mode: 'bypassPermissions'
  }
end

Yields:



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

Examples:

Simple prompt

prompt = create_prompt(
  name: 'code_review',
  description: 'Review code for best practices'
) do |args|
  {
    messages: [
      {
        role: 'user',
        content: {
          type: 'text',
          text: 'Please review this code for best practices and suggest improvements.'
        }
      }
    ]
  }
end

Prompt with arguments

prompt = create_prompt(
  name: 'git_commit',
  description: 'Generate a git commit message',
  arguments: [
    { name: 'changes', description: 'Description of changes', required: true }
  ]
) do |args|
  {
    messages: [
      {
        role: 'user',
        content: {
          type: 'text',
          text: "Generate a concise git commit message for: #{args[:changes]}"
        }
      }
    ]
  }
end

Parameters:

  • name (String)

    Unique identifier for the prompt

  • description (String, nil) (defaults to: nil)

    Optional description

  • arguments (Array<Hash>, nil) (defaults to: nil)

    Optional argument definitions

  • generator (Proc)

    Block that generates prompt messages

Returns:

Raises:

  • (ArgumentError)


587
588
589
590
591
592
593
594
595
596
# File 'lib/claude_agent_sdk/sdk_mcp_server.rb', line 587

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

Examples:

File resource

resource = create_resource(
  uri: 'file:///config/settings.json',
  name: 'Application Settings',
  description: 'Current application configuration',
  mime_type: 'application/json'
) do
  content = File.read('/path/to/settings.json')
  {
    contents: [{
      uri: 'file:///config/settings.json',
      mimeType: 'application/json',
      text: content
    }]
  }
end

Database resource

resource = create_resource(
  uri: 'db://users/count',
  name: 'User Count',
  description: 'Total number of registered users'
) do
  count = User.count
  {
    contents: [{
      uri: 'db://users/count',
      mimeType: 'text/plain',
      text: count.to_s
    }]
  }
end

Parameters:

  • uri (String)

    Unique identifier for the resource (e.g., "file:///path/to/file")

  • name (String)

    Human-readable name

  • description (String, nil) (defaults to: nil)

    Optional description

  • mime_type (String, nil) (defaults to: nil)

    Optional MIME type (e.g., "text/plain", "application/json")

  • reader (Proc)

    Block that returns the resource content

Returns:

Raises:

  • (ArgumentError)


529
530
531
532
533
534
535
536
537
538
539
# File 'lib/claude_agent_sdk/sdk_mcp_server.rb', line 529

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

Examples:

Simple calculator server

add_tool = ClaudeAgentSDK.create_tool('add', 'Add numbers', { a: :number, b: :number }) do |args|
  { content: [{ type: 'text', text: "Sum: #{args[:a] + args[:b]}" }] }
end

calculator = ClaudeAgentSDK.create_sdk_mcp_server(
  name: 'calculator',
  version: '2.0.0',
  tools: [add_tool]
)

options = ClaudeAgentOptions.new(
  mcp_servers: { calc: calculator },
  allowed_tools: ['mcp__calc__add']
)

Server with resources and prompts

config_resource = ClaudeAgentSDK.create_resource(
  uri: 'config://app',
  name: 'App Config'
) { { contents: [{ uri: 'config://app', text: 'config data' }] } }

review_prompt = ClaudeAgentSDK.create_prompt(
  name: 'review',
  description: 'Code review'
) { { messages: [{ role: 'user', content: { type: 'text', text: 'Review this' } }] } }

server = ClaudeAgentSDK.create_sdk_mcp_server(
  name: 'dev-tools',
  resources: [config_resource],
  prompts: [review_prompt]
)

Parameters:

  • name (String)

    Unique identifier for the server

  • version (String) (defaults to: '1.0.0')

    Server version (default: '1.0.0')

  • tools (Array<SdkMcpTool>) (defaults to: [])

    List of tool definitions

  • resources (Array<SdkMcpResource>) (defaults to: [])

    List of resource definitions

  • prompts (Array<SdkMcpPrompt>) (defaults to: [])

    List of prompt definitions

Returns:

  • (Hash)

    MCP server configuration for ClaudeAgentOptions



639
640
641
642
643
644
645
646
647
648
649
650
651
652
653
654
# File 'lib/claude_agent_sdk/sdk_mcp_server.rb', line 639

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

Examples:

Simple tool

tool = create_tool('greet', 'Greet a user', { name: :string }) do |args|
  { content: [{ type: 'text', text: "Hello, #{args[:name]}!" }] }
end

Tool with multiple parameters

tool = create_tool('add', 'Add two numbers', { a: :number, b: :number }) do |args|
  result = args[:a] + args[:b]
  { content: [{ type: 'text', text: "Result: #{result}" }] }
end

Tool with error handling

tool = create_tool('divide', 'Divide numbers', { a: :number, b: :number }) do |args|
  if args[:b] == 0
    { content: [{ type: 'text', text: 'Error: Division by zero' }], is_error: true }
  else
    result = args[:a] / args[:b]
    { content: [{ type: 'text', text: "Result: #{result}" }] }
  end
end

Parameters:

  • name (String)

    Unique identifier for the tool

  • description (String)

    Human-readable description

  • input_schema (Hash)

    Schema defining input parameters

  • handler (Proc)

    Block that implements the tool logic

Returns:

Raises:

  • (ArgumentError)


463
464
465
466
467
468
469
470
471
472
473
474
475
476
477
478
479
480
481
482
483
484
485
486
# File 'lib/claude_agent_sdk/sdk_mcp_server.rb', line 463

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
  resolved_meta = meta
  if resolved_meta.nil? && annotations
    max_chars = annotations[:maxResultSizeChars] || annotations['maxResultSizeChars']
    resolved_meta = { '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: resolved_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_optionsHash

Get merged default options for use with ClaudeAgentOptions

Returns:

  • (Hash)

    Default options hash



70
71
72
# File 'lib/claude_agent_sdk/configuration.rb', line 70

def default_options
  configuration.default_options || {}
end

.delete_session(session_id:, directory: nil) ⇒ Object

Delete a session by removing its JSONL file (hard delete).

Parameters:

  • session_id (String)

    UUID of the session to delete

  • directory (String, nil) (defaults to: nil)

    Project directory path



222
223
224
# File 'lib/claude_agent_sdk.rb', line 222

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).

Raises:

  • (ArgumentError)

    if session_id is invalid



312
313
314
315
# File 'lib/claude_agent_sdk.rb', line 312

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_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.



84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
# File 'lib/claude_agent_sdk.rb', line 84

def self.extract_user_prompt_text(message)
  data = case message
         when Hash then message
         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 message.include?('user')

           begin
             JSON.parse(message)
           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.



147
148
149
150
151
152
153
# File 'lib/claude_agent_sdk.rb', line 147

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).

Parameters:

  • prev (Hash, nil)

    previous summary entry for this key

  • key (Hash)

    the SessionKey (string keys)

  • entries (Array<Hash>)

    newly appended transcript entries

Returns:

  • (Hash)

    the updated summary entry



253
254
255
# File 'lib/claude_agent_sdk.rb', line 253

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.

Parameters:

  • session_id (String)

    UUID of the session to fork

  • directory (String, nil) (defaults to: nil)

    Project directory path

  • up_to_message_id (String, nil) (defaults to: nil)

    Truncate the fork at this message UUID

  • title (String, nil) (defaults to: nil)

    Custom title for the fork

Returns:



232
233
234
235
# File 'lib/claude_agent_sdk.rb', line 232

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: 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).

Returns:

Raises:

  • (ArgumentError)

    if session_id/up_to_message_id is invalid or there are no messages

  • (Errno::ENOENT)

    if the source session is not found in the store



322
323
324
325
# File 'lib/claude_agent_sdk.rb', line 322

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: 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)

Parameters:

  • session_id (String)

    UUID of the session to look up

  • directory (String, nil) (defaults to: nil)

    Project directory path

Returns:



169
170
171
# File 'lib/claude_agent_sdk.rb', line 169

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.

Returns:



266
267
268
# File 'lib/claude_agent_sdk.rb', line 266

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

Parameters:

  • session_id (String)

    The session UUID

  • directory (String, nil) (defaults to: nil)

    Working directory to search in

  • limit (Integer, nil) (defaults to: nil)

    Maximum number of messages

  • offset (Integer) (defaults to: 0)

    Number of messages to skip

Returns:



179
180
181
# File 'lib/claude_agent_sdk.rb', line 179

def self.get_session_messages(session_id:, directory: nil, limit: nil, offset: 0)
  Sessions.get_session_messages(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.

Returns:



272
273
274
275
# File 'lib/claude_agent_sdk.rb', line 272

def self.get_session_messages_from_store(session_store:, session_id:, directory: nil, limit: nil, offset: 0)
  Sessions.get_session_messages_from_store(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

Parameters:

  • session_id (String)

    The session UUID

  • agent_id (String)

    The subagent ID (without the agent- prefix)

  • directory (String, nil) (defaults to: nil)

    Working directory to search in

  • limit (Integer, nil) (defaults to: nil)

    Maximum number of messages

  • offset (Integer) (defaults to: 0)

    Number of messages to skip

Returns:



198
199
200
201
# File 'lib/claude_agent_sdk.rb', line 198

def self.get_subagent_messages(session_id:, agent_id:, directory: nil, limit: nil, offset: 0)
  Sessions.get_subagent_messages(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.

Returns:



285
286
287
288
289
# File 'lib/claude_agent_sdk.rb', line 285

def self.get_subagent_messages_from_store(session_store:, session_id:, agent_id:, directory: nil, limit: nil,
                                          offset: 0)
  Sessions.get_subagent_messages_from_store(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.

Raises:

  • (ArgumentError)

    if session_id is not a valid UUID

  • (Errno::ENOENT)

    if the session JSONL cannot be found



332
333
334
335
336
# File 'lib/claude_agent_sdk.rb', line 332

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)

Parameters:

  • directory (String, nil) (defaults to: nil)

    Working directory to list sessions for

  • limit (Integer, nil) (defaults to: nil)

    Maximum number of sessions to return

  • offset (Integer) (defaults to: 0)

    Number of sessions to skip (for pagination)

  • include_worktrees (Boolean) (defaults to: true)

    Whether to include git worktree sessions

Returns:

  • (Array<SDKSessionInfo>)

    Sessions sorted by last_modified descending



161
162
163
# File 'lib/claude_agent_sdk.rb', line 161

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).

Parameters:

Returns:



260
261
262
# File 'lib/claude_agent_sdk.rb', line 260

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

Parameters:

  • session_id (String)

    The session UUID

  • directory (String, nil) (defaults to: nil)

    Working directory to search in

Returns:

  • (Array<String>)

    Subagent IDs



187
188
189
# File 'lib/claude_agent_sdk.rb', line 187

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).

Returns:

  • (Array<String>)


279
280
281
# File 'lib/claude_agent_sdk.rb', line 279

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).



43
44
45
46
47
48
49
50
51
52
53
54
# File 'lib/claude_agent_sdk/sdk_mcp_server.rb', line 43

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.



69
70
71
72
73
74
75
76
77
78
# File 'lib/claude_agent_sdk.rb', line 69

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.



133
134
135
136
137
138
139
140
141
142
143
# File 'lib/claude_agent_sdk.rb', line 133

def self.observing_prompt_stream(prompt, observers)
  return prompt if observers.empty?

  Enumerator.new do |yielder|
    prompt.each do |message|
      text = extract_user_prompt_text(message)
      notify_observers(observers, :on_user_prompt, text) if text
      yielder << message
    end
  end
end

.prebuilt_json_schema?(schema) ⇒ Boolean

A prebuilt JSON Schema is detected by type == 'object' (String or Symbol) AND a Hash properties value. Deliberately stricter than Python's rule: Ruby's simple-schema idiom uses Symbols as type VALUES, so { type: :string, properties: :string } is a legal simple schema with params literally named type/properties.

Returns:

  • (Boolean)


32
33
34
35
36
37
38
# File 'lib/claude_agent_sdk/sdk_mcp_server.rb', line 32

def self.prebuilt_json_schema?(schema)
  return false unless schema.is_a?(Hash)

  type_val = schema[:type] || schema['type']
  props_val = schema[:properties] || schema['properties']
  (type_val.is_a?(String) || type_val.is_a?(Symbol)) && type_val.to_s == 'object' && props_val.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.

Parameters:

  • directory (String, Pathname, nil) (defaults to: nil)

    Directory to key (nil = cwd)

Returns:

  • (String)

    The project key



242
243
244
# File 'lib/claude_agent_sdk.rb', line 242

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.



114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
# File 'lib/claude_agent_sdk.rb', line 114

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

Note:

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.

Examples:

Simple query

ClaudeAgentSDK.query(prompt: "What is 2 + 2?") do |message|
  puts message
end

With options

options = ClaudeAgentSDK::ClaudeAgentOptions.new(
  allowed_tools: ['Read', 'Bash'],
  permission_mode: 'acceptEdits'
)
ClaudeAgentSDK.query(prompt: "Create a hello.rb file", options: options) do |msg|
  puts msg.text if msg.is_a?(ClaudeAgentSDK::AssistantMessage)
end

Streaming input

messages = Streaming.from_array(['Hello', 'What is 2+2?', 'Thanks!'])
ClaudeAgentSDK.query(prompt: messages) do |message|
  puts message
end

Parameters:

  • prompt (String, Enumerator)

    The prompt to send to Claude, or an Enumerator for streaming input

  • options (ClaudeAgentOptions) (defaults to: nil)

    Optional configuration

Yields:

  • (Message)

    Each message from the conversation

Returns:

  • (Enumerator)

    if no block given. Internal iteration only: consume with #each or each-driven Enumerable methods (#first, #take, #map, #to_a). External iteration (#next, #peek, #rewind) is NOT supported — message delivery runs inside the SDK's Async reactor, which cannot run on the Enumerator's fiber; #next raises or hangs depending on context.

Raises:

  • (ArgumentError)


373
374
375
376
377
378
379
380
381
382
383
384
385
386
387
388
389
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
# File 'lib/claude_agent_sdk.rb', line 373

def self.query(prompt:, options: nil, transport: nil, &block)
  return enum_for(:query, prompt: prompt, options: options, transport: transport) unless block

  options ||= ClaudeAgentOptions.new

  configured_options = options
  if options.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 options.permission_prompt_tool_name

    configured_options = options.dup_with(permission_prompt_tool_name: 'stdio')
  end

  # Fail fast on invalid session_store combinations before spawning the CLI.
  SessionStores.validate_session_store_options(configured_options)

  # Resolve callable observers into fresh instances (thread-safe for global defaults)
  resolved_observers = ClaudeAgentSDK.resolve_observers(configured_options.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(configured_options)
        configured_options = SessionResume.apply_materialized_options(configured_options, 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(configured_options)
      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(configured_options.mcp_servers)

      hooks = nil
      if configured_options.hooks
        hooks = {}
        configured_options.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: configured_options.can_use_tool,
        hooks: hooks,
        agents: configured_options.agents,
        sdk_mcp_servers: sdk_mcp_servers,
        skills: configured_options.skills
      )

      # Mirror transcripts to the session_store, if configured. Installed
      # before #start so the read loop captures transcript_mirror frames.
      if configured_options.session_store
        query_handler.set_transcript_mirror_batcher(
          SessionResume.build_mirror_batcher(
            store: configured_options.session_store,
            env: configured_options.env,
            on_error: ->(key, message) { query_handler.report_mirror_error(key, message) },
            eager: configured_options.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)
        message = {
          type: 'user',
          message: { role: 'user', content: prompt },
          parent_tool_use_id: nil,
          session_id: ''
        }
        transport.write(JSON.generate(message) + "\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.receive_messages do |data|
        message = MessageParser.parse(data)
        next unless message

        ClaudeAgentSDK.notify_observers(resolved_observers, :on_message, message)
        signal = FiberBoundary.invoke_iteration(block, message)
        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.
        materialized&.cleanup
      end
    end
  end.wait
end

.rename_session(session_id:, title:, directory: nil) ⇒ Object

Rename a session by appending a custom-title entry

Parameters:

  • session_id (String)

    UUID of the session to rename

  • title (String)

    New session title

  • directory (String, nil) (defaults to: nil)

    Project directory path



207
208
209
# File 'lib/claude_agent_sdk.rb', line 207

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.

Raises:

  • (ArgumentError)

    if session_id is invalid or title is empty



295
296
297
298
# File 'lib/claude_agent_sdk.rb', line 295

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_configurationObject

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



56
57
58
59
60
61
62
63
64
# File 'lib/claude_agent_sdk/sdk_mcp_server.rb', line 56

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.

Parameters:

  • session_id (String)

    UUID of the session to tag

  • tag (String, nil)

    Tag string, or nil to clear

  • directory (String, nil) (defaults to: nil)

    Project directory path



215
216
217
# File 'lib/claude_agent_sdk.rb', line 215

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.

Raises:

  • (ArgumentError)

    if session_id is invalid or tag is empty after sanitization



303
304
305
306
# File 'lib/claude_agent_sdk.rb', line 303

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