claude-agent-sdk 0.33.1 → 0.35.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.
Files changed (37) hide show
  1. checksums.yaml +4 -4
  2. data/CHANGELOG.md +93 -0
  3. data/README.md +54 -19
  4. data/docs/cli-installer.md +40 -9
  5. data/docs/client.md +27 -18
  6. data/docs/configuration.md +5 -5
  7. data/docs/errors.md +4 -3
  8. data/docs/mcp-servers.md +22 -0
  9. data/docs/observability.md +6 -0
  10. data/docs/rails.md +92 -51
  11. data/docs/sessions.md +66 -15
  12. data/lib/claude_agent_sdk/cli_installer.rb +34 -18
  13. data/lib/claude_agent_sdk/command_builder.rb +11 -3
  14. data/lib/claude_agent_sdk/configuration.rb +54 -2
  15. data/lib/claude_agent_sdk/errors.rb +11 -3
  16. data/lib/claude_agent_sdk/fiber_boundary.rb +42 -3
  17. data/lib/claude_agent_sdk/instrumentation/otel.rb +21 -2
  18. data/lib/claude_agent_sdk/query.rb +140 -59
  19. data/lib/claude_agent_sdk/railtie.rb +94 -0
  20. data/lib/claude_agent_sdk/sdk_mcp_server.rb +46 -4
  21. data/lib/claude_agent_sdk/session_mutations.rb +39 -12
  22. data/lib/claude_agent_sdk/session_resume.rb +112 -39
  23. data/lib/claude_agent_sdk/session_store.rb +19 -3
  24. data/lib/claude_agent_sdk/session_summary.rb +5 -5
  25. data/lib/claude_agent_sdk/sessions.rb +123 -55
  26. data/lib/claude_agent_sdk/streaming.rb +0 -8
  27. data/lib/claude_agent_sdk/subprocess_cli_transport.rb +319 -54
  28. data/lib/claude_agent_sdk/tasks/claude_agent_sdk.rake +30 -0
  29. data/lib/claude_agent_sdk/tasks.rb +13 -0
  30. data/lib/claude_agent_sdk/testing/session_store_conformance.rb +13 -3
  31. data/lib/claude_agent_sdk/transcript_mirror_batcher.rb +77 -18
  32. data/lib/claude_agent_sdk/types.rb +349 -39
  33. data/lib/claude_agent_sdk/version.rb +1 -1
  34. data/lib/claude_agent_sdk.rb +53 -44
  35. data/lib/generators/claude_agent_sdk/install/install_generator.rb +63 -0
  36. data/lib/generators/claude_agent_sdk/install/templates/claude_agent_sdk.rb.tt +35 -0
  37. metadata +29 -13
