claude-agent-sdk 0.36.0 → 1.0.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 (64) hide show
  1. checksums.yaml +4 -4
  2. data/CHANGELOG.md +34 -0
  3. data/README.md +7 -3
  4. data/UPGRADING-1.0.md +151 -0
  5. data/docs/client.md +26 -1
  6. data/docs/errors.md +6 -0
  7. data/docs/hooks-and-permissions.md +5 -3
  8. data/docs/mcp-servers.md +1 -2
  9. data/docs/sessions.md +101 -3
  10. data/docs/types.md +109 -4
  11. data/lib/claude_agent_sdk/cli_installer.rb +30 -3
  12. data/lib/claude_agent_sdk/command_builder.rb +109 -98
  13. data/lib/claude_agent_sdk/deprecation.rb +1 -1
  14. data/lib/claude_agent_sdk/errors.rb +10 -0
  15. data/lib/claude_agent_sdk/fiber_boundary.rb +2 -0
  16. data/lib/claude_agent_sdk/instrumentation/otel.rb +15 -7
  17. data/lib/claude_agent_sdk/message_parser.rb +23 -9
  18. data/lib/claude_agent_sdk/observer.rb +2 -1
  19. data/lib/claude_agent_sdk/option_warnings.rb +2 -0
  20. data/lib/claude_agent_sdk/query.rb +50 -43
  21. data/lib/claude_agent_sdk/sdk_mcp_server.rb +22 -13
  22. data/lib/claude_agent_sdk/session_mutations.rb +20 -8
  23. data/lib/claude_agent_sdk/session_resume.rb +31 -16
  24. data/lib/claude_agent_sdk/session_store.rb +7 -3
  25. data/lib/claude_agent_sdk/session_summary.rb +4 -2
  26. data/lib/claude_agent_sdk/sessions.rb +8 -6
  27. data/lib/claude_agent_sdk/streaming.rb +1 -1
  28. data/lib/claude_agent_sdk/subprocess_cli_transport.rb +72 -40
  29. data/lib/claude_agent_sdk/testing/session_store_conformance.rb +14 -10
  30. data/lib/claude_agent_sdk/transcript_mirror_batcher.rb +2 -0
  31. data/lib/claude_agent_sdk/types/attributes.rb +236 -0
  32. data/lib/claude_agent_sdk/types/base.rb +322 -0
  33. data/lib/claude_agent_sdk/types/content_blocks.rb +57 -0
  34. data/lib/claude_agent_sdk/types/hooks.rb +640 -0
  35. data/lib/claude_agent_sdk/types/mcp.rb +232 -0
  36. data/lib/claude_agent_sdk/types/messages.rb +614 -0
  37. data/lib/claude_agent_sdk/types/option_values.rb +302 -0
  38. data/lib/claude_agent_sdk/types/options.rb +352 -0
  39. data/lib/claude_agent_sdk/types/permissions.rb +107 -0
  40. data/lib/claude_agent_sdk/types/sessions.rb +10 -0
  41. data/lib/claude_agent_sdk/types.rb +13 -2534
  42. data/lib/claude_agent_sdk/version.rb +1 -1
  43. data/lib/claude_agent_sdk.rb +62 -28
  44. data/sig/claude_agent_sdk/cancellation_signal.rbs +14 -0
  45. data/sig/claude_agent_sdk/configuration.rbs +14 -0
  46. data/sig/claude_agent_sdk/errors.rbs +86 -0
  47. data/sig/claude_agent_sdk/observer.rbs +42 -0
  48. data/sig/claude_agent_sdk/railtie.rbs +10 -0
  49. data/sig/claude_agent_sdk/sdk_mcp_server.rbs +76 -0
  50. data/sig/claude_agent_sdk/session_store.rbs +105 -0
  51. data/sig/claude_agent_sdk/streaming.rbs +15 -0
  52. data/sig/claude_agent_sdk/transport.rbs +98 -0
  53. data/sig/claude_agent_sdk/types/base.rbs +39 -0
  54. data/sig/claude_agent_sdk/types/content_blocks.rbs +79 -0
  55. data/sig/claude_agent_sdk/types/hooks.rbs +528 -0
  56. data/sig/claude_agent_sdk/types/mcp.rbs +216 -0
  57. data/sig/claude_agent_sdk/types/messages.rbs +586 -0
  58. data/sig/claude_agent_sdk/types/option_values.rbs +245 -0
  59. data/sig/claude_agent_sdk/types/options.rbs +288 -0
  60. data/sig/claude_agent_sdk/types/permissions.rbs +108 -0
  61. data/sig/claude_agent_sdk/types/sessions.rbs +66 -0
  62. data/sig/claude_agent_sdk.rbs +231 -0
  63. data/sig/manifest.yaml +5 -0
  64. metadata +32 -1
@@ -1,2536 +1,15 @@
1
1
  # frozen_string_literal: true
2
2
 
