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.
- checksums.yaml +4 -4
- data/CHANGELOG.md +74 -0
- data/README.md +17 -8
- data/docs/cli-installer.md +16 -2
- data/docs/client.md +44 -4
- data/docs/errors.md +15 -1
- data/docs/hooks-and-permissions.md +27 -3
- data/docs/mcp-servers.md +36 -7
- data/docs/rails.md +3 -4
- data/docs/sessions.md +149 -34
- data/docs/types.md +106 -4
- data/lib/claude_agent_sdk/cancellation_signal.rb +2 -1
- data/lib/claude_agent_sdk/cli_installer.rb +68 -11
- data/lib/claude_agent_sdk/command_builder.rb +109 -98
- data/lib/claude_agent_sdk/deprecation.rb +90 -0
- data/lib/claude_agent_sdk/errors.rb +8 -0
- data/lib/claude_agent_sdk/fiber_boundary.rb +113 -2
- data/lib/claude_agent_sdk/instrumentation/otel.rb +15 -7
- data/lib/claude_agent_sdk/message_parser.rb +23 -9
- data/lib/claude_agent_sdk/observer.rb +2 -1
- data/lib/claude_agent_sdk/option_warnings.rb +2 -2
- data/lib/claude_agent_sdk/query.rb +99 -51
- data/lib/claude_agent_sdk/railtie.rb +14 -3
- data/lib/claude_agent_sdk/sdk_mcp_server.rb +58 -38
- data/lib/claude_agent_sdk/session_mutations.rb +28 -16
- data/lib/claude_agent_sdk/session_resume.rb +39 -35
- data/lib/claude_agent_sdk/session_store.rb +35 -21
- data/lib/claude_agent_sdk/session_summary.rb +12 -5
- data/lib/claude_agent_sdk/sessions.rb +112 -24
- data/lib/claude_agent_sdk/streaming.rb +1 -1
- data/lib/claude_agent_sdk/subprocess_cli_transport.rb +75 -52
- data/lib/claude_agent_sdk/tasks/claude_agent_sdk.rake +12 -5
- data/lib/claude_agent_sdk/testing/session_store_conformance.rb +15 -11
- data/lib/claude_agent_sdk/transcript_mirror_batcher.rb +19 -1
- data/lib/claude_agent_sdk/types/attributes.rb +271 -0
- data/lib/claude_agent_sdk/types/base.rb +320 -0
- data/lib/claude_agent_sdk/types/content_blocks.rb +57 -0
- data/lib/claude_agent_sdk/types/hooks.rb +640 -0
- data/lib/claude_agent_sdk/types/mcp.rb +232 -0
- data/lib/claude_agent_sdk/types/messages.rb +614 -0
- data/lib/claude_agent_sdk/types/option_values.rb +302 -0
- data/lib/claude_agent_sdk/types/options.rb +352 -0
- data/lib/claude_agent_sdk/types/permissions.rb +107 -0
- data/lib/claude_agent_sdk/types/sessions.rb +10 -0
- data/lib/claude_agent_sdk/types.rb +13 -2534
- data/lib/claude_agent_sdk/version.rb +1 -1
- data/lib/claude_agent_sdk.rb +308 -73
- data/lib/generators/claude_agent_sdk/install/templates/claude_agent_sdk.rb.tt +3 -3
- 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
|