claude-agent-sdk 0.36.0 → 0.37.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- checksums.yaml +4 -4
- data/CHANGELOG.md +17 -0
- data/README.md +2 -2
- data/docs/client.md +26 -1
- data/docs/hooks-and-permissions.md +5 -3
- data/docs/mcp-servers.md +1 -2
- data/docs/sessions.md +81 -2
- data/docs/types.md +106 -4
- data/lib/claude_agent_sdk/cli_installer.rb +30 -3
- data/lib/claude_agent_sdk/command_builder.rb +109 -98
- data/lib/claude_agent_sdk/deprecation.rb +39 -0
- data/lib/claude_agent_sdk/fiber_boundary.rb +2 -0
- data/lib/claude_agent_sdk/instrumentation/otel.rb +15 -7
- data/lib/claude_agent_sdk/message_parser.rb +23 -9
- data/lib/claude_agent_sdk/observer.rb +2 -1
- data/lib/claude_agent_sdk/option_warnings.rb +2 -0
- data/lib/claude_agent_sdk/query.rb +50 -43
- data/lib/claude_agent_sdk/sdk_mcp_server.rb +22 -13
- data/lib/claude_agent_sdk/session_mutations.rb +20 -8
- data/lib/claude_agent_sdk/session_resume.rb +20 -11
- data/lib/claude_agent_sdk/session_store.rb +7 -3
- data/lib/claude_agent_sdk/session_summary.rb +4 -2
- data/lib/claude_agent_sdk/sessions.rb +8 -6
- data/lib/claude_agent_sdk/streaming.rb +1 -1
- data/lib/claude_agent_sdk/subprocess_cli_transport.rb +72 -40
- data/lib/claude_agent_sdk/testing/session_store_conformance.rb +14 -10
- data/lib/claude_agent_sdk/transcript_mirror_batcher.rb +2 -0
- data/lib/claude_agent_sdk/types/attributes.rb +271 -0
- data/lib/claude_agent_sdk/types/base.rb +320 -0
- data/lib/claude_agent_sdk/types/content_blocks.rb +57 -0
- data/lib/claude_agent_sdk/types/hooks.rb +640 -0
- data/lib/claude_agent_sdk/types/mcp.rb +232 -0
- data/lib/claude_agent_sdk/types/messages.rb +614 -0
- data/lib/claude_agent_sdk/types/option_values.rb +302 -0
- data/lib/claude_agent_sdk/types/options.rb +352 -0
- data/lib/claude_agent_sdk/types/permissions.rb +107 -0
- data/lib/claude_agent_sdk/types/sessions.rb +10 -0
- data/lib/claude_agent_sdk/types.rb +13 -2534
- data/lib/claude_agent_sdk/version.rb +1 -1
- data/lib/claude_agent_sdk.rb +51 -17
- metadata +11 -1
|
@@ -0,0 +1,302 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
require_relative 'base'
|
|
4
|
+
|
|
5
|
+
module ClaudeAgentSDK
|
|
6
|
+
# Thinking configuration types
|
|
7
|
+
#
|
|
8
|
+
# `display` controls how thinking content appears in responses. Valid values
|
|
9
|
+
# are `"summarized"` (plaintext summary) and `"omitted"` (empty thinking
|
|
10
|
+
# field, signature only). Defaults are model-dependent: Opus 4.6/Sonnet 4.6
|
|
11
|
+
# default to `"summarized"`; Opus 4.7 and every later model default to
|
|
12
|
+
# `"omitted"`. Pass `display: "summarized"` explicitly on those to get
|
|
13
|
+
# visible thinking text. Not supported with `ThinkingConfigDisabled`.
|
|
14
|
+
THINKING_DISPLAY_VALUES = %w[summarized omitted].freeze
|
|
15
|
+
|
|
16
|
+
# Adaptive thinking: the model decides when and how much to think
|
|
17
|
+
# (sent as `--thinking adaptive`, no budget); control depth with `effort`.
|
|
18
|
+
class ThinkingConfigAdaptive < Type
|
|
19
|
+
include Type::OptionValue
|
|
20
|
+
|
|
21
|
+
strict_attributes
|
|
22
|
+
|
|
23
|
+
attr_reader :type, :display
|
|
24
|
+
|
|
25
|
+
def initialize(attributes = {})
|
|
26
|
+
super
|
|
27
|
+
@type = 'adaptive'
|
|
28
|
+
end
|
|
29
|
+
|
|
30
|
+
def display=(value)
|
|
31
|
+
@display = validate_display(value)
|
|
32
|
+
end
|
|
33
|
+
|
|
34
|
+
private
|
|
35
|
+
|
|
36
|
+
def validate_display(value)
|
|
37
|
+
return nil if value.nil?
|
|
38
|
+
return value if THINKING_DISPLAY_VALUES.include?(value.to_s)
|
|
39
|
+
|
|
40
|
+
raise ArgumentError,
|
|
41
|
+
"invalid thinking display #{value.inspect}; expected one of #{THINKING_DISPLAY_VALUES.inspect}"
|
|
42
|
+
end
|
|
43
|
+
end
|
|
44
|
+
|
|
45
|
+
# Enabled thinking: uses a user-specified budget
|
|
46
|
+
class ThinkingConfigEnabled < Type
|
|
47
|
+
include Type::OptionValue
|
|
48
|
+
|
|
49
|
+
strict_attributes
|
|
50
|
+
|
|
51
|
+
attr_accessor :budget_tokens
|
|
52
|
+
attr_reader :type, :display
|
|
53
|
+
|
|
54
|
+
def initialize(attributes = {})
|
|
55
|
+
super
|
|
56
|
+
@type = 'enabled'
|
|
57
|
+
end
|
|
58
|
+
|
|
59
|
+
def display=(value)
|
|
60
|
+
@display = validate_display(value)
|
|
61
|
+
end
|
|
62
|
+
|
|
63
|
+
private
|
|
64
|
+
|
|
65
|
+
def validate_display(value)
|
|
66
|
+
return nil if value.nil?
|
|
67
|
+
return value if THINKING_DISPLAY_VALUES.include?(value.to_s)
|
|
68
|
+
|
|
69
|
+
raise ArgumentError,
|
|
70
|
+
"invalid thinking display #{value.inspect}; expected one of #{THINKING_DISPLAY_VALUES.inspect}"
|
|
71
|
+
end
|
|
72
|
+
end
|
|
73
|
+
|
|
74
|
+
# Disabled thinking: sets thinking tokens to 0
|
|
75
|
+
class ThinkingConfigDisabled < Type
|
|
76
|
+
include Type::OptionValue
|
|
77
|
+
|
|
78
|
+
strict_attributes
|
|
79
|
+
|
|
80
|
+
attr_reader :type
|
|
81
|
+
|
|
82
|
+
def initialize(attributes = {})
|
|
83
|
+
super
|
|
84
|
+
@type = 'disabled'
|
|
85
|
+
end
|
|
86
|
+
end
|
|
87
|
+
|
|
88
|
+
# Agent definition configuration
|
|
89
|
+
class AgentDefinition < Type
|
|
90
|
+
include Type::OptionValue
|
|
91
|
+
|
|
92
|
+
strict_attributes
|
|
93
|
+
|
|
94
|
+
attr_accessor :description, :prompt, :tools, :disallowed_tools, :model, :skills, :memory, :mcp_servers,
|
|
95
|
+
:initial_prompt, :max_turns, :background, :effort, :permission_mode
|
|
96
|
+
end
|
|
97
|
+
|
|
98
|
+
# SDK Plugin configuration
|
|
99
|
+
class SdkPluginConfig < Type
|
|
100
|
+
include Type::OptionValue
|
|
101
|
+
|
|
102
|
+
strict_attributes
|
|
103
|
+
|
|
104
|
+
attr_accessor :path
|
|
105
|
+
attr_reader :type
|
|
106
|
+
|
|
107
|
+
def initialize(attributes = {})
|
|
108
|
+
super
|
|
109
|
+
@type = 'local'
|
|
110
|
+
end
|
|
111
|
+
|
|
112
|
+
def to_h
|
|
113
|
+
{ type: @type, path: @path }
|
|
114
|
+
end
|
|
115
|
+
end
|
|
116
|
+
|
|
117
|
+
# Sandbox network configuration
|
|
118
|
+
class SandboxNetworkConfig < Type
|
|
119
|
+
include Type::OptionValue
|
|
120
|
+
|
|
121
|
+
strict_attributes
|
|
122
|
+
|
|
123
|
+
attr_accessor :allowed_domains, :denied_domains, :allow_managed_domains_only,
|
|
124
|
+
:allow_unix_sockets, :allow_all_unix_sockets, :allow_local_binding,
|
|
125
|
+
:allow_mach_lookup, :http_proxy_port, :socks_proxy_port
|
|
126
|
+
|
|
127
|
+
def to_h
|
|
128
|
+
result = {}
|
|
129
|
+
result[:allowedDomains] = @allowed_domains if @allowed_domains
|
|
130
|
+
result[:deniedDomains] = @denied_domains if @denied_domains
|
|
131
|
+
result[:allowManagedDomainsOnly] = @allow_managed_domains_only unless @allow_managed_domains_only.nil?
|
|
132
|
+
result[:allowUnixSockets] = @allow_unix_sockets unless @allow_unix_sockets.nil?
|
|
133
|
+
result[:allowAllUnixSockets] = @allow_all_unix_sockets unless @allow_all_unix_sockets.nil?
|
|
134
|
+
result[:allowLocalBinding] = @allow_local_binding unless @allow_local_binding.nil?
|
|
135
|
+
result[:allowMachLookup] = @allow_mach_lookup if @allow_mach_lookup
|
|
136
|
+
result[:httpProxyPort] = @http_proxy_port if @http_proxy_port
|
|
137
|
+
result[:socksProxyPort] = @socks_proxy_port if @socks_proxy_port
|
|
138
|
+
result
|
|
139
|
+
end
|
|
140
|
+
end
|
|
141
|
+
|
|
142
|
+
# Sandbox filesystem configuration
|
|
143
|
+
class SandboxFilesystemConfig < Type
|
|
144
|
+
include Type::OptionValue
|
|
145
|
+
|
|
146
|
+
strict_attributes
|
|
147
|
+
|
|
148
|
+
attr_accessor :allow_write, :deny_write, :deny_read, :allow_read, :allow_managed_read_paths_only
|
|
149
|
+
|
|
150
|
+
def to_h
|
|
151
|
+
result = {}
|
|
152
|
+
result[:allowWrite] = @allow_write if @allow_write
|
|
153
|
+
result[:denyWrite] = @deny_write if @deny_write
|
|
154
|
+
result[:denyRead] = @deny_read if @deny_read
|
|
155
|
+
result[:allowRead] = @allow_read if @allow_read
|
|
156
|
+
result[:allowManagedReadPathsOnly] = @allow_managed_read_paths_only unless @allow_managed_read_paths_only.nil?
|
|
157
|
+
result
|
|
158
|
+
end
|
|
159
|
+
end
|
|
160
|
+
|
|
161
|
+
# Sandbox settings for isolated command execution
|
|
162
|
+
class SandboxSettings < Type
|
|
163
|
+
include Type::OptionValue
|
|
164
|
+
|
|
165
|
+
strict_attributes
|
|
166
|
+
|
|
167
|
+
attr_accessor :enabled, :fail_if_unavailable, :auto_allow_bash_if_sandboxed,
|
|
168
|
+
:excluded_commands, :allow_unsandboxed_commands, :network, :filesystem,
|
|
169
|
+
:ignore_violations, :enable_weaker_nested_sandbox,
|
|
170
|
+
:enable_weaker_network_isolation, :ripgrep
|
|
171
|
+
|
|
172
|
+
def to_h # rubocop:disable Metrics/CyclomaticComplexity, Metrics/PerceivedComplexity -- one optional key per sandbox field
|
|
173
|
+
result = {}
|
|
174
|
+
result[:enabled] = @enabled unless @enabled.nil?
|
|
175
|
+
result[:failIfUnavailable] = @fail_if_unavailable unless @fail_if_unavailable.nil?
|
|
176
|
+
result[:autoAllowBashIfSandboxed] = @auto_allow_bash_if_sandboxed unless @auto_allow_bash_if_sandboxed.nil?
|
|
177
|
+
result[:excludedCommands] = @excluded_commands if @excluded_commands
|
|
178
|
+
result[:allowUnsandboxedCommands] = @allow_unsandboxed_commands unless @allow_unsandboxed_commands.nil?
|
|
179
|
+
result[:network] = @network.is_a?(SandboxNetworkConfig) ? @network.to_h : @network if @network
|
|
180
|
+
result[:filesystem] = @filesystem.is_a?(SandboxFilesystemConfig) ? @filesystem.to_h : @filesystem if @filesystem
|
|
181
|
+
result[:ignoreViolations] = @ignore_violations if @ignore_violations
|
|
182
|
+
result[:enableWeakerNestedSandbox] = @enable_weaker_nested_sandbox unless @enable_weaker_nested_sandbox.nil?
|
|
183
|
+
unless @enable_weaker_network_isolation.nil?
|
|
184
|
+
result[:enableWeakerNetworkIsolation] = @enable_weaker_network_isolation
|
|
185
|
+
end
|
|
186
|
+
result[:ripgrep] = @ripgrep if @ripgrep
|
|
187
|
+
result
|
|
188
|
+
end
|
|
189
|
+
end
|
|
190
|
+
|
|
191
|
+
# API-side task budget in tokens.
|
|
192
|
+
# When set, the model is made aware of its remaining token budget so it can
|
|
193
|
+
# pace tool use and wrap up before the limit.
|
|
194
|
+
class TaskBudget < Type
|
|
195
|
+
include Type::OptionValue
|
|
196
|
+
|
|
197
|
+
strict_attributes
|
|
198
|
+
|
|
199
|
+
attr_accessor :total
|
|
200
|
+
|
|
201
|
+
def to_h
|
|
202
|
+
{ total: @total }
|
|
203
|
+
end
|
|
204
|
+
end
|
|
205
|
+
|
|
206
|
+
# System prompt file configuration — loads system prompt from a file path
|
|
207
|
+
class SystemPromptFile < Type
|
|
208
|
+
include Type::OptionValue
|
|
209
|
+
|
|
210
|
+
strict_attributes
|
|
211
|
+
|
|
212
|
+
attr_accessor :path
|
|
213
|
+
attr_reader :type
|
|
214
|
+
|
|
215
|
+
def initialize(attributes = {})
|
|
216
|
+
super
|
|
217
|
+
@type = 'file'
|
|
218
|
+
end
|
|
219
|
+
|
|
220
|
+
def to_h
|
|
221
|
+
{ type: @type, path: @path }
|
|
222
|
+
end
|
|
223
|
+
end
|
|
224
|
+
|
|
225
|
+
# System prompt preset configuration.
|
|
226
|
+
#
|
|
227
|
+
# +snapshot+ controls whether the session keeps the system prompt it
|
|
228
|
+
# recorded on its first request. When true, every later request (including
|
|
229
|
+
# after resume) sends the recorded prompt, so a changed +append+ has no
|
|
230
|
+
# effect until the session is compacted or a new session starts. When
|
|
231
|
+
# false, the prompt is rebuilt on every request — useful while iterating on
|
|
232
|
+
# +append+ text across calls that resume the same session. When nil
|
|
233
|
+
# (omitted), the CLI treats it as true, except in bare mode (+--bare+),
|
|
234
|
+
# where it acts as false. Sent on the control-protocol +initialize+ request
|
|
235
|
+
# (never as a CLI flag); requires Claude Code CLI 2.1.257 or later, and
|
|
236
|
+
# before 2.1.265 a session with an +append+ prompt recorded it only when
|
|
237
|
+
# +snapshot+ was true. Older CLIs silently ignore it.
|
|
238
|
+
class SystemPromptPreset < Type
|
|
239
|
+
include Type::OptionValue
|
|
240
|
+
|
|
241
|
+
strict_attributes
|
|
242
|
+
|
|
243
|
+
attr_reader :type
|
|
244
|
+
attr_accessor :preset, :append, :exclude_dynamic_sections, :snapshot
|
|
245
|
+
|
|
246
|
+
def initialize(attributes = {})
|
|
247
|
+
super
|
|
248
|
+
@type = 'preset'
|
|
249
|
+
end
|
|
250
|
+
|
|
251
|
+
def to_h
|
|
252
|
+
result = { type: @type, preset: @preset }
|
|
253
|
+
result[:append] = @append if @append
|
|
254
|
+
result[:exclude_dynamic_sections] = @exclude_dynamic_sections unless @exclude_dynamic_sections.nil?
|
|
255
|
+
result[:snapshot] = @snapshot unless @snapshot.nil?
|
|
256
|
+
result
|
|
257
|
+
end
|
|
258
|
+
end
|
|
259
|
+
|
|
260
|
+
# Custom system prompt configuration — the object form of passing a String
|
|
261
|
+
# as +system_prompt+. Reaches the CLI the same way a String does
|
|
262
|
+
# (+--system-prompt <prompt>+); the object form exists so +snapshot+ can be
|
|
263
|
+
# set alongside it (see SystemPromptPreset#snapshot for its semantics).
|
|
264
|
+
class SystemPromptCustom < Type
|
|
265
|
+
include Type::OptionValue
|
|
266
|
+
|
|
267
|
+
strict_attributes
|
|
268
|
+
|
|
269
|
+
attr_reader :type
|
|
270
|
+
attr_accessor :prompt, :snapshot
|
|
271
|
+
|
|
272
|
+
def initialize(attributes = {})
|
|
273
|
+
super
|
|
274
|
+
@type = 'custom'
|
|
275
|
+
end
|
|
276
|
+
|
|
277
|
+
def to_h
|
|
278
|
+
result = { type: @type, prompt: @prompt }
|
|
279
|
+
result[:snapshot] = @snapshot unless @snapshot.nil?
|
|
280
|
+
result
|
|
281
|
+
end
|
|
282
|
+
end
|
|
283
|
+
|
|
284
|
+
# Tools preset configuration
|
|
285
|
+
class ToolsPreset < Type
|
|
286
|
+
include Type::OptionValue
|
|
287
|
+
|
|
288
|
+
strict_attributes
|
|
289
|
+
|
|
290
|
+
attr_reader :type
|
|
291
|
+
attr_accessor :preset
|
|
292
|
+
|
|
293
|
+
def initialize(attributes = {})
|
|
294
|
+
super
|
|
295
|
+
@type = 'preset'
|
|
296
|
+
end
|
|
297
|
+
|
|
298
|
+
def to_h
|
|
299
|
+
{ type: @type, preset: @preset }
|
|
300
|
+
end
|
|
301
|
+
end
|
|
302
|
+
end
|
|
@@ -0,0 +1,352 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
require_relative 'base'
|
|
4
|
+
|
|
5
|
+
module ClaudeAgentSDK
|
|
6
|
+
# Type constants for setting sources
|
|
7
|
+
SETTING_SOURCES = %w[user project local].freeze
|
|
8
|
+
|
|
9
|
+
# Effort levels for `ClaudeAgentOptions#effort`. The CLI (Claude Code 2.1.111+)
|
|
10
|
+
# accepts these values; the set of *supported* levels is model-dependent
|
|
11
|
+
# (e.g. `xhigh` arrived with Opus 4.7 and falls back to `high` on
|
|
12
|
+
# Opus 4.6 / Sonnet 4.6). An Integer is also accepted and forwarded verbatim.
|
|
13
|
+
EFFORT_LEVELS = %w[low medium high xhigh max].freeze
|
|
14
|
+
|
|
15
|
+
# Type constants for SDK beta features
|
|
16
|
+
# Available beta features that can be enabled via the betas option
|
|
17
|
+
SDK_BETAS = %w[context-1m-2025-08-07].freeze
|
|
18
|
+
|
|
19
|
+
# Claude Agent Options for configuring queries
|
|
20
|
+
class ClaudeAgentOptions < Type
|
|
21
|
+
# `env` routinely carries credentials (ANTHROPIC_API_KEY, ...).
|
|
22
|
+
inspect_filtered :env
|
|
23
|
+
|
|
24
|
+
attr_accessor :allowed_tools, :system_prompt, :mcp_servers, :permission_mode,
|
|
25
|
+
:resume, :resume_session_at, :session_id, :max_turns, :disallowed_tools,
|
|
26
|
+
:model, :permission_prompt_tool_name, :cwd, :cli_path, :settings,
|
|
27
|
+
:add_dirs, :env, :extra_args, :max_buffer_size, :stderr,
|
|
28
|
+
:can_use_tool, :hooks, :user,
|
|
29
|
+
:agents, :setting_sources, :skills,
|
|
30
|
+
:output_format, :max_budget_usd, :max_thinking_tokens,
|
|
31
|
+
:fallback_model, :advisor_model, :plugins, :debug_stderr,
|
|
32
|
+
:betas, :tools, :sandbox,
|
|
33
|
+
:thinking, :effort, :observers, :task_budget,
|
|
34
|
+
:session_store, :session_store_flush, :load_timeout_ms
|
|
35
|
+
attr_reader :bare, :fork_session, :enable_file_checkpointing,
|
|
36
|
+
:include_partial_messages, :continue_conversation,
|
|
37
|
+
:include_hook_events, :strict_mcp_config,
|
|
38
|
+
:callback_scheduling, :callback_wrapper
|
|
39
|
+
|
|
40
|
+
# With {#resume_session_at}: the UUID of the user prompt whose turn this
|
|
41
|
+
# truncating resume intends to discard.
|
|
42
|
+
#
|
|
43
|
+
# When set, the CLI validates at load time that every transcript entry
|
|
44
|
+
# after the `resume_session_at` point is attributable to that turn, and
|
|
45
|
+
# refuses the resume otherwise — e.g. when the discarded range contains a
|
|
46
|
+
# queued user message or task notification the session absorbed mid-turn
|
|
47
|
+
# that the caller had not yet observed. Leave unset to keep the
|
|
48
|
+
# unvalidated truncation behavior.
|
|
49
|
+
#
|
|
50
|
+
# **Choosing the fork point.** Set `resume_session_at` to the *last*
|
|
51
|
+
# transcript entry of the turn you are keeping — whatever its type — and
|
|
52
|
+
# `resume_drops_turn` to the prompt UUID of the turn immediately after it
|
|
53
|
+
# (e.g. the next `SessionMessage` of `type == "user"` from
|
|
54
|
+
# {ClaudeAgentSDK.get_session_messages}, or the `uuid` you supplied on a
|
|
55
|
+
# streamed user message). Note that with structured output
|
|
56
|
+
# ({#output_format}) or end-turn MCP tools a kept turn ends on entries
|
|
57
|
+
# *after* its last assistant message, so forking at the assistant UUID is
|
|
58
|
+
# refused by design.
|
|
59
|
+
#
|
|
60
|
+
# **On refusal.** The CLI reports an `error_during_execution` result whose
|
|
61
|
+
# message starts with `Resume rejected by --resume-drops-turn:` — match on
|
|
62
|
+
# that text. Treat it as deterministic: clear the pending fork target and
|
|
63
|
+
# resume plainly rather than retrying the same request.
|
|
64
|
+
#
|
|
65
|
+
# Forwarded whenever it is not `nil`. An empty string reaches the CLI and
|
|
66
|
+
# is rejected there as a malformed declaration rather than being dropped by
|
|
67
|
+
# the SDK, which would silently disarm the guard you believe is armed. The
|
|
68
|
+
# SDK does not validate the option combination (`resume` /
|
|
69
|
+
# `resume_session_at`); like the TypeScript and Python SDKs that is the
|
|
70
|
+
# CLI's call.
|
|
71
|
+
#
|
|
72
|
+
# @return [String, nil]
|
|
73
|
+
# @see #resume_session_at
|
|
74
|
+
attr_accessor :resume_drops_turn
|
|
75
|
+
|
|
76
|
+
def initialize(attributes = {})
|
|
77
|
+
self.fork_session = false
|
|
78
|
+
self.continue_conversation = false
|
|
79
|
+
self.include_partial_messages = false
|
|
80
|
+
self.enable_file_checkpointing = false
|
|
81
|
+
self.include_hook_events = false
|
|
82
|
+
self.strict_mcp_config = false
|
|
83
|
+
self.forward_subagent_text = false
|
|
84
|
+
|
|
85
|
+
super(merge_with_defaults(attributes || {}))
|
|
86
|
+
|
|
87
|
+
# Non-nil defaults for options that need them.
|
|
88
|
+
self.env ||= {}
|
|
89
|
+
self.extra_args ||= {}
|
|
90
|
+
self.mcp_servers ||= {}
|
|
91
|
+
self.add_dirs ||= []
|
|
92
|
+
self.observers ||= []
|
|
93
|
+
self.allowed_tools ||= []
|
|
94
|
+
self.disallowed_tools ||= []
|
|
95
|
+
self.session_store_flush ||= 'batched'
|
|
96
|
+
# 0 is a valid (immediate) timeout, so only fill in the default for nil.
|
|
97
|
+
self.load_timeout_ms = 60_000 if load_timeout_ms.nil?
|
|
98
|
+
self.callback_scheduling = :thread if callback_scheduling.nil?
|
|
99
|
+
end
|
|
100
|
+
|
|
101
|
+
def dup_with(**changes)
|
|
102
|
+
new_options = dup
|
|
103
|
+
# A shallow #dup shares nested containers and typed option values, so
|
|
104
|
+
# mutating a derived copy (e.g. `variant.allowed_tools << 'Bash'` or
|
|
105
|
+
# `variant.sandbox.enabled = false`) would bleed into the base and every
|
|
106
|
+
# sibling — including the security-relevant allow/deny lists and sandbox
|
|
107
|
+
# rules. Deep-dup Hash/Array containers and option value types (Type#
|
|
108
|
+
# dup_for_options, including those nested inside containers such as
|
|
109
|
+
# agents[:x]); every other leaf (procs, SDK MCP server instances, store
|
|
110
|
+
# adapters) keeps its identity.
|
|
111
|
+
new_options.instance_variables.each do |ivar|
|
|
112
|
+
new_options.instance_variable_set(ivar, Type.deep_dup_for_options(new_options.instance_variable_get(ivar)))
|
|
113
|
+
end
|
|
114
|
+
changes.each { |key, value| new_options[key] = value }
|
|
115
|
+
new_options
|
|
116
|
+
end
|
|
117
|
+
|
|
118
|
+
def bare?
|
|
119
|
+
!!bare
|
|
120
|
+
end
|
|
121
|
+
|
|
122
|
+
def bare=(value)
|
|
123
|
+
@bare = coerce_boolean(value)
|
|
124
|
+
end
|
|
125
|
+
|
|
126
|
+
def fork_session?
|
|
127
|
+
!!fork_session
|
|
128
|
+
end
|
|
129
|
+
|
|
130
|
+
def fork_session=(value)
|
|
131
|
+
@fork_session = coerce_boolean(value)
|
|
132
|
+
end
|
|
133
|
+
|
|
134
|
+
def enable_file_checkpointing?
|
|
135
|
+
!!enable_file_checkpointing
|
|
136
|
+
end
|
|
137
|
+
|
|
138
|
+
def enable_file_checkpointing=(value)
|
|
139
|
+
@enable_file_checkpointing = coerce_boolean(value)
|
|
140
|
+
end
|
|
141
|
+
|
|
142
|
+
def include_partial_messages?
|
|
143
|
+
!!include_partial_messages
|
|
144
|
+
end
|
|
145
|
+
|
|
146
|
+
def include_partial_messages=(value)
|
|
147
|
+
@include_partial_messages = coerce_boolean(value)
|
|
148
|
+
end
|
|
149
|
+
|
|
150
|
+
def continue_conversation?
|
|
151
|
+
!!continue_conversation
|
|
152
|
+
end
|
|
153
|
+
|
|
154
|
+
def continue_conversation=(value)
|
|
155
|
+
@continue_conversation = coerce_boolean(value)
|
|
156
|
+
end
|
|
157
|
+
|
|
158
|
+
def include_hook_events?
|
|
159
|
+
!!include_hook_events
|
|
160
|
+
end
|
|
161
|
+
|
|
162
|
+
def include_hook_events=(value)
|
|
163
|
+
@include_hook_events = coerce_boolean(value)
|
|
164
|
+
end
|
|
165
|
+
|
|
166
|
+
def strict_mcp_config?
|
|
167
|
+
!!strict_mcp_config
|
|
168
|
+
end
|
|
169
|
+
|
|
170
|
+
def strict_mcp_config=(value)
|
|
171
|
+
@strict_mcp_config = coerce_boolean(value)
|
|
172
|
+
end
|
|
173
|
+
|
|
174
|
+
# Forward subagent text and thinking blocks as messages in the stream.
|
|
175
|
+
# Defaults to `false`.
|
|
176
|
+
#
|
|
177
|
+
# By default only `tool_use` / `tool_result` blocks from subagents
|
|
178
|
+
# (spawned via the Agent tool) are emitted, as {AssistantMessage} /
|
|
179
|
+
# {UserMessage} objects whose `parent_tool_use_id` is the spawning Agent
|
|
180
|
+
# `tool_use` id — enough for a progress heartbeat. When true, the
|
|
181
|
+
# subagent's text and thinking blocks are forwarded the same way, so
|
|
182
|
+
# consumers can render the full nested transcript. Matches the TypeScript
|
|
183
|
+
# SDK's `forwardSubagentText`.
|
|
184
|
+
#
|
|
185
|
+
# Sent as the `forwardSubagentText` initialize capability rather than a CLI
|
|
186
|
+
# flag, and only when enabled, so an older CLI never sees an unknown key on
|
|
187
|
+
# the common path. Both {ClaudeAgentSDK.query} and {Client} run the control
|
|
188
|
+
# protocol, so the option applies to either entry point.
|
|
189
|
+
#
|
|
190
|
+
# Assigning coerces to a Boolean; {#forward_subagent_text?} is the
|
|
191
|
+
# predicate form.
|
|
192
|
+
#
|
|
193
|
+
# @return [Boolean]
|
|
194
|
+
attr_reader :forward_subagent_text
|
|
195
|
+
|
|
196
|
+
# @return [Boolean] {#forward_subagent_text}, as a strict Boolean.
|
|
197
|
+
def forward_subagent_text?
|
|
198
|
+
!!forward_subagent_text
|
|
199
|
+
end
|
|
200
|
+
|
|
201
|
+
# @see #forward_subagent_text
|
|
202
|
+
def forward_subagent_text=(value)
|
|
203
|
+
@forward_subagent_text = coerce_boolean(value)
|
|
204
|
+
end
|
|
205
|
+
|
|
206
|
+
# Request model-generated progress summaries for subagent (`local_agent`)
|
|
207
|
+
# tasks. `true` *requests* generation: while the CLI has it enabled, a
|
|
208
|
+
# subagent's {TaskProgressMessage#summary} **may** carry a one-line status.
|
|
209
|
+
# `summary` stays optional on the wire even then — not every progress
|
|
210
|
+
# frame has one — so read it nil-safely. `false` / `nil` do not enable
|
|
211
|
+
# generation; they do not promise that `summary` is absent (a process that
|
|
212
|
+
# already enabled summaries keeps them, and a backgrounded `mcp_task`
|
|
213
|
+
# reports its own status there regardless of this option). Matches the
|
|
214
|
+
# CLI's `agentProgressSummaries` initialize field.
|
|
215
|
+
#
|
|
216
|
+
# Defaults to `nil` (unset): the key is omitted from the `initialize`
|
|
217
|
+
# control request. `true` and `false` are forwarded verbatim. This is an
|
|
218
|
+
# enable switch, not a live toggle: CLI 2.1.278 only acts on a truthy
|
|
219
|
+
# value, so `false` is schema-valid but equivalent to leaving the option
|
|
220
|
+
# unset — it does not switch summaries off on a process that already
|
|
221
|
+
# enabled them. Both {ClaudeAgentSDK.query} and {Client} run the control
|
|
222
|
+
# protocol, so the option applies to either entry point.
|
|
223
|
+
#
|
|
224
|
+
# Assigning coerces to a Boolean and keeps `nil` as `nil`.
|
|
225
|
+
#
|
|
226
|
+
# @return [Boolean, nil]
|
|
227
|
+
attr_reader :agent_progress_summaries
|
|
228
|
+
|
|
229
|
+
# @see #agent_progress_summaries
|
|
230
|
+
def agent_progress_summaries=(value)
|
|
231
|
+
@agent_progress_summaries = coerce_boolean(value)
|
|
232
|
+
end
|
|
233
|
+
|
|
234
|
+
CALLBACK_SCHEDULING_MODES = %i[thread inline].freeze
|
|
235
|
+
|
|
236
|
+
# Where user callbacks (hooks, can_use_tool, SDK MCP handlers, message
|
|
237
|
+
# blocks, observers) run when the SDK is hosted inside an Async reactor:
|
|
238
|
+
# :thread (default) — each callback hops to a plain thread, so
|
|
239
|
+
# thread-keyed libraries (ActiveRecord, pg, ...) behave as usual.
|
|
240
|
+
# :inline — callbacks run in place on the reactor fiber. Only for
|
|
241
|
+
# hosts that are fiber-isolated end to end (e.g. solid_queue fiber
|
|
242
|
+
# workers with IsolatedExecutionState.isolation_level = :fiber).
|
|
243
|
+
# Scheduler-opaque blocking (CPU-bound work, GVL-holding C
|
|
244
|
+
# extensions) then stalls the whole reactor — wrap GVL-releasing
|
|
245
|
+
# blocking and Ruby CPU work in ClaudeAgentSDK.offload { }; work
|
|
246
|
+
# that holds the GVL throughout needs a subprocess.
|
|
247
|
+
# Named after the mechanism, not a safety claim: whether inline is safe
|
|
248
|
+
# depends on the host satisfying the fiber-isolation precondition.
|
|
249
|
+
def callback_scheduling=(value)
|
|
250
|
+
if value.nil?
|
|
251
|
+
@callback_scheduling = nil
|
|
252
|
+
return
|
|
253
|
+
end
|
|
254
|
+
|
|
255
|
+
mode = value.respond_to?(:to_sym) ? value.to_sym : value
|
|
256
|
+
unless CALLBACK_SCHEDULING_MODES.include?(mode)
|
|
257
|
+
raise ArgumentError,
|
|
258
|
+
"callback_scheduling must be one of #{CALLBACK_SCHEDULING_MODES.map(&:inspect).join(', ')} " \
|
|
259
|
+
"(got #{value.inspect})"
|
|
260
|
+
end
|
|
261
|
+
|
|
262
|
+
@callback_scheduling = mode
|
|
263
|
+
end
|
|
264
|
+
|
|
265
|
+
# Middleware wrapped around EVERY user-callback dispatch (message
|
|
266
|
+
# blocks, observers, hooks, permission callbacks, SDK MCP handlers).
|
|
267
|
+
# A callable receiving a zero-arg invocation; it MUST call it and
|
|
268
|
+
# return its value:
|
|
269
|
+
#
|
|
270
|
+
# callback_wrapper: ->(invocation) { MyApm.trace('agent.callback') { invocation.call } }
|
|
271
|
+
#
|
|
272
|
+
# The wrapper runs on the same execution context as the callback —
|
|
273
|
+
# inside the worker thread in :thread mode, in place on the reactor
|
|
274
|
+
# fiber in :inline mode. Exceptions propagate through it unchanged; it
|
|
275
|
+
# must not swallow them. Default nil (no wrapping).
|
|
276
|
+
#
|
|
277
|
+
# Rails apps: use ClaudeAgentSDK::Railtie.callback_wrapper, which runs
|
|
278
|
+
# callbacks in the Rails executor (AR connections check back in when the
|
|
279
|
+
# callback ends). A bare `Rails.application.executor.wrap` deadlocks
|
|
280
|
+
# under development code reloading in :thread mode.
|
|
281
|
+
def callback_wrapper=(value)
|
|
282
|
+
unless value.nil? || value.respond_to?(:call)
|
|
283
|
+
raise ArgumentError, "callback_wrapper must be a callable or nil (got #{value.inspect})"
|
|
284
|
+
end
|
|
285
|
+
|
|
286
|
+
@callback_wrapper = value
|
|
287
|
+
end
|
|
288
|
+
|
|
289
|
+
private
|
|
290
|
+
|
|
291
|
+
# Strict key validation: unlike other Type subclasses (which silently drop
|
|
292
|
+
# unknown keys for forward-compat with newer CLI output), ClaudeAgentOptions
|
|
293
|
+
# is a developer-facing config object — typos should fail loudly.
|
|
294
|
+
def assign_attribute(name, value)
|
|
295
|
+
setter = :"#{normalize_name(name)}="
|
|
296
|
+
raise ArgumentError, "unknown ClaudeAgentOptions option: #{name.inspect}" unless respond_to?(setter)
|
|
297
|
+
|
|
298
|
+
public_send(setter, value)
|
|
299
|
+
end
|
|
300
|
+
|
|
301
|
+
# Merge caller-provided attributes with configured defaults.
|
|
302
|
+
# Only keys the caller explicitly passed are treated as overrides;
|
|
303
|
+
# method-signature defaults ([], {}, false) are NOT present unless the caller wrote them.
|
|
304
|
+
#
|
|
305
|
+
# Both sides are keyed by the option they name, not by their literal
|
|
306
|
+
# spelling: Type accepts symbol/string and snake_case/camelCase names, so a
|
|
307
|
+
# caller's `'permissionMode' => nil` must still inherit a configured
|
|
308
|
+
# `permission_mode:` (and a Hash must still merge into it) rather than
|
|
309
|
+
# riding along as a second entry that overwrites the default on assignment.
|
|
310
|
+
def merge_with_defaults(attributes)
|
|
311
|
+
return attributes unless defined?(ClaudeAgentSDK) && ClaudeAgentSDK.respond_to?(:default_options)
|
|
312
|
+
|
|
313
|
+
defaults = ClaudeAgentSDK.default_options
|
|
314
|
+
return attributes unless defaults.any?
|
|
315
|
+
|
|
316
|
+
# Start from configured defaults. Container values, typed option
|
|
317
|
+
# values (SandboxSettings, SystemPromptPreset, AgentDefinition, ...)
|
|
318
|
+
# and mutable Strings are recursively copied (Type.deep_dup_for_options)
|
|
319
|
+
# so per-instance mutation (options.allowed_tools << 'Bash',
|
|
320
|
+
# options.sandbox.enabled = false) can never corrupt the global
|
|
321
|
+
# defaults or reach another session; other leaves (frozen Strings,
|
|
322
|
+
# Procs, SdkMcpServer instances, store adapters) intentionally keep
|
|
323
|
+
# identity. The stored defaults are a frozen snapshot
|
|
324
|
+
# (Configuration#default_options=), and the copy is what makes each
|
|
325
|
+
# session's containers and values mutable again — its Strings stay the
|
|
326
|
+
# snapshot's frozen ones, so `options.model << 'x'` fails loudly rather
|
|
327
|
+
# than reaching other sessions; reassign instead.
|
|
328
|
+
result = {}
|
|
329
|
+
defaults.each { |key, value| result[option_key(key)] = Type.deep_dup_for_options(value) }
|
|
330
|
+
attributes.each do |key, value|
|
|
331
|
+
key = option_key(key)
|
|
332
|
+
default_val = result[key]
|
|
333
|
+
result[key] = if value.nil?
|
|
334
|
+
default_val # nil means "no preference" — keep the configured default
|
|
335
|
+
elsif default_val.is_a?(Hash) && value.is_a?(Hash)
|
|
336
|
+
default_val.merge(value)
|
|
337
|
+
else
|
|
338
|
+
value
|
|
339
|
+
end
|
|
340
|
+
end
|
|
341
|
+
result
|
|
342
|
+
end
|
|
343
|
+
|
|
344
|
+
# The canonical Symbol for a known option, whatever its spelling. An
|
|
345
|
+
# unknown name is returned untouched so assign_attribute's strict check
|
|
346
|
+
# reports the typo exactly as the developer wrote it.
|
|
347
|
+
def option_key(name)
|
|
348
|
+
normalized = normalize_name(name)
|
|
349
|
+
respond_to?(:"#{normalized}=") ? normalized.to_sym : name
|
|
350
|
+
end
|
|
351
|
+
end
|
|
352
|
+
end
|