@@ -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` is only supported on Opus 4.7 and falls back to `high` on
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,8 +90,245 @@ 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
+
160
+ # Bounded, human-oriented #inspect listing the non-nil instance variables
161
+ # in definition order:
162
+ #
163
+ # #<ClaudeAgentSDK::ResultMessage subtype="success" num_turns=3 ...>
164
+ #
165
+ # Messages carry whole transcripts, tool payloads and usage maps, so the
166
+ # output is bounded rather than faithful: long Strings are truncated,
167
+ # long Arrays/Hashes abbreviated, and nesting past INSPECT_MAX_DEPTH (or
168
+ # a reference cycle) collapses to a placeholder. Other objects keep their
169
+ # own #inspect (truncated) unless they only have Kernel#inspect, which
170
+ # dumps every ivar recursively — those (SDK MCP server instances, store
171
+ # adapters, observers) show as `#<ClassName>`. For display only: nothing
172
+ # sent to the CLI goes through #inspect or #to_s (wire output uses #to_h).
173
+ def inspect
174
+ inspect_with(0, {}.compare_by_identity)
175
+ end
176
+
177
+ # Object#to_s ignores instance variables, so `puts message` would print
178
+ # only a class name and an address. Types with a natural textual form
179
+ # (UserMessage, AssistantMessage, TextBlock, ResultMessage, SystemMessage)
180
+ # override this.
181
+ def to_s
182
+ inspect
183
+ end
184
+
185
+ # Declares attributes that carry credentials (env vars, auth headers).
186
+ # Objects get logged, so #inspect shows them filtered; #to_h and
187
+ # everything sent to the CLI are unaffected. Inherited by subclasses.
188
+ def self.inspect_filtered(*names)
189
+ @inspect_filtered_attributes = (inspect_filtered_attributes + names.map(&:to_s)).uniq.freeze
190
+ end
191
+
192
+ def self.inspect_filtered_attributes
193
+ @inspect_filtered_attributes || (superclass <= Type ? superclass.inspect_filtered_attributes : [].freeze)
194
+ end
195
+
196
+ INSPECT_MAX_STRING = 80
197
+ INSPECT_MAX_ITEMS = 5
198
+ INSPECT_MAX_DEPTH = 2
199
+ private_constant :INSPECT_MAX_STRING, :INSPECT_MAX_ITEMS, :INSPECT_MAX_DEPTH
200
+
201
+ protected
202
+
203
+ # `seen` holds the Types/containers on the current rendering path (not
204
+ # every one rendered so far), so a shared-but-acyclic value still renders
205
+ # in full wherever it appears.
206
+ def inspect_with(depth, seen)
207
+ return "#<#{inspect_class_name} …>" if depth > INSPECT_MAX_DEPTH || seen.key?(self)
208
+
209
+ seen[self] = true
210
+ begin
211
+ attributes = inspect_attributes.map do |name, value|
212
+ " #{name}=#{inspect_bounded(value, depth + 1, seen)}"
213
+ end
214
+ "#<#{inspect_class_name}#{attributes.join}>"
215
+ ensure
216
+ seen.delete(self)
217
+ end
218
+ end
219
+
93
220
  private
94
221
 
