claude-agent-sdk 0.35.0 → 0.37.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (49) hide show
  1. checksums.yaml +4 -4
  2. data/CHANGELOG.md +74 -0
  3. data/README.md +17 -8
  4. data/docs/cli-installer.md +16 -2
  5. data/docs/client.md +44 -4
  6. data/docs/errors.md +15 -1
  7. data/docs/hooks-and-permissions.md +27 -3
  8. data/docs/mcp-servers.md +36 -7
  9. data/docs/rails.md +3 -4
  10. data/docs/sessions.md +149 -34
  11. data/docs/types.md +106 -4
  12. data/lib/claude_agent_sdk/cancellation_signal.rb +2 -1
  13. data/lib/claude_agent_sdk/cli_installer.rb +68 -11
  14. data/lib/claude_agent_sdk/command_builder.rb +109 -98
  15. data/lib/claude_agent_sdk/deprecation.rb +90 -0
  16. data/lib/claude_agent_sdk/errors.rb +8 -0
  17. data/lib/claude_agent_sdk/fiber_boundary.rb +113 -2
  18. data/lib/claude_agent_sdk/instrumentation/otel.rb +15 -7
  19. data/lib/claude_agent_sdk/message_parser.rb +23 -9
  20. data/lib/claude_agent_sdk/observer.rb +2 -1
  21. data/lib/claude_agent_sdk/option_warnings.rb +2 -2
  22. data/lib/claude_agent_sdk/query.rb +99 -51
  23. data/lib/claude_agent_sdk/railtie.rb +14 -3
  24. data/lib/claude_agent_sdk/sdk_mcp_server.rb +58 -38
  25. data/lib/claude_agent_sdk/session_mutations.rb +28 -16
  26. data/lib/claude_agent_sdk/session_resume.rb +39 -35
  27. data/lib/claude_agent_sdk/session_store.rb +35 -21
  28. data/lib/claude_agent_sdk/session_summary.rb +12 -5
  29. data/lib/claude_agent_sdk/sessions.rb +112 -24
  30. data/lib/claude_agent_sdk/streaming.rb +1 -1
  31. data/lib/claude_agent_sdk/subprocess_cli_transport.rb +75 -52
  32. data/lib/claude_agent_sdk/tasks/claude_agent_sdk.rake +12 -5
  33. data/lib/claude_agent_sdk/testing/session_store_conformance.rb +15 -11
  34. data/lib/claude_agent_sdk/transcript_mirror_batcher.rb +19 -1
  35. data/lib/claude_agent_sdk/types/attributes.rb +271 -0
  36. data/lib/claude_agent_sdk/types/base.rb +320 -0
  37. data/lib/claude_agent_sdk/types/content_blocks.rb +57 -0
  38. data/lib/claude_agent_sdk/types/hooks.rb +640 -0
  39. data/lib/claude_agent_sdk/types/mcp.rb +232 -0
  40. data/lib/claude_agent_sdk/types/messages.rb +614 -0
  41. data/lib/claude_agent_sdk/types/option_values.rb +302 -0
  42. data/lib/claude_agent_sdk/types/options.rb +352 -0
  43. data/lib/claude_agent_sdk/types/permissions.rb +107 -0
  44. data/lib/claude_agent_sdk/types/sessions.rb +10 -0
  45. data/lib/claude_agent_sdk/types.rb +13 -2534
  46. data/lib/claude_agent_sdk/version.rb +1 -1
  47. data/lib/claude_agent_sdk.rb +308 -73
  48. data/lib/generators/claude_agent_sdk/install/templates/claude_agent_sdk.rb.tt +3 -3
  49. metadata +12 -1
