claude-agent-sdk 0.33.0 → 0.34.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 +81 -0
- data/README.md +1 -1
- data/docs/cli-installer.md +17 -9
- data/docs/client.md +1 -1
- data/docs/configuration.md +5 -5
- data/docs/errors.md +4 -3
- data/docs/mcp-servers.md +22 -0
- data/docs/observability.md +6 -0
- data/docs/rails.md +2 -0
- data/docs/sessions.md +66 -15
- data/lib/claude_agent_sdk/cli_installer.rb +34 -18
- data/lib/claude_agent_sdk/command_builder.rb +11 -3
- data/lib/claude_agent_sdk/configuration.rb +54 -2
- data/lib/claude_agent_sdk/errors.rb +11 -3
- data/lib/claude_agent_sdk/fiber_boundary.rb +42 -3
- data/lib/claude_agent_sdk/instrumentation/otel.rb +21 -2
- data/lib/claude_agent_sdk/query.rb +146 -62
- data/lib/claude_agent_sdk/sdk_mcp_server.rb +56 -4
- data/lib/claude_agent_sdk/session_mutations.rb +41 -16
- data/lib/claude_agent_sdk/session_resume.rb +112 -39
- data/lib/claude_agent_sdk/session_store.rb +19 -3
- data/lib/claude_agent_sdk/session_summary.rb +5 -5
- data/lib/claude_agent_sdk/sessions.rb +123 -55
- data/lib/claude_agent_sdk/streaming.rb +0 -8
- data/lib/claude_agent_sdk/subprocess_cli_transport.rb +319 -54
- data/lib/claude_agent_sdk/testing/session_store_conformance.rb +13 -3
- data/lib/claude_agent_sdk/transcript_mirror_batcher.rb +77 -18
- data/lib/claude_agent_sdk/types.rb +130 -36
- data/lib/claude_agent_sdk/version.rb +1 -1
- data/lib/claude_agent_sdk.rb +49 -44
- metadata +16 -10
|
@@ -18,6 +18,12 @@ module ClaudeAgentSDK
|
|
|
18
18
|
# background flush). This keeps adapter latency off the hot path during
|
|
19
19
|
# model streaming.
|
|
20
20
|
#
|
|
21
|
+
# At most ONE background drain task is live at a time. Frames arriving
|
|
22
|
+
# while it is busy stay in the pending buffer, and the drainer picks them up
|
|
23
|
+
# as one coalesced append when its current one completes — so a store slower
|
|
24
|
+
# than the frame rate costs buffered entries, never a task (plus detached
|
|
25
|
+
# batch) per frame, and #enqueue never suspends the read loop.
|
|
26
|
+
#
|
|
21
27
|
# Adapter failures are retried (MIRROR_APPEND_MAX_ATTEMPTS total) with short
|
|
22
28
|
# backoff; timeouts are not retried since the in-flight call may still land.
|
|
23
29
|
# Failures never raise — the local-disk transcript is already durable, so the
|
|
@@ -25,17 +31,19 @@ module ClaudeAgentSDK
|
|
|
25
31
|
# (which surfaces them as a MirrorErrorMessage). Adapters should dedupe by
|
|
26
32
|
# entry["uuid"] when present, since a retried batch may overlap a prior write.
|
|
27
33
|
#
|
|
28
|
-
# The semaphore serializes appends,
|
|
29
|
-
#
|
|
30
|
-
#
|
|
31
|
-
#
|
|
32
|
-
#
|
|
33
|
-
#
|
|
34
|
-
#
|
|
35
|
-
#
|
|
36
|
-
#
|
|
37
|
-
#
|
|
38
|
-
#
|
|
34
|
+
# The semaphore serializes appends (FIFO, and every drain detaches its batch
|
|
35
|
+
# immediately before queueing on it, so append order matches enqueue order),
|
|
36
|
+
# but a #send that exceeds send_timeout is abandoned (its worker thread keeps
|
|
37
|
+
# running) and the next drain proceeds, so two #append calls for the SAME
|
|
38
|
+
# key can briefly overlap. SessionStore#append must be thread-safe per key
|
|
39
|
+
# (see that method's contract). For adapters declaring
|
|
40
|
+
# `callback_scheduling -> :inline` the timed-out call is instead cancelled
|
|
41
|
+
# cooperatively (interrupted at its next suspension point, ensure runs) —
|
|
42
|
+
# but only on the adapter's own fiber; work the adapter offloaded may still
|
|
43
|
+
# land, so timeouts are not retried in either mode and the cancelled append
|
|
44
|
+
# may remain permanently HALF-applied in the store. The drop is surfaced
|
|
45
|
+
# (MirrorErrorMessage, batches_dropped?); the local transcript remains the
|
|
46
|
+
# source of truth.
|
|
39
47
|
class TranscriptMirrorBatcher
|
|
40
48
|
# Eager-flush thresholds (exposed for tests).
|
|
41
49
|
MAX_PENDING_ENTRIES = 500
|
|
@@ -72,6 +80,12 @@ module ClaudeAgentSDK
|
|
|
72
80
|
@pending = []
|
|
73
81
|
@pending_entries = 0
|
|
74
82
|
@pending_bytes = 0
|
|
83
|
+
# True while the background drain task is live; #enqueue spawns another
|
|
84
|
+
# only once it has exited (see #schedule_background_drain).
|
|
85
|
+
@background_drain_live = false
|
|
86
|
+
# Set when #close starts; from then on anything still in @pending counts
|
|
87
|
+
# as dropped (see #batches_dropped?).
|
|
88
|
+
@closed = false
|
|
75
89
|
# Batches that exhausted retries (or could not be keyed) and never
|
|
76
90
|
# reached the store. Written only under @lock; read cross-thread by
|
|
77
91
|
# #batches_dropped? (a plain Integer read is safe under the GVL).
|
|
@@ -86,12 +100,24 @@ module ClaudeAgentSDK
|
|
|
86
100
|
# mirror copy is incomplete. Consulted at teardown by the resume-from-store
|
|
87
101
|
# cleanup so the materialized temp dir (which then holds the only copy of
|
|
88
102
|
# the dropped turns) is preserved instead of deleted.
|
|
103
|
+
#
|
|
104
|
+
# Frames still buffered once #close has started count too. Query#close
|
|
105
|
+
# stops the read task (the background drainer's parent) right after
|
|
106
|
+
# #close returns, so a frame the read loop enqueued during the close
|
|
107
|
+
# window — or any below-threshold frame after it — is never appended.
|
|
108
|
+
# The old per-frame drain detached it into a parked task whose
|
|
109
|
+
# cancellation counted it; with coalescing it stays in @pending, so it is
|
|
110
|
+
# accounted here. Evaluated lazily: frames the drainer still manages to
|
|
111
|
+
# deliver before the check are not drops, and nothing past #close has to
|
|
112
|
+
# run for a stranded frame to be counted. (A plain ivar/Array#empty? read
|
|
113
|
+
# is safe cross-thread under the GVL, like @dropped_batches.)
|
|
89
114
|
def batches_dropped?
|
|
90
|
-
@dropped_batches.positive?
|
|
115
|
+
@dropped_batches.positive? || (@closed && !@pending.empty?)
|
|
91
116
|
end
|
|
92
117
|
|
|
93
118
|
# Buffer a frame; schedule an eager background flush if thresholds are
|
|
94
|
-
# exceeded
|
|
119
|
+
# exceeded and no background drain is already live (a live one will pick
|
|
120
|
+
# this frame up). Synchronous and fire-and-forget.
|
|
95
121
|
#
|
|
96
122
|
# +entries+ are deep-stringified because the transport parses CLI output
|
|
97
123
|
# with symbolized keys, but SessionStore entries are opaque JSON blobs that
|
|
@@ -109,12 +135,10 @@ module ClaudeAgentSDK
|
|
|
109
135
|
@pending << { file_path: file_path, entries: entries }
|
|
110
136
|
@pending_entries += entries.length
|
|
111
137
|
@pending_bytes += size
|
|
112
|
-
return unless
|
|
138
|
+
return unless over_threshold?
|
|
113
139
|
|
|
114
140
|
task = Async::Task.current?
|
|
115
|
-
|
|
116
|
-
# in-flight flush so append ordering holds. #drain never raises.
|
|
117
|
-
task ? task.async { drain } : drain
|
|
141
|
+
task ? schedule_background_drain(task) : drain
|
|
118
142
|
end
|
|
119
143
|
|
|
120
144
|
# Flush all pending entries, serialized after any in-flight eager flush.
|
|
@@ -122,8 +146,11 @@ module ClaudeAgentSDK
|
|
|
122
146
|
drain
|
|
123
147
|
end
|
|
124
148
|
|
|
125
|
-
# Final flush before teardown. Never raises.
|
|
149
|
+
# Final flush before teardown. Never raises. Bounded: it waits behind at
|
|
150
|
+
# most the in-flight append, and frames arriving after it detached are
|
|
151
|
+
# not chased (they count as dropped unless delivered; #batches_dropped?).
|
|
126
152
|
def close
|
|
153
|
+
@closed = true
|
|
127
154
|
flush
|
|
128
155
|
rescue StandardError => e
|
|
129
156
|
warn "Claude SDK: TranscriptMirrorBatcher close flush failed: #{e.message}"
|
|
@@ -131,6 +158,38 @@ module ClaudeAgentSDK
|
|
|
131
158
|
|
|
132
159
|
private
|
|
133
160
|
|
|
161
|
+
def over_threshold?
|
|
162
|
+
@pending_entries > @max_pending_entries || @pending_bytes > @max_pending_bytes
|
|
163
|
+
end
|
|
164
|
+
|
|
165
|
+
# Fire-and-forget on the reactor. The drainer loops until the buffer is
|
|
166
|
+
# back under the thresholds, so while it is live every later frame is
|
|
167
|
+
# simply buffered and coalesced into its next #drain — live background
|
|
168
|
+
# tasks <= 1 and detached batches <= 1 + one per #flush / #close caller
|
|
169
|
+
# parked on @lock, independent of frame rate and store latency.
|
|
170
|
+
#
|
|
171
|
+
# No lost wakeup: the loop's final over_threshold? check and the ensure
|
|
172
|
+
# clearing the flag run with no suspension point between them, so an
|
|
173
|
+
# #enqueue that saw the flag set is always observed by that check.
|
|
174
|
+
# Every drainer (this one, #flush, #close) keeps detach-then-acquire, so
|
|
175
|
+
# detach order == @lock FIFO order and append ordering holds. #drain
|
|
176
|
+
# never raises.
|
|
177
|
+
def schedule_background_drain(task)
|
|
178
|
+
return if @background_drain_live
|
|
179
|
+
|
|
180
|
+
@background_drain_live = true
|
|
181
|
+
task.async do
|
|
182
|
+
drain while over_threshold?
|
|
183
|
+
ensure
|
|
184
|
+
@background_drain_live = false
|
|
185
|
+
end
|
|
186
|
+
rescue StandardError
|
|
187
|
+
# Task#async raises synchronously (e.g. FinishedError) before the block
|
|
188
|
+
# runs; don't let a stuck flag disable background drains for good.
|
|
189
|
+
@background_drain_live = false
|
|
190
|
+
raise
|
|
191
|
+
end
|
|
192
|
+
|
|
134
193
|
# Detach the pending buffer, await any prior flush, then send. Detaching
|
|
135
194
|
# before acquiring the lock lets #enqueue keep accumulating into a fresh
|
|
136
195
|
# buffer while a prior flush is in flight. Never raises.
|
|
@@ -9,7 +9,7 @@ module ClaudeAgentSDK
|
|
|
9
9
|
|
|
10
10
|
# Effort levels for `ClaudeAgentOptions#effort`. The CLI (Claude Code 2.1.111+)
|
|
11
11
|
# accepts these values; the set of *supported* levels is model-dependent
|
|
12
|
-
# (e.g. `xhigh`
|
|
12
|
+
# (e.g. `xhigh` arrived with Opus 4.7 and falls back to `high` on
|
|
13
13
|
# Opus 4.6 / Sonnet 4.6). An Integer is also accepted and forwarded verbatim.
|
|
14
14
|
EFFORT_LEVELS = %w[low medium high xhigh max].freeze
|
|
15
15
|
|
|
@@ -90,6 +90,73 @@ module ClaudeAgentSDK
|
|
|
90
90
|
{}
|
|
91
91
|
end
|
|
92
92
|
|
|
93
|
+
# The copy hook used wherever ClaudeAgentOptions are copied (dup_with and
|
|
94
|
+
# the configured-defaults merge). Identity by default: most Type instances
|
|
95
|
+
# are messages or callback payloads that never live inside options, and a
|
|
96
|
+
# user-supplied object that does (an observer, a store adapter) must stay
|
|
97
|
+
# the same object. Option VALUE types include OptionValue to opt in to
|
|
98
|
+
# copying, so a per-session change to e.g. sandbox rules can never reach
|
|
99
|
+
# another session or the configured defaults.
|
|
100
|
+
def dup_for_options
|
|
101
|
+
self
|
|
102
|
+
end
|
|
103
|
+
|
|
104
|
+
# Mixed into the mutable value types that ClaudeAgentOptions holds
|
|
105
|
+
# (SandboxSettings, SystemPromptPreset, AgentDefinition, ...). The copy
|
|
106
|
+
# recurses into the value's own state with Type.deep_dup_for_options, so
|
|
107
|
+
# nested containers and nested value types (SandboxSettings#network) are
|
|
108
|
+
# copied too while identity leaves (McpSdkServerConfig#instance, the
|
|
109
|
+
# callables in HookMatcher#hooks) stay shared. #dup never copies frozen
|
|
110
|
+
# state, so a copy of a frozen value (the configured-defaults snapshot) is
|
|
111
|
+
# mutable.
|
|
112
|
+
module OptionValue
|
|
113
|
+
def dup_for_options
|
|
114
|
+
copy = dup
|
|
115
|
+
copy.instance_variables.each do |ivar|
|
|
116
|
+
copy.instance_variable_set(ivar, Type.deep_dup_for_options(copy.instance_variable_get(ivar)))
|
|
117
|
+
end
|
|
118
|
+
copy
|
|
119
|
+
end
|
|
120
|
+
end
|
|
121
|
+
|
|
122
|
+
# Recurse into Hash/Array containers and option value types, and copy
|
|
123
|
+
# mutable (unfrozen) Strings — a caller-built prompt, model or
|
|
124
|
+
# allowed_tools entry is as much shared state as an Array, and `str <<
|
|
125
|
+
# 'x'` on one copy would otherwise change every other. A frozen String
|
|
126
|
+
# (any literal under frozen_string_literal) is immutable and keeps
|
|
127
|
+
# identity. Every other leaf keeps object identity (observer factories,
|
|
128
|
+
# callbacks, SDK MCP server instances must not be duped). Rebuild
|
|
129
|
+
# containers via dup.clear (never Hash#to_h / Array#map) to preserve
|
|
130
|
+
# container SUBCLASSES: to_h flattens e.g. Rails'
|
|
131
|
+
# HashWithIndifferentAccess into a plain Hash, silently breaking symbol
|
|
132
|
+
# lookups on the copy (config[:type] == 'sdk' → nil). Hash keys: Ruby
|
|
133
|
+
# already stores a dup'd, frozen copy of an unfrozen plain String key, but
|
|
134
|
+
# not of a String SUBCLASS key, so those are copied (and frozen, as keys
|
|
135
|
+
# should be) here. A compare_by_identity Hash is left keyed by the
|
|
136
|
+
# caller's objects: copying a key would break the caller's own lookups.
|
|
137
|
+
def self.deep_dup_for_options(value)
|
|
138
|
+
case value
|
|
139
|
+
when Hash
|
|
140
|
+
copy = value.dup.clear
|
|
141
|
+
value.each { |k, v| copy[option_hash_key(k, value)] = deep_dup_for_options(v) }
|
|
142
|
+
copy
|
|
143
|
+
when Array
|
|
144
|
+
copy = value.dup.clear
|
|
145
|
+
value.each { |v| copy << deep_dup_for_options(v) }
|
|
146
|
+
copy
|
|
147
|
+
when Type then value.dup_for_options
|
|
148
|
+
when String then value.frozen? ? value : value.dup # #dup keeps subclass and encoding
|
|
149
|
+
else value
|
|
150
|
+
end
|
|
151
|
+
end
|
|
152
|
+
|
|
153
|
+
def self.option_hash_key(key, hash)
|
|
154
|
+
return key unless key.is_a?(String) && !key.frozen? && !hash.compare_by_identity?
|
|
155
|
+
|
|
156
|
+
key.dup.freeze
|
|
157
|
+
end
|
|
158
|
+
private_class_method :option_hash_key
|
|
159
|
+
|
|
93
160
|
private
|
|
94
161
|
|
|
95
162
|
# Allow camelCase attribute access
|
|
@@ -785,13 +852,16 @@ module ClaudeAgentSDK
|
|
|
785
852
|
# `display` controls how thinking content appears in responses. Valid values
|
|
786
853
|
# are `"summarized"` (plaintext summary) and `"omitted"` (empty thinking
|
|
787
854
|
# field, signature only). Defaults are model-dependent: Opus 4.6/Sonnet 4.6
|
|
788
|
-
# default to `"summarized"`; Opus 4.7 and
|
|
789
|
-
# `"omitted"`. Pass `display: "summarized"` explicitly on
|
|
855
|
+
# default to `"summarized"`; Opus 4.7 and every later model default to
|
|
856
|
+
# `"omitted"`. Pass `display: "summarized"` explicitly on those to get
|
|
790
857
|
# visible thinking text. Not supported with `ThinkingConfigDisabled`.
|
|
791
858
|
THINKING_DISPLAY_VALUES = %w[summarized omitted].freeze
|
|
792
859
|
|
|
793
|
-
# Adaptive thinking:
|
|
860
|
+
# Adaptive thinking: the model decides when and how much to think
|
|
861
|
+
# (sent as `--thinking adaptive`, no budget); control depth with `effort`.
|
|
794
862
|
class ThinkingConfigAdaptive < Type
|
|
863
|
+
include Type::OptionValue
|
|
864
|
+
|
|
795
865
|
attr_reader :type, :display
|
|
796
866
|
|
|
797
867
|
def initialize(attributes = {})
|
|
@@ -816,6 +886,8 @@ module ClaudeAgentSDK
|
|
|
816
886
|
|
|
817
887
|
# Enabled thinking: uses a user-specified budget
|
|
818
888
|
class ThinkingConfigEnabled < Type
|
|
889
|
+
include Type::OptionValue
|
|
890
|
+
|
|
819
891
|
attr_accessor :budget_tokens
|
|
820
892
|
attr_reader :type, :display
|
|
821
893
|
|
|
@@ -841,6 +913,8 @@ module ClaudeAgentSDK
|
|
|
841
913
|
|
|
842
914
|
# Disabled thinking: sets thinking tokens to 0
|
|
843
915
|
class ThinkingConfigDisabled < Type
|
|
916
|
+
include Type::OptionValue
|
|
917
|
+
|
|
844
918
|
attr_reader :type
|
|
845
919
|
|
|
846
920
|
def initialize(attributes = {})
|
|
@@ -851,6 +925,8 @@ module ClaudeAgentSDK
|
|
|
851
925
|
|
|
852
926
|
# Agent definition configuration
|
|
853
927
|
class AgentDefinition < Type
|
|
928
|
+
include Type::OptionValue
|
|
929
|
+
|
|
854
930
|
attr_accessor :description, :prompt, :tools, :disallowed_tools, :model, :skills, :memory, :mcp_servers,
|
|
855
931
|
:initial_prompt, :max_turns, :background, :effort, :permission_mode
|
|
856
932
|
end
|
|
@@ -942,6 +1018,8 @@ module ClaudeAgentSDK
|
|
|
942
1018
|
|
|
943
1019
|
# Hook matcher configuration
|
|
944
1020
|
class HookMatcher < Type
|
|
1021
|
+
include Type::OptionValue
|
|
1022
|
+
|
|
945
1023
|
attr_accessor :matcher, :hooks, :timeout
|
|
946
1024
|
|
|
947
1025
|
def initialize(attributes = {})
|
|
@@ -1640,6 +1718,8 @@ module ClaudeAgentSDK
|
|
|
1640
1718
|
|
|
1641
1719
|
# MCP Server configurations
|
|
1642
1720
|
class McpStdioServerConfig < Type
|
|
1721
|
+
include Type::OptionValue
|
|
1722
|
+
|
|
1643
1723
|
attr_accessor :command, :args, :env
|
|
1644
1724
|
attr_reader :type
|
|
1645
1725
|
|
|
@@ -1657,6 +1737,8 @@ module ClaudeAgentSDK
|
|
|
1657
1737
|
end
|
|
1658
1738
|
|
|
1659
1739
|
class McpSSEServerConfig < Type
|
|
1740
|
+
include Type::OptionValue
|
|
1741
|
+
|
|
1660
1742
|
attr_accessor :url, :headers
|
|
1661
1743
|
attr_reader :type
|
|
1662
1744
|
|
|
@@ -1673,6 +1755,8 @@ module ClaudeAgentSDK
|
|
|
1673
1755
|
end
|
|
1674
1756
|
|
|
1675
1757
|
class McpHttpServerConfig < Type
|
|
1758
|
+
include Type::OptionValue
|
|
1759
|
+
|
|
1676
1760
|
attr_accessor :url, :headers
|
|
1677
1761
|
attr_reader :type
|
|
1678
1762
|
|
|
@@ -1689,6 +1773,8 @@ module ClaudeAgentSDK
|
|
|
1689
1773
|
end
|
|
1690
1774
|
|
|
1691
1775
|
class McpSdkServerConfig < Type
|
|
1776
|
+
include Type::OptionValue
|
|
1777
|
+
|
|
1692
1778
|
attr_accessor :name, :instance
|
|
1693
1779
|
attr_reader :type
|
|
1694
1780
|
|
|
@@ -1704,6 +1790,8 @@ module ClaudeAgentSDK
|
|
|
1704
1790
|
|
|
1705
1791
|
# SDK Plugin configuration
|
|
1706
1792
|
class SdkPluginConfig < Type
|
|
1793
|
+
include Type::OptionValue
|
|
1794
|
+
|
|
1707
1795
|
attr_accessor :path
|
|
1708
1796
|
attr_reader :type
|
|
1709
1797
|
|
|
@@ -1719,6 +1807,8 @@ module ClaudeAgentSDK
|
|
|
1719
1807
|
|
|
1720
1808
|
# Sandbox network configuration
|
|
1721
1809
|
class SandboxNetworkConfig < Type
|
|
1810
|
+
include Type::OptionValue
|
|
1811
|
+
|
|
1722
1812
|
attr_accessor :allowed_domains, :denied_domains, :allow_managed_domains_only,
|
|
1723
1813
|
:allow_unix_sockets, :allow_all_unix_sockets, :allow_local_binding,
|
|
1724
1814
|
:allow_mach_lookup, :http_proxy_port, :socks_proxy_port
|
|
@@ -1740,6 +1830,8 @@ module ClaudeAgentSDK
|
|
|
1740
1830
|
|
|
1741
1831
|
# Sandbox filesystem configuration
|
|
1742
1832
|
class SandboxFilesystemConfig < Type
|
|
1833
|
+
include Type::OptionValue
|
|
1834
|
+
|
|
1743
1835
|
attr_accessor :allow_write, :deny_write, :deny_read, :allow_read, :allow_managed_read_paths_only
|
|
1744
1836
|
|
|
1745
1837
|
def to_h
|
|
@@ -1755,6 +1847,8 @@ module ClaudeAgentSDK
|
|
|
1755
1847
|
|
|
1756
1848
|
# Sandbox settings for isolated command execution
|
|
1757
1849
|
class SandboxSettings < Type
|
|
1850
|
+
include Type::OptionValue
|
|
1851
|
+
|
|
1758
1852
|
attr_accessor :enabled, :fail_if_unavailable, :auto_allow_bash_if_sandboxed,
|
|
1759
1853
|
:excluded_commands, :allow_unsandboxed_commands, :network, :filesystem,
|
|
1760
1854
|
:ignore_violations, :enable_weaker_nested_sandbox,
|
|
@@ -1786,6 +1880,8 @@ module ClaudeAgentSDK
|
|
|
1786
1880
|
# When set, the model is made aware of its remaining token budget so it can
|
|
1787
1881
|
# pace tool use and wrap up before the limit.
|
|
1788
1882
|
class TaskBudget < Type
|
|
1883
|
+
include Type::OptionValue
|
|
1884
|
+
|
|
1789
1885
|
attr_accessor :total
|
|
1790
1886
|
|
|
1791
1887
|
def to_h
|
|
@@ -1795,6 +1891,8 @@ module ClaudeAgentSDK
|
|
|
1795
1891
|
|
|
1796
1892
|
# System prompt file configuration — loads system prompt from a file path
|
|
1797
1893
|
class SystemPromptFile < Type
|
|
1894
|
+
include Type::OptionValue
|
|
1895
|
+
|
|
1798
1896
|
attr_accessor :path
|
|
1799
1897
|
attr_reader :type
|
|
1800
1898
|
|
|
@@ -1822,6 +1920,8 @@ module ClaudeAgentSDK
|
|
|
1822
1920
|
# before 2.1.265 a session with an +append+ prompt recorded it only when
|
|
1823
1921
|
# +snapshot+ was true. Older CLIs silently ignore it.
|
|
1824
1922
|
class SystemPromptPreset < Type
|
|
1923
|
+
include Type::OptionValue
|
|
1924
|
+
|
|
1825
1925
|
attr_reader :type
|
|
1826
1926
|
attr_accessor :preset, :append, :exclude_dynamic_sections, :snapshot
|
|
1827
1927
|
|
|
@@ -1844,6 +1944,8 @@ module ClaudeAgentSDK
|
|
|
1844
1944
|
# (+--system-prompt <prompt>+); the object form exists so +snapshot+ can be
|
|
1845
1945
|
# set alongside it (see SystemPromptPreset#snapshot for its semantics).
|
|
1846
1946
|
class SystemPromptCustom < Type
|
|
1947
|
+
include Type::OptionValue
|
|
1948
|
+
|
|
1847
1949
|
attr_reader :type
|
|
1848
1950
|
attr_accessor :prompt, :snapshot
|
|
1849
1951
|
|
|
@@ -1861,6 +1963,8 @@ module ClaudeAgentSDK
|
|
|
1861
1963
|
|
|
1862
1964
|
# Tools preset configuration
|
|
1863
1965
|
class ToolsPreset < Type
|
|
1966
|
+
include Type::OptionValue
|
|
1967
|
+
|
|
1864
1968
|
attr_reader :type
|
|
1865
1969
|
attr_accessor :preset
|
|
1866
1970
|
|
|
@@ -1955,14 +2059,16 @@ module ClaudeAgentSDK
|
|
|
1955
2059
|
|
|
1956
2060
|
def dup_with(**changes)
|
|
1957
2061
|
new_options = self.dup
|
|
1958
|
-
# A shallow #dup shares nested containers
|
|
1959
|
-
# (e.g. `variant.allowed_tools << 'Bash'`
|
|
1960
|
-
#
|
|
1961
|
-
#
|
|
1962
|
-
#
|
|
2062
|
+
# A shallow #dup shares nested containers and typed option values, so
|
|
2063
|
+
# mutating a derived copy (e.g. `variant.allowed_tools << 'Bash'` or
|
|
2064
|
+
# `variant.sandbox.enabled = false`) would bleed into the base and every
|
|
2065
|
+
# sibling — including the security-relevant allow/deny lists and sandbox
|
|
2066
|
+
# rules. Deep-dup Hash/Array containers and option value types (Type#
|
|
2067
|
+
# dup_for_options, including those nested inside containers such as
|
|
2068
|
+
# agents[:x]); every other leaf (procs, SDK MCP server instances, store
|
|
2069
|
+
# adapters) keeps its identity.
|
|
1963
2070
|
new_options.instance_variables.each do |ivar|
|
|
1964
|
-
|
|
1965
|
-
new_options.instance_variable_set(ivar, deep_dup_containers(value)) if value.is_a?(Hash) || value.is_a?(Array)
|
|
2071
|
+
new_options.instance_variable_set(ivar, Type.deep_dup_for_options(new_options.instance_variable_get(ivar)))
|
|
1966
2072
|
end
|
|
1967
2073
|
changes.each { |key, value| new_options[key] = value }
|
|
1968
2074
|
new_options
|
|
@@ -2160,12 +2266,20 @@ module ClaudeAgentSDK
|
|
|
2160
2266
|
defaults = ClaudeAgentSDK.default_options
|
|
2161
2267
|
return attributes unless defaults.any?
|
|
2162
2268
|
|
|
2163
|
-
# Start from configured defaults. Container values
|
|
2164
|
-
#
|
|
2165
|
-
#
|
|
2166
|
-
#
|
|
2269
|
+
# Start from configured defaults. Container values, typed option
|
|
2270
|
+
# values (SandboxSettings, SystemPromptPreset, AgentDefinition, ...)
|
|
2271
|
+
# and mutable Strings are recursively copied (Type.deep_dup_for_options)
|
|
2272
|
+
# so per-instance mutation (options.allowed_tools << 'Bash',
|
|
2273
|
+
# options.sandbox.enabled = false) can never corrupt the global
|
|
2274
|
+
# defaults or reach another session; other leaves (frozen Strings,
|
|
2275
|
+
# Procs, SdkMcpServer instances, store adapters) intentionally keep
|
|
2276
|
+
# identity. The stored defaults are a frozen snapshot
|
|
2277
|
+
# (Configuration#default_options=), and the copy is what makes each
|
|
2278
|
+
# session's containers and values mutable again — its Strings stay the
|
|
2279
|
+
# snapshot's frozen ones, so `options.model << 'x'` fails loudly rather
|
|
2280
|
+
# than reaching other sessions; reassign instead.
|
|
2167
2281
|
result = {}
|
|
2168
|
-
defaults.each { |key, value| result[option_key(key)] =
|
|
2282
|
+
defaults.each { |key, value| result[option_key(key)] = Type.deep_dup_for_options(value) }
|
|
2169
2283
|
attributes.each do |key, value|
|
|
2170
2284
|
key = option_key(key)
|
|
2171
2285
|
default_val = result[key]
|
|
@@ -2187,26 +2301,6 @@ module ClaudeAgentSDK
|
|
|
2187
2301
|
normalized = normalize_name(name)
|
|
2188
2302
|
respond_to?(:"#{normalized}=") ? normalized.to_sym : name
|
|
2189
2303
|
end
|
|
2190
|
-
|
|
2191
|
-
# Recurse ONLY into Hash/Array; leaves keep object identity (observer
|
|
2192
|
-
# factories, callbacks, SDK MCP server instances must not be duped).
|
|
2193
|
-
# Rebuild via dup.clear (never Hash#to_h / Array#map) to preserve
|
|
2194
|
-
# container SUBCLASSES: to_h flattens e.g. Rails'
|
|
2195
|
-
# HashWithIndifferentAccess into a plain Hash, silently breaking symbol
|
|
2196
|
-
# lookups on the copy (config[:type] == 'sdk' → nil).
|
|
2197
|
-
def deep_dup_containers(value)
|
|
2198
|
-
case value
|
|
2199
|
-
when Hash
|
|
2200
|
-
copy = value.dup.clear
|
|
2201
|
-
value.each { |k, v| copy[k] = deep_dup_containers(v) }
|
|
2202
|
-
copy
|
|
2203
|
-
when Array
|
|
2204
|
-
copy = value.dup.clear
|
|
2205
|
-
value.each { |v| copy << deep_dup_containers(v) }
|
|
2206
|
-
copy
|
|
2207
|
-
else value
|
|
2208
|
-
end
|
|
2209
|
-
end
|
|
2210
2304
|
end
|
|
2211
2305
|
|
|
2212
2306
|
# SDK MCP Tool definition
|
data/lib/claude_agent_sdk.rb
CHANGED
|
@@ -52,18 +52,43 @@ module ClaudeAgentSDK
|
|
|
52
52
|
# Internal: pull live SDK MCP server instances out of an mcp_servers Hash.
|
|
53
53
|
# Accepts both raw Hash configs and typed Mcp*ServerConfig objects — a
|
|
54
54
|
# McpSdkServerConfig passed without .to_h previously failed the Hash-only
|
|
55
|
-
# guard, so its in-process server was silently never registered.
|
|
55
|
+
# guard, so its in-process server was silently never registered. Hash
|
|
56
|
+
# configs may use String or Symbol keys (and a Symbol :sdk type) — the
|
|
57
|
+
# recognition rule must match CommandBuilder#append_mcp_servers, which
|
|
58
|
+
# strips the instance from exactly these entries.
|
|
56
59
|
def self.extract_sdk_mcp_servers(mcp_servers)
|
|
57
60
|
return {} unless mcp_servers.is_a?(Hash)
|
|
58
61
|
|
|
59
62
|
servers = {}
|
|
60
63
|
mcp_servers.each do |name, config|
|
|
61
64
|
config = config.to_h if config.is_a?(Type)
|
|
62
|
-
|
|
65
|
+
next unless config.is_a?(Hash) && (config[:type] || config['type']).to_s == 'sdk'
|
|
66
|
+
|
|
67
|
+
servers[name] = config.key?(:instance) ? config[:instance] : config['instance']
|
|
63
68
|
end
|
|
64
69
|
servers
|
|
65
70
|
end
|
|
66
71
|
|
|
72
|
+
# Internal: normalize hook lists for the control protocol. An absent or
|
|
73
|
+
# disabled event must not become an empty registration in initialize.
|
|
74
|
+
def self.convert_hooks_to_internal_format(hooks)
|
|
75
|
+
return nil unless hooks
|
|
76
|
+
|
|
77
|
+
internal_hooks = {}
|
|
78
|
+
hooks.each do |event, matchers|
|
|
79
|
+
next if matchers.nil? || matchers.empty?
|
|
80
|
+
|
|
81
|
+
entries = []
|
|
82
|
+
matchers.each do |matcher|
|
|
83
|
+
config = { matcher: matcher.matcher, hooks: matcher.hooks }
|
|
84
|
+
config[:timeout] = matcher.timeout if matcher.timeout
|
|
85
|
+
entries << config
|
|
86
|
+
end
|
|
87
|
+
internal_hooks[event.to_s] = entries unless entries.empty?
|
|
88
|
+
end
|
|
89
|
+
internal_hooks.empty? ? nil : internal_hooks
|
|
90
|
+
end
|
|
91
|
+
|
|
67
92
|
# Internal: validate can_use_tool and route permission prompts over stdio.
|
|
68
93
|
#
|
|
69
94
|
# Shared by query() and Client#connect so both entry points enforce the
|
|
@@ -422,6 +447,7 @@ module ClaudeAgentSDK
|
|
|
422
447
|
# rename_session). Appends a custom-title entry carrying a fresh uuid +
|
|
423
448
|
# timestamp via SessionStore#append.
|
|
424
449
|
# @raise [ArgumentError] if session_id is invalid or title is empty
|
|
450
|
+
# @raise [Errno::ENOENT] if the session is not found in the store (nothing is appended)
|
|
425
451
|
def self.rename_session_via_store(session_store:, session_id:, title:, directory: nil)
|
|
426
452
|
SessionMutations.rename_session_via_store(session_store: session_store, session_id: session_id,
|
|
427
453
|
title: title, directory: directory)
|
|
@@ -430,6 +456,7 @@ module ClaudeAgentSDK
|
|
|
430
456
|
# Tag a session in a SessionStore (store-backed counterpart to tag_session).
|
|
431
457
|
# Pass nil to clear the tag.
|
|
432
458
|
# @raise [ArgumentError] if session_id is invalid or tag is empty after sanitization
|
|
459
|
+
# @raise [Errno::ENOENT] if the session is not found in the store (nothing is appended)
|
|
433
460
|
def self.tag_session_via_store(session_store:, session_id:, tag:, directory: nil)
|
|
434
461
|
SessionMutations.tag_session_via_store(session_store: session_store, session_id: session_id,
|
|
435
462
|
tag: tag, directory: directory)
|
|
@@ -528,7 +555,7 @@ module ClaudeAgentSDK
|
|
|
528
555
|
|
|
529
556
|
raise ArgumentError, 'transport must respond to #connect (see ClaudeAgentSDK::Transport)' if transport && !transport.respond_to?(:connect)
|
|
530
557
|
|
|
531
|
-
Async do
|
|
558
|
+
Async(&FiberBoundary.capture_otel_context do
|
|
532
559
|
materialized = nil
|
|
533
560
|
query_handler = nil
|
|
534
561
|
begin
|
|
@@ -556,25 +583,7 @@ module ClaudeAgentSDK
|
|
|
556
583
|
# Extract SDK MCP servers
|
|
557
584
|
sdk_mcp_servers = extract_sdk_mcp_servers(configured_options.mcp_servers)
|
|
558
585
|
|
|
559
|
-
hooks =
|
|
560
|
-
if configured_options.hooks
|
|
561
|
-
hooks = {}
|
|
562
|
-
configured_options.hooks.each do |event, matchers|
|
|
563
|
-
next if matchers.nil? || matchers.empty?
|
|
564
|
-
|
|
565
|
-
entries = []
|
|
566
|
-
matchers.each do |matcher|
|
|
567
|
-
config = {
|
|
568
|
-
matcher: matcher.matcher,
|
|
569
|
-
hooks: matcher.hooks
|
|
570
|
-
}
|
|
571
|
-
config[:timeout] = matcher.timeout if matcher.timeout
|
|
572
|
-
entries << config
|
|
573
|
-
end
|
|
574
|
-
hooks[event.to_s] = entries unless entries.empty?
|
|
575
|
-
end
|
|
576
|
-
hooks = nil if hooks.empty?
|
|
577
|
-
end
|
|
586
|
+
hooks = convert_hooks_to_internal_format(configured_options.hooks)
|
|
578
587
|
|
|
579
588
|
# Create Query handler for control protocol
|
|
580
589
|
query_handler = Query.new(
|
|
@@ -686,7 +695,7 @@ module ClaudeAgentSDK
|
|
|
686
695
|
end
|
|
687
696
|
end
|
|
688
697
|
end
|
|
689
|
-
end.wait
|
|
698
|
+
end).wait
|
|
690
699
|
end
|
|
691
700
|
|
|
692
701
|
# Client for bidirectional, interactive conversations with Claude Code
|
|
@@ -762,7 +771,7 @@ module ClaudeAgentSDK
|
|
|
762
771
|
def self.open(prompt = nil, options: nil, transport_class: SubprocessCLITransport, transport_args: {})
|
|
763
772
|
raise ArgumentError, 'Client.open requires a block' unless block_given?
|
|
764
773
|
|
|
765
|
-
Sync do
|
|
774
|
+
Sync(&FiberBoundary.capture_otel_context do
|
|
766
775
|
client = new(options: options, transport_class: transport_class, transport_args: transport_args)
|
|
767
776
|
# connect failures self-clean via connect's rescue -> disconnect ->
|
|
768
777
|
# raise, and disconnect is idempotent — no double-teardown.
|
|
@@ -772,7 +781,7 @@ module ClaudeAgentSDK
|
|
|
772
781
|
ensure
|
|
773
782
|
client.disconnect
|
|
774
783
|
end
|
|
775
|
-
end
|
|
784
|
+
end)
|
|
776
785
|
end
|
|
777
786
|
|
|
778
787
|
# Connect to Claude with optional initial prompt.
|
|
@@ -792,6 +801,7 @@ module ClaudeAgentSDK
|
|
|
792
801
|
def connect(prompt = nil)
|
|
793
802
|
return if @connected
|
|
794
803
|
|
|
804
|
+
raise ArgumentError, 'prompt must be a String or an Enumerable of message Hashes/JSONL Strings (got Hash)' if prompt.is_a?(Hash)
|
|
795
805
|
raise ArgumentError, "prompt must be a String, an Enumerator, or nil (got #{prompt.class})" unless prompt.nil? || prompt.is_a?(String) || prompt.respond_to?(:each)
|
|
796
806
|
|
|
797
807
|
# Validate and configure permission settings
|
|
@@ -1050,6 +1060,19 @@ module ClaudeAgentSDK
|
|
|
1050
1060
|
end
|
|
1051
1061
|
|
|
1052
1062
|
# Disconnect from Claude
|
|
1063
|
+
#
|
|
1064
|
+
# Callable from inside a user callback (tool handler / hook /
|
|
1065
|
+
# can_use_tool). With the default `callback_scheduling: :thread` the
|
|
1066
|
+
# callback runs on a worker thread: the close is marshalled to the
|
|
1067
|
+
# reactor, returns normally once the teardown completed, and the
|
|
1068
|
+
# callback's return value is dropped (its reactor task was stopped). With
|
|
1069
|
+
# `:inline` the callback's task is a child of the read task being
|
|
1070
|
+
# stopped, so once the teardown has completed the deferred Async::Stop
|
|
1071
|
+
# unwinds the callback — disconnect raises rather than returns there;
|
|
1072
|
+
# ensure blocks run, `rescue StandardError` does not see it. The same
|
|
1073
|
+
# holds, in every scheduling mode, for a streaming-input enumerator that
|
|
1074
|
+
# calls disconnect: it is iterated on the reactor inside a task the close
|
|
1075
|
+
# stops, so it unwinds with Async::Stop once the teardown has completed.
|
|
1053
1076
|
def disconnect
|
|
1054
1077
|
if @connected
|
|
1055
1078
|
ClaudeAgentSDK.notify_observers(@resolved_observers || [], :on_close,
|
|
@@ -1125,7 +1148,7 @@ module ClaudeAgentSDK
|
|
|
1125
1148
|
sdk_mcp_servers = ClaudeAgentSDK.extract_sdk_mcp_servers(configured_options.mcp_servers)
|
|
1126
1149
|
|
|
1127
1150
|
# Convert hooks to internal format
|
|
1128
|
-
hooks = convert_hooks_to_internal_format(configured_options.hooks)
|
|
1151
|
+
hooks = ClaudeAgentSDK.convert_hooks_to_internal_format(configured_options.hooks)
|
|
1129
1152
|
|
|
1130
1153
|
# Extract exclude_dynamic_sections and snapshot from the system prompt
|
|
1131
1154
|
# for the initialize request (older CLIs ignore unknown initialize fields)
|
|
@@ -1236,24 +1259,6 @@ module ClaudeAgentSDK
|
|
|
1236
1259
|
@query_handler.set_transcript_mirror_batcher(batcher)
|
|
1237
1260
|
end
|
|
1238
1261
|
|
|
1239
|
-
def convert_hooks_to_internal_format(hooks)
|
|
1240
|
-
return nil unless hooks
|
|
1241
|
-
|
|
1242
|
-
internal_hooks = {}
|
|
1243
|
-
hooks.each do |event, matchers|
|
|
1244
|
-
internal_hooks[event.to_s] = []
|
|
1245
|
-
matchers.each do |matcher|
|
|
1246
|
-
config = {
|
|
1247
|
-
matcher: matcher.matcher,
|
|
1248
|
-
hooks: matcher.hooks
|
|
1249
|
-
}
|
|
1250
|
-
config[:timeout] = matcher.timeout if matcher.timeout
|
|
1251
|
-
internal_hooks[event.to_s] << config
|
|
1252
|
-
end
|
|
1253
|
-
end
|
|
1254
|
-
internal_hooks
|
|
1255
|
-
end
|
|
1256
|
-
|
|
1257
1262
|
def writeln(string)
|
|
1258
1263
|
write string.end_with?("\n") ? string : "#{string}\n"
|
|
1259
1264
|
end
|