222
+ # [name, value] pairs rendered by #inspect. Subclasses override to hide
223
+ # redundant state or redact secrets — never by mutating the object.
224
+ def inspect_attributes
225
+ filtered = self.class.inspect_filtered_attributes
226
+ instance_variables.filter_map do |ivar|
227
+ value = instance_variable_get(ivar)
228
+ next if value.nil?
229
+
230
+ name = ivar.to_s.delete_prefix('@')
231
+ [name, filtered.include?(name) ? inspect_filter(value) : value]
232
+ end
233
+ end
234
+
235
+ # A credential-bearing Hash keeps its keys (useful when debugging which
236
+ # variables are set) with every value replaced; anything else is replaced
237
+ # outright. Builds a new Hash; the object itself is never touched.
238
+ def inspect_filter(value)
239
+ value.respond_to?(:each_key) ? value.each_key.to_h { |key| [key, '[FILTERED]'] } : '[FILTERED]'
240
+ end
241
+
242
+ def inspect_class_name
243
+ self.class.name || self.class.inspect
244
+ end
245
+
246
+ def inspect_bounded(value, depth, seen)
247
+ case value
248
+ when Type then value.inspect_with(depth, seen)
249
+ when String then inspect_truncated(value)
250
+ when Array then inspect_container(value, '[', ']', depth, seen) { |item| inspect_bounded(item, depth + 1, seen) }
251
+ when Hash
252
+ inspect_container(value, '{', '}', depth, seen) do |key, item|
253
+ "#{inspect_hash_key(key, depth + 1, seen)}#{inspect_bounded(item, depth + 1, seen)}"
254
+ end
255
+ when Proc, Method, UnboundMethod then inspect_callable(value)
256
+ else inspect_leaf(value)
257
+ end
258
+ end
259
+
260
+ def inspect_container(value, open, close, depth, seen, &render)
261
+ return "#{open}#{close}" if value.empty?
262
+ return "#{open}…(#{value.size})#{close}" if depth > INSPECT_MAX_DEPTH || seen.key?(value)
263
+
264
+ seen[value] = true
265
+ begin
266
+ parts = value.first(INSPECT_MAX_ITEMS).map(&render)
267
+ parts << "…(+#{value.size - INSPECT_MAX_ITEMS} more)" if value.size > INSPECT_MAX_ITEMS
268
+ "#{open}#{parts.join(', ')}#{close}"
269
+ ensure
270
+ seen.delete(value)
271
+ end
272
+ end
273
+
274
+ # Rendered by hand rather than via Hash#inspect, whose format differs
275
+ # between Ruby 3.3 (`{:a=>1}`) and 3.4 (`{a: 1}`).
276
+ def inspect_hash_key(key, depth, seen)
277
+ return "#{key.name}: " if key.is_a?(Symbol) && key.inspect.match?(/\A:\w+[?!]?\z/)
278
+
279
+ "#{inspect_bounded(key, depth, seen)} => "
280
+ end
281
+
282
+ def inspect_truncated(string)
283
+ return string.inspect if string.length <= INSPECT_MAX_STRING
284
+
285
+ "#{string[0, INSPECT_MAX_STRING].inspect}…(+#{string.length - INSPECT_MAX_STRING} chars)"
286
+ end
287
+
288
+ # Callbacks (can_use_tool, hooks, callback_wrapper, ...) are user-supplied:
289
+ # render them from source_location rather than their own #inspect, which
290
+ # a subclass may override (and raise from) and which embeds an absolute
291
+ # path — `#<Proc(lambda) permissions.rb:17>`, `#<Method Policy#call>`.
292
+ def inspect_callable(value)
293
+ label = if value.is_a?(Proc)
294
+ value.lambda? ? 'Proc(lambda)' : 'Proc'
295
+ else
296
+ "#{value.class.name} #{value.owner.name || value.owner.inspect}##{value.name}"
297
+ end
298
+ file, line = value.source_location
299
+ rendered = file ? "#<#{label} #{File.basename(file)}:#{line}>" : "#<#{label}>"
300
+ inspect_truncated_text(rendered)
301
+ rescue StandardError
302
+ inspect_leaf(value)
303
+ end
304
+
305
+ def inspect_truncated_text(rendered)
306
+ return rendered if rendered.length <= INSPECT_MAX_STRING
307
+
308
+ "#{rendered[0, INSPECT_MAX_STRING]}…(+#{rendered.length - INSPECT_MAX_STRING} chars)"
309
+ end
310
+
311
+ # Printing must never raise (it runs inside loggers and `puts`), so an
312
+ # object whose #inspect raises, or a BasicObject without one, falls back
313
+ # to a placeholder.
314
+ def inspect_leaf(value)
315
+ return "#<#{value.class}>" if kernel_inspect_only?(value)
316
+
317
+ inspect_truncated_text(value.inspect)
318
+ rescue StandardError
319
+ begin
320
+ "#<#{value.class}>"
321
+ rescue StandardError
322
+ '#<?>'
323
+ end
324
+ end
325
+
326
+ def kernel_inspect_only?(value)
327
+ Kernel.instance_method(:method).bind_call(value, :inspect).owner == Kernel
328
+ rescue TypeError # not a Kernel object: BasicObject, Delegator
329
+ false
330
+ end
331
+
95
332
  # Allow camelCase attribute access
96
333
  def method_missing(method_name, ...)
97
334
  normalized = normalize_name(method_name)
@@ -163,6 +400,10 @@ module ClaudeAgentSDK
163
400
  # Text content block
164
401
  class TextBlock < Type
165
402
  attr_accessor :text
403
+
404
+ def to_s
405
+ text.to_s
406
+ end
166
407
  end
167
408
 
168
409
  # Thinking content block
@@ -308,6 +549,22 @@ module ClaudeAgentSDK
308
549
  super
309
550
  @data ||= attributes if attributes.is_a?(Hash)
310
551
  end
552
+
553
+ def to_s
554
+ subtype.nil? ? '[system]' : "[system: #{subtype}]"
555
+ end
556
+
557
+ private
558
+
559
+ # A typed subclass (InitMessage, TaskStartedMessage, ...) already exposes
560
+ # the fields of its raw frame as attributes; repeating @data would double
561
+ # the output. A bare SystemMessage (unrecognized subtype) keeps it, since
562
+ # @data is the only place its payload lives.
563
+ def inspect_attributes
564
+ return super if instance_of?(SystemMessage)
565
+
566
+ super.reject { |pair| pair.first == 'data' }
567
+ end
311
568
  end
