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

.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

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


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

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)


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

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



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

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)


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



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

Raises:

  • (ArgumentError)

    if session_id is invalid



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



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

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



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.

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:



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



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



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.

Returns:



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

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:



244
245
246
# File 'lib/claude_agent_sdk.rb', line 244

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:



337
338
339
340
# File 'lib/claude_agent_sdk.rb', line 337

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:



263
264
265
266
# File 'lib/claude_agent_sdk.rb', line 263

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:



350
351
352
353
354
# File 'lib/claude_agent_sdk.rb', line 350

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



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)

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



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

Parameters:

Returns:



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

Parameters:

  • session_id (String)

    The session UUID

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

    Working directory to search in

Returns:

  • (Array<String>)

    Subagent IDs



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

Returns:

  • (Array<String>)


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 |message|
      text = extract_user_prompt_text(message)
      notify_observers(observers, :on_user_prompt, text, scheduling: scheduling, wrapper: wrapper) if text
      yielder << message
    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.

Examples:

Inside an inline-mode tool handler

ClaudeAgentSDK.offload { blocking_db_call }


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.

Returns:

  • (Boolean)


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.

Parameters:

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

    Directory to key (nil = cwd)

Returns:

  • (String)

    The project key



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

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)


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

  # 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(options)

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

  # Where user callbacks run (see ClaudeAgentOptions#callback_scheduling)
  # and the middleware wrapped around them (#callback_wrapper).
  callback_scheduling = configured_options.callback_scheduling || :thread
  callback_wrapper = configured_options.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(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,
        exclude_dynamic_sections: ClaudeAgentSDK.extract_exclude_dynamic_sections(configured_options.system_prompt),
        skills: configured_options.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 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',
            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)
        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,
                                                                 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.receive_messages do |data|
        message = MessageParser.parse(data)
        next unless message

        ClaudeAgentSDK.notify_observers(resolved_observers, :on_message, message,
                                        scheduling: callback_scheduling, wrapper: callback_wrapper)
        signal = FiberBoundary.invoke_iteration(block, message, 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

Parameters:

  • session_id (String)

    UUID of the session to rename

  • title (String)

    New session title

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

    Project directory path



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.

Raises:

  • (ArgumentError)

    if session_id is invalid or title is empty



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



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.

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



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.

Raises:

  • (ArgumentError)

    if session_id is invalid or tag is empty after sanitization



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