claude-agent-sdk 1.0.0 → 1.2.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/.yardopts +10 -0
- data/CHANGELOG.md +110 -0
- data/README.md +43 -31
- data/docs/cli-installer.md +26 -4
- data/docs/client.md +40 -11
- data/docs/configuration.md +206 -1
- data/docs/errors.md +32 -2
- data/docs/hooks-and-permissions.md +30 -10
- data/docs/mcp-servers.md +30 -9
- data/docs/observability.md +61 -10
- data/docs/options.md +232 -0
- data/docs/rails.md +263 -18
- data/docs/sessions.md +40 -12
- data/docs/subagents.md +1 -1
- data/docs/types.md +100 -11
- data/lib/claude_agent_sdk/cli_installer.rb +140 -19
- data/lib/claude_agent_sdk/command_builder.rb +84 -27
- data/lib/claude_agent_sdk/fiber_boundary.rb +45 -2
- data/lib/claude_agent_sdk/instrumentation/otel.rb +90 -28
- data/lib/claude_agent_sdk/query.rb +547 -132
- data/lib/claude_agent_sdk/railtie.rb +27 -2
- data/lib/claude_agent_sdk/sdk_mcp_server.rb +78 -26
- data/lib/claude_agent_sdk/session_mutations.rb +112 -92
- data/lib/claude_agent_sdk/session_resume.rb +356 -39
- data/lib/claude_agent_sdk/session_store.rb +31 -2
- data/lib/claude_agent_sdk/sessions.rb +720 -138
- data/lib/claude_agent_sdk/subprocess_cli_transport.rb +252 -29
- data/lib/claude_agent_sdk/testing/session_store_conformance.rb +18 -7
- data/lib/claude_agent_sdk/transcript_mirror_batcher.rb +45 -37
- data/lib/claude_agent_sdk/transport.rb +28 -12
- data/lib/claude_agent_sdk/types/attributes.rb +9 -0
- data/lib/claude_agent_sdk/types/base.rb +85 -15
- data/lib/claude_agent_sdk/types/hooks.rb +73 -0
- data/lib/claude_agent_sdk/types/mcp.rb +37 -1
- data/lib/claude_agent_sdk/types/messages.rb +7 -1
- data/lib/claude_agent_sdk/types/option_values.rb +186 -4
- data/lib/claude_agent_sdk/types/options.rb +104 -17
- data/lib/claude_agent_sdk/types/permissions.rb +18 -9
- data/lib/claude_agent_sdk/version.rb +1 -1
- data/lib/claude_agent_sdk.rb +111 -53
- data/lib/generators/claude_agent_sdk/install/templates/claude_agent_sdk.rb.tt +6 -0
- data/sig/claude_agent_sdk/types/hooks.rbs +6 -3
- data/sig/claude_agent_sdk/types/option_values.rbs +23 -6
- data/sig/claude_agent_sdk/types/options.rbs +20 -7
- data/sig/claude_agent_sdk/types/permissions.rbs +4 -2
- metadata +6 -4
|
@@ -18,8 +18,10 @@ module ClaudeAgentSDK
|
|
|
18
18
|
|
|
19
19
|
# Claude Agent Options for configuring queries
|
|
20
20
|
class ClaudeAgentOptions < Type
|
|
21
|
-
# `env` routinely carries credentials (ANTHROPIC_API_KEY, ...).
|
|
22
|
-
|
|
21
|
+
# `env` routinely carries credentials (ANTHROPIC_API_KEY, ...). So does
|
|
22
|
+
# `settings` (its own `env`, `apiKeyHelper`), as a Hash or a JSON String,
|
|
23
|
+
# and an `extra_args` value can be one.
|
|
24
|
+
inspect_filtered :env, :settings, :extra_args
|
|
23
25
|
|
|
24
26
|
attr_accessor :allowed_tools, :system_prompt, :mcp_servers, :permission_mode,
|
|
25
27
|
:resume, :resume_session_at, :session_id, :max_turns, :disallowed_tools,
|
|
@@ -81,21 +83,11 @@ module ClaudeAgentSDK
|
|
|
81
83
|
self.include_hook_events = false
|
|
82
84
|
self.strict_mcp_config = false
|
|
83
85
|
self.forward_subagent_text = false
|
|
86
|
+
self.verbatim_prompts = false
|
|
84
87
|
|
|
85
88
|
super(merge_with_defaults(attributes || {}))
|
|
86
89
|
|
|
87
|
-
|
|
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?
|
|
90
|
+
fill_nil_defaults
|
|
99
91
|
end
|
|
100
92
|
|
|
101
93
|
def dup_with(**changes)
|
|
@@ -203,6 +195,58 @@ module ClaudeAgentSDK
|
|
|
203
195
|
@forward_subagent_text = coerce_boolean(value)
|
|
204
196
|
end
|
|
205
197
|
|
|
198
|
+
# Deliver every prompt to Claude as written.
|
|
199
|
+
#
|
|
200
|
+
# When true, every user message the SDK sends is marked `client_composed`:
|
|
201
|
+
# a String prompt to {ClaudeAgentSDK.query}, {Client#connect} or
|
|
202
|
+
# {Client#query}, and every message of a streamed (Enumerable) prompt to
|
|
203
|
+
# any of them. Claude Code then delivers the text exactly as given: no
|
|
204
|
+
# `@path` file-mention expansion and no slash-command dispatch. Use it when
|
|
205
|
+
# the prompt is assembled from content your end user did not type (earlier
|
|
206
|
+
# turns, tool output, third-party text), so an `@/absolute/path` inside it
|
|
207
|
+
# cannot make Claude Code read a local file. `tools: []`, `allowed_tools`
|
|
208
|
+
# and `disallowed_tools` do not stop that expansion: it happens before the
|
|
209
|
+
# model runs, without a tool call.
|
|
210
|
+
#
|
|
211
|
+
# While the option is on there is no per-message opt-out: a
|
|
212
|
+
# `client_composed` key on a streamed message Hash (either spelling) is
|
|
213
|
+
# overwritten. For per-turn control, leave the option off and set
|
|
214
|
+
# `client_composed: true` on individual streamed messages. The caller's
|
|
215
|
+
# Hashes are never mutated. A streamed JSONL String is parsed, marked and
|
|
216
|
+
# re-serialized; one that is not a single JSON object raises
|
|
217
|
+
# `ArgumentError` rather than being sent unmarked (on the background
|
|
218
|
+
# streaming paths that ends the stream with a warning, like any other
|
|
219
|
+
# stream error).
|
|
220
|
+
#
|
|
221
|
+
# On current Claude Code versions a turn delivered this way also skips the
|
|
222
|
+
# turn-start attachment pass as a whole: `@server:resource` MCP mentions
|
|
223
|
+
# are not expanded either, and the prompt goes without the context Claude
|
|
224
|
+
# Code normally attaches (nested `CLAUDE.md` and rules files, skill and
|
|
225
|
+
# tool listings, other per-turn reminders). The pass between tool calls is
|
|
226
|
+
# unaffected, so most of that context arrives after the turn's first tool
|
|
227
|
+
# call instead.
|
|
228
|
+
#
|
|
229
|
+
# Requires Claude Code 2.1.248 or later; older versions ignore the field,
|
|
230
|
+
# so prompts are still expanded there, and the SDK warns when it connects
|
|
231
|
+
# to one with this option on. Read once when the session starts. Not a
|
|
232
|
+
# CLI flag. Matches the Python SDK's `verbatim_prompts`.
|
|
233
|
+
#
|
|
234
|
+
# Assigning coerces to a Boolean; {#verbatim_prompts?} is the predicate
|
|
235
|
+
# form.
|
|
236
|
+
#
|
|
237
|
+
# @return [Boolean]
|
|
238
|
+
attr_reader :verbatim_prompts
|
|
239
|
+
|
|
240
|
+
# @return [Boolean] {#verbatim_prompts}, as a strict Boolean.
|
|
241
|
+
def verbatim_prompts?
|
|
242
|
+
!!verbatim_prompts
|
|
243
|
+
end
|
|
244
|
+
|
|
245
|
+
# @see #verbatim_prompts
|
|
246
|
+
def verbatim_prompts=(value)
|
|
247
|
+
@verbatim_prompts = coerce_boolean(value)
|
|
248
|
+
end
|
|
249
|
+
|
|
206
250
|
# Request model-generated progress summaries for subagent (`local_agent`)
|
|
207
251
|
# tasks. `true` *requests* generation: while the CLI has it enabled, a
|
|
208
252
|
# subagent's {TaskProgressMessage#summary} **may** carry a one-line status.
|
|
@@ -288,16 +332,59 @@ module ClaudeAgentSDK
|
|
|
288
332
|
|
|
289
333
|
private
|
|
290
334
|
|
|
335
|
+
# Non-nil defaults for options that need them.
|
|
336
|
+
def fill_nil_defaults
|
|
337
|
+
self.env ||= {}
|
|
338
|
+
self.extra_args ||= {}
|
|
339
|
+
self.mcp_servers ||= {}
|
|
340
|
+
self.add_dirs ||= []
|
|
341
|
+
self.observers ||= []
|
|
342
|
+
self.allowed_tools ||= []
|
|
343
|
+
self.disallowed_tools ||= []
|
|
344
|
+
self.session_store_flush ||= 'batched'
|
|
345
|
+
# 0 is a valid (immediate) timeout, so only fill in the default for nil.
|
|
346
|
+
self.load_timeout_ms = 60_000 if load_timeout_ms.nil?
|
|
347
|
+
self.callback_scheduling = :thread if callback_scheduling.nil?
|
|
348
|
+
end
|
|
349
|
+
|
|
350
|
+
# `mcp_servers` holds typed configs, which filter themselves, next to raw
|
|
351
|
+
# Hash configs carrying the same credentials: those are rendered by
|
|
352
|
+
# Type#inspect_mcp_server_config. Anything but a Hash (the JSON of a
|
|
353
|
+
# config as a String, or the path to one) is replaced outright, like a
|
|
354
|
+
# String `settings`.
|
|
355
|
+
def inspect_attributes
|
|
356
|
+
super.map { |name, value| [name, name == 'mcp_servers' ? inspect_mcp_servers(value) : value] }
|
|
357
|
+
end
|
|
358
|
+
|
|
359
|
+
def inspect_mcp_servers(servers)
|
|
360
|
+
return '[FILTERED]' unless servers.is_a?(Hash)
|
|
361
|
+
|
|
362
|
+
servers.to_h { |name, config| [name, config.is_a?(Hash) ? inspect_mcp_server_config(config) : config] }
|
|
363
|
+
rescue StandardError
|
|
364
|
+
'[FILTERED]'
|
|
365
|
+
end
|
|
366
|
+
|
|
291
367
|
# Strict key validation: unlike other Type subclasses (which silently drop
|
|
292
368
|
# unknown keys for forward-compat with newer CLI output), ClaudeAgentOptions
|
|
293
369
|
# is a developer-facing config object — typos should fail loudly.
|
|
294
370
|
def assign_attribute(name, value)
|
|
295
|
-
setter =
|
|
296
|
-
raise ArgumentError, "unknown ClaudeAgentOptions option: #{name.inspect}" unless
|
|
371
|
+
setter = option_setter(normalize_name(name))
|
|
372
|
+
raise ArgumentError, "unknown ClaudeAgentOptions option: #{name.inspect}" unless setter
|
|
297
373
|
|
|
298
374
|
public_send(setter, value)
|
|
299
375
|
end
|
|
300
376
|
|
|
377
|
+
# The writer of the option with this normalized name, nil when the name
|
|
378
|
+
# is not an option. An option is a declared attribute, or a setter user
|
|
379
|
+
# code defined (on a subclass, a module it includes, or the object);
|
|
380
|
+
# respond_to? alone would also answer for '[]' (#[]=) and '=' (#==).
|
|
381
|
+
def option_setter(normalized)
|
|
382
|
+
setter = :"#{normalized}="
|
|
383
|
+
return unless respond_to?(setter)
|
|
384
|
+
|
|
385
|
+
setter if self.class.attribute?(normalized) || user_defined_method?(setter)
|
|
386
|
+
end
|
|
387
|
+
|
|
301
388
|
# Merge caller-provided attributes with configured defaults.
|
|
302
389
|
# Only keys the caller explicitly passed are treated as overrides;
|
|
303
390
|
# method-signature defaults ([], {}, false) are NOT present unless the caller wrote them.
|
|
@@ -346,7 +433,7 @@ module ClaudeAgentSDK
|
|
|
346
433
|
# reports the typo exactly as the developer wrote it.
|
|
347
434
|
def option_key(name)
|
|
348
435
|
normalized = normalize_name(name)
|
|
349
|
-
|
|
436
|
+
option_setter(normalized) ? normalized.to_sym : name
|
|
350
437
|
end
|
|
351
438
|
end
|
|
352
439
|
end
|
|
@@ -34,20 +34,21 @@ module ClaudeAgentSDK
|
|
|
34
34
|
@rules = value&.map { |rule| rule.is_a?(Hash) ? PermissionRuleValue.new(rule) : rule }
|
|
35
35
|
end
|
|
36
36
|
|
|
37
|
+
# The wire form. The CLI validates the updatedPermissions of a
|
|
38
|
+
# can_use_tool reply as one unit and drops the whole array when a single
|
|
39
|
+
# entry does not fit its schema, so two things it rejects are never
|
|
40
|
+
# written: a rule without content has no ruleContent key (the schema
|
|
41
|
+
# wants a String or no key, not null), and an update that has a type but
|
|
42
|
+
# no destination goes to 'session' — the narrowest one: it lasts for this
|
|
43
|
+
# run and writes no settings file (the default Python PR #1330 proposes).
|
|
37
44
|
def to_h
|
|
38
45
|
result = { type: @type }
|
|
39
|
-
|
|
46
|
+
destination = @destination || ('session' unless @type.nil?)
|
|
47
|
+
result[:destination] = destination if destination
|
|
40
48
|
|
|
41
49
|
case @type
|
|
42
50
|
when 'addRules', 'replaceRules', 'removeRules'
|
|
43
|
-
if @rules
|
|
44
|
-
result[:rules] = @rules.map do |rule|
|
|
45
|
-
{
|
|
46
|
-
toolName: rule.tool_name,
|
|
47
|
-
ruleContent: rule.rule_content
|
|
48
|
-
}
|
|
49
|
-
end
|
|
50
|
-
end
|
|
51
|
+
result[:rules] = @rules.map { |rule| rule_to_h(rule) } if @rules
|
|
51
52
|
result[:behavior] = @behavior if @behavior
|
|
52
53
|
when 'setMode'
|
|
53
54
|
result[:mode] = @mode if @mode
|
|
@@ -57,6 +58,14 @@ module ClaudeAgentSDK
|
|
|
57
58
|
|
|
58
59
|
result
|
|
59
60
|
end
|
|
61
|
+
|
|
62
|
+
private
|
|
63
|
+
|
|
64
|
+
def rule_to_h(rule)
|
|
65
|
+
wire_rule = { toolName: rule.tool_name }
|
|
66
|
+
wire_rule[:ruleContent] = rule.rule_content unless rule.rule_content.nil?
|
|
67
|
+
wire_rule
|
|
68
|
+
end
|
|
60
69
|
end
|
|
61
70
|
|
|
62
71
|
# Tool permission context delivered to `can_use_tool` callbacks.
|
data/lib/claude_agent_sdk.rb
CHANGED
|
@@ -79,7 +79,10 @@ module ClaudeAgentSDK # rubocop:disable Metrics/ModuleLength -- the public entry
|
|
|
79
79
|
end
|
|
80
80
|
|
|
81
81
|
# Internal: normalize hook lists for the control protocol. An absent or
|
|
82
|
-
# disabled event must not become an empty registration in initialize.
|
|
82
|
+
# disabled event must not become an empty registration in initialize. An
|
|
83
|
+
# event written as a String under one key and as a Symbol under another
|
|
84
|
+
# ('PreToolUse' and :PreToolUse) is one event: its matcher lists are
|
|
85
|
+
# joined in the order they were written, never replaced.
|
|
83
86
|
# @api private
|
|
84
87
|
def self.convert_hooks_to_internal_format(hooks)
|
|
85
88
|
return nil unless hooks
|
|
@@ -94,7 +97,7 @@ module ClaudeAgentSDK # rubocop:disable Metrics/ModuleLength -- the public entry
|
|
|
94
97
|
config[:timeout] = matcher.timeout if matcher.timeout
|
|
95
98
|
entries << config
|
|
96
99
|
end
|
|
97
|
-
internal_hooks[event.to_s]
|
|
100
|
+
(internal_hooks[event.to_s] ||= []).concat(entries) unless entries.empty?
|
|
98
101
|
end
|
|
99
102
|
internal_hooks.empty? ? nil : internal_hooks
|
|
100
103
|
end
|
|
@@ -139,7 +142,8 @@ module ClaudeAgentSDK # rubocop:disable Metrics/ModuleLength -- the public entry
|
|
|
139
142
|
eds = system_prompt.exclude_dynamic_sections
|
|
140
143
|
return eds if [true, false].include?(eds)
|
|
141
144
|
elsif system_prompt.is_a?(Hash)
|
|
142
|
-
|
|
145
|
+
# The tag may be a Symbol (type: :preset), as CommandBuilder reads it.
|
|
146
|
+
type = (system_prompt[:type] || system_prompt['type']).to_s
|
|
143
147
|
if type == 'preset'
|
|
144
148
|
eds = system_prompt.fetch(:exclude_dynamic_sections) { system_prompt['exclude_dynamic_sections'] }
|
|
145
149
|
return eds if [true, false].include?(eds)
|
|
@@ -160,7 +164,7 @@ module ClaudeAgentSDK # rubocop:disable Metrics/ModuleLength -- the public entry
|
|
|
160
164
|
snapshot = system_prompt.snapshot
|
|
161
165
|
return snapshot if [true, false].include?(snapshot)
|
|
162
166
|
when Hash
|
|
163
|
-
type = system_prompt[:type] || system_prompt['type']
|
|
167
|
+
type = (system_prompt[:type] || system_prompt['type']).to_s
|
|
164
168
|
if %w[preset custom].include?(type)
|
|
165
169
|
snapshot = system_prompt.fetch(:snapshot) { system_prompt['snapshot'] }
|
|
166
170
|
return snapshot if [true, false].include?(snapshot)
|
|
@@ -173,14 +177,39 @@ module ClaudeAgentSDK # rubocop:disable Metrics/ModuleLength -- the public entry
|
|
|
173
177
|
# Each observer is invoked through FiberBoundary so that user code runs
|
|
174
178
|
# on a plain thread (no Fiber scheduler) even when called from inside
|
|
175
179
|
# the SDK's Async reactor — or in place when scheduling is :inline.
|
|
180
|
+
#
|
|
181
|
+
# What the observer raises, and what its callback_wrapper raises, is
|
|
182
|
+
# contained where it happens: inside the hop, on the execution context
|
|
183
|
+
# the observer runs on. The rescue must not sit around the hop, because
|
|
184
|
+
# the calling fiber WAITS there, and an exception raised into a waiting
|
|
185
|
+
# fiber is not the observer's — a caller's `task.with_timeout` /
|
|
186
|
+
# `Timeout.timeout` deadline is delivered exactly that way. Rescued out
|
|
187
|
+
# here, an expired deadline was swallowed as if the observer had failed,
|
|
188
|
+
# and the turn carried on. So the wrapper is composed inside the rescued
|
|
189
|
+
# body rather than handed to FiberBoundary.invoke, which would run it
|
|
190
|
+
# outside that rescue.
|
|
191
|
+
#
|
|
192
|
+
# Known residue with `scheduling: :inline` (and with no scheduler at all):
|
|
193
|
+
# the observer runs on the calling fiber itself, so a deadline that lands
|
|
194
|
+
# while the observer is suspended is raised inside the observer's own
|
|
195
|
+
# frames, cannot be told from the observer's own timeout, and is still
|
|
196
|
+
# swallowed here.
|
|
176
197
|
# @api private
|
|
177
198
|
def self.notify_observers(observers, method, *args, scheduling: :thread, wrapper: nil)
|
|
178
199
|
observers.each do |obs|
|
|
179
|
-
FiberBoundary.invoke(scheduling: scheduling
|
|
180
|
-
|
|
181
|
-
|
|
182
|
-
|
|
183
|
-
|
|
200
|
+
FiberBoundary.invoke(scheduling: scheduling) do
|
|
201
|
+
invocation = proc { obs.send(method, *args) }
|
|
202
|
+
wrapper ? wrapper.call(invocation) : invocation.call
|
|
203
|
+
rescue StandardError, ScriptError
|
|
204
|
+
# ScriptError too: NotImplementedError < ScriptError (not
|
|
205
|
+
# StandardError), and a stubbed observer must never mask the original
|
|
206
|
+
# error being notified or abort connect/teardown cleanup.
|
|
207
|
+
nil
|
|
208
|
+
end
|
|
209
|
+
rescue ScriptError
|
|
210
|
+
# A ScriptError raised around the body rather than in it (the hop's own
|
|
211
|
+
# plumbing). No deadline is a ScriptError, so nothing a caller injects
|
|
212
|
+
# into the waiting fiber is caught here.
|
|
184
213
|
nil
|
|
185
214
|
end
|
|
186
215
|
end
|
|
@@ -703,7 +732,14 @@ module ClaudeAgentSDK # rubocop:disable Metrics/ModuleLength -- the public entry
|
|
|
703
732
|
raise ArgumentError, 'transport must respond to #connect (see ClaudeAgentSDK::Transport)'
|
|
704
733
|
end
|
|
705
734
|
|
|
706
|
-
|
|
735
|
+
# finished: false tells Async that a waiter handles this task's failure
|
|
736
|
+
# (the .wait at the end re-raises it), as Kernel#Sync does for its own
|
|
737
|
+
# task. Without it a task that fails before anyone waits for it — always
|
|
738
|
+
# the case outside a reactor, where Async runs the whole task before it
|
|
739
|
+
# returns — is also logged as "Task may have ended with unhandled
|
|
740
|
+
# exception", message and backtrace included, although the caller gets
|
|
741
|
+
# the same error raised and may well rescue it.
|
|
742
|
+
Async(finished: false, &FiberBoundary.capture_otel_context do # rubocop:disable Metrics/BlockLength -- the reactor task body of query()
|
|
707
743
|
materialized = nil
|
|
708
744
|
query_handler = nil
|
|
709
745
|
begin
|
|
@@ -749,7 +785,9 @@ module ClaudeAgentSDK # rubocop:disable Metrics/ModuleLength -- the public entry
|
|
|
749
785
|
forward_subagent_text: configured_options.forward_subagent_text?,
|
|
750
786
|
agent_progress_summaries: configured_options.agent_progress_summaries,
|
|
751
787
|
callback_scheduling: callback_scheduling,
|
|
752
|
-
callback_wrapper: callback_wrapper
|
|
788
|
+
callback_wrapper: callback_wrapper,
|
|
789
|
+
verbatim_prompts: configured_options.verbatim_prompts?,
|
|
790
|
+
run_end_ceiling_ms: Query.run_end_ceiling_ms(configured_options.env)
|
|
753
791
|
)
|
|
754
792
|
|
|
755
793
|
# Mirror transcripts to the session_store, if configured. Installed
|
|
@@ -782,7 +820,7 @@ module ClaudeAgentSDK # rubocop:disable Metrics/ModuleLength -- the public entry
|
|
|
782
820
|
parent_tool_use_id: nil,
|
|
783
821
|
session_id: ''
|
|
784
822
|
}
|
|
785
|
-
transport.write("#{
|
|
823
|
+
transport.write("#{Query.serialize_user_message(message, configured_options.verbatim_prompts?)}\n")
|
|
786
824
|
# Background-spawn so messages stream to the user block while stdin
|
|
787
825
|
# close waits (without timeout) for the first result; a synchronous
|
|
788
826
|
# call would defer all delivery until the turn completes (mirrors
|
|
@@ -901,16 +939,13 @@ module ClaudeAgentSDK # rubocop:disable Metrics/ModuleLength -- the public entry
|
|
|
901
939
|
# The Client class always uses streaming mode for bidirectional communication.
|
|
902
940
|
#
|
|
903
941
|
# @example Basic usage
|
|
904
|
-
#
|
|
905
|
-
#
|
|
906
|
-
#
|
|
907
|
-
#
|
|
942
|
+
# # Client.open connects, yields the client and always disconnects,
|
|
943
|
+
# # also when the block raises.
|
|
944
|
+
# ClaudeAgentSDK::Client.open do |client|
|
|
908
945
|
# client.query("What is the capital of France?")
|
|
909
946
|
# client.receive_response do |msg|
|
|
910
947
|
# puts msg if msg.is_a?(ClaudeAgentSDK::AssistantMessage)
|
|
911
948
|
# end
|
|
912
|
-
#
|
|
913
|
-
# client.disconnect
|
|
914
949
|
# end
|
|
915
950
|
#
|
|
916
951
|
# @example With hooks
|
|
@@ -1041,17 +1076,22 @@ module ClaudeAgentSDK # rubocop:disable Metrics/ModuleLength -- the public entry
|
|
|
1041
1076
|
# (The enumerator branch streams in the background and cannot raise
|
|
1042
1077
|
# out of connect.) No on_close follows for pre-handshake failures
|
|
1043
1078
|
# (disconnect gates it on @connected): the session never opened.
|
|
1044
|
-
notify_error(e) if e.is_a?(StandardError) && !@connected
|
|
1045
|
-
# Tear down the partial connect, but never let a cleanup failure (e.g. a
|
|
1046
|
-
# custom transport whose #close raises) mask the original connect error.
|
|
1047
|
-
# Rescue Exception (not StandardError) so reactor cancellation
|
|
1048
|
-
# (Async::Stop < Exception) after materialize_resume set @materialized
|
|
1049
|
-
# still runs disconnect -> @materialized.cleanup, never leaking the temp
|
|
1050
|
-
# CLAUDE_CONFIG_DIR that holds the redacted .credentials.json copy.
|
|
1051
1079
|
begin
|
|
1052
|
-
|
|
1053
|
-
|
|
1054
|
-
|
|
1080
|
+
notify_error(e) if e.is_a?(StandardError) && !@connected
|
|
1081
|
+
ensure
|
|
1082
|
+
# Tear down the partial connect, but never let a cleanup failure (e.g. a
|
|
1083
|
+
# custom transport whose #close raises) mask the original connect error.
|
|
1084
|
+
# Rescue Exception (not StandardError) so reactor cancellation
|
|
1085
|
+
# (Async::Stop < Exception) after materialize_resume set @materialized
|
|
1086
|
+
# still runs disconnect -> @materialized.cleanup, never leaking the temp
|
|
1087
|
+
# CLAUDE_CONFIG_DIR that holds the redacted .credentials.json copy.
|
|
1088
|
+
# In an ensure: an exception raised into this fiber while it waits
|
|
1089
|
+
# for the on_error observer (a caller's deadline) must not skip it.
|
|
1090
|
+
begin
|
|
1091
|
+
disconnect
|
|
1092
|
+
rescue StandardError => cleanup_error
|
|
1093
|
+
warn "Claude SDK: cleanup after failed connect raised: #{cleanup_error.message}"
|
|
1094
|
+
end
|
|
1055
1095
|
end
|
|
1056
1096
|
raise
|
|
1057
1097
|
end
|
|
@@ -1084,7 +1124,7 @@ module ClaudeAgentSDK # rubocop:disable Metrics/ModuleLength -- the public entry
|
|
|
1084
1124
|
parent_tool_use_id: nil,
|
|
1085
1125
|
session_id: session_id
|
|
1086
1126
|
}
|
|
1087
|
-
writeln(
|
|
1127
|
+
writeln(Query.serialize_user_message(message, @verbatim_prompts))
|
|
1088
1128
|
elsif prompt.respond_to?(:each)
|
|
1089
1129
|
# Inline iteration on the caller, Python client.py parity — NOT
|
|
1090
1130
|
# Query#stream_input, whose ensure always ends input after
|
|
@@ -1312,11 +1352,11 @@ module ClaudeAgentSDK # rubocop:disable Metrics/ModuleLength -- the public entry
|
|
|
1312
1352
|
# holds, in every scheduling mode, for a streaming-input enumerator that
|
|
1313
1353
|
# calls disconnect: it is iterated on the reactor inside a task the close
|
|
1314
1354
|
# stops, so it unwinds with Async::Stop once the teardown has completed.
|
|
1355
|
+
#
|
|
1356
|
+
# Interrupted while an observer's on_close runs — the caller's deadline
|
|
1357
|
+
# expires, or its task is stopped — disconnect still completes the
|
|
1358
|
+
# teardown, then lets the interruption propagate.
|
|
1315
1359
|
def disconnect
|
|
1316
|
-
if @connected
|
|
1317
|
-
ClaudeAgentSDK.notify_observers(@resolved_observers || [], :on_close,
|
|
1318
|
-
scheduling: @callback_scheduling, wrapper: @callback_wrapper)
|
|
1319
|
-
end
|
|
1320
1360
|
# Tear down whatever exists — robust to a partial/failed connect, where
|
|
1321
1361
|
# @connected is still false but a transport and/or materialized temp dir
|
|
1322
1362
|
# were already created. #close on the query handler also closes the
|
|
@@ -1333,26 +1373,36 @@ module ClaudeAgentSDK # rubocop:disable Metrics/ModuleLength -- the public entry
|
|
|
1333
1373
|
# and the materialized-dir decision at the bottom needs to ask it.
|
|
1334
1374
|
query_handler = @query_handler
|
|
1335
1375
|
begin
|
|
1336
|
-
|
|
1376
|
+
# Notified inside the begin: an exception raised into this fiber while
|
|
1377
|
+
# it waits for an observer (a caller's deadline) propagates, and must
|
|
1378
|
+
# not skip the teardown below.
|
|
1379
|
+
if @connected
|
|
1380
|
+
ClaudeAgentSDK.notify_observers(@resolved_observers || [], :on_close,
|
|
1381
|
+
scheduling: @callback_scheduling, wrapper: @callback_wrapper)
|
|
1382
|
+
end
|
|
1337
1383
|
ensure
|
|
1338
|
-
@query_handler = nil
|
|
1339
1384
|
begin
|
|
1340
|
-
@
|
|
1385
|
+
@query_handler&.close
|
|
1341
1386
|
ensure
|
|
1342
|
-
@
|
|
1343
|
-
|
|
1344
|
-
|
|
1345
|
-
|
|
1346
|
-
|
|
1347
|
-
|
|
1348
|
-
|
|
1349
|
-
|
|
1350
|
-
|
|
1351
|
-
|
|
1352
|
-
|
|
1353
|
-
|
|
1387
|
+
@query_handler = nil
|
|
1388
|
+
begin
|
|
1389
|
+
@transport&.close
|
|
1390
|
+
ensure
|
|
1391
|
+
@transport = nil
|
|
1392
|
+
@connected = false
|
|
1393
|
+
# Remove the materialized resume temp dir AFTER the subprocess
|
|
1394
|
+
# exited — unless the mirror dropped batches: the store copy is then
|
|
1395
|
+
# incomplete and the temp dir holds the only copy of the dropped
|
|
1396
|
+
# turns, so it is preserved (scrubbed of credentials) with a warning
|
|
1397
|
+
# instead of deleted.
|
|
1398
|
+
if @materialized
|
|
1399
|
+
if query_handler&.mirror_batches_dropped?
|
|
1400
|
+
@materialized.preserve_transcripts
|
|
1401
|
+
else
|
|
1402
|
+
@materialized.cleanup
|
|
1403
|
+
end
|
|
1404
|
+
@materialized = nil
|
|
1354
1405
|
end
|
|
1355
|
-
@materialized = nil
|
|
1356
1406
|
end
|
|
1357
1407
|
end
|
|
1358
1408
|
end
|
|
@@ -1394,6 +1444,10 @@ module ClaudeAgentSDK # rubocop:disable Metrics/ModuleLength -- the public entry
|
|
|
1394
1444
|
exclude_dynamic_sections = ClaudeAgentSDK.extract_exclude_dynamic_sections(configured_options.system_prompt)
|
|
1395
1445
|
system_prompt_snapshot = ClaudeAgentSDK.extract_system_prompt_snapshot(configured_options.system_prompt)
|
|
1396
1446
|
|
|
1447
|
+
# Captured once, so String and streamed prompts in one session are
|
|
1448
|
+
# stamped alike (the Query stamps the streamed ones with this value).
|
|
1449
|
+
@verbatim_prompts = configured_options.verbatim_prompts?
|
|
1450
|
+
|
|
1397
1451
|
# Create Query handler
|
|
1398
1452
|
@query_handler = Query.new(
|
|
1399
1453
|
transport: @transport,
|
|
@@ -1408,7 +1462,9 @@ module ClaudeAgentSDK # rubocop:disable Metrics/ModuleLength -- the public entry
|
|
|
1408
1462
|
forward_subagent_text: configured_options.forward_subagent_text?,
|
|
1409
1463
|
agent_progress_summaries: configured_options.agent_progress_summaries,
|
|
1410
1464
|
callback_scheduling: @callback_scheduling,
|
|
1411
|
-
callback_wrapper: @callback_wrapper
|
|
1465
|
+
callback_wrapper: @callback_wrapper,
|
|
1466
|
+
verbatim_prompts: @verbatim_prompts,
|
|
1467
|
+
run_end_ceiling_ms: Query.run_end_ceiling_ms(configured_options.env)
|
|
1412
1468
|
)
|
|
1413
1469
|
|
|
1414
1470
|
# Mirror transcripts to the session_store, if configured.
|
|
@@ -1450,7 +1506,9 @@ module ClaudeAgentSDK # rubocop:disable Metrics/ModuleLength -- the public entry
|
|
|
1450
1506
|
# key styles — an explicit nil is preserved, mirroring Python's
|
|
1451
1507
|
# `"session_id" not in msg`). Strings pass through verbatim (Ruby
|
|
1452
1508
|
# superset: Streaming.user_message emits pre-serialized JSONL; no
|
|
1453
|
-
# parse-stamp-regenerate, which would block the reactor on huge frames)
|
|
1509
|
+
# parse-stamp-regenerate, which would block the reactor on huge frames),
|
|
1510
|
+
# except that verbatim_prompts must mark them `client_composed`, so with
|
|
1511
|
+
# that option on they are parsed and re-serialized (Query.stamp_user_message).
|
|
1454
1512
|
def stream_query_messages(prompt, session_id)
|
|
1455
1513
|
prompt.each do |msg|
|
|
1456
1514
|
case msg
|
|
@@ -1460,13 +1518,13 @@ module ClaudeAgentSDK # rubocop:disable Metrics/ModuleLength -- the public entry
|
|
|
1460
1518
|
ClaudeAgentSDK.notify_observers(@resolved_observers, :on_user_prompt, text,
|
|
1461
1519
|
scheduling: @callback_scheduling, wrapper: @callback_wrapper)
|
|
1462
1520
|
end
|
|
1463
|
-
writeln(
|
|
1521
|
+
writeln(Query.serialize_user_message(msg, @verbatim_prompts))
|
|
1464
1522
|
when String
|
|
1465
1523
|
if (text = ClaudeAgentSDK.extract_user_prompt_text(msg))
|
|
1466
1524
|
ClaudeAgentSDK.notify_observers(@resolved_observers, :on_user_prompt, text,
|
|
1467
1525
|
scheduling: @callback_scheduling, wrapper: @callback_wrapper)
|
|
1468
1526
|
end
|
|
1469
|
-
writeln(msg)
|
|
1527
|
+
writeln(Query.serialize_user_message(msg, @verbatim_prompts))
|
|
1470
1528
|
else
|
|
1471
1529
|
# No to_s fallback — silently serializing arbitrary objects is the
|
|
1472
1530
|
# exact inspect-garbage bug class this method exists to prevent.
|
|
@@ -22,6 +22,12 @@ ClaudeAgentSDK.configure do |config|
|
|
|
22
22
|
# whatever the process cwd. To run a binary from somewhere else instead:
|
|
23
23
|
# cli_path: '/usr/local/bin/claude',
|
|
24
24
|
|
|
25
|
+
# Multi-user apps: turn off the CLI's auto-memory — kept per project
|
|
26
|
+
# directory, read by every session and written by claude_code-preset
|
|
27
|
+
# sessions, so one user's "remember ..." reaches all others (the value
|
|
28
|
+
# must stay '1'; '0' forces it on). See docs/rails.md ("Per-user isolation").
|
|
29
|
+
# env: { 'CLAUDE_CODE_DISABLE_AUTO_MEMORY' => '1' },
|
|
30
|
+
|
|
25
31
|
# OpenTelemetry tracing (Langfuse, Honeycomb, ...). A factory lambda gives
|
|
26
32
|
# every query/session its own observer — safe under Puma and job threads.
|
|
27
33
|
# observers: [-> { ClaudeAgentSDK::Instrumentation::OTelObserver.new }],
|
|
@@ -47,8 +47,9 @@ module ClaudeAgentSDK
|
|
|
47
47
|
| CwdChangedHookSpecificOutput
|
|
48
48
|
| FileChangedHookSpecificOutput
|
|
49
49
|
|
|
50
|
-
# What a hook callback returns: a Hash (
|
|
51
|
-
# typed output, or nil
|
|
50
|
+
# What a hook callback returns: a Hash (Symbol or String keys, snake_case
|
|
51
|
+
# or camelCase, also inside hook_specific_output), a typed output, or nil
|
|
52
|
+
# (treated as {}).
|
|
52
53
|
type hook_output = Hash[Symbol | String, untyped] | SyncHookJSONOutput | AsyncHookJSONOutput | nil
|
|
53
54
|
|
|
54
55
|
# A hook callback (HookMatcher#hooks entries). tool_use_id is nil for
|
|
@@ -520,7 +521,9 @@ module ClaudeAgentSDK
|
|
|
520
521
|
|
|
521
522
|
attr_accessor reason: String?
|
|
522
523
|
|
|
523
|
-
# A typed output, or the equivalent Hash (camelCase
|
|
524
|
+
# A typed output, or the equivalent Hash (snake_case or camelCase keys).
|
|
525
|
+
# #to_h returns a Hash as written; the SDK puts its keys in wire spelling
|
|
526
|
+
# when it sends the output.
|
|
524
527
|
attr_accessor hook_specific_output: (hook_specific_output | Hash[Symbol | String, untyped])?
|
|
525
528
|
|
|
526
529
|
def to_h: () -> Hash[Symbol, untyped]
|
|
@@ -7,6 +7,16 @@ module ClaudeAgentSDK
|
|
|
7
7
|
# the Hash form (String or camelCase keys, e.g. `klass.new(value.to_h)`)
|
|
8
8
|
# is accepted too. An unknown key raises ArgumentError. Discriminators
|
|
9
9
|
# (`type`) are fixed by the class and read-only.
|
|
10
|
+
#
|
|
11
|
+
# The keyword overload documents the accepted keys and their types; no
|
|
12
|
+
# type checker enforces it. `klass.new(typo: 1)` is also a Symbol-keyed
|
|
13
|
+
# Hash, which the second overload accepts whatever its keys and values, so
|
|
14
|
+
# Steep reports neither a misspelled keyword nor a wrong value type, and
|
|
15
|
+
# the runtime checker behind `rake rbs:test` never reports a constructor
|
|
16
|
+
# mismatch (it only checks the attribute setters the constructor calls).
|
|
17
|
+
# The enforcement is the ArgumentError the constructor raises at runtime
|
|
18
|
+
# for an unknown key. This holds for every constructor in sig/ that has
|
|
19
|
+
# both overloads.
|
|
10
20
|
|
|
11
21
|
# Adaptive thinking: the model decides when and how much to think.
|
|
12
22
|
class ThinkingConfigAdaptive < Type
|
|
@@ -81,15 +91,15 @@ module ClaudeAgentSDK
|
|
|
81
91
|
|
|
82
92
|
# A local plugin directory (ClaudeAgentOptions#plugins entries).
|
|
83
93
|
class SdkPluginConfig < Type
|
|
84
|
-
def initialize: (?path: String?) -> void
|
|
94
|
+
def initialize: (?path: (String | Pathname)?) -> void
|
|
85
95
|
| (Hash[Symbol | String, untyped]? attributes) -> void
|
|
86
96
|
|
|
87
|
-
attr_accessor path: String?
|
|
97
|
+
attr_accessor path: (String | Pathname)?
|
|
88
98
|
|
|
89
99
|
# "local".
|
|
90
100
|
attr_reader type: String
|
|
91
101
|
|
|
92
|
-
def to_h: () -> { type: String, path: String? }
|
|
102
|
+
def to_h: () -> { type: String, path: (String | Pathname)? }
|
|
93
103
|
end
|
|
94
104
|
|
|
95
105
|
class SandboxNetworkConfig < Type
|
|
@@ -138,6 +148,13 @@ module ClaudeAgentSDK
|
|
|
138
148
|
|
|
139
149
|
# Sandbox settings for isolated command execution
|
|
140
150
|
# (ClaudeAgentOptions#sandbox).
|
|
151
|
+
#
|
|
152
|
+
# network / filesystem take the typed config or a Hash. A Hash may spell
|
|
153
|
+
# the fields of the typed class in snake_case or in the CLI's camelCase,
|
|
154
|
+
# with Symbol or String keys; other keys are sent as written. A snake_case
|
|
155
|
+
# key is sent under the CLI's name only when its value has the shape the
|
|
156
|
+
# CLI accepts for it; otherwise it is sent as written, which the CLI
|
|
157
|
+
# ignores.
|
|
141
158
|
class SandboxSettings < Type
|
|
142
159
|
def initialize: (?enabled: bool?, ?fail_if_unavailable: bool?, ?auto_allow_bash_if_sandboxed: bool?, ?excluded_commands: Array[String]?, ?allow_unsandboxed_commands: bool?, ?network: (SandboxNetworkConfig | Hash[Symbol | String, untyped])?, ?filesystem: (SandboxFilesystemConfig | Hash[Symbol | String, untyped])?, ?ignore_violations: Hash[Symbol | String, Array[String]]?, ?enable_weaker_nested_sandbox: bool?, ?enable_weaker_network_isolation: bool?, ?ripgrep: Hash[Symbol | String, untyped]?) -> void
|
|
143
160
|
| (Hash[Symbol | String, untyped]? attributes) -> void
|
|
@@ -181,15 +198,15 @@ module ClaudeAgentSDK
|
|
|
181
198
|
|
|
182
199
|
# A system prompt loaded from a file.
|
|
183
200
|
class SystemPromptFile < Type
|
|
184
|
-
def initialize: (?path: String?) -> void
|
|
201
|
+
def initialize: (?path: (String | Pathname)?) -> void
|
|
185
202
|
| (Hash[Symbol | String, untyped]? attributes) -> void
|
|
186
203
|
|
|
187
|
-
attr_accessor path: String?
|
|
204
|
+
attr_accessor path: (String | Pathname)?
|
|
188
205
|
|
|
189
206
|
# "file".
|
|
190
207
|
attr_reader type: String
|
|
191
208
|
|
|
192
|
-
def to_h: () -> { type: String, path: String? }
|
|
209
|
+
def to_h: () -> { type: String, path: (String | Pathname)? }
|
|
193
210
|
end
|
|
194
211
|
|
|
195
212
|
# The Claude Code preset system prompt, optionally extended.
|