312
569
 
313
570
  # Init system message (emitted at session start and after /clear)
@@ -688,6 +945,19 @@ module ClaudeAgentSDK
688
945
  # @return [Hash{Symbol => Object}, nil]
689
946
  # @see UserMessage#origin
690
947
  attr_accessor :origin
948
+
949
+ # One human-readable line, e.g. `[result: success, 3 turns, 4.2s, $0.0120]`
950
+ # (parts the CLI did not report are left out). An error result appends its
951
+ # `errors`. Use #inspect for every field.
952
+ def to_s
953
+ parts = [subtype].compact
954
+ parts << "#{num_turns} #{num_turns == 1 ? 'turn' : 'turns'}" unless num_turns.nil?
955
+ parts << format('%.1fs', duration_ms / 1000.0) if duration_ms.is_a?(Numeric)
956
+ parts << format('$%.4f', total_cost_usd) if total_cost_usd.is_a?(Numeric)
957
+ line = parts.empty? ? '[result]' : "[result: #{parts.join(', ')}]"
958
+ line += " - #{Array(errors).join('; ')}" if is_error && !Array(errors).empty?
959
+ line
960
+ end
691
961
  end
692
962
 
693
963
  # Stream event for partial message updates
@@ -785,13 +1055,16 @@ module ClaudeAgentSDK
785
1055
  # `display` controls how thinking content appears in responses. Valid values
786
1056
  # are `"summarized"` (plaintext summary) and `"omitted"` (empty thinking
787
1057
  # field, signature only). Defaults are model-dependent: Opus 4.6/Sonnet 4.6
788
- # default to `"summarized"`; Opus 4.7 and Mythos Preview default to
789
- # `"omitted"`. Pass `display: "summarized"` explicitly on Opus 4.7 to get
1058
+ # default to `"summarized"`; Opus 4.7 and every later model default to
1059
+ # `"omitted"`. Pass `display: "summarized"` explicitly on those to get
790
1060
  # visible thinking text. Not supported with `ThinkingConfigDisabled`.
791
1061
  THINKING_DISPLAY_VALUES = %w[summarized omitted].freeze
792
1062
 
793
- # Adaptive thinking: uses a default budget of 32000 tokens
1063
+ # Adaptive thinking: the model decides when and how much to think
1064
+ # (sent as `--thinking adaptive`, no budget); control depth with `effort`.
794
1065
  class ThinkingConfigAdaptive < Type
1066
+ include Type::OptionValue
1067
+
795
1068
  attr_reader :type, :display
796
1069
 
797
1070
  def initialize(attributes = {})
@@ -816,6 +1089,8 @@ module ClaudeAgentSDK
816
1089
 
817
1090
  # Enabled thinking: uses a user-specified budget
818
1091
  class ThinkingConfigEnabled < Type
1092
+ include Type::OptionValue
1093
+
819
1094
  attr_accessor :budget_tokens
820
1095
  attr_reader :type, :display
821
1096
 
@@ -841,6 +1116,8 @@ module ClaudeAgentSDK
841
1116
 
842
1117
  # Disabled thinking: sets thinking tokens to 0
843
1118
  class ThinkingConfigDisabled < Type
1119
+ include Type::OptionValue
1120
+
844
1121
  attr_reader :type
845
1122
 
846
1123
  def initialize(attributes = {})
@@ -851,6 +1128,8 @@ module ClaudeAgentSDK
851
1128
 
852
1129
  # Agent definition configuration
853
1130
  class AgentDefinition < Type
1131
+ include Type::OptionValue
1132
+
854
1133
  attr_accessor :description, :prompt, :tools, :disallowed_tools, :model, :skills, :memory, :mcp_servers,
855
1134
  :initial_prompt, :max_turns, :background, :effort, :permission_mode
856
1135
  end
@@ -942,6 +1221,8 @@ module ClaudeAgentSDK
942
1221
 