@@ -0,0 +1,271 @@
1
+ # frozen_string_literal: true
2
+
3
+ require_relative '../deprecation'
4
+
5
+ module ClaudeAgentSDK
6
+ # Attribute declarations and the rules #[], #[]=, .new and the camelCase
7
+ # readers follow (issue #126). Loaded by base.rb right after Type itself,
8
+ # before any subclass declares an attribute.
9
+ class Type
10
+ # The 1.0 switch for attribute access, false through 0.x. When true:
11
+ # an unknown key on a strict type (see .strict_attributes) raises
12
+ # ArgumentError, and #[], #[]= and the camelCase readers reach declared
13
+ # attributes only. While false, both cases warn once per class and name.
14
+ ENFORCE_ATTRIBUTES = false
15
+
16
+ LENIENT_KEY = :__claude_agent_sdk_lenient_attributes
17
+ private_constant :ENFORCE_ATTRIBUTES, :LENIENT_KEY
18
+
19
+ class << self
20
+ # attr_accessor / attr_reader / attr_writer also declare the names as
21
+ # attributes (see .attribute_names). Inherited by subclasses.
22
+ #
23
+ # @api private
24
+ def attr_accessor(*names)
25
+ declare_attributes(*names)
26
+ super
27
+ end
28
+
29
+ # @api private
30
+ def attr_reader(*names)
31
+ declare_attributes(*names)
32
+ super
33
+ end
34
+
35
+ # @api private
36
+ def attr_writer(*names)
37
+ declare_attributes(*names)
38
+ super
39
+ end
40
+
41
+ private
42
+
43
+ def own_attribute_names
44
+ @own_attribute_names ||= Set.new
45
+ end
46
+ end
47
+
48
+ # Declares hand-written readers or writers (a method not generated by
49
+ # attr_*, such as a backward-compatible reader) as attributes, so #[],
50
+ # #[]= and the camelCase readers reach them.
51
+ #
52
+ # @api private
53
+ def self.declare_attributes(*names)
54
+ names.each { |name| own_attribute_names << name.to_s }
55
+ end
56
+
57
+ # Whether +name+ (snake_case String) is a declared attribute of this
58
+ # class or an ancestor.
59
+ #
60
+ # @api private
61
+ def self.attribute?(name)
62
+ own_attribute_names.include?(name) || (superclass <= Type && superclass.attribute?(name))
63
+ end
64
+
65
+ # Whether +method_name+ (a normalized reader, writer or predicate:
66
+ # `session_id`, `session_id=`, `fork_session?`) accesses a declared
67
+ # attribute.
68
+ #
69
+ # @api private
70
+ def self.attribute_method?(method_name)
71
+ attribute?(method_name.delete_suffix('=').delete_suffix('?'))
72
+ end
73
+
74
+ # Per-class caches of the names, exactly as callers spell them (:sessionId,
75
+ # 'session_id', ...), that reached a declared attribute: name => method.
76
+ # Hits skip name normalization and the checks below. Only positives are
77
+ # cached: the registry only grows, so they never go stale (an answer that
78
+ # depends on user-defined methods is never cached). Bounded by attributes
79
+ # x spellings. Replaced, never mutated, so readers need no lock.
80
+ #
81
+ # @api private
82
+ def self.cached_attribute_reader(name)
83
+ @attribute_readers&.[](name)
84
+ end
85
+
86
+ # @api private
87
+ def self.cached_attribute_writer(name)
88
+ @attribute_writers&.[](name)
89
+ end
90
+
91
+ # @api private
92
+ def self.cache_attribute_reader(name, method_name)
93
+ @attribute_readers = (@attribute_readers || {}).merge(name => method_name).freeze
94
+ end
95
+
96
+ # @api private
97
+ def self.cache_attribute_writer(name, method_name)
98
+ @attribute_writers = (@attribute_writers || {}).merge(name => method_name).freeze
99
+ end
100
+
101
+ # Every declared attribute name (snake_case), sorted.
102
+ #
103
+ # @api private
104
+ def self.attribute_names
105
+ inherited = superclass <= Type ? superclass.attribute_names : []
106
+ (inherited | own_attribute_names.to_a).sort
107
+ end
108
+
109
+ # Declares a type the user constructs and passes IN (option values, hook
110
+ # matchers and outputs, permission results and updates). Constructing
111
+ # one directly (.new or #[]=) with a key that is not an attribute warns
112
+ # once per class and key — a typo would otherwise be dropped silently —
113
+ # and raises ArgumentError from 1.0. A read-only attribute (the +type+,
114
+ # +hook_event_name+ or +behavior+ discriminator a type sets itself, which
115
+ # its own #to_h emits) is accepted and ignored. Types the SDK parses from
116
+ # CLI output stay lenient so a newer CLI's extra fields never break an
117
+ # older SDK; so does every construction through .from_hash or .wrap.
118
+ # Inherited by subclasses.
119
+ #
120
+ # @api private
121
+ def self.strict_attributes
122
+ @strict_attributes = true
123
+ end
124
+
125
+ # @api private
126
+ def self.strict_attributes?
127
+ @strict_attributes || (superclass <= Type && superclass.strict_attributes?)
128
+ end
129
+
130
+ # Runs the block with the strict-attribute check off on this fiber, for
131
+ # the SDK's own parse paths (CLI payloads and their nested values).
132
+ def self.lenient
133
+ previous = Thread.current[LENIENT_KEY]
134
+ Thread.current[LENIENT_KEY] = true
135
+ yield
136
+ ensure
137
+ Thread.current[LENIENT_KEY] = previous
138
+ end
139
+ private_class_method :lenient
140
+
141
+ private
142
+
143
+ # Allow camelCase attribute access
144
+ def method_missing(method_name, ...)
145
+ target = self.class.cached_attribute_reader(method_name)
146
+ return public_send(target, ...) if target
147
+
148
+ normalized = normalize_name(method_name)
149
+ if normalized != method_name.to_s && respond_to?(normalized)
150
+ if self.class.attribute_method?(normalized)
151
+ self.class.cache_attribute_reader(method_name, normalized.to_sym)
152
+ return public_send(normalized, ...)
153
+ end
154
+ return public_send(normalized, ...) if reachable?(normalized, method_name, :camel_case)
155
+ end
156
+ super
157
+ end
158
+
159
+ def respond_to_missing?(method_name, include_private = false)
160
+ normalized = normalize_name(method_name)
161
+ (normalized != method_name.to_s && respond_to?(normalized) &&
162
+ (!ENFORCE_ATTRIBUTES || attribute_method?(normalized))) || super
163
+ end
164
+
165
+ def assign_attributes(attributes)
166
+ unless attributes.respond_to?(:each_pair)
167
+ raise ArgumentError,
168
+ "When assigning attributes, you must pass a hash as an argument, #{attributes.inspect} passed."
169
+ end
170
+
171
+ return if attributes.empty?
172
+
173
+ attributes.each_pair { |name, value| assign_attribute(name, value) }
174
+ end
175
+
176
+ def assign_attribute(name, value)
177
+ writer = self.class.cached_attribute_writer(name)
178
+ return public_send(writer, value) if writer
179
+
180
+ normalized = normalize_name(name)
181
+ setter = :"#{normalized}="
182
+ if respond_to?(setter)
183
+ if self.class.attribute?(normalized)
184
+ self.class.cache_attribute_writer(name, setter)
185
+ return public_send(setter, value)
186
+ end
187
+ return public_send(setter, value) if reachable?(setter.to_s, name, :write)
188
+ end
189
+ return unless self.class.strict_attributes? && !Thread.current[LENIENT_KEY] && !self.class.attribute?(normalized)
190
+ return if respond_to?(normalized) && user_defined_method?(normalized)
191
+
192
+ unknown_attribute(name, normalized)
193
+ end
194
+
195
+ def read_attribute(name)
196
+ reader = self.class.cached_attribute_reader(name)
197
+ return public_send(reader) if reader
198
+
199
+ getter = normalize_name(name)
200
+ return unless respond_to?(getter)
201
+
202
+ if self.class.attribute_method?(getter)
203
+ self.class.cache_attribute_reader(name, getter.to_sym)
204
+ public_send(getter)
205
+ elsif reachable?(getter, name, :read)
206
+ public_send(getter)
207
+ end
208
+ end
209
+
210
+ # Whether a normalized reader, writer or predicate name belongs to an
211
+ # attribute: a declared one (`session_id`, `session_id=`,
212
+ # `fork_session?`), or a method user code defined on its own subclass, a
213
+ # module it includes, or the object itself. Only methods the SDK, Ruby or
214
+ # another gem defines at Type or above (to_h, freeze, to_json, ...) are
215
+ # not attributes.
216
+ def attribute_method?(method_name)
217
+ self.class.attribute_method?(method_name) || user_defined_method?(method_name)
218
+ end
219
+
220
+ # Not memoized: user code can define methods at any time.
221
+ def user_defined_method?(method_name)
222
+ owner = method(method_name).owner
223
+ return false if sdk_module?(owner)
224
+
225
+ ancestors = self.class.ancestors
226
+ index = ancestors.index(owner)
227
+ index.nil? || index < ancestors.index(Type)
228
+ rescue NameError
229
+ false
230
+ end
231
+
232
+ def sdk_module?(mod)
233
+ name = mod.name
234
+ !name.nil? && (name == 'ClaudeAgentSDK' || name.start_with?('ClaudeAgentSDK::'))
235
+ end
236
+
237
+ # #[], #[]= (and so .new) or a camelCase call resolved to +method_name+, a
238
+ # public method. An attribute passes. Any other method (to_h, freeze, ...)
239
+ # passes with a warning once per class and name through 0.x, and is
240
+ # treated as undefined from 1.0.
241
+ def reachable?(method_name, name, access)
242
+ return true if attribute_method?(method_name)
243
+ return false if ENFORCE_ATTRIBUTES
244
+
245
+ class_name = self.class.name || self.class.inspect
246
+ message = case access
247
+ when :read then "#{class_name}#[]: #{name.inspect} is not an attribute; " \
248
+ 'Type#[] will only read attributes in 1.0'
249
+ when :write then "#{class_name}#[]=: #{name.inspect} is not an attribute; " \
250
+ 'Type#[]= and .new will only write attributes in 1.0'
251
+ else "#{class_name}##{name}: #{method_name} is not an attribute; " \
252
+ 'camelCase methods will only reach attributes in 1.0'
253
+ end
254
+ Deprecation.warn_once_at_caller([:non_attribute, self.class, method_name], message)
255
+ true
256
+ end
257
+
258
+ def unknown_attribute(name, normalized)
259
+ klass = self.class
260
+ class_name = klass.name || klass.inspect
261
+ known = klass.attribute_names.join(', ')
262
+ raise ArgumentError, "#{class_name}: unknown attribute #{name.inspect} (known: #{known})" if ENFORCE_ATTRIBUTES
263
+
264
+ Deprecation.warn_once_at_caller(
265
+ [:unknown_attribute, klass, normalized],
266
+ "#{class_name}: unknown attribute #{name.inspect} ignored; " \
267
+ "this will raise ArgumentError in 1.0 (known: #{known})"
268
+ )
269
+ end
270
+ end
271
+ end
@@ -0,0 +1,320 @@
1
+ # frozen_string_literal: true
2
+
3
+ module ClaudeAgentSDK
4
+ # Base class for all types.
5
+ class Type
6
+ # Lenient, like every parse path: never warns or raises on an unknown key.
7
+ def self.wrap(object)
8
+ return object if object.is_a?(self)
9
+ return nil if object.nil?
10
+
11
+ lenient { new(object) }
12
+ end
13
+
14
+ # Lenient, like every parse path: never warns or raises on an unknown key.
15
+ def self.from_hash(hash)
16
+ return unless hash.is_a?(Hash)
17
+
18
+ lenient { new(hash) }
19
+ end
20
+
21
+ def initialize(attributes = {})
22
+ assign_attributes(attributes) if attributes
23
+ super()
24
+ end
25
+
26
+ def [](name)
27
+ read_attribute(name)
28
+ end
29
+
30
+ def []=(name, value)
31
+ assign_attribute(name, value)
32
+ end
33
+
34
+ # Subclasses should override this to return a hash representation of the object.
35
+ def to_h
36
+ {}
37
+ end
38
+
39
+ # The copy hook used wherever ClaudeAgentOptions are copied (dup_with and
40
+ # the configured-defaults merge). Identity by default: most Type instances
41
+ # are messages or callback payloads that never live inside options, and a
42
+ # user-supplied object that does (an observer, a store adapter) must stay
43
+ # the same object. Option VALUE types include OptionValue to opt in to
44
+ # copying, so a per-session change to e.g. sandbox rules can never reach
45
+ # another session or the configured defaults.
46
+ #
47
+ # @api private
48
+ def dup_for_options
49
+ self
50
+ end
51
+
52
+ # Mixed into the mutable value types that ClaudeAgentOptions holds
53
+ # (SandboxSettings, SystemPromptPreset, AgentDefinition, ...). The copy
54
+ # recurses into the value's own state with Type.deep_dup_for_options, so
55
+ # nested containers and nested value types (SandboxSettings#network) are
56
+ # copied too while identity leaves (McpSdkServerConfig#instance, the
57
+ # callables in HookMatcher#hooks) stay shared. #dup never copies frozen
58
+ # state, so a copy of a frozen value (the configured-defaults snapshot) is
59
+ # mutable.
60
+ #
61
+ # @api private
62
+ module OptionValue
63
+ def dup_for_options
64
+ copy = dup
65
+ copy.instance_variables.each do |ivar|
66
+ copy.instance_variable_set(ivar, Type.deep_dup_for_options(copy.instance_variable_get(ivar)))
67
+ end
68
+ copy
69
+ end
70
+ end
71
+
72
+ # Recurse into Hash/Array containers and option value types, and copy
73
+ # mutable (unfrozen) Strings — a caller-built prompt, model or
74
+ # allowed_tools entry is as much shared state as an Array, and `str <<
75
+ # 'x'` on one copy would otherwise change every other. A frozen String
76
+ # (any literal under frozen_string_literal) is immutable and keeps
77
+ # identity. Every other leaf keeps object identity (observer factories,
78
+ # callbacks, SDK MCP server instances must not be duped). Rebuild
79
+ # containers via dup.clear (never Hash#to_h / Array#map) to preserve
80
+ # container SUBCLASSES: to_h flattens e.g. Rails'
81
+ # HashWithIndifferentAccess into a plain Hash, silently breaking symbol
82
+ # lookups on the copy (config[:type] == 'sdk' → nil). Hash keys: Ruby
83
+ # already stores a dup'd, frozen copy of an unfrozen plain String key, but
84
+ # not of a String SUBCLASS key, so those are copied (and frozen, as keys
85
+ # should be) here. A compare_by_identity Hash is left keyed by the
86
+ # caller's objects: copying a key would break the caller's own lookups.
87
+ #
88
+ # @api private
89
+ def self.deep_dup_for_options(value)
90
+ case value
91
+ when Hash
92
+ copy = value.dup.clear
93
+ value.each { |k, v| copy[option_hash_key(k, value)] = deep_dup_for_options(v) }
94
+ copy
95
+ when Array
96
+ copy = value.dup.clear
97
+ value.each { |v| copy << deep_dup_for_options(v) }
98
+ copy
99
+ when Type then value.dup_for_options
100
+ when String then value.frozen? ? value : value.dup # #dup keeps subclass and encoding
101
+ else value
102
+ end
103
+ end
104
+
105
+ def self.option_hash_key(key, hash)
106
+ return key unless key.is_a?(String) && !key.frozen? && !hash.compare_by_identity?
107
+
108
+ key.dup.freeze
109
+ end
110
+ private_class_method :option_hash_key
111
+
112
+ # Bounded, human-oriented #inspect listing the non-nil instance variables
113
+ # in definition order:
114
+ #
115
+ # #<ClaudeAgentSDK::ResultMessage subtype="success" num_turns=3 ...>
116
+ #
117
+ # Messages carry whole transcripts, tool payloads and usage maps, so the
118
+ # output is bounded rather than faithful: long Strings are truncated,
119
+ # long Arrays/Hashes abbreviated, and nesting past INSPECT_MAX_DEPTH (or
120
+ # a reference cycle) collapses to a placeholder. Other objects keep their
121
+ # own #inspect (truncated) unless they only have Kernel#inspect, which
122
+ # dumps every ivar recursively — those (SDK MCP server instances, store
123
+ # adapters, observers) show as `#<ClassName>`. For display only: nothing
124
+ # sent to the CLI goes through #inspect or #to_s (wire output uses #to_h).
125
+ def inspect
126
+ inspect_with(0, {}.compare_by_identity)
127
+ end
128
+
129
+ # Object#to_s ignores instance variables, so `puts message` would print
130
+ # only a class name and an address. Types with a natural textual form
131
+ # (UserMessage, AssistantMessage, TextBlock, ResultMessage, SystemMessage)
132
+ # override this.
133
+ def to_s
134
+ inspect
135
+ end
136
+
137
+ # Declares attributes that carry credentials (env vars, auth headers).
138
+ # Objects get logged, so #inspect shows them filtered; #to_h and
139
+ # everything sent to the CLI are unaffected. Inherited by subclasses.
140
+ #
141
+ # @api private
142
+ def self.inspect_filtered(*names)
143
+ @inspect_filtered_attributes = (inspect_filtered_attributes + names.map(&:to_s)).uniq.freeze
144
+ end
145
+
146
+ # @api private
147
+ def self.inspect_filtered_attributes
148
+ @inspect_filtered_attributes || (superclass <= Type ? superclass.inspect_filtered_attributes : [].freeze)
149
+ end
150
+
151
+ INSPECT_MAX_STRING = 80
152
+ INSPECT_MAX_ITEMS = 5
153
+ INSPECT_MAX_DEPTH = 2
154
+ private_constant :INSPECT_MAX_STRING, :INSPECT_MAX_ITEMS, :INSPECT_MAX_DEPTH
155
+
156
+ protected
157
+
158
+ # `seen` holds the Types/containers on the current rendering path (not
159
+ # every one rendered so far), so a shared-but-acyclic value still renders
160
+ # in full wherever it appears.
161
+ def inspect_with(depth, seen)
162
+ return "#<#{inspect_class_name} …>" if depth > INSPECT_MAX_DEPTH || seen.key?(self)
163
+
164
+ seen[self] = true
165
+ begin
166
+ attributes = inspect_attributes.map do |name, value|
167
+ " #{name}=#{inspect_bounded(value, depth + 1, seen)}"
168
+ end
169
+ "#<#{inspect_class_name}#{attributes.join}>"
170
+ ensure
171
+ seen.delete(self)
172
+ end
173
+ end
174
+
175
+ private
176
+
177
+ # [name, value] pairs rendered by #inspect. Subclasses override to hide
178
+ # redundant state or redact secrets — never by mutating the object.
179
+ def inspect_attributes
180
+ filtered = self.class.inspect_filtered_attributes
181
+ instance_variables.filter_map do |ivar|
182
+ value = instance_variable_get(ivar)
183
+ next if value.nil?
184
+
185
+ name = ivar.to_s.delete_prefix('@')
186
+ [name, filtered.include?(name) ? inspect_filter(value) : value]
187
+ end
188
+ end
189
+
190
+ # A credential-bearing Hash keeps its keys (useful when debugging which
191
+ # variables are set) with every value replaced; anything else is replaced
192
+ # outright. Builds a new Hash; the object itself is never touched.
193
+ def inspect_filter(value)
194
+ value.respond_to?(:each_key) ? value.each_key.to_h { |key| [key, '[FILTERED]'] } : '[FILTERED]'
195
+ end
196
+
197
+ def inspect_class_name
198
+ self.class.name || self.class.inspect
199
+ end
200
+
201
+ def inspect_bounded(value, depth, seen)
202
+ case value
203
+ when Type then value.inspect_with(depth, seen)
204
+ when String then inspect_truncated(value)
205
+ when Array then inspect_container(value, '[', ']', depth, seen) { |item| inspect_bounded(item, depth + 1, seen) }
206
+ when Hash
207
+ inspect_container(value, '{', '}', depth, seen) do |key, item|
208
+ "#{inspect_hash_key(key, depth + 1, seen)}#{inspect_bounded(item, depth + 1, seen)}"
209
+ end
210
+ when Proc, Method, UnboundMethod then inspect_callable(value)
211
+ else inspect_leaf(value)
212
+ end
213
+ end
214
+
215
+ def inspect_container(value, open, close, depth, seen, &)
216
+ return "#{open}#{close}" if value.empty?
217
+ return "#{open}…(#{value.size})#{close}" if depth > INSPECT_MAX_DEPTH || seen.key?(value)
218
+
219
+ seen[value] = true
220
+ begin
221
+ parts = value.first(INSPECT_MAX_ITEMS).map(&)
222
+ parts << "…(+#{value.size - INSPECT_MAX_ITEMS} more)" if value.size > INSPECT_MAX_ITEMS
223
+ "#{open}#{parts.join(', ')}#{close}"
224
+ ensure
225
+ seen.delete(value)
226
+ end
227
+ end
228
+
229
+ # Rendered by hand rather than via Hash#inspect, whose format differs
230
+ # between Ruby 3.3 (`{:a=>1}`) and 3.4 (`{a: 1}`).
231
+ def inspect_hash_key(key, depth, seen)
232
+ return "#{key.name}: " if key.is_a?(Symbol) && key.inspect.match?(/\A:\w+[?!]?\z/)
233
+
234
+ "#{inspect_bounded(key, depth, seen)} => "
235
+ end
236
+
237
+ def inspect_truncated(string)
238
+ return string.inspect if string.length <= INSPECT_MAX_STRING
239
+
240
+ "#{string[0, INSPECT_MAX_STRING].inspect}…(+#{string.length - INSPECT_MAX_STRING} chars)"
241
+ end
242
+
243
+ # Callbacks (can_use_tool, hooks, callback_wrapper, ...) are user-supplied:
244
+ # render them from source_location rather than their own #inspect, which
245
+ # a subclass may override (and raise from) and which embeds an absolute
246
+ # path — `#<Proc(lambda) permissions.rb:17>`, `#<Method Policy#call>`.
247
+ def inspect_callable(value)
248
+ label = if value.is_a?(Proc)
249
+ value.lambda? ? 'Proc(lambda)' : 'Proc'
250
+ else
251
+ "#{value.class.name} #{value.owner.name || value.owner.inspect}##{value.name}"
252
+ end
253
+ file, line = value.source_location
254
+ rendered = file ? "#<#{label} #{File.basename(file)}:#{line}>" : "#<#{label}>"
255
+ inspect_truncated_text(rendered)
256
+ rescue StandardError
257
+ inspect_leaf(value)
258
+ end
259
+
260
+ def inspect_truncated_text(rendered)
261
+ return rendered if rendered.length <= INSPECT_MAX_STRING
262
+
263
+ "#{rendered[0, INSPECT_MAX_STRING]}…(+#{rendered.length - INSPECT_MAX_STRING} chars)"
264
+ end
265
+
266
+ # Printing must never raise (it runs inside loggers and `puts`), so an
267
+ # object whose #inspect raises, or a BasicObject without one, falls back
268
+ # to a placeholder.
269
+ def inspect_leaf(value)
270
+ return "#<#{value.class}>" if kernel_inspect_only?(value)
271
+
272
+ inspect_truncated_text(value.inspect)
273
+ rescue StandardError
274
+ begin
275
+ "#<#{value.class}>"
276
+ rescue StandardError
277
+ '#<?>'
278
+ end
279
+ end
280
+
281
+ def kernel_inspect_only?(value)
282
+ Kernel.instance_method(:method).bind_call(value, :inspect).owner == Kernel
283
+ rescue TypeError # not a Kernel object: BasicObject, Delegator
284
+ false
285
+ end
286
+
287
+ def normalize_name(name)
288
+ name = name.to_s.dup
289
+ name.gsub!(/(?<=[A-Z])(?=[A-Z][a-z])|(?<=[a-z\d])(?=[A-Z])/, '_')
290
+ name.tr!('-', '_')
291
+ name.downcase!
292
+ name
293
+ end
294
+
295
+ FALSE_VALUES = [
296
+ false, 0,
297
+ '0', :'0',
298
+ 'f', :f,
299
+ 'F', :F,
300
+ 'false', :false, # rubocop:disable Lint/BooleanSymbol
301
+ 'FALSE', :FALSE,
302
+ 'off', :off,
303
+ 'OFF', :OFF
304
+ ].to_set.freeze
305
+
306
+ private_constant :FALSE_VALUES
307
+
308
+ def coerce_boolean(value)
309
+ return if value.nil?
310
+
311
+ if value == ''
312
+ nil
313
+ else
314
+ !FALSE_VALUES.include?(value)
315
+ end
316
+ end
317
+ end
318
+ end
319
+
320
+ require_relative 'attributes'
@@ -0,0 +1,57 @@
1
+ # frozen_string_literal: true
2
+
3
+ require_relative 'base'
4
+
5
+ module ClaudeAgentSDK
6
+ # Content Blocks
7
+
8
+ # Text content block
9
+ class TextBlock < Type
10
+ attr_accessor :text
11
+
12
+ def to_s
13
+ text.to_s
14
+ end
15
+ end
16
+
17
+ # Thinking content block
18
+ class ThinkingBlock < Type
19
+ attr_accessor :thinking, :signature
20
+ end
21
+
22
+ # Tool use content block
23
+ class ToolUseBlock < Type
24
+ attr_accessor :id, :name, :input
25
+ end
26
+
27
+ # Tool result content block
28
+ class ToolResultBlock < Type
29
+ attr_accessor :tool_use_id, :content, :is_error
30
+ end
31
+
32
+ # Server-side tool use (CLI's built-in tools that execute server-side
33
+ # rather than as MCP tools — advisor, web_search, code_execution, etc.).
34
+ # Mirrors Python's `ServerToolUseBlock`.
35
+ class ServerToolUseBlock < Type
36
+ attr_accessor :id, :name, :input
37
+ end
38
+
39
+ # Result of a server-side tool execution. Mirrors Python's
40
+ # `ServerToolResultBlock`.
41
+ class ServerToolResultBlock < Type
42
+ attr_accessor :tool_use_id, :content, :is_error
43
+ end
44
+
45
+ # Generic content block for types the SDK doesn't explicitly handle (e.g., "document", "image").
46
+ # Preserves the raw hash data for forward compatibility with newer CLI versions.
47
+ class UnknownBlock < Type
48
+ attr_accessor :type, :data
49
+ end
50
+
51
+ # Deferred tool use, emitted on `ResultMessage` when a PreToolUse hook
52
+ # returned `permissionDecision: "defer"`. The session can be resumed later
53
+ # to execute the deferred call. Mirrors Python's `DeferredToolUse`.
54
+ class DeferredToolUse < Type
55
+ attr_accessor :id, :name, :input
56
+ end
57
+ end