3
- module ClaudeAgentSDK
4
- # Type constants for permission modes
5
- PERMISSION_MODES = %w[default acceptEdits plan bypassPermissions dontAsk auto].freeze
6
-
7
- # Type constants for setting sources
8
- SETTING_SOURCES = %w[user project local].freeze
9
-
10
- # Effort levels for `ClaudeAgentOptions#effort`. The CLI (Claude Code 2.1.111+)
11
- # accepts these values; the set of *supported* levels is model-dependent
12
- # (e.g. `xhigh` arrived with Opus 4.7 and falls back to `high` on
13
- # Opus 4.6 / Sonnet 4.6). An Integer is also accepted and forwarded verbatim.
14
- EFFORT_LEVELS = %w[low medium high xhigh max].freeze
15
-
16
- # Type constants for permission update destinations
17
- PERMISSION_UPDATE_DESTINATIONS = %w[userSettings projectSettings localSettings session].freeze
18
-
19
- # Type constants for permission behaviors
20
- PERMISSION_BEHAVIORS = %w[allow deny ask].freeze
21
-
22
- # Type constants for hook events
23
- HOOK_EVENTS = %w[
24
- PreToolUse
25
- PostToolUse
26
- PostToolUseFailure
27
- Notification
28
- UserPromptSubmit
29
- SessionStart
30
- SessionEnd
31
- Stop
32
- StopFailure
33
- SubagentStart
34
- SubagentStop
35
- PreCompact
36
- PostCompact
37
- PermissionRequest
38
- PermissionDenied
39
- Setup
40
- TeammateIdle
41
- TaskCreated
42
- TaskCompleted
43
- Elicitation
44
- ElicitationResult
45
- ConfigChange
46
- WorktreeCreate
47
- WorktreeRemove
48
- InstructionsLoaded
49
- CwdChanged
50
- FileChanged
51
- ].freeze
52
-
53
- # Type constants for assistant message errors
54
- ASSISTANT_MESSAGE_ERRORS = %w[authentication_failed billing_error rate_limit invalid_request server_error max_output_tokens unknown].freeze
55
-
56
- # Type constants for SDK beta features
57
- # Available beta features that can be enabled via the betas option
58
- SDK_BETAS = %w[context-1m-2025-08-07].freeze
59
-
60
- # Base class for all types.
61
- class Type
62
- def self.wrap(object)
63
- return object if object.is_a?(self)
64
- return nil if object.nil?
65
-
66
- new(object)
67
- end
68
-
69
- def self.from_hash(hash)
70
- return unless hash.is_a?(Hash)
71
-
72
- new(hash)
73
- end
74
-
75
- def initialize(attributes = {})
76
- assign_attributes(attributes) if attributes
77
- super()
78
- end
79
-
80
- def [](name)
81
- read_attribute(name)
82
- end
83
-
84
- def []=(name, value)
85
- assign_attribute(name, value)
86
- end
87
-
88
- # Subclasses should override this to return a hash representation of the object.
89
- def to_h
90
- {}
91
- end
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
-
220
- private
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, &)
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(&)
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
-
332
- # Allow camelCase attribute access
333
- def method_missing(method_name, ...)
334
- normalized = normalize_name(method_name)
335
-
336
- if normalized != method_name.to_s && respond_to?(normalized)
337
- public_send(normalized, ...)
338
- else
339
- super
340
- end
341
- end
342
-
343
- def respond_to_missing?(method_name, include_private = false)
344
- normalized = normalize_name(method_name)
345
- (normalized != method_name.to_s && respond_to?(normalized)) || super
346
- end
347
-
348
- def assign_attributes(attributes)
349
- raise ArgumentError, "When assigning attributes, you must pass a hash as an argument, #{attributes.inspect} passed." unless attributes.respond_to?(:each_pair)
350
-
351
- return if attributes.empty?
352
-
353
- attributes.each_pair { |name, value| assign_attribute(name, value) }
354
- end
355
-
356
- def assign_attribute(name, value)
357
- setter = :"#{normalize_name(name)}="
358
- public_send(setter, value) if respond_to?(setter)
359
- end
360
-
361
- def read_attribute(name)
362
- getter = normalize_name(name)
363
- public_send(getter) if respond_to?(getter)
364
- end
365
-
366
- def normalize_name(name)
367
- name = name.to_s.dup
368
- name.gsub!(/(?<=[A-Z])(?=[A-Z][a-z])|(?<=[a-z\d])(?=[A-Z])/, "_")
369
- name.tr!("-", "_")
370
- name.downcase!
371
- name
372
- end
373
-
374
- FALSE_VALUES = [
375
- false, 0,
376
- "0", :'0',
377
- "f", :f,
378
- "F", :F,
379
- "false", :false, # rubocop:disable Lint/BooleanSymbol
380
- "FALSE", :FALSE,
381
- "off", :off,
382
- "OFF", :OFF
383
- ].to_set.freeze
384
-
385
- private_constant :FALSE_VALUES
386
-
387
- def coerce_boolean(value)
388
- return if value.nil?
389
-
390
- if value == ""
391
- nil
392
- else
393
- !FALSE_VALUES.include?(value)
394
- end
395
- end
396
- end
397
-
398
- # Content Blocks
399
-
400
- # Text content block
401
- class TextBlock < Type
402
- attr_accessor :text
403
-
404
- def to_s
405
- text.to_s
406
- end
407
- end
408
-
409
- # Thinking content block
410
- class ThinkingBlock < Type
411
- attr_accessor :thinking, :signature
412
- end
413
-
414
- # Tool use content block
415
- class ToolUseBlock < Type
416
- attr_accessor :id, :name, :input
417
- end
418
-
419
- # Tool result content block
420
- class ToolResultBlock < Type
421
- attr_accessor :tool_use_id, :content, :is_error
422
- end
423
-
424
- # Server-side tool use (CLI's built-in tools that execute server-side
425
- # rather than as MCP tools — advisor, web_search, code_execution, etc.).
426
- # Mirrors Python's `ServerToolUseBlock`.
427
- class ServerToolUseBlock < Type
428
- attr_accessor :id, :name, :input
429
- end
430
-
431
- # Result of a server-side tool execution. Mirrors Python's
432
- # `ServerToolResultBlock`.
433
- class ServerToolResultBlock < Type
434
- attr_accessor :tool_use_id, :content, :is_error
435
- end
436
-
437
- # Generic content block for types the SDK doesn't explicitly handle (e.g., "document", "image").
438
- # Preserves the raw hash data for forward compatibility with newer CLI versions.
439
- class UnknownBlock < Type
440
- attr_accessor :type, :data
441
- end
442
-
443
- # Deferred tool use, emitted on `ResultMessage` when a PreToolUse hook
444
- # returned `permissionDecision: "defer"`. The session can be resumed later
445
- # to execute the deferred call. Mirrors Python's `DeferredToolUse`.
446
- class DeferredToolUse < Type
447
- attr_accessor :id, :name, :input
448
- end
449
-
450
- # Message Types
451
-
452
- # User message
453
- class UserMessage < Type
454
- attr_accessor :content, :uuid, :parent_tool_use_id, :tool_use_result
455
-
456
- # Provenance of this message — where the turn came from.
457
- #
458
- # In streaming-input mode a single connection interleaves the turns you
459
- # send with turns the session injects on its own (background-task
460
- # notifications, fired scheduled-task prompts, MCP channel messages,
461
- # messages relayed from peer sessions, ...). `origin` tells them apart —
462
- # see {ResultMessage#origin} for deciding whether a result answers *your*
463
- # prompt.
464
- #
465
- # **Key form — read this before indexing into it.** A plain Hash, passed
466
- # through from the CLI untouched: the SDK does not model it, whitelist its
467
- # keys, or rewrite them, so kinds and fields newer CLI versions add stay
468
- # visible. Keys therefore follow the transport's JSON parsing, which uses
469
- # `symbolize_names: true` — they are **Symbols with the wire spelling
470
- # preserved**, so camelCase keys stay camelCase and you index with
471
- # `origin[:kind]`, `origin[:fromSession]`, `origin[:senderTaskId]`,
472
- # `origin[:verifiedPeerPid]`. This is unlike the snake_case attributes
473
- # elsewhere in this SDK, and unlike the Python SDK's string keys: a
474
- # `origin["kind"]` or `origin[:from_session]` lookup silently returns nil
475
- # and makes every attributed turn look unattributed. Only `:kind` is
476
- # guaranteed present; the rest depend on it.
477
- #
478
- # `nil` means the CLI did not attribute the message — that is the normal
479
- # case for prompts you send through {ClaudeAgentSDK.query} / {Client#query},
480
- # unless the host stamps `origin: { kind: 'human' }` on the message Hash
481
- # itself (only the `human` kind is honored from an SDK host). Populated on
482
- # injected turns (task notifications, channel/peer messages, ...) and on
483
- # user messages the CLI replays; tool-result messages never carry it.
484
- #
485
- # Known `:kind` values — documentation, not validation; treat anything
486
- # unrecognized as "not human":
487
- #
488
- # - `'human'` — a turn submitted by the SDK host
489
- # - `'channel'` — arrived on an MCP channel; `:server` names the MCP server
490
- # - `'peer'` — relayed from a peer session. `:from` (sender address,
491
- # sender-asserted — for reply routing or display, never as proof of
492
- # identity), `:name` (display name, already normalized by the CLI),
493
- # `:fromSession` (the sender's host-openable session id, a navigation
494
- # target only), `:senderTaskId` (task id of the in-process background
495
- # subagent that sent it; absent for cross-session peers), `:body`
496
- # (decoded message body with the peer envelope stripped, byte-exact with
497
- # what the model saw — render this instead of re-parsing the message
498
- # text), `:verifiedPeerPid` (kernel-verified pid of the process that
499
- # connected to this session's local messaging socket — the *connecting*
500
- # process, which for relayed traffic is the relay; absent when
501
- # unverifiable)
502
- # - `'task-notification'` — a background task's delivery. `:subkind` is
503
- # `'scheduled-trigger'` (the fired prompt of a scheduled task) or
504
- # `'peer-send-message'` (a message sent from another of the user's
505
- # sessions); absent for ordinary background-task notifications
506
- # - `'coordinator'`, `'unclassified'`, `'observer'` (`:from` /
507
- # `:senderTaskId` as for `peer`), `'auto-continuation'`,
508
- # `'observer-activity'`
509
- #
510
- # @return [Hash{Symbol => Object}, nil]
511
- # @see ResultMessage#origin
512
- attr_accessor :origin
513
-
514
- # Concatenated text of this message. Handles both String content
515
- # (plain-text user prompt) and Array-of-blocks content (typed content).
516
- # Returns "" when there is no text.
517
- def text
518
- case content
519
- when String then content
520
- when Array then content.grep(TextBlock).map(&:text).join("\n\n")
521
- else ''
522
- end
523
- end
524
-
525
- alias to_s text
526
- end
527
-
528
- # Assistant message with content blocks
529
- class AssistantMessage < Type
530
- attr_accessor :content, :model, :parent_tool_use_id, :error, :usage,
531
- :message_id, :stop_reason, :session_id, :uuid
532
-
533
- # Concatenated text across every TextBlock in this message's content.
534
- # Returns "" when the message has no text (e.g., a pure tool_use turn).
535
- def text
536
- Array(content).grep(TextBlock).map(&:text).join("\n\n")
537
- end
538
-
539
- alias to_s text
540
- end
541
-
542
- # System message with metadata.
543
- # When constructed from a raw CLI hash, the whole hash is stored in `#data`
544
- # unless the caller explicitly provides a `:data` entry.
545
- class SystemMessage < Type
546
- attr_accessor :subtype, :data
547
-
548
- def initialize(attributes = {})
549
- super
550
- @data ||= attributes if attributes.is_a?(Hash)
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
568
- end
569
-
570
- # Init system message (emitted at session start and after /clear)
571
- class InitMessage < SystemMessage
572
- attr_accessor :uuid, :session_id, :agents, :api_key_source, :betas,
573
- :claude_code_version, :cwd, :tools, :mcp_servers, :model,
574
- :permission_mode, :slash_commands, :output_style, :skills, :plugins,
575
- :fast_mode_state # "off", "cooldown", or "on"
576
- end
577
-
578
- # Compact boundary system message (emitted after context compaction completes)
579
- class CompactBoundaryMessage < SystemMessage
580
- attr_accessor :uuid, :session_id
581
- attr_reader :compact_metadata
582
-
583
- def compact_metadata=(value)
584
- @compact_metadata = value.is_a?(Hash) ? CompactMetadata.new(value) : value
585
- end
586
- end
587
-
588
- # Metadata about a compaction event
589
- class CompactMetadata < Type
590
- attr_accessor :pre_tokens, :post_tokens, :trigger, :custom_instructions, :preserved_segment
591
- end
592
-
593
- # Status system message (compacting status, permission mode changes)
594
- class StatusMessage < SystemMessage
595
- attr_accessor :uuid, :session_id, :status, :permission_mode
596
- end
597
-
598
- # API retry system message
599
- class APIRetryMessage < SystemMessage
600
- attr_accessor :uuid, :session_id, :attempt, :max_retries, :retry_delay_ms, :error_status, :error
601
- end
602
-
603
- # Local command output system message
604
- class LocalCommandOutputMessage < SystemMessage
605
- attr_accessor :uuid, :session_id, :content
606
- end
607
-
608
- # Emitted when a session_store mirror batch fails terminally and is
609
- # dropped — timeouts immediately (never retried), other failures after up
610
- # to three attempts. The local-disk transcript is still durable; this is
611
- # the consumer's only signal that the external store missed a batch
612
- # (at-most-once delivery).
613
- class MirrorErrorMessage < SystemMessage
614
- attr_accessor :uuid, :session_id, :error, :key
615
- end
616
-
617
- # Hook started system message
618
- class HookStartedMessage < SystemMessage
619
- attr_accessor :uuid, :session_id, :hook_id, :hook_name, :hook_event
620
- end
621
-
622
- # Hook progress system message
623
- class HookProgressMessage < SystemMessage
624
- attr_accessor :uuid, :session_id, :hook_id, :hook_name, :hook_event, :stdout, :stderr, :output
625
- end
626
-
627
- # Hook response system message
628
- class HookResponseMessage < SystemMessage
629
- attr_accessor :uuid, :session_id, :hook_id, :hook_name, :hook_event,
630
- :output, :stdout, :stderr, :exit_code,
631
- :outcome # "success", "error", or "cancelled"
632
- end
633
-
634
- # Session state changed system message
635
- class SessionStateChangedMessage < SystemMessage
636
- attr_accessor :uuid, :session_id,
637
- :state # "idle", "running", or "requires_action"
638
- end
639
-
640
- # Files persisted system message
641
- class FilesPersistedMessage < SystemMessage
642
- attr_accessor :uuid, :session_id, :files, :failed, :processed_at
643
- end
644
-
645
- # Elicitation complete system message
646
- class ElicitationCompleteMessage < SystemMessage
647
- attr_accessor :uuid, :session_id, :mcp_server_name, :elicitation_id
648
- end
649
-
650
- # Task lifecycle notification statuses
651
- TASK_NOTIFICATION_STATUSES = %w[completed failed stopped].freeze
652
-
653
- # Possible status values reported inside a `task_updated` patch.
654
- # pending/running/paused are non-terminal; completed/failed/killed are
655
- # terminal. Note: task_updated reports the raw "killed"; the CLI maps that to
656
- # "stopped" only when it emits a task_notification.
657
- TASK_UPDATED_STATUSES = %w[pending running paused completed failed killed].freeze
658
-
659
- # Task statuses that mean the task has finished and should be cleared from any
660
- # "active task" tracking. Spans both lifecycle vocabularies: task_notification
661
- # reports "stopped" (the CLI's mapped form of a killed task) while task_updated
662
- # reports the raw "killed". Treat the status of a TaskNotificationMessage and a
663
- # TaskUpdatedMessage the same way.
664
- TERMINAL_TASK_STATUSES = %w[completed failed stopped killed].freeze
665
-
666
- # Typed usage data for task progress and notifications
667
- class TaskUsage < Type
668
- attr_accessor :total_tokens, :tool_uses, :duration_ms
669
-
670
- def initialize(attributes = {})
671
- super
672
- @total_tokens ||= 0
673
- @tool_uses ||= 0
674
- @duration_ms ||= 0
675
- end
676
- end
677
-
678
- # Task started system message (subagent/background task started)
679
- class TaskStartedMessage < SystemMessage
680
- attr_accessor :task_id, :description, :uuid, :session_id, :tool_use_id, :task_type,
681
- :workflow_name, :prompt,
682
- :subagent_type # Subagent type, for Task/Agent tool subagents; nil otherwise
683
-
684
- # Whether the task was registered in the background (`true`) or in the
685
- # foreground with the spawning tool call blocking on it (`false`). `nil`
686
- # means the CLI did not say (the field is optional, and only set for
687
- # `local_agent` and `local_bash` tasks) — so test `== false`, never
688
- # falsiness, to detect a foreground/blocking task. A resumed subagent is
689
- # always registered in the background. A later move to the background does
690
- # not re-emit task_started; it arrives as {TaskUpdatedMessage#is_backgrounded}.
691
- #
692
- # @return [Boolean, nil]
693
- attr_accessor :is_backgrounded
694
-
695
- # Nesting depth of a spawned subagent (`local_agent`) task: 1 for a
696
- # top-level spawn, N+1 when spawned from inside a depth-N agent. `nil` on
697
- # other task types and on CLIs that do not report it.
698
- #
699
- # @return [Integer, nil]
700
- attr_accessor :spawn_depth
701
-
702
- # Display flags, passed through for the host to act on — the SDK never
703
- # filters frames or computes activity from them. Both are optional
704
- # Booleans: `nil` when absent, an explicit `false` preserved.
705
- #
706
- # - `skip_transcript`: an ambient/housekeeping task. Hide it from the
707
- # inline transcript; it may still appear in a tasks panel.
708
- # - `ambient`: true for tasks that are not activity — every
709
- # `skip_transcript` task, plus every live-update watcher (requested or
710
- # auto-started). Exclude these from activity indicators.
711
- #
712
- # @return [Boolean, nil]
713
- attr_accessor :skip_transcript, :ambient
714
- end
715
-
716
- # Task progress system message (periodic update from a running task).
717
- #
718
- # `summary` is an optional one-line status for the task's row — `nil` on any
719
- # frame that lacks one. For a `local_agent` task it is the model-generated
720
- # progress summary, which the CLI produces only while generation is enabled
721
- # (see {ClaudeAgentOptions#agent_progress_summaries}); for a backgrounded
722
- # `mcp_task` it is the MCP server's own status message and needs no option.
723
- class TaskProgressMessage < SystemMessage
724
- attr_accessor :task_id, :description, :usage, :uuid, :session_id, :tool_use_id, :last_tool_name, :summary,
725
- :subagent_type # Subagent type, for Task/Agent tool subagents; nil otherwise
726
- end
727
-
728
- # Task notification system message (task completed/failed/stopped).
729
- #
730
- # Note: not every terminal task emits this message. Background tasks may
731
- # instead report completion only via a TaskUpdatedMessage whose patch["status"]
732
- # is terminal (see TERMINAL_TASK_STATUSES). Consumers tracking active task IDs
733
- # should clear them on a terminal status from *either* message.
734
- class TaskNotificationMessage < SystemMessage
735
- attr_accessor :task_id, :status, :output_file, :summary, :uuid, :session_id, :tool_use_id, :usage
736
-
737
- # Machine-readable cause, set only when the task did not end through an
738
- # ordinary completion, failure, or stop. The one known value is
739
- # `'worker_restart'` (the worker process restarted and the resumed process
740
- # found the task orphaned; always with status `'stopped'`). Documentation,
741
- # not validation: newer CLIs may add values.
742
- #
743
- # @return [String, nil]
744
- attr_accessor :reason
745
-
746
- # For a backgrounded MCP task (`task_type: 'mcp_task'`) that completed: the
747
- # `resource_link` content blocks of its final result — the files it
748
- # returned by reference. A backgrounded task's tool_result is placeholder
749
- # text, so this is where a host learns which files the call produced; join
750
- # to the originating call via `tool_use_id`. `nil` when the result had
751
- # none or the task is any other type.
752
- #
753
- # Passed through from the CLI untouched, so each element is a Hash whose
754
- # keys are **Symbols with the wire spelling preserved**: `:uri` and `:name`
755
- # (Strings, always present), and optionally `:title`, `:description`,
756
- # `:mimeType` (camelCase — a `:mime_type` lookup returns nil), `:size` (a
757
- # Number, not necessarily an Integer), `:annotations` (a Hash of arbitrary
758
- # values). Elements carry no `type: 'resource_link'` discriminator. The CLI
759
- # describes its own output as at most 50 links / 64 KiB serialized; that is
760
- # a producer-side note, and the SDK neither enforces nor truncates.
761
- #
762
- # @return [Array<Hash{Symbol => Object}>, nil]
763
- attr_accessor :resource_links
764
-
765
- # Display flags with the same meaning as on {TaskStartedMessage}:
766
- # `skip_transcript` (hide from the inline transcript) and `ambient` (not
767
- # activity — exclude from activity indicators). Optional Booleans: `nil`
768
- # when absent, an explicit `false` preserved. The SDK does not act on them.
769
- #
770
- # @return [Boolean, nil]
771
- attr_accessor :skip_transcript, :ambient
772
- end
773
-
774
- # Task updated system message (background task lifecycle state change).
775
- #
776
- # The CLI emits system/task_updated events as a task moves through its
777
- # lifecycle. `patch` carries the changed fields (e.g. status, end_time); when
778
- # patch["status"] is terminal (see TERMINAL_TASK_STATUSES) the task has
779
- # finished. A background task's terminal state can arrive *only* as a
780
- # TaskUpdatedMessage with no accompanying TaskNotificationMessage — e.g. a task
781
- # stopped via TaskStop reports status "killed" here and the matching
782
- # notification is sometimes suppressed. Consumers tracking active task IDs
783
- # should clear them on a terminal status from *either* message.
784
- #
785
- # Parsed defensively in the constructor — a lifecycle event must never raise:
786
- # `status` is derived from patch["status"] (not a top-level field); a non-Hash
787
- # or absent patch falls back to {}; and `task_id` defaults to "" (never nil,
788
- # matching the Python SDK) so consumers can rely on it always being a String.
789
- # The full patch is preserved on `#patch` for callers that need more than the
790
- # derived readers.
791
- #
792
- # A patch carries only the fields that changed, so every derived reader is
793
- # `nil` when its field is absent. That matters most for `is_backgrounded`:
794
- # `true` means the task just moved to the background (e.g. after
795
- # {Client#background_tasks}), while `nil` means "this patch does not mention
796
- # it" — not "foreground". Merge patches into your own task map rather than
797
- # reading any single one as the task's full state.
798
- class TaskUpdatedMessage < SystemMessage
799
- attr_accessor :task_id, :patch, :status, :uuid, :session_id,
800
- :description, # patch[:description] — String, nil when unchanged
801
- :error, # patch[:error] — String, nil when unchanged
802
- :end_time, # patch[:end_time] — Integer (epoch ms), nil when unchanged
803
- :total_paused_ms, # patch[:total_paused_ms] — Integer, nil when unchanged
804
- :is_backgrounded # patch[:is_backgrounded] — true/false, nil when unchanged
805
-
806
- def initialize(attributes = {})
807
- super
808
- @task_id ||= ''
809
- @patch = {} unless @patch.is_a?(Hash)
810
- @status = patch_value(:status)
811
- @description = patch_value(:description)
812
- @error = patch_value(:error)
813
- @end_time = patch_value(:end_time)
814
- @total_paused_ms = patch_value(:total_paused_ms)
815
- @is_backgrounded = patch_value(:is_backgrounded)
816
- end
817
-
818
- private
819
-
820
- # The parser always hands over a symbol-keyed patch; a hand-built message
821
- # may use string keys. `fetch` with a block (not `||`) keeps an explicit
822
- # `false` from falling through to the string-key lookup's nil.
823
- def patch_value(key)
824
- @patch.fetch(key) { @patch[key.to_s] }
825
- end
826
- end
827
-
828
- # Background tasks changed system message: the full set of live background
829
- # tasks, emitted whenever membership changes (start, completion, kill, a
830
- # foreground agent being backgrounded) or an entry's `ambient` flag flips.
831
- #
832
- # A **level** signal with **REPLACE semantics** — `tasks` is every live
833
- # background task after the change, so swap your set for each payload rather
834
- # than pairing task_started / task_notification edges; a missed edge then
835
- # cannot wedge a stale "running" indicator. Per the CLI's contract:
836
- #
837
- # - Ordering relative to the edge frames for the same transition is
838
- # unspecified, and the payload carries ids only — do not correlate it
839
- # with the edge stream.
840
- # - The level is per-process: nothing is emitted at startup, so reset to the
841
- # empty set whenever the session's CLI process (re)starts.
842
- # - `tasks: []` is an authoritative empty snapshot for that process, not a
843
- # missing value.
844
- # - A repeated `initialize` on an already-running process is answered with a
845
- # snapshot of the current set (even an empty one) right behind its success
846
- # response; older CLIs send nothing there. This SDK initializes once per
847
- # connection, so that only matters to custom transports that reconnect.
848
- # - It covers *background* tasks only. A foreground subagent (the spawning
849
- # tool call still blocking) is not listed until it is backgrounded.
850
- #
851
- # `tasks` is passed through untouched: an Array of symbol-keyed Hashes
852
- # `{ task_id:, task_type:, description:, ambient: }`. `:ambient` is optional;
853
- # true marks tasks that are not activity (housekeeping, live-update
854
- # watchers), which hosts should exclude from activity indicators.
855
- #
856
- # The SDK itself deliberately does not consume this frame for its own
857
- # stdin-close bookkeeping; it is typed purely for consumers.
858
- class BackgroundTasksChangedMessage < SystemMessage
859
- attr_accessor :tasks, :uuid, :session_id
860
- end
861
-
862
- # Permission denied system message: a tool call was auto-denied without an
863
- # interactive permission prompt (auto-mode classifier, dontAsk mode,
864
- # headless-agent auto-deny, a deny rule, or — with no can_use_tool callback —
865
- # an "ask" decision that nobody can answer). The "ask" path with a callback
866
- # surfaces through can_use_tool instead.
867
- #
868
- # **Best-effort advisory, not a complete denial feed**:
869
- # {ResultMessage#permission_denials} is the authoritative record. In rare
870
- # races a booked denial has no frame, or a frame has no booked denial — so do
871
- # not derive counts or permission state from this stream. Not covered at all:
872
- # PreToolUse hook denies, deny-rule overrides of a hook's allow/ask decision,
873
- # Read/Edit/Write calls refused by a path-scoped deny rule (all resolve before
874
- # the permission check), and the MCP `--permission-prompt-tool` surface.
875
- #
876
- # `agent_id` is a subagent id for host-side routing; it is NOT a permission
877
- # `request_id`, and this message is not a pending permission request.
878
- # `decision_reason_type` is an open String (the values below are examples,
879
- # not an enum). The CLI's `decision_reason_code` is marked internal and is
880
- # left to `#data` with no stability promise.
881
- class PermissionDeniedMessage < SystemMessage
882
- attr_accessor :uuid, :session_id, :tool_name, :tool_use_id,
883
- :agent_id, # Subagent ID when the denied call originated inside a subagent; nil otherwise
884
- :decision_reason_type, # Open String, e.g. "classifier", "asyncAgent", "mode", "rule"; nil when not reported
885
- :decision_reason, # Human-readable reason from the deciding component; nil when not reported
886
- :message # The rejection message returned to the model in the tool_result
887
- end
888
-
889
- # Result message with cost and usage information
890
- class ResultMessage < Type
891
- # model_usage maps model name => per-model usage Hash, passed through
892
- # verbatim from the CLI's modelUsage field, so its keys are camelCase
893
- # (matches the TypeScript/Python SDKs' ModelUsage shape): inputTokens,
894
- # outputTokens, cacheReadInputTokens, cacheCreationInputTokens,
895
- # webSearchRequests, costUSD, contextWindow, maxOutputTokens, plus
896
- # optional canonicalModel (canonical id used for the pricing lookup —
897
- # may differ from the raw model-string key for provider-specific
898
- # ids/aliases) and provider ('firstParty', 'bedrock', 'vertex', ...).
899
- #
900
- # terminal_reason says why the query loop ended ("completed",
901
- # "max_turns", "aborted_streaming", ...). "aborted_streaming" /
902
- # "aborted_tools" mean the turn was cancelled via Client#interrupt (an
903
- # interrupt control request). nil when the CLI did not report one
904
- # (older CLI versions, or a result that bypassed the query loop such
905
- # as a local slash command).
906
- attr_accessor :subtype, :duration_ms, :duration_api_ms, :is_error,
907
- :num_turns, :session_id, :stop_reason, :total_cost_usd, :usage,
908
- :result, :structured_output,
909
- :model_usage, # Hash of { model_name => usage_data }, see above
910
- :permission_denials, # Array of { tool_name:, tool_use_id:, tool_input: }
911
- :errors, # Array of error strings (present on error subtypes)
912
- :uuid,
913
- :fast_mode_state, # "off", "cooldown", or "on"
914
- :api_error_status, # Integer HTTP status (429, 500, 529) on api_error subtype (CLI 2.1.110+)
915
- :terminal_reason # why the query loop ended, see above
916
-
917
- attr_reader :deferred_tool_use # DeferredToolUse, populated when a PreToolUse hook deferred
918
-
919
- def deferred_tool_use=(value)
920
- @deferred_tool_use = value.is_a?(Hash) ? DeferredToolUse.from_hash(value) : value
921
- end
922
-
923
- # Provenance of the user message that triggered this turn — `nil` when the
924
- # CLI did not attribute it. Lets a streaming-input consumer distinguish the
925
- # result of its own prompt from the result of a turn the session injected
926
- # on its own:
927
- #
928
- # if result.origin.nil? || result.origin[:kind] == 'human'
929
- # # a turn this application submitted
930
- # elsif result.origin[:kind] == 'task-notification'
931
- # # follow-up turn driven by a background task
932
- # end
933
- #
934
- # **Key form.** A plain Hash passed through from the CLI untouched, so its
935
- # keys are **Symbols with the wire spelling preserved** — camelCase stays
936
- # camelCase (`origin[:kind]`, `origin[:fromSession]`,
937
- # `origin[:verifiedPeerPid]`), unlike the snake_case attributes elsewhere
938
- # in this SDK and unlike the Python SDK's string keys. Indexing with
939
- # `origin["kind"]` silently returns nil and makes every attributed turn
940
- # look unattributed.
941
- #
942
- # See {UserMessage#origin} for the full list of known `:kind` values and
943
- # their per-kind keys.
944
- #
945
- # @return [Hash{Symbol => Object}, nil]
946
- # @see UserMessage#origin
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
961
- end
962
-
963
- # Stream event for partial message updates
964
- class StreamEvent < Type
965
- attr_accessor :uuid, :session_id, :event, :parent_tool_use_id
966
- end
967
-
968
- # Tool progress message (type: 'tool_progress')
969
- class ToolProgressMessage < Type
970
- attr_accessor :uuid, :session_id, :tool_use_id, :tool_name, :parent_tool_use_id,
971
- :elapsed_time_seconds, :task_id
972
- end
973
-
974
- # Auth status message (type: 'auth_status')
975
- class AuthStatusMessage < Type
976
- attr_accessor :uuid, :session_id, :is_authenticating, :output, :error
977
- end
978
-
979
- # Tool use summary message (type: 'tool_use_summary')
980
- class ToolUseSummaryMessage < Type
981
- attr_accessor :uuid, :session_id, :summary, :preceding_tool_use_ids
982
- end
983
-
984
- # Prompt suggestion message (type: 'prompt_suggestion')
985
- class PromptSuggestionMessage < Type
986
- attr_accessor :uuid, :session_id, :suggestion
987
- end
988
-
989
- # Type constants for rate limit statuses
990
- RATE_LIMIT_STATUSES = %w[allowed allowed_warning rejected].freeze
991
-
992
- # Type constants for rate limit types
993
- RATE_LIMIT_TYPES = %w[five_hour seven_day seven_day_opus seven_day_sonnet overage].freeze
994
-
995
- # Rate limit info with typed fields
996
- class RateLimitInfo < Type
997
- attr_accessor :status, :resets_at, :rate_limit_type, :utilization,
998
- :overage_status, :overage_resets_at, :overage_disabled_reason, :raw
999
-
1000
- def initialize(attributes = {})
1001
- super
1002
- @raw ||= {}
1003
- end
1004
- end
1005
-
1006
- # Rate limit event emitted when rate limit info changes
1007
- class RateLimitEvent < Type
1008
- attr_accessor :uuid, :session_id, :raw_data
1009
- attr_reader :rate_limit_info
1010
-
1011
- def initialize(attributes = {})
1012
- super
1013
- @rate_limit_info ||= RateLimitInfo.new
1014
- end
1015
-
1016
- def rate_limit_info=(value)
1017
- @rate_limit_info = value.is_a?(Hash) ? RateLimitInfo.new(value.merge(raw: value)) : value
1018
- end
1019
-
1020
- # Backward-compatible accessor returning the full raw event payload
1021
- def data
1022
- @raw_data || {}
1023
- end
1024
- end
1025
-
1026
- # Emitted when the session's conversation is replaced without ending the
1027
- # connection — e.g. after `/clear` or any other flow that discards the
1028
- # transcript mid-session (type: 'conversation_reset').
1029
- #
1030
- # In streaming-input mode a single connection carries many user turns, and a
1031
- # reset clears the conversation history *and* zeroes the running totals
1032
- # reported on subsequent {ResultMessage} objects (e.g. `total_cost_usd`). If
1033
- # you accumulate those totals across a long-lived session, snapshot them when
1034
- # this message arrives.
1035
- #
1036
- # @!attribute [rw] new_conversation_id
1037
- # Opaque identifier for the fresh conversation, for UIs to key an empty
1038
- # transcript on (and to discard any cached session title). This is *not*
1039
- # the `session_id` of subsequent messages — read that from the next
1040
- # message.
1041
- # @return [String]
1042
- # @!attribute [rw] uuid
1043
- # Unique ID of this message.
1044
- # @return [String]
1045
- # @!attribute [rw] session_id
1046
- # ID of the session that was reset (the outgoing session; messages after
1047
- # the reset carry a new `session_id`).
1048
- # @return [String]
1049
- class ConversationResetMessage < Type
1050
- attr_accessor :new_conversation_id, :uuid, :session_id
1051
- end
1052
-
1053
- # Thinking configuration types
1054
- #
1055
- # `display` controls how thinking content appears in responses. Valid values
1056
- # are `"summarized"` (plaintext summary) and `"omitted"` (empty thinking
1057
- # field, signature only). Defaults are model-dependent: Opus 4.6/Sonnet 4.6
1058
- # default to `"summarized"`; Opus 4.7 and every later model default to
1059
- # `"omitted"`. Pass `display: "summarized"` explicitly on those to get
1060
- # visible thinking text. Not supported with `ThinkingConfigDisabled`.
1061
- THINKING_DISPLAY_VALUES = %w[summarized omitted].freeze
1062
-
1063
- # Adaptive thinking: the model decides when and how much to think
1064
- # (sent as `--thinking adaptive`, no budget); control depth with `effort`.
1065
- class ThinkingConfigAdaptive < Type
1066
- include Type::OptionValue
1067
-
1068
- attr_reader :type, :display
1069
-
1070
- def initialize(attributes = {})
1071
- super
1072
- @type = 'adaptive'
1073
- end
1074
-
1075
- def display=(value)
1076
- @display = validate_display(value)
1077
- end
1078
-
1079
- private
1080
-
1081
- def validate_display(value)
1082
- return nil if value.nil?
1083
- return value if THINKING_DISPLAY_VALUES.include?(value.to_s)
1084
-
1085
- raise ArgumentError,
1086
- "invalid thinking display #{value.inspect}; expected one of #{THINKING_DISPLAY_VALUES.inspect}"
1087
- end
1088
- end
1089
-
1090
- # Enabled thinking: uses a user-specified budget
1091
- class ThinkingConfigEnabled < Type
1092
- include Type::OptionValue
1093
-
1094
- attr_accessor :budget_tokens
1095
- attr_reader :type, :display
1096
-
1097
- def initialize(attributes = {})
1098
- super
1099
- @type = 'enabled'
1100
- end
1101
-
1102
- def display=(value)
1103
- @display = validate_display(value)
1104
- end
1105
-
1106
- private
1107
-
1108
- def validate_display(value)
1109
- return nil if value.nil?
1110
- return value if THINKING_DISPLAY_VALUES.include?(value.to_s)
1111
-
1112
- raise ArgumentError,
1113
- "invalid thinking display #{value.inspect}; expected one of #{THINKING_DISPLAY_VALUES.inspect}"
1114
- end
1115
- end
1116
-
1117
- # Disabled thinking: sets thinking tokens to 0
1118
- class ThinkingConfigDisabled < Type
1119
- include Type::OptionValue
1120
-
1121
- attr_reader :type
1122
-
1123
- def initialize(attributes = {})
1124
- super
1125
- @type = 'disabled'
1126
- end
1127
- end
1128
-
1129
- # Agent definition configuration
1130
- class AgentDefinition < Type
1131
- include Type::OptionValue
1132
-
1133
- attr_accessor :description, :prompt, :tools, :disallowed_tools, :model, :skills, :memory, :mcp_servers,
1134
- :initial_prompt, :max_turns, :background, :effort, :permission_mode
1135
- end
1136
-
1137
- # Permission rule value
1138
- class PermissionRuleValue < Type
1139
- attr_accessor :tool_name, :rule_content
1140
- end
1141
-
1142
- # Permission update configuration
1143
- class PermissionUpdate < Type
1144
- attr_accessor :type, :behavior, :mode, :directories, :destination
1145
- attr_reader :rules
1146
-
1147
- # Wire-format parity with Python PermissionUpdate.from_dict (#920): the CLI
1148
- # sends rules as camelCase hashes ({toolName:, ruleContent:}); hydrate them
1149
- # into PermissionRuleValue (Type#assign_attribute normalizes the camelCase
1150
- # keys). Already-typed PermissionRuleValue entries pass through unchanged.
1151
- def rules=(value)
1152
- @rules = value&.map { |rule| rule.is_a?(Hash) ? PermissionRuleValue.new(rule) : rule }
1153
- end
1154
-
1155
- def to_h
1156
- result = { type: @type }
1157
- result[:destination] = @destination if @destination
1158
-
1159
- case @type
1160
- when 'addRules', 'replaceRules', 'removeRules'
1161
- if @rules
1162
- result[:rules] = @rules.map do |rule|
1163
- {
1164
- toolName: rule.tool_name,
1165
- ruleContent: rule.rule_content
1166
- }
1167
- end
1168
- end
1169
- result[:behavior] = @behavior if @behavior
1170
- when 'setMode'
1171
- result[:mode] = @mode if @mode
1172
- when 'addDirectories', 'removeDirectories'
1173
- result[:directories] = @directories if @directories
1174
- end
1175
-
1176
- result
1177
- end
1178
- end
1179
-
1180
- # Tool permission context delivered to `can_use_tool` callbacks.
1181
- # CLI 2.1.110+ began populating the four pre-formatted display fields
1182
- # (`title`, `display_name`, `description`, `blocked_path`,
1183
- # `decision_reason`) so the SDK consumer can render the same prompt UI
1184
- # the CLI would have shown. Older fields (`signal`, `suggestions`,
1185
- # `tool_use_id`, `agent_id`) remain unchanged.
1186
- # `signal` is a CancellationSignal on dispatched callbacks; `request_id`
1187
- # identifies this permission request (distinct from the tool invocation).
1188
- class ToolPermissionContext < Type
1189
- attr_accessor :signal, :request_id, :suggestions, :tool_use_id, :agent_id,
1190
- :title, :display_name, :description,
1191
- :blocked_path, :decision_reason
1192
-
1193
- def initialize(attributes = {})
1194
- super
1195
- @suggestions ||= []
1196
- end
1197
- end
1198
-
1199
- # Permission results
1200
- class PermissionResultAllow < Type
1201
- attr_accessor :updated_input, :updated_permissions
1202
- attr_reader :behavior
1203
-
1204
- def initialize(attributes = {})
1205
- super
1206
- @behavior = 'allow'
1207
- end
1208
- end
1209
-
1210
- class PermissionResultDeny < Type
1211
- attr_accessor :message, :interrupt
1212
- attr_reader :behavior
1213
-
1214
- def initialize(attributes = {})
1215
- super
1216
- @behavior = 'deny'
1217
- @message ||= ''
1218
- @interrupt = false if @interrupt.nil?
1219
- end
1220
- end
1221
-
1222
- # Hook matcher configuration
1223
- class HookMatcher < Type
1224
- include Type::OptionValue
1225
-
1226
- attr_accessor :matcher, :hooks, :timeout
1227
-
1228
- def initialize(attributes = {})
1229
- super
1230
- @hooks ||= []
1231
- end
1232
- end
1233
-
1234
- # Hook context passed to hook callbacks. Dispatched hooks receive the control
1235
- # request ID and a CancellationSignal (including on HookMatcher timeout).
1236
- class HookContext < Type
1237
- attr_accessor :signal, :request_id
1238
- end
1239
-
1240
- # Base hook input with common fields
1241
- class BaseHookInput < Type
1242
- attr_accessor :session_id, :transcript_path, :cwd, :permission_mode
1243
- attr_reader :hook_event_name
1244
- end
1245
-
1246
- # PreToolUse hook input
1247
- class PreToolUseHookInput < BaseHookInput
1248
- attr_accessor :tool_name, :tool_input, :tool_use_id, :agent_id, :agent_type
1249
-
1250
- def initialize(attributes = {})
1251
- super
1252
- @hook_event_name = 'PreToolUse'
1253
- end
1254
- end
1255
-
1256
- # PostToolUse hook input
1257
- class PostToolUseHookInput < BaseHookInput
1258
- attr_accessor :tool_name, :tool_input, :tool_response, :tool_use_id, :agent_id, :agent_type
1259
-
1260
- def initialize(attributes = {})
1261
- super
1262
- @hook_event_name = 'PostToolUse'
1263
- end
1264
- end
1265
-
1266
- # UserPromptSubmit hook input
1267
- class UserPromptSubmitHookInput < BaseHookInput
1268
- attr_accessor :prompt
1269
-
1270
- def initialize(attributes = {})
1271
- super
1272
- @hook_event_name = 'UserPromptSubmit'
1273
- end
1274
- end
1275
-
1276
- # Stop hook input
1277
- # Snapshot arrays are passed through unchanged. nil means unavailable;
1278
- # [] means the CLI provided an empty snapshot. They cover the parent
1279
- # session's background work, NOT all foreground and background agents.
1280
- class StopHookInput < BaseHookInput
1281
- attr_accessor :stop_hook_active, :last_assistant_message, :background_tasks, :session_crons
1282
-
1283
- def initialize(attributes = {})
1284
- super
1285
- @hook_event_name = 'Stop'
1286
- @stop_hook_active = false if @stop_hook_active.nil?
1287
- end
1288
- end
1289
-
1290
- # SubagentStop hook input
1291
- class SubagentStopHookInput < BaseHookInput
1292
- attr_accessor :stop_hook_active, :agent_id, :agent_transcript_path, :agent_type,
1293
- :last_assistant_message, :background_tasks, :session_crons
1294
-
1295
- def initialize(attributes = {})
1296
- super
1297
- @hook_event_name = 'SubagentStop'
1298
- @stop_hook_active = false if @stop_hook_active.nil?
1299
- end
1300
- end
1301
-
1302
- # PostToolUseFailure hook input
1303
- class PostToolUseFailureHookInput < BaseHookInput
1304
- attr_accessor :tool_name, :tool_input, :tool_use_id, :error, :is_interrupt,
1305
- :agent_id, :agent_type
1306
-
1307
- def initialize(attributes = {})
1308
- super
1309
- @hook_event_name = 'PostToolUseFailure'
1310
- end
1311
- end
1312
-
1313
- # Notification hook input
1314
- class NotificationHookInput < BaseHookInput
1315
- attr_accessor :message, :title, :notification_type
1316
-
1317
- def initialize(attributes = {})
1318
- super
1319
- @hook_event_name = 'Notification'
1320
- end
1321
- end
1322
-
1323
- # SubagentStart hook input
1324
- class SubagentStartHookInput < BaseHookInput
1325
- attr_accessor :agent_id, :agent_type
1326
-
1327
- def initialize(attributes = {})
1328
- super
1329
- @hook_event_name = 'SubagentStart'
1330
- end
1331
- end
1332
-
1333
- # PermissionRequest hook input
1334
- class PermissionRequestHookInput < BaseHookInput
1335
- attr_accessor :tool_name, :tool_input, :permission_suggestions, :agent_id, :agent_type
1336
-
1337
- def initialize(attributes = {})
1338
- super
1339
- @hook_event_name = 'PermissionRequest'
1340
- end
1341
- end
1342
-
1343
- # PreCompact hook input
1344
- class PreCompactHookInput < BaseHookInput
1345
- attr_accessor :trigger, :custom_instructions
1346
-
1347
- def initialize(attributes = {})
1348
- super
1349
- @hook_event_name = 'PreCompact'
1350
- end
1351
- end
1352
-
1353
- # SessionStart hook input
1354
- class SessionStartHookInput < BaseHookInput
1355
- attr_accessor :source, :agent_type, :model
1356
-
1357
- def initialize(attributes = {})
1358
- super
1359
- @hook_event_name = 'SessionStart'
1360
- end
1361
- end
1362
-
1363
- # SessionEnd hook input
1364
- class SessionEndHookInput < BaseHookInput
1365
- attr_accessor :reason
1366
-
1367
- def initialize(attributes = {})
1368
- super
1369
- @hook_event_name = 'SessionEnd'
1370
- end
1371
- end
1372
-
1373
- # Setup hook input
1374
- class SetupHookInput < BaseHookInput
1375
- attr_accessor :trigger
1376
-
1377
- def initialize(attributes = {})
1378
- super
1379
- @hook_event_name = 'Setup'
1380
- end
1381
- end
1382
-
1383
- # TeammateIdle hook input
1384
- class TeammateIdleHookInput < BaseHookInput
1385
- attr_accessor :teammate_name, :team_name
1386
-
1387
- def initialize(attributes = {})
1388
- super
1389
- @hook_event_name = 'TeammateIdle'
1390
- end
1391
- end
1392
-
1393
- # TaskCompleted hook input
1394
- class TaskCompletedHookInput < BaseHookInput
1395
- attr_accessor :task_id, :task_subject, :task_description, :teammate_name, :team_name
1396
-
1397
- def initialize(attributes = {})
1398
- super
1399
- @hook_event_name = 'TaskCompleted'
1400
- end
1401
- end
1402
-
1403
- # ConfigChange hook input
1404
- class ConfigChangeHookInput < BaseHookInput
1405
- attr_accessor :source, :file_path
1406
-
1407
- def initialize(attributes = {})
1408
- super
1409
- @hook_event_name = 'ConfigChange'
1410
- end
1411
- end
1412
-
1413
- # WorktreeCreate hook input
1414
- class WorktreeCreateHookInput < BaseHookInput
1415
- attr_accessor :name
1416
-
1417
- def initialize(attributes = {})
1418
- super
1419
- @hook_event_name = 'WorktreeCreate'
1420
- end
1421
- end
1422
-
1423
- # WorktreeRemove hook input
1424
- class WorktreeRemoveHookInput < BaseHookInput
1425
- attr_accessor :worktree_path
1426
-
1427
- def initialize(attributes = {})
1428
- super
1429
- @hook_event_name = 'WorktreeRemove'
1430
- end
1431
- end
1432
-
1433
- # StopFailure hook input
1434
- class StopFailureHookInput < BaseHookInput
1435
- attr_accessor :error, :error_details, :last_assistant_message
1436
-
1437
- def initialize(attributes = {})
1438
- super
1439
- @hook_event_name = 'StopFailure'
1440
- end
1441
- end
1442
-
1443
- # PostCompact hook input
1444
- class PostCompactHookInput < BaseHookInput
1445
- attr_accessor :trigger, :compact_summary
1446
-
1447
- def initialize(attributes = {})
1448
- super
1449
- @hook_event_name = 'PostCompact'
1450
- end
1451
- end
1452
-
1453
- # PermissionDenied hook input
1454
- class PermissionDeniedHookInput < BaseHookInput
1455
- attr_accessor :tool_name, :tool_input, :tool_use_id, :reason, :agent_id, :agent_type
1456
-
1457
- def initialize(attributes = {})
1458
- super
1459
- @hook_event_name = 'PermissionDenied'
1460
- end
1461
- end
1462
-
1463
- # TaskCreated hook input
1464
- class TaskCreatedHookInput < BaseHookInput
1465
- attr_accessor :task_id, :task_subject, :task_description, :teammate_name, :team_name
1466
-
1467
- def initialize(attributes = {})
1468
- super
1469
- @hook_event_name = 'TaskCreated'
1470
- end
1471
- end
1472
-
1473
- # Elicitation hook input
1474
- class ElicitationHookInput < BaseHookInput
1475
- attr_accessor :mcp_server_name, :message, :mode, :url,
1476
- :elicitation_id, :requested_schema
1477
-
1478
- def initialize(attributes = {})
1479
- super
1480
- @hook_event_name = 'Elicitation'
1481
- end
1482
- end
1483
-
1484
- # ElicitationResult hook input
1485
- class ElicitationResultHookInput < BaseHookInput
1486
- attr_accessor :mcp_server_name, :elicitation_id, :mode, :action, :content
1487
-
1488
- def initialize(attributes = {})
1489
- super
1490
- @hook_event_name = 'ElicitationResult'
1491
- end
1492
- end
1493
-
1494
- # InstructionsLoaded hook input
1495
- class InstructionsLoadedHookInput < BaseHookInput
1496
- attr_accessor :file_path, :memory_type, :load_reason, :globs, :trigger_file_path
1497
-
1498
- def initialize(attributes = {})
1499
- super
1500
- @hook_event_name = 'InstructionsLoaded'
1501
- end
1502
- end
1503
-
1504
- # CwdChanged hook input
1505
- class CwdChangedHookInput < BaseHookInput
1506
- attr_accessor :old_cwd, :new_cwd
1507
-
1508
- def initialize(attributes = {})
1509
- super
1510
- @hook_event_name = 'CwdChanged'
1511
- end
1512
- end
1513
-
1514
- # FileChanged hook input
1515
- class FileChangedHookInput < BaseHookInput
1516
- attr_accessor :file_path, :event
1517
-
1518
- def initialize(attributes = {})
1519
- super
1520
- @hook_event_name = 'FileChanged'
1521
- end
1522
- end
1523
-
1524
- # Fallback for hook events the SDK does not yet model. Carries the wire
1525
- # event name and the complete raw payload so no fields are lost (Python
1526
- # passes hook input through as a raw dict, so unknown events lose
1527
- # nothing there).
1528
- class UnknownHookInput < BaseHookInput
1529
- attr_accessor :raw_input
1530
-
1531
- def initialize(attributes = {})
1532
- super
1533
- # Direct assignment: BaseHookInput exposes hook_event_name as
1534
- # attr_reader only, and Type#assign_attribute silently drops keys
1535
- # without public setters.
1536
- @hook_event_name = attributes[:hook_event_name] || attributes['hook_event_name']
1537
- end
1538
- end
1539
-
1540
- # Setup hook specific output
1541
- class SetupHookSpecificOutput < Type
1542
- attr_accessor :additional_context
1543
- attr_reader :hook_event_name
1544
-
1545
- def initialize(attributes = {})
1546
- super
1547
- @hook_event_name = 'Setup'
1548
- end
1549
-
1550
- def to_h
1551
- result = { hookEventName: @hook_event_name }
1552
- result[:additionalContext] = @additional_context if @additional_context
1553
- result
1554
- end
1555
- end
1556
-
1557
- # PreToolUse hook specific output
1558
- class PreToolUseHookSpecificOutput < Type
1559
- attr_accessor :permission_decision, :permission_decision_reason,
1560
- :updated_input, :additional_context
1561
- attr_reader :hook_event_name
1562
-
1563
- def initialize(attributes = {})
1564
- super
1565
- @hook_event_name = 'PreToolUse'
1566
- end
1567
-
1568
- def to_h
1569
- result = { hookEventName: @hook_event_name }
1570
- result[:permissionDecision] = @permission_decision if @permission_decision
1571
- result[:permissionDecisionReason] = @permission_decision_reason if @permission_decision_reason
1572
- result[:updatedInput] = @updated_input if @updated_input
1573
- result[:additionalContext] = @additional_context if @additional_context
1574
- result
1575
- end
1576
- end
1577
-
1578
- # PostToolUse hook specific output.
1579
- #
1580
- # `updated_tool_output` (CLI 2.1.110+) replaces the tool's output entirely
1581
- # — works for any tool, MCP or built-in. `updated_mcp_tool_output` is the
1582
- # legacy MCP-only field that pre-dates the unified one; the CLI still
1583
- # honors it, so both are emitted when set. Mirrors Python's
1584
- # `PostToolUseHookSpecificOutput`.
1585
- class PostToolUseHookSpecificOutput < Type
1586
- attr_accessor :additional_context, :updated_mcp_tool_output, :updated_tool_output
1587
- attr_reader :hook_event_name
1588
-
1589
- def initialize(attributes = {})
1590
- super
1591
- @hook_event_name = 'PostToolUse'
1592
- end
1593
-
1594
- def to_h
1595
- result = { hookEventName: @hook_event_name }
1596
- result[:additionalContext] = @additional_context if @additional_context
1597
- result[:updatedToolOutput] = @updated_tool_output unless @updated_tool_output.nil?
1598
- result[:updatedMCPToolOutput] = @updated_mcp_tool_output if @updated_mcp_tool_output
1599
- result
1600
- end
1601
- end
1602
-
1603
- # PostToolUseFailure hook specific output
1604
- class PostToolUseFailureHookSpecificOutput < Type
1605
- attr_accessor :additional_context
1606
- attr_reader :hook_event_name
1607
-
1608
- def initialize(attributes = {})
1609
- super
1610
- @hook_event_name = 'PostToolUseFailure'
1611
- end
1612
-
1613
- def to_h
1614
- result = { hookEventName: @hook_event_name }
1615
- result[:additionalContext] = @additional_context if @additional_context
1616
- result
1617
- end
1618
- end
1619
-
1620
- # UserPromptSubmit hook specific output
1621
- class UserPromptSubmitHookSpecificOutput < Type
1622
- attr_accessor :additional_context
1623
- attr_reader :hook_event_name
1624
-
1625
- def initialize(attributes = {})
1626
- super
1627
- @hook_event_name = 'UserPromptSubmit'
1628
- end
1629
-
1630
- def to_h
1631
- result = { hookEventName: @hook_event_name }
1632
- result[:additionalContext] = @additional_context if @additional_context
1633
- result
1634
- end
1635
- end
1636
-
1637
- # Notification hook specific output
1638
- class NotificationHookSpecificOutput < Type
1639
- attr_accessor :additional_context
1640
- attr_reader :hook_event_name
1641
-
1642
- def initialize(attributes = {})
1643
- super
1644
- @hook_event_name = 'Notification'
1645
- end
1646
-
1647
- def to_h
1648
- result = { hookEventName: @hook_event_name }
1649
- result[:additionalContext] = @additional_context if @additional_context
1650
- result
1651
- end
1652
- end
1653
-
1654
- # SubagentStart hook specific output
1655
- class SubagentStartHookSpecificOutput < Type
1656
- attr_accessor :additional_context
1657
- attr_reader :hook_event_name
1658
-
1659
- def initialize(attributes = {})
1660
- super
1661
- @hook_event_name = 'SubagentStart'
1662
- end
1663
-
1664
- def to_h
1665
- result = { hookEventName: @hook_event_name }
1666
- result[:additionalContext] = @additional_context if @additional_context
1667
- result
1668
- end
1669
- end
1670
-
1671
- # PermissionRequest hook specific output
1672
- class PermissionRequestHookSpecificOutput < Type
1673
- attr_accessor :decision
1674
- attr_reader :hook_event_name
1675
-
1676
- def initialize(attributes = {})
1677
- super
1678
- @hook_event_name = 'PermissionRequest'
1679
- end
1680
-
1681
- def to_h
1682
- result = { hookEventName: @hook_event_name }
1683
- result[:decision] = @decision if @decision
1684
- result
1685
- end
1686
- end
1687
-
1688
- # SessionStart hook specific output
1689
- class SessionStartHookSpecificOutput < Type
1690
- attr_accessor :additional_context
1691
- attr_reader :hook_event_name
1692
-
1693
- def initialize(attributes = {})
1694
- super
1695
- @hook_event_name = 'SessionStart'
1696
- end
1697
-
1698
- def to_h
1699
- result = { hookEventName: @hook_event_name }
1700
- result[:additionalContext] = @additional_context if @additional_context
1701
- result
1702
- end
1703
- end
1704
-
1705
- # PermissionDenied hook specific output
1706
- class PermissionDeniedHookSpecificOutput < Type
1707
- attr_accessor :retry
1708
- attr_reader :hook_event_name
1709
-
1710
- def initialize(attributes = {})
1711
- super
1712
- @hook_event_name = 'PermissionDenied'
1713
- @retry = false if @retry.nil?
1714
- end
1715
-
1716
- def to_h
1717
- result = { hookEventName: @hook_event_name }
1718
- result[:retry] = @retry unless @retry.nil?
1719
- result
1720
- end
1721
- end
1722
-
1723
- # CwdChanged hook specific output
1724
- class CwdChangedHookSpecificOutput < Type
1725
- attr_accessor :watch_paths
1726
- attr_reader :hook_event_name
1727
-
1728
- def initialize(attributes = {})
1729
- super
1730
- @hook_event_name = 'CwdChanged'
1731
- end
1732
-
1733
- def to_h
1734
- result = { hookEventName: @hook_event_name }
1735
- result[:watchPaths] = @watch_paths if @watch_paths
1736
- result
1737
- end
1738
- end
1739
-
1740
- # FileChanged hook specific output
1741
- class FileChangedHookSpecificOutput < Type
1742
- attr_accessor :watch_paths
1743
- attr_reader :hook_event_name
1744
-
1745
- def initialize(attributes = {})
1746
- super
1747
- @hook_event_name = 'FileChanged'
1748
- end
1749
-
1750
- def to_h
1751
- result = { hookEventName: @hook_event_name }
1752
- result[:watchPaths] = @watch_paths if @watch_paths
1753
- result
1754
- end
1755
- end
1756
-
1757
- # Async hook JSON output
1758
- class AsyncHookJSONOutput < Type
1759
- attr_accessor :async, :async_timeout
1760
-
1761
- def initialize(attributes = {})
1762
- super
1763
- @async = true if @async.nil?
1764
- end
1765
-
1766
- def to_h
1767
- result = { async: @async }
1768
- result[:asyncTimeout] = @async_timeout if @async_timeout
1769
- result
1770
- end
1771
- end
1772
-
1773
- # Sync hook JSON output
1774
- class SyncHookJSONOutput < Type
1775
- attr_accessor :continue, :suppress_output, :stop_reason, :decision,
1776
- :system_message, :reason, :hook_specific_output
1777
-
1778
- def initialize(attributes = {})
1779
- super
1780
- @continue = true if @continue.nil?
1781
- @suppress_output = false if @suppress_output.nil?
1782
- end
1783
-
1784
- def to_h
1785
- result = { continue: @continue }
1786
- result[:suppressOutput] = @suppress_output if @suppress_output
1787
- result[:stopReason] = @stop_reason if @stop_reason
1788
- result[:decision] = @decision if @decision
1789
- result[:systemMessage] = @system_message if @system_message
1790
- result[:reason] = @reason if @reason
1791
- result[:hookSpecificOutput] = @hook_specific_output.to_h if @hook_specific_output
1792
- result
1793
- end
1794
- end
1795
-
1796
- # MCP status response types
1797
-
1798
- # MCP server connection status values
1799
- MCP_SERVER_CONNECTION_STATUSES = %w[connected failed needs-auth pending disabled].freeze
1800
-
1801
- # MCP server info (name and version)
1802
- class McpServerInfo < Type
1803
- attr_accessor :name, :version
1804
- end
1805
-
1806
- # MCP tool annotation hints
1807
- class McpToolAnnotations < Type
1808
- attr_accessor :read_only, :destructive, :open_world
1809
-
1810
- # Backwards-compatible parse; returns nil for nil input.
1811
- def self.parse(data)
1812
- from_hash(data)
1813
- end
1814
- end
1815
-
1816
- # MCP tool info (name, description, annotations)
1817
- class McpToolInfo < Type
1818
- attr_accessor :name, :description
1819
- attr_reader :annotations
1820
-
1821
- def annotations=(value)
1822
- @annotations = value.is_a?(Hash) ? McpToolAnnotations.new(value) : value
1823
- end
1824
-
1825
- # Backwards-compatible parse; returns nil for nil input.
1826
- def self.parse(data)
1827
- from_hash(data)
1828
- end
1829
- end
1830
-
1831
- # Output-only serializable version of McpSdkServerConfig (without live instance)
1832
- # Returned in MCP status responses
1833
- class McpSdkServerConfigStatus < Type
1834
- attr_accessor :name
1835
- attr_reader :type
1836
-
1837
- def initialize(attributes = {})
1838
- super
1839
- @type = 'sdk'
1840
- end
1841
-
1842
- def to_h
1843
- { type: @type, name: @name }
1844
- end
1845
- end
1846
-
1847
- # Claude.ai proxy MCP server config
1848
- # Output-only type that appears in status responses for servers proxied through Claude.ai
1849
- class McpClaudeAIProxyServerConfig < Type
1850
- attr_accessor :url, :id
1851
- attr_reader :type
1852
-
1853
- def initialize(attributes = {})
1854
- super
1855
- @type = 'claudeai-proxy'
1856
- end
1857
-
1858
- def to_h
1859
- { type: @type, url: @url, id: @id }
1860
- end
1861
- end
1862
-
1863
- # Status of a single MCP server connection
1864
- class McpServerStatus < Type
1865
- attr_accessor :name, :status, :error, :scope
1866
- attr_reader :server_info, :config, :tools
1867
-
1868
- def server_info=(value)
1869
- @server_info = value.is_a?(Hash) ? McpServerInfo.new(value) : value
1870
- end
1871
-
1872
- def tools=(value)
1873
- @tools = if value.is_a?(Array)
1874
- value.map { |t| t.is_a?(Hash) ? McpToolInfo.new(t) : t }
1875
- else
1876
- value
1877
- end
1878
- end
1879
-
1880
- def config=(value)
1881
- @config = self.class.parse_config(value) || value
1882
- end
1883
-
1884
- # Backwards-compatible parse; normalizes camelCase `serverInfo` and
1885
- # polymorphically builds the nested `config`.
1886
- def self.parse(data)
1887
- from_hash(data)
1888
- end
1889
-
1890
- def self.parse_config(config)
1891
- return nil unless config.is_a?(Hash) && config[:type]
1892
-
1893
- case config[:type]
1894
- when 'claudeai-proxy'
1895
- McpClaudeAIProxyServerConfig.new(url: config[:url], id: config[:id])
1896
- when 'sdk'
1897
- McpSdkServerConfigStatus.new(name: config[:name])
1898
- else
1899
- config
1900
- end
1901
- end
1902
- end
1903
-
1904
- # Response from get_mcp_status containing all server statuses
1905
- class McpStatusResponse < Type
1906
- attr_reader :mcp_servers
1907
-
1908
- def mcp_servers=(value)
1909
- @mcp_servers = if value.is_a?(Array)
1910
- value.map { |s| s.is_a?(Hash) ? McpServerStatus.new(s) : s }
1911
- else
1912
- value
1913
- end
1914
- end
1915
-
1916
- # Backwards-compatible parse; returns nil for nil input.
1917
- def self.parse(data)
1918
- from_hash(data)
1919
- end
1920
- end
1921
-
1922
- # MCP Server configurations
1923
- class McpStdioServerConfig < Type
1924
- include Type::OptionValue
1925
-
1926
- attr_accessor :command, :args, :env
1927
- attr_reader :type
1928
-
1929
- inspect_filtered :env
1930
-
1931
- def initialize(attributes = {})
1932
- super
1933
- @type = 'stdio'
1934
- end
1935
-
1936
- def to_h
1937
- result = { type: @type, command: @command }
1938
- result[:args] = @args if @args
1939
- result[:env] = @env if @env
1940
- result
1941
- end
1942
- end
1943
-
1944
- class McpSSEServerConfig < Type
1945
- include Type::OptionValue
1946
-
1947
- attr_accessor :url, :headers
1948
- attr_reader :type
1949
-
1950
- inspect_filtered :headers
1951
-
1952
- def initialize(attributes = {})
1953
- super
1954
- @type = 'sse'
1955
- end
1956
-
1957
- def to_h
1958
- result = { type: @type, url: @url }
1959
- result[:headers] = @headers if @headers
1960
- result
1961
- end
1962
- end
1963
-
1964
- class McpHttpServerConfig < Type
1965
- include Type::OptionValue
1966
-
1967
- attr_accessor :url, :headers
1968
- attr_reader :type
1969
-
1970
- inspect_filtered :headers
1971
-
1972
- def initialize(attributes = {})
1973
- super
1974
- @type = 'http'
1975
- end
1976
-
1977
- def to_h
1978
- result = { type: @type, url: @url }
1979
- result[:headers] = @headers if @headers
1980
- result
1981
- end
1982
- end
1983
-
1984
- class McpSdkServerConfig < Type
1985
- include Type::OptionValue
1986
-
1987
- attr_accessor :name, :instance
1988
- attr_reader :type
1989
-
1990
- def initialize(attributes = {})
1991
- super
1992
- @type = 'sdk'
1993
- end
1994
-
1995
- def to_h
1996
- { type: @type, name: @name, instance: @instance }
1997
- end
1998
- end
1999
-
2000
- # SDK Plugin configuration
2001
- class SdkPluginConfig < Type
2002
- include Type::OptionValue
2003
-
2004
- attr_accessor :path
2005
- attr_reader :type
2006
-
2007
- def initialize(attributes = {})
2008
- super
2009
- @type = 'local'
2010
- end
2011
-
2012
- def to_h
2013
- { type: @type, path: @path }
2014
- end
2015
- end
2016
-
2017
- # Sandbox network configuration
2018
- class SandboxNetworkConfig < Type
2019
- include Type::OptionValue
2020
-
2021
- attr_accessor :allowed_domains, :denied_domains, :allow_managed_domains_only,
2022
- :allow_unix_sockets, :allow_all_unix_sockets, :allow_local_binding,
2023
- :allow_mach_lookup, :http_proxy_port, :socks_proxy_port
2024
-
2025
- def to_h
2026
- result = {}
2027
- result[:allowedDomains] = @allowed_domains if @allowed_domains
2028
- result[:deniedDomains] = @denied_domains if @denied_domains
2029
- result[:allowManagedDomainsOnly] = @allow_managed_domains_only unless @allow_managed_domains_only.nil?
2030
- result[:allowUnixSockets] = @allow_unix_sockets unless @allow_unix_sockets.nil?
2031
- result[:allowAllUnixSockets] = @allow_all_unix_sockets unless @allow_all_unix_sockets.nil?
2032
- result[:allowLocalBinding] = @allow_local_binding unless @allow_local_binding.nil?
2033
- result[:allowMachLookup] = @allow_mach_lookup if @allow_mach_lookup
2034
- result[:httpProxyPort] = @http_proxy_port if @http_proxy_port
2035
- result[:socksProxyPort] = @socks_proxy_port if @socks_proxy_port
2036
- result
2037
- end
2038
- end
2039
-
2040
- # Sandbox filesystem configuration
2041
- class SandboxFilesystemConfig < Type
2042
- include Type::OptionValue
2043
-
2044
- attr_accessor :allow_write, :deny_write, :deny_read, :allow_read, :allow_managed_read_paths_only
2045
-
2046
- def to_h
2047
- result = {}
2048
- result[:allowWrite] = @allow_write if @allow_write
2049
- result[:denyWrite] = @deny_write if @deny_write
2050
- result[:denyRead] = @deny_read if @deny_read
2051
- result[:allowRead] = @allow_read if @allow_read
2052
- result[:allowManagedReadPathsOnly] = @allow_managed_read_paths_only unless @allow_managed_read_paths_only.nil?
2053
- result
2054
- end
2055
- end
2056
-
2057
- # Sandbox settings for isolated command execution
2058
- class SandboxSettings < Type
2059
- include Type::OptionValue
2060
-
2061
- attr_accessor :enabled, :fail_if_unavailable, :auto_allow_bash_if_sandboxed,
2062
- :excluded_commands, :allow_unsandboxed_commands, :network, :filesystem,
2063
- :ignore_violations, :enable_weaker_nested_sandbox,
2064
- :enable_weaker_network_isolation, :ripgrep
2065
-
2066
- def to_h
2067
- result = {}
2068
- result[:enabled] = @enabled unless @enabled.nil?
2069
- result[:failIfUnavailable] = @fail_if_unavailable unless @fail_if_unavailable.nil?
2070
- result[:autoAllowBashIfSandboxed] = @auto_allow_bash_if_sandboxed unless @auto_allow_bash_if_sandboxed.nil?
2071
- result[:excludedCommands] = @excluded_commands if @excluded_commands
2072
- result[:allowUnsandboxedCommands] = @allow_unsandboxed_commands unless @allow_unsandboxed_commands.nil?
2073
- result[:network] = @network.is_a?(SandboxNetworkConfig) ? @network.to_h : @network if @network
2074
- result[:filesystem] = @filesystem.is_a?(SandboxFilesystemConfig) ? @filesystem.to_h : @filesystem if @filesystem
2075
- result[:ignoreViolations] = @ignore_violations if @ignore_violations
2076
- result[:enableWeakerNestedSandbox] = @enable_weaker_nested_sandbox unless @enable_weaker_nested_sandbox.nil?
2077
- result[:enableWeakerNetworkIsolation] = @enable_weaker_network_isolation unless @enable_weaker_network_isolation.nil?
2078
- result[:ripgrep] = @ripgrep if @ripgrep
2079
- result
2080
- end
2081
- end
2082
-
2083
- # Result of a session fork operation
2084
- class ForkSessionResult < Type
2085
- attr_accessor :session_id
2086
- end
2087
-
2088
- # API-side task budget in tokens.
2089
- # When set, the model is made aware of its remaining token budget so it can
2090
- # pace tool use and wrap up before the limit.
2091
- class TaskBudget < Type
2092
- include Type::OptionValue
2093
-
2094
- attr_accessor :total
2095
-
2096
- def to_h
2097
- { total: @total }
2098
- end
2099
- end
2100
-
2101
- # System prompt file configuration — loads system prompt from a file path
2102
- class SystemPromptFile < Type
2103
- include Type::OptionValue
2104
-
2105
- attr_accessor :path
2106
- attr_reader :type
2107
-
2108
- def initialize(attributes = {})
2109
- super
2110
- @type = 'file'
2111
- end
2112
-
2113
- def to_h
2114
- { type: @type, path: @path }
2115
- end
2116
- end
2117
-
2118
- # System prompt preset configuration.
2119
- #
2120
- # +snapshot+ controls whether the session keeps the system prompt it
2121
- # recorded on its first request. When true, every later request (including
2122
- # after resume) sends the recorded prompt, so a changed +append+ has no
2123
- # effect until the session is compacted or a new session starts. When
2124
- # false, the prompt is rebuilt on every request — useful while iterating on
2125
- # +append+ text across calls that resume the same session. When nil
2126
- # (omitted), the CLI treats it as true, except in bare mode (+--bare+),
2127
- # where it acts as false. Sent on the control-protocol +initialize+ request
2128
- # (never as a CLI flag); requires Claude Code CLI 2.1.257 or later, and
2129
- # before 2.1.265 a session with an +append+ prompt recorded it only when
2130
- # +snapshot+ was true. Older CLIs silently ignore it.
2131
- class SystemPromptPreset < Type
2132
- include Type::OptionValue
2133
-
2134
- attr_reader :type
2135
- attr_accessor :preset, :append, :exclude_dynamic_sections, :snapshot
2136
-
2137
- def initialize(attributes = {})
2138
- super
2139
- @type = 'preset'
2140
- end
2141
-
2142
- def to_h
2143
- result = { type: @type, preset: @preset }
2144
- result[:append] = @append if @append
2145
- result[:exclude_dynamic_sections] = @exclude_dynamic_sections unless @exclude_dynamic_sections.nil?
2146
- result[:snapshot] = @snapshot unless @snapshot.nil?
2147
- result
2148
- end
2149
- end
2150
-
2151
- # Custom system prompt configuration — the object form of passing a String
2152
- # as +system_prompt+. Reaches the CLI the same way a String does
2153
- # (+--system-prompt <prompt>+); the object form exists so +snapshot+ can be
2154
- # set alongside it (see SystemPromptPreset#snapshot for its semantics).
2155
- class SystemPromptCustom < Type
2156
- include Type::OptionValue
2157
-
2158
- attr_reader :type
2159
- attr_accessor :prompt, :snapshot
2160
-
2161
- def initialize(attributes = {})
2162
- super
2163
- @type = 'custom'
2164
- end
2165
-
2166
- def to_h
2167
- result = { type: @type, prompt: @prompt }
2168
- result[:snapshot] = @snapshot unless @snapshot.nil?
2169
- result
2170
- end
2171
- end
2172
-
2173
- # Tools preset configuration
2174
- class ToolsPreset < Type
2175
- include Type::OptionValue
2176
-
2177
- attr_reader :type
2178
- attr_accessor :preset
2179
-
2180
- def initialize(attributes = {})
2181
- super
2182
- @type = 'preset'
2183
- end
2184
-
2185
- def to_h
2186
- { type: @type, preset: @preset }
2187
- end
2188
- end
2189
-
2190
- # Claude Agent Options for configuring queries
2191
- class ClaudeAgentOptions < Type
2192
- # `env` routinely carries credentials (ANTHROPIC_API_KEY, ...).
2193
- inspect_filtered :env
2194
-
2195
- attr_accessor :allowed_tools, :system_prompt, :mcp_servers, :permission_mode,
2196
- :resume, :resume_session_at, :session_id, :max_turns, :disallowed_tools,
2197
- :model, :permission_prompt_tool_name, :cwd, :cli_path, :settings,
2198
- :add_dirs, :env, :extra_args, :max_buffer_size, :stderr,
2199
- :can_use_tool, :hooks, :user,
2200
- :agents, :setting_sources, :skills,
2201
- :output_format, :max_budget_usd, :max_thinking_tokens,
2202
- :fallback_model, :advisor_model, :plugins, :debug_stderr,
2203
- :betas, :tools, :sandbox,
2204
- :thinking, :effort, :observers, :task_budget,
2205
- :session_store, :session_store_flush, :load_timeout_ms
2206
- attr_reader :bare, :fork_session, :enable_file_checkpointing,
2207
- :include_partial_messages, :continue_conversation,
2208
- :include_hook_events, :strict_mcp_config,
2209
- :callback_scheduling, :callback_wrapper
2210
-
2211
- # With {#resume_session_at}: the UUID of the user prompt whose turn this
2212
- # truncating resume intends to discard.
2213
- #
2214
- # When set, the CLI validates at load time that every transcript entry
2215
- # after the `resume_session_at` point is attributable to that turn, and
2216
- # refuses the resume otherwise — e.g. when the discarded range contains a
2217
- # queued user message or task notification the session absorbed mid-turn
2218
- # that the caller had not yet observed. Leave unset to keep the
2219
- # unvalidated truncation behavior.
2220
- #
2221
- # **Choosing the fork point.** Set `resume_session_at` to the *last*
2222
- # transcript entry of the turn you are keeping — whatever its type — and
2223
- # `resume_drops_turn` to the prompt UUID of the turn immediately after it
2224
- # (e.g. the next `SessionMessage` of `type == "user"` from
2225
- # {ClaudeAgentSDK.get_session_messages}, or the `uuid` you supplied on a
2226
- # streamed user message). Note that with structured output
2227
- # ({#output_format}) or end-turn MCP tools a kept turn ends on entries
2228
- # *after* its last assistant message, so forking at the assistant UUID is
2229
- # refused by design.
2230
- #
2231
- # **On refusal.** The CLI reports an `error_during_execution` result whose
2232
- # message starts with `Resume rejected by --resume-drops-turn:` — match on
2233
- # that text. Treat it as deterministic: clear the pending fork target and
2234
- # resume plainly rather than retrying the same request.
2235
- #
2236
- # Forwarded whenever it is not `nil`. An empty string reaches the CLI and
2237
- # is rejected there as a malformed declaration rather than being dropped by
2238
- # the SDK, which would silently disarm the guard you believe is armed. The
2239
- # SDK does not validate the option combination (`resume` /
2240
- # `resume_session_at`); like the TypeScript and Python SDKs that is the
2241
- # CLI's call.
2242
- #
2243
- # @return [String, nil]
2244
- # @see #resume_session_at
2245
- attr_accessor :resume_drops_turn
2246
-
2247
- def initialize(attributes = {})
2248
- self.fork_session = false
2249
- self.continue_conversation = false
2250
- self.include_partial_messages = false
2251
- self.enable_file_checkpointing = false
2252
- self.include_hook_events = false
2253
- self.strict_mcp_config = false
2254
- self.forward_subagent_text = false
2255
-
2256
- super(merge_with_defaults(attributes || {}))
2257
-
2258
- # Non-nil defaults for options that need them.
2259
- self.env ||= {}
2260
- self.extra_args ||= {}
2261
- self.mcp_servers ||= {}
2262
- self.add_dirs ||= []
2263
- self.observers ||= []
2264
- self.allowed_tools ||= []
2265
- self.disallowed_tools ||= []
2266
- self.session_store_flush ||= 'batched'
2267
- # 0 is a valid (immediate) timeout, so only fill in the default for nil.
2268
- self.load_timeout_ms = 60_000 if load_timeout_ms.nil?
2269
- self.callback_scheduling = :thread if callback_scheduling.nil?
2270
- end
2271
-
2272
- def dup_with(**changes)
2273
- new_options = self.dup
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.
2282
- new_options.instance_variables.each do |ivar|
2283
- new_options.instance_variable_set(ivar, Type.deep_dup_for_options(new_options.instance_variable_get(ivar)))
2284
- end
2285
- changes.each { |key, value| new_options[key] = value }
2286
- new_options
2287
- end
2288
-
2289
- def bare?
2290
- !!bare
2291
- end
2292
-
2293
- def bare=(value)
2294
- @bare = coerce_boolean(value)
2295
- end
2296
-
2297
- def fork_session?
2298
- !!fork_session
2299
- end
2300
-
2301
- def fork_session=(value)
2302
- @fork_session = coerce_boolean(value)
2303
- end
2304
-
2305
- def enable_file_checkpointing?
2306
- !!enable_file_checkpointing
2307
- end
2308
-
2309
- def enable_file_checkpointing=(value)
2310
- @enable_file_checkpointing = coerce_boolean(value)
2311
- end
2312
-
2313
- def include_partial_messages?
2314
- !!include_partial_messages
2315
- end
2316
-
2317
- def include_partial_messages=(value)
2318
- @include_partial_messages = coerce_boolean(value)
2319
- end
2320
-
2321
- def continue_conversation?
2322
- !!continue_conversation
2323
- end
2324
-
2325
- def continue_conversation=(value)
2326
- @continue_conversation = coerce_boolean(value)
2327
- end
2328
-
2329
- def include_hook_events?
2330
- !!include_hook_events
2331
- end
2332
-
2333
- def include_hook_events=(value)
2334
- @include_hook_events = coerce_boolean(value)
2335
- end
2336
-
2337
- def strict_mcp_config?
2338
- !!strict_mcp_config
2339
- end
2340
-
2341
- def strict_mcp_config=(value)
2342
- @strict_mcp_config = coerce_boolean(value)
2343
- end
2344
-
2345
- # Forward subagent text and thinking blocks as messages in the stream.
2346
- # Defaults to `false`.
2347
- #
2348
- # By default only `tool_use` / `tool_result` blocks from subagents
2349
- # (spawned via the Agent tool) are emitted, as {AssistantMessage} /
2350
- # {UserMessage} objects whose `parent_tool_use_id` is the spawning Agent
2351
- # `tool_use` id — enough for a progress heartbeat. When true, the
2352
- # subagent's text and thinking blocks are forwarded the same way, so
2353
- # consumers can render the full nested transcript. Matches the TypeScript
2354
- # SDK's `forwardSubagentText`.
2355
- #
2356
- # Sent as the `forwardSubagentText` initialize capability rather than a CLI
2357
- # flag, and only when enabled, so an older CLI never sees an unknown key on
2358
- # the common path. Both {ClaudeAgentSDK.query} and {Client} run the control
2359
- # protocol, so the option applies to either entry point.
2360
- #
2361
- # Assigning coerces to a Boolean; {#forward_subagent_text?} is the
2362
- # predicate form.
2363
- #
2364
- # @return [Boolean]
2365
- attr_reader :forward_subagent_text
2366
-
2367
- # @return [Boolean] {#forward_subagent_text}, as a strict Boolean.
2368
- def forward_subagent_text?
2369
- !!forward_subagent_text
2370
- end
2371
-
2372
- # @see #forward_subagent_text
2373
- def forward_subagent_text=(value)
2374
- @forward_subagent_text = coerce_boolean(value)
2375
- end
2376
-
2377
- # Request model-generated progress summaries for subagent (`local_agent`)
2378
- # tasks. `true` *requests* generation: while the CLI has it enabled, a
2379
- # subagent's {TaskProgressMessage#summary} **may** carry a one-line status.
2380
- # `summary` stays optional on the wire even then — not every progress
2381
- # frame has one — so read it nil-safely. `false` / `nil` do not enable
2382
- # generation; they do not promise that `summary` is absent (a process that
2383
- # already enabled summaries keeps them, and a backgrounded `mcp_task`
2384
- # reports its own status there regardless of this option). Matches the
2385
- # CLI's `agentProgressSummaries` initialize field.
2386
- #
2387
- # Defaults to `nil` (unset): the key is omitted from the `initialize`
2388
- # control request. `true` and `false` are forwarded verbatim. This is an
2389
- # enable switch, not a live toggle: CLI 2.1.278 only acts on a truthy
2390
- # value, so `false` is schema-valid but equivalent to leaving the option
2391
- # unset — it does not switch summaries off on a process that already
2392
- # enabled them. Both {ClaudeAgentSDK.query} and {Client} run the control
2393
- # protocol, so the option applies to either entry point.
2394
- #
2395
- # Assigning coerces to a Boolean and keeps `nil` as `nil`.
2396
- #
2397
- # @return [Boolean, nil]
2398
- attr_reader :agent_progress_summaries
2399
-
2400
- # @see #agent_progress_summaries
2401
- def agent_progress_summaries=(value)
2402
- @agent_progress_summaries = coerce_boolean(value)
2403
- end
2404
-
2405
- CALLBACK_SCHEDULING_MODES = %i[thread inline].freeze
2406
-
2407
- # Where user callbacks (hooks, can_use_tool, SDK MCP handlers, message
2408
- # blocks, observers) run when the SDK is hosted inside an Async reactor:
2409
- # :thread (default) — each callback hops to a plain thread, so
2410
- # thread-keyed libraries (ActiveRecord, pg, ...) behave as usual.
2411
- # :inline — callbacks run in place on the reactor fiber. Only for
2412
- # hosts that are fiber-isolated end to end (e.g. solid_queue fiber
2413
- # workers with IsolatedExecutionState.isolation_level = :fiber).
2414
- # Scheduler-opaque blocking (CPU-bound work, GVL-holding C
2415
- # extensions) then stalls the whole reactor — wrap GVL-releasing
2416
- # blocking and Ruby CPU work in ClaudeAgentSDK.offload { }; work
2417
- # that holds the GVL throughout needs a subprocess.
2418
- # Named after the mechanism, not a safety claim: whether inline is safe
2419
- # depends on the host satisfying the fiber-isolation precondition.
2420
- def callback_scheduling=(value)
2421
- if value.nil?
2422
- @callback_scheduling = nil
2423
- return
2424
- end
2425
-
2426
- mode = value.respond_to?(:to_sym) ? value.to_sym : value
2427
- unless CALLBACK_SCHEDULING_MODES.include?(mode)
2428
- raise ArgumentError,
2429
- "callback_scheduling must be one of #{CALLBACK_SCHEDULING_MODES.map(&:inspect).join(', ')} " \
2430
- "(got #{value.inspect})"
2431
- end
2432
-
2433
- @callback_scheduling = mode
2434
- end
2435
-
2436
- # Middleware wrapped around EVERY user-callback dispatch (message
2437
- # blocks, observers, hooks, permission callbacks, SDK MCP handlers).
2438
- # A callable receiving a zero-arg invocation; it MUST call it and
2439
- # return its value:
2440
- #
2441
- # callback_wrapper: ->(invocation) { MyApm.trace('agent.callback') { invocation.call } }
2442
- #
2443
- # The wrapper runs on the same execution context as the callback —
2444
- # inside the worker thread in :thread mode, in place on the reactor
2445
- # fiber in :inline mode. Exceptions propagate through it unchanged; it
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.
2452
- def callback_wrapper=(value)
2453
- raise ArgumentError, "callback_wrapper must be a callable or nil (got #{value.inspect})" unless value.nil? || value.respond_to?(:call)
2454
-
2455
- @callback_wrapper = value
2456
- end
2457
-
2458
- private
2459
-
2460
- # Strict key validation: unlike other Type subclasses (which silently drop
2461
- # unknown keys for forward-compat with newer CLI output), ClaudeAgentOptions
2462
- # is a developer-facing config object — typos should fail loudly.
2463
- def assign_attribute(name, value)
2464
- setter = :"#{normalize_name(name)}="
2465
- raise ArgumentError, "unknown ClaudeAgentOptions option: #{name.inspect}" unless respond_to?(setter)
2466
-
2467
- public_send(setter, value)
2468
- end
2469
-
2470
- # Merge caller-provided attributes with configured defaults.
2471
- # Only keys the caller explicitly passed are treated as overrides;
2472
- # method-signature defaults ([], {}, false) are NOT present unless the caller wrote them.
2473
- #
2474
- # Both sides are keyed by the option they name, not by their literal
2475
- # spelling: Type accepts symbol/string and snake_case/camelCase names, so a
2476
- # caller's `'permissionMode' => nil` must still inherit a configured
2477
- # `permission_mode:` (and a Hash must still merge into it) rather than
2478
- # riding along as a second entry that overwrites the default on assignment.
2479
- def merge_with_defaults(attributes)
2480
- return attributes unless defined?(ClaudeAgentSDK) && ClaudeAgentSDK.respond_to?(:default_options)
2481
-
2482
- defaults = ClaudeAgentSDK.default_options
2483
- return attributes unless defaults.any?
2484
-
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.
2497
- result = {}
2498
- defaults.each { |key, value| result[option_key(key)] = Type.deep_dup_for_options(value) }
2499
- attributes.each do |key, value|
2500
- key = option_key(key)
2501
- default_val = result[key]
2502
- result[key] = if value.nil?
2503
- default_val # nil means "no preference" — keep the configured default
2504
- elsif default_val.is_a?(Hash) && value.is_a?(Hash)
2505
- default_val.merge(value)
2506
- else
2507
- value
2508
- end
2509
- end
2510
- result
2511
- end
2512
-
2513
- # The canonical Symbol for a known option, whatever its spelling. An
2514
- # unknown name is returned untouched so assign_attribute's strict check
2515
- # reports the typo exactly as the developer wrote it.
2516
- def option_key(name)
2517
- normalized = normalize_name(name)
2518
- respond_to?(:"#{normalized}=") ? normalized.to_sym : name
2519
- end
2520
- end
2521
-
2522
- # SDK MCP Tool definition
2523
- class SdkMcpTool < Type
2524
- attr_accessor :name, :description, :input_schema, :handler, :annotations, :meta
2525
- end
2526
-
2527
- # SDK MCP Resource definition
2528
- class SdkMcpResource < Type
2529
- attr_accessor :uri, :name, :description, :mime_type, :reader
2530
- end
2531
-
2532
- # SDK MCP Prompt definition
2533
- class SdkMcpPrompt < Type
2534
- attr_accessor :name, :description, :arguments, :generator
2535
- end
2536
- end
3
+ # Entry point for the SDK's value types. The definitions live in
4
+ # lib/claude_agent_sdk/types/, one file per area; this file loads all of them,
5
+ # so `require 'claude_agent_sdk/types'` still defines every type. `base` (the
6
+ # Type superclass) comes first; each part also requires it itself.
7
+ require_relative 'types/base'
8
+ require_relative 'types/content_blocks'
9
+ require_relative 'types/messages'
10
+ require_relative 'types/option_values'
11
+ require_relative 'types/permissions'
12
+ require_relative 'types/hooks'
13
+ require_relative 'types/mcp'
14
+ require_relative 'types/sessions'
15
+ require_relative 'types/options'