943
1222
  # Hook matcher configuration
944
1223
  class HookMatcher < Type
1224
+ include Type::OptionValue
1225
+
945
1226
  attr_accessor :matcher, :hooks, :timeout
946
1227
 
947
1228
  def initialize(attributes = {})
@@ -1640,9 +1921,13 @@ module ClaudeAgentSDK
1640
1921
 
1641
1922
  # MCP Server configurations
1642
1923
  class McpStdioServerConfig < Type
1924
+ include Type::OptionValue
1925
+
1643
1926
  attr_accessor :command, :args, :env
1644
1927
  attr_reader :type
1645
1928
 
1929
+ inspect_filtered :env
1930
+
1646
1931
  def initialize(attributes = {})
1647
1932
  super
1648
1933
  @type = 'stdio'
@@ -1657,9 +1942,13 @@ module ClaudeAgentSDK
1657
1942
  end
1658
1943
 
1659
1944
  class McpSSEServerConfig < Type
1945
+ include Type::OptionValue
1946
+
1660
1947
  attr_accessor :url, :headers
1661
1948
  attr_reader :type
1662
1949
 
1950
+ inspect_filtered :headers
1951
+
1663
1952
  def initialize(attributes = {})
1664
1953
  super
1665
1954
  @type = 'sse'
@@ -1673,9 +1962,13 @@ module ClaudeAgentSDK
1673
1962
  end
1674
1963
 
1675
1964
  class McpHttpServerConfig < Type
1965
+ include Type::OptionValue
1966
+
1676
1967
  attr_accessor :url, :headers
1677
1968
  attr_reader :type
1678
1969
 
1970
+ inspect_filtered :headers
1971
+
1679
1972
  def initialize(attributes = {})
1680
1973
  super
1681
1974
  @type = 'http'
@@ -1689,6 +1982,8 @@ module ClaudeAgentSDK
1689
1982
  end
1690
1983
 
1691
1984
  class McpSdkServerConfig < Type
1985
+ include Type::OptionValue
1986
+
1692
1987
  attr_accessor :name, :instance
1693
1988
  attr_reader :type
1694
1989
 
@@ -1704,6 +1999,8 @@ module ClaudeAgentSDK
1704
1999
 
1705
2000
  # SDK Plugin configuration
1706
2001
  class SdkPluginConfig < Type
2002
+ include Type::OptionValue
2003
+
1707
2004
  attr_accessor :path
1708
2005
  attr_reader :type
1709
2006
 
@@ -1719,6 +2016,8 @@ module ClaudeAgentSDK
1719
2016
 
1720
2017
  # Sandbox network configuration
1721
2018
  class SandboxNetworkConfig < Type
2019
+ include Type::OptionValue
2020
+
1722
2021
  attr_accessor :allowed_domains, :denied_domains, :allow_managed_domains_only,
1723
2022
  :allow_unix_sockets, :allow_all_unix_sockets, :allow_local_binding,
1724
2023
  :allow_mach_lookup, :http_proxy_port, :socks_proxy_port
@@ -1740,6 +2039,8 @@ module ClaudeAgentSDK
1740
2039
 
1741
2040
  # Sandbox filesystem configuration
1742
2041
  class SandboxFilesystemConfig < Type
2042
+ include Type::OptionValue
2043
+
1743
2044
  attr_accessor :allow_write, :deny_write, :deny_read, :allow_read, :allow_managed_read_paths_only
1744
2045
 
1745
2046
  def to_h
@@ -1755,6 +2056,8 @@ module ClaudeAgentSDK
1755
2056
 
1756
2057
  # Sandbox settings for isolated command execution
1757
2058
  class SandboxSettings < Type
2059
+ include Type::OptionValue
2060
+
1758
2061
  attr_accessor :enabled, :fail_if_unavailable, :auto_allow_bash_if_sandboxed,
1759
2062
  :excluded_commands, :allow_unsandboxed_commands, :network, :filesystem,
1760
2063
  :ignore_violations, :enable_weaker_nested_sandbox,
@@ -1786,6 +2089,8 @@ module ClaudeAgentSDK
1786
2089
  # When set, the model is made aware of its remaining token budget so it can
1787
2090
  # pace tool use and wrap up before the limit.
1788
2091
  class TaskBudget < Type
2092
+ include Type::OptionValue
2093
+
1789
2094
  attr_accessor :total
1790
2095
 
1791
2096
  def to_h
@@ -1795,6 +2100,8 @@ module ClaudeAgentSDK
1795
2100
 
1796
2101
  # System prompt file configuration — loads system prompt from a file path
1797
2102
  class SystemPromptFile < Type
2103
+ include Type::OptionValue
2104
+
1798
2105
  attr_accessor :path
1799
2106
  attr_reader :type
1800
2107
 
@@ -1822,6 +2129,8 @@ module ClaudeAgentSDK
1822
2129
  # before 2.1.265 a session with an +append+ prompt recorded it only when
1823
2130
  # +snapshot+ was true. Older CLIs silently ignore it.
1824
2131
  class SystemPromptPreset < Type
2132
+ include Type::OptionValue
2133
+
1825
2134
  attr_reader :type
1826
2135
  attr_accessor :preset, :append, :exclude_dynamic_sections, :snapshot
1827
2136
 
@@ -1844,6 +2153,8 @@ module ClaudeAgentSDK
1844
2153
  # (+--system-prompt <prompt>+); the object form exists so +snapshot+ can be
1845
2154
  # set alongside it (see SystemPromptPreset#snapshot for its semantics).
1846
2155
  class SystemPromptCustom < Type
2156
+ include Type::OptionValue
2157
+
1847
2158
  attr_reader :type
1848
2159
  attr_accessor :prompt, :snapshot
1849
2160
 
@@ -1861,6 +2172,8 @@ module ClaudeAgentSDK
1861
2172
 
1862
2173
  # Tools preset configuration
1863
2174
  class ToolsPreset < Type
2175
+ include Type::OptionValue
2176
+
1864
2177
  attr_reader :type
1865
2178
  attr_accessor :preset
1866
2179
 
@@ -1876,6 +2189,9 @@ module ClaudeAgentSDK
1876
2189
 
1877
2190
  # Claude Agent Options for configuring queries
1878
2191
  class ClaudeAgentOptions < Type
2192
+ # `env` routinely carries credentials (ANTHROPIC_API_KEY, ...).
2193
+ inspect_filtered :env
2194
+
1879
2195
  attr_accessor :allowed_tools, :system_prompt, :mcp_servers, :permission_mode,
1880
2196
  :resume, :resume_session_at, :session_id, :max_turns, :disallowed_tools,
1881
2197
  :model, :permission_prompt_tool_name, :cwd, :cli_path, :settings,
@@ -1955,14 +2271,16 @@ module ClaudeAgentSDK
1955
2271
 
1956
2272
  def dup_with(**changes)
1957
2273
  new_options = self.dup
1958
- # A shallow #dup shares nested containers, so mutating a derived copy
1959
- # (e.g. `variant.allowed_tools << 'Bash'`) would bleed into the base and
1960
- # every sibling — including the security-relevant allow/deny lists.
1961
- # Deep-dup Hash/Array containers only; non-container values (procs,
1962
- # SDK MCP server instances, store adapters) must keep their identity.
2274
+ # A shallow #dup shares nested containers and typed option values, so
2275
+ # mutating a derived copy (e.g. `variant.allowed_tools << 'Bash'` or
2276
+ # `variant.sandbox.enabled = false`) would bleed into the base and every
2277
+ # sibling — including the security-relevant allow/deny lists and sandbox
2278
+ # rules. Deep-dup Hash/Array containers and option value types (Type#
2279
+ # dup_for_options, including those nested inside containers such as
2280
+ # agents[:x]); every other leaf (procs, SDK MCP server instances, store
2281
+ # adapters) keeps its identity.
1963
2282
  new_options.instance_variables.each do |ivar|
1964
- value = new_options.instance_variable_get(ivar)
1965
- new_options.instance_variable_set(ivar, deep_dup_containers(value)) if value.is_a?(Hash) || value.is_a?(Array)
2283
+ new_options.instance_variable_set(ivar, Type.deep_dup_for_options(new_options.instance_variable_get(ivar)))
1966
2284
  end
1967
2285
  changes.each { |key, value| new_options[key] = value }
1968
2286
  new_options
@@ -2120,13 +2438,17 @@ module ClaudeAgentSDK
2120
2438
  # A callable receiving a zero-arg invocation; it MUST call it and
2121
2439
  # return its value:
2122
2440
  #
2123
- # callback_wrapper: ->(invocation) { Rails.application.executor.wrap { invocation.call } }
2441
+ # callback_wrapper: ->(invocation) { MyApm.trace('agent.callback') { invocation.call } }
2124
2442
  #
2125
2443
  # The wrapper runs on the same execution context as the callback —
2126
- # inside the worker thread in :thread mode (so executor.wrap checks AR
2127
- # connections back in when the callback ends), in place on the reactor
2444
+ # inside the worker thread in :thread mode, in place on the reactor
2128
2445
  # fiber in :inline mode. Exceptions propagate through it unchanged; it
2129
2446
  # must not swallow them. Default nil (no wrapping).
2447
+ #
2448
+ # Rails apps: use ClaudeAgentSDK::Railtie.callback_wrapper, which runs
2449
+ # callbacks in the Rails executor (AR connections check back in when the
2450
+ # callback ends). A bare `Rails.application.executor.wrap` deadlocks
2451
+ # under development code reloading in :thread mode.
2130
2452
  def callback_wrapper=(value)
2131
2453
  raise ArgumentError, "callback_wrapper must be a callable or nil (got #{value.inspect})" unless value.nil? || value.respond_to?(:call)
2132
2454
 
@@ -2160,12 +2482,20 @@ module ClaudeAgentSDK
2160
2482
  defaults = ClaudeAgentSDK.default_options
2161
2483
  return attributes unless defaults.any?
2162
2484
 
2163
- # Start from configured defaults. Container values are recursively
2164
- # duped so per-instance mutation (options.allowed_tools << 'Bash')
2165
- # can never corrupt the global defaults; non-container leaves
2166
- # (Strings, Procs, SdkMcpServer instances) intentionally keep identity.
2485
+ # Start from configured defaults. Container values, typed option
2486
+ # values (SandboxSettings, SystemPromptPreset, AgentDefinition, ...)
2487
+ # and mutable Strings are recursively copied (Type.deep_dup_for_options)
2488
+ # so per-instance mutation (options.allowed_tools << 'Bash',
2489
+ # options.sandbox.enabled = false) can never corrupt the global
2490
+ # defaults or reach another session; other leaves (frozen Strings,
2491
+ # Procs, SdkMcpServer instances, store adapters) intentionally keep
2492
+ # identity. The stored defaults are a frozen snapshot
2493
+ # (Configuration#default_options=), and the copy is what makes each
2494
+ # session's containers and values mutable again — its Strings stay the
2495
+ # snapshot's frozen ones, so `options.model << 'x'` fails loudly rather
2496
+ # than reaching other sessions; reassign instead.
2167
2497
  result = {}
2168
- defaults.each { |key, value| result[option_key(key)] = deep_dup_containers(value) }
2498
+ defaults.each { |key, value| result[option_key(key)] = Type.deep_dup_for_options(value) }
2169
2499
  attributes.each do |key, value|
2170
2500
  key = option_key(key)
2171
2501
  default_val = result[key]
@@ -2187,26 +2517,6 @@ module ClaudeAgentSDK
2187
2517
  normalized = normalize_name(name)
2188
2518
  respond_to?(:"#{normalized}=") ? normalized.to_sym : name
2189
2519
  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
2520
  end
2211
2521
 
2212
2522
  # SDK MCP Tool definition
@@ -1,5 +1,5 @@
1
1
  # frozen_string_literal: true
2
2
 
3
3
  module ClaudeAgentSDK
4
- VERSION = '0.33.1'
4
+ VERSION = '0.35.0'
5
5
  end