claude-agent-sdk 0.36.0 → 0.37.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (41) hide show
  1. checksums.yaml +4 -4
  2. data/CHANGELOG.md +17 -0
  3. data/README.md +2 -2
  4. data/docs/client.md +26 -1
  5. data/docs/hooks-and-permissions.md +5 -3
  6. data/docs/mcp-servers.md +1 -2
  7. data/docs/sessions.md +81 -2
  8. data/docs/types.md +106 -4
  9. data/lib/claude_agent_sdk/cli_installer.rb +30 -3
  10. data/lib/claude_agent_sdk/command_builder.rb +109 -98
  11. data/lib/claude_agent_sdk/deprecation.rb +39 -0
  12. data/lib/claude_agent_sdk/fiber_boundary.rb +2 -0
  13. data/lib/claude_agent_sdk/instrumentation/otel.rb +15 -7
  14. data/lib/claude_agent_sdk/message_parser.rb +23 -9
  15. data/lib/claude_agent_sdk/observer.rb +2 -1
  16. data/lib/claude_agent_sdk/option_warnings.rb +2 -0
  17. data/lib/claude_agent_sdk/query.rb +50 -43
  18. data/lib/claude_agent_sdk/sdk_mcp_server.rb +22 -13
  19. data/lib/claude_agent_sdk/session_mutations.rb +20 -8
  20. data/lib/claude_agent_sdk/session_resume.rb +20 -11
  21. data/lib/claude_agent_sdk/session_store.rb +7 -3
  22. data/lib/claude_agent_sdk/session_summary.rb +4 -2
  23. data/lib/claude_agent_sdk/sessions.rb +8 -6
  24. data/lib/claude_agent_sdk/streaming.rb +1 -1
  25. data/lib/claude_agent_sdk/subprocess_cli_transport.rb +72 -40
  26. data/lib/claude_agent_sdk/testing/session_store_conformance.rb +14 -10
  27. data/lib/claude_agent_sdk/transcript_mirror_batcher.rb +2 -0
  28. data/lib/claude_agent_sdk/types/attributes.rb +271 -0
  29. data/lib/claude_agent_sdk/types/base.rb +320 -0
  30. data/lib/claude_agent_sdk/types/content_blocks.rb +57 -0
  31. data/lib/claude_agent_sdk/types/hooks.rb +640 -0
  32. data/lib/claude_agent_sdk/types/mcp.rb +232 -0
  33. data/lib/claude_agent_sdk/types/messages.rb +614 -0
  34. data/lib/claude_agent_sdk/types/option_values.rb +302 -0
  35. data/lib/claude_agent_sdk/types/options.rb +352 -0
  36. data/lib/claude_agent_sdk/types/permissions.rb +107 -0
  37. data/lib/claude_agent_sdk/types/sessions.rb +10 -0
  38. data/lib/claude_agent_sdk/types.rb +13 -2534
  39. data/lib/claude_agent_sdk/version.rb +1 -1
  40. data/lib/claude_agent_sdk.rb +51 -17
  41. metadata +11 -1
@@ -0,0 +1,614 @@
1
+ # frozen_string_literal: true
2
+
3
+ require_relative 'base'
4
+
5
+ module ClaudeAgentSDK
6
+ # Type constants for assistant message errors
7
+ ASSISTANT_MESSAGE_ERRORS = %w[
8
+ authentication_failed billing_error rate_limit invalid_request server_error max_output_tokens unknown
9
+ ].freeze
10
+
11
+ # Message Types
12
+
13
+ # User message
14
+ class UserMessage < Type
15
+ attr_accessor :content, :uuid, :parent_tool_use_id, :tool_use_result
16
+
17
+ # Provenance of this message — where the turn came from.
18
+ #
19
+ # In streaming-input mode a single connection interleaves the turns you
20
+ # send with turns the session injects on its own (background-task
21
+ # notifications, fired scheduled-task prompts, MCP channel messages,
22
+ # messages relayed from peer sessions, ...). `origin` tells them apart —
23
+ # see {ResultMessage#origin} for deciding whether a result answers *your*
24
+ # prompt.
25
+ #
26
+ # **Key form — read this before indexing into it.** A plain Hash, passed
27
+ # through from the CLI untouched: the SDK does not model it, whitelist its
28
+ # keys, or rewrite them, so kinds and fields newer CLI versions add stay
29
+ # visible. Keys therefore follow the transport's JSON parsing, which uses
30
+ # `symbolize_names: true` — they are **Symbols with the wire spelling
31
+ # preserved**, so camelCase keys stay camelCase and you index with
32
+ # `origin[:kind]`, `origin[:fromSession]`, `origin[:senderTaskId]`,
33
+ # `origin[:verifiedPeerPid]`. This is unlike the snake_case attributes
34
+ # elsewhere in this SDK, and unlike the Python SDK's string keys: a
35
+ # `origin["kind"]` or `origin[:from_session]` lookup silently returns nil
36
+ # and makes every attributed turn look unattributed. Only `:kind` is
37
+ # guaranteed present; the rest depend on it.
38
+ #
39
+ # `nil` means the CLI did not attribute the message — that is the normal
40
+ # case for prompts you send through {ClaudeAgentSDK.query} / {Client#query},
41
+ # unless the host stamps `origin: { kind: 'human' }` on the message Hash
42
+ # itself (only the `human` kind is honored from an SDK host). Populated on
43
+ # injected turns (task notifications, channel/peer messages, ...) and on
44
+ # user messages the CLI replays; tool-result messages never carry it.
45
+ #
46
+ # Known `:kind` values — documentation, not validation; treat anything
47
+ # unrecognized as "not human":
48
+ #
49
+ # - `'human'` — a turn submitted by the SDK host
50
+ # - `'channel'` — arrived on an MCP channel; `:server` names the MCP server
51
+ # - `'peer'` — relayed from a peer session. `:from` (sender address,
52
+ # sender-asserted — for reply routing or display, never as proof of
53
+ # identity), `:name` (display name, already normalized by the CLI),
54
+ # `:fromSession` (the sender's host-openable session id, a navigation
55
+ # target only), `:senderTaskId` (task id of the in-process background
56
+ # subagent that sent it; absent for cross-session peers), `:body`
57
+ # (decoded message body with the peer envelope stripped, byte-exact with
58
+ # what the model saw — render this instead of re-parsing the message
59
+ # text), `:verifiedPeerPid` (kernel-verified pid of the process that
60
+ # connected to this session's local messaging socket — the *connecting*
61
+ # process, which for relayed traffic is the relay; absent when
62
+ # unverifiable)
63
+ # - `'task-notification'` — a background task's delivery. `:subkind` is
64
+ # `'scheduled-trigger'` (the fired prompt of a scheduled task) or
65
+ # `'peer-send-message'` (a message sent from another of the user's
66
+ # sessions); absent for ordinary background-task notifications
67
+ # - `'coordinator'`, `'unclassified'`, `'observer'` (`:from` /
68
+ # `:senderTaskId` as for `peer`), `'auto-continuation'`,
69
+ # `'observer-activity'`
70
+ #
71
+ # @return [Hash{Symbol => Object}, nil]
72
+ # @see ResultMessage#origin
73
+ attr_accessor :origin
74
+
75
+ # Concatenated text of this message. Handles both String content
76
+ # (plain-text user prompt) and Array-of-blocks content (typed content).
77
+ # Returns "" when there is no text.
78
+ def text
79
+ case content
80
+ when String then content
81
+ when Array then content.grep(TextBlock).map(&:text).join("\n\n")
82
+ else ''
83
+ end
84
+ end
85
+
86
+ alias to_s text
87
+ end
88
+
89
+ # Assistant message with content blocks
90
+ class AssistantMessage < Type
91
+ attr_accessor :content, :model, :parent_tool_use_id, :error, :usage,
92
+ :message_id, :stop_reason, :session_id, :uuid
93
+
94
+ # Concatenated text across every TextBlock in this message's content.
95
+ # Returns "" when the message has no text (e.g., a pure tool_use turn).
96
+ def text
97
+ Array(content).grep(TextBlock).map(&:text).join("\n\n")
98
+ end
99
+
100
+ alias to_s text
101
+ end
102
+
103
+ # System message with metadata.
104
+ # When constructed from a raw CLI hash, the whole hash is stored in `#data`
105
+ # unless the caller explicitly provides a `:data` entry.
106
+ class SystemMessage < Type
107
+ attr_accessor :subtype, :data
108
+
109
+ def initialize(attributes = {})
110
+ super
111
+ @data ||= attributes if attributes.is_a?(Hash)
112
+ end
113
+
114
+ def to_s
115
+ subtype.nil? ? '[system]' : "[system: #{subtype}]"
116
+ end
117
+
118
+ private
119
+
120
+ # A typed subclass (InitMessage, TaskStartedMessage, ...) already exposes
121
+ # the fields of its raw frame as attributes; repeating @data would double
122
+ # the output. A bare SystemMessage (unrecognized subtype) keeps it, since
123
+ # @data is the only place its payload lives.
124
+ def inspect_attributes
125
+ return super if instance_of?(SystemMessage)
126
+
127
+ super.reject { |pair| pair.first == 'data' }
128
+ end
129
+ end
130
+
131
+ # Init system message (emitted at session start and after /clear)
132
+ class InitMessage < SystemMessage
133
+ attr_accessor :uuid, :session_id, :agents, :api_key_source, :betas,
134
+ :claude_code_version, :cwd, :tools, :mcp_servers, :model,
135
+ :permission_mode, :slash_commands, :output_style, :skills, :plugins,
136
+ :fast_mode_state # "off", "cooldown", or "on"
137
+ end
138
+
139
+ # Compact boundary system message (emitted after context compaction completes)
140
+ class CompactBoundaryMessage < SystemMessage
141
+ attr_accessor :uuid, :session_id
142
+ attr_reader :compact_metadata
143
+
144
+ def compact_metadata=(value)
145
+ @compact_metadata = value.is_a?(Hash) ? CompactMetadata.new(value) : value
146
+ end
147
+ end
148
+
149
+ # Metadata about a compaction event
150
+ class CompactMetadata < Type
151
+ attr_accessor :pre_tokens, :post_tokens, :trigger, :custom_instructions, :preserved_segment
152
+ end
153
+
154
+ # Status system message (compacting status, permission mode changes)
155
+ class StatusMessage < SystemMessage
156
+ attr_accessor :uuid, :session_id, :status, :permission_mode
157
+ end
158
+
159
+ # API retry system message
160
+ class APIRetryMessage < SystemMessage
161
+ attr_accessor :uuid, :session_id, :attempt, :max_retries, :retry_delay_ms, :error_status, :error
162
+ end
163
+
164
+ # Local command output system message
165
+ class LocalCommandOutputMessage < SystemMessage
166
+ attr_accessor :uuid, :session_id, :content
167
+ end
168
+
169
+ # Emitted when a session_store mirror batch fails terminally and is
170
+ # dropped — timeouts immediately (never retried), other failures after up
171
+ # to three attempts. The local-disk transcript is still durable; this is
172
+ # the consumer's only signal that the external store missed a batch
173
+ # (at-most-once delivery).
174
+ class MirrorErrorMessage < SystemMessage
175
+ attr_accessor :uuid, :session_id, :error, :key
176
+ end
177
+
178
+ # Hook started system message
179
+ class HookStartedMessage < SystemMessage
180
+ attr_accessor :uuid, :session_id, :hook_id, :hook_name, :hook_event
181
+ end
182
+
183
+ # Hook progress system message
184
+ class HookProgressMessage < SystemMessage
185
+ attr_accessor :uuid, :session_id, :hook_id, :hook_name, :hook_event, :stdout, :stderr, :output
186
+ end
187
+
188
+ # Hook response system message
189
+ class HookResponseMessage < SystemMessage
190
+ attr_accessor :uuid, :session_id, :hook_id, :hook_name, :hook_event,
191
+ :output, :stdout, :stderr, :exit_code,
192
+ :outcome # "success", "error", or "cancelled"
193
+ end
194
+
195
+ # Session state changed system message
196
+ class SessionStateChangedMessage < SystemMessage
197
+ attr_accessor :uuid, :session_id,
198
+ :state # "idle", "running", or "requires_action"
199
+ end
200
+
201
+ # Files persisted system message
202
+ class FilesPersistedMessage < SystemMessage
203
+ attr_accessor :uuid, :session_id, :files, :failed, :processed_at
204
+ end
205
+
206
+ # Elicitation complete system message
207
+ class ElicitationCompleteMessage < SystemMessage
208
+ attr_accessor :uuid, :session_id, :mcp_server_name, :elicitation_id
209
+ end
210
+
211
+ # Task lifecycle notification statuses
212
+ TASK_NOTIFICATION_STATUSES = %w[completed failed stopped].freeze
213
+
214
+ # Possible status values reported inside a `task_updated` patch.
215
+ # pending/running/paused are non-terminal; completed/failed/killed are
216
+ # terminal. Note: task_updated reports the raw "killed"; the CLI maps that to
217
+ # "stopped" only when it emits a task_notification.
218
+ TASK_UPDATED_STATUSES = %w[pending running paused completed failed killed].freeze
219
+
220
+ # Task statuses that mean the task has finished and should be cleared from any
221
+ # "active task" tracking. Spans both lifecycle vocabularies: task_notification
222
+ # reports "stopped" (the CLI's mapped form of a killed task) while task_updated
223
+ # reports the raw "killed". Treat the status of a TaskNotificationMessage and a
224
+ # TaskUpdatedMessage the same way.
225
+ TERMINAL_TASK_STATUSES = %w[completed failed stopped killed].freeze
226
+
227
+ # Typed usage data for task progress and notifications
228
+ class TaskUsage < Type
229
+ attr_accessor :total_tokens, :tool_uses, :duration_ms
230
+
231
+ def initialize(attributes = {})
232
+ super
233
+ @total_tokens ||= 0
234
+ @tool_uses ||= 0
235
+ @duration_ms ||= 0
236
+ end
237
+ end
238
+
239
+ # Task started system message (subagent/background task started)
240
+ class TaskStartedMessage < SystemMessage
241
+ attr_accessor :task_id, :description, :uuid, :session_id, :tool_use_id, :task_type,
242
+ :workflow_name, :prompt,
243
+ :subagent_type # Subagent type, for Task/Agent tool subagents; nil otherwise
244
+
245
+ # Whether the task was registered in the background (`true`) or in the
246
+ # foreground with the spawning tool call blocking on it (`false`). `nil`
247
+ # means the CLI did not say (the field is optional, and only set for
248
+ # `local_agent` and `local_bash` tasks) — so test `== false`, never
249
+ # falsiness, to detect a foreground/blocking task. A resumed subagent is
250
+ # always registered in the background. A later move to the background does
251
+ # not re-emit task_started; it arrives as {TaskUpdatedMessage#is_backgrounded}.
252
+ #
253
+ # @return [Boolean, nil]
254
+ attr_accessor :is_backgrounded
255
+
256
+ # Nesting depth of a spawned subagent (`local_agent`) task: 1 for a
257
+ # top-level spawn, N+1 when spawned from inside a depth-N agent. `nil` on
258
+ # other task types and on CLIs that do not report it.
259
+ #
260
+ # @return [Integer, nil]
261
+ attr_accessor :spawn_depth
262
+
263
+ # Display flags, passed through for the host to act on — the SDK never
264
+ # filters frames or computes activity from them. Both are optional
265
+ # Booleans: `nil` when absent, an explicit `false` preserved.
266
+ #
267
+ # - `skip_transcript`: an ambient/housekeeping task. Hide it from the
268
+ # inline transcript; it may still appear in a tasks panel.
269
+ # - `ambient`: true for tasks that are not activity — every
270
+ # `skip_transcript` task, plus every live-update watcher (requested or
271
+ # auto-started). Exclude these from activity indicators.
272
+ #
273
+ # @return [Boolean, nil]
274
+ attr_accessor :skip_transcript, :ambient
275
+ end
276
+
277
+ # Task progress system message (periodic update from a running task).
278
+ #
279
+ # `summary` is an optional one-line status for the task's row — `nil` on any
280
+ # frame that lacks one. For a `local_agent` task it is the model-generated
281
+ # progress summary, which the CLI produces only while generation is enabled
282
+ # (see {ClaudeAgentOptions#agent_progress_summaries}); for a backgrounded
283
+ # `mcp_task` it is the MCP server's own status message and needs no option.
284
+ class TaskProgressMessage < SystemMessage
285
+ attr_accessor :task_id, :description, :usage, :uuid, :session_id, :tool_use_id, :last_tool_name, :summary,
286
+ :subagent_type # Subagent type, for Task/Agent tool subagents; nil otherwise
287
+ end
288
+
289
+ # Task notification system message (task completed/failed/stopped).
290
+ #
291
+ # Note: not every terminal task emits this message. Background tasks may
292
+ # instead report completion only via a TaskUpdatedMessage whose patch["status"]
293
+ # is terminal (see TERMINAL_TASK_STATUSES). Consumers tracking active task IDs
294
+ # should clear them on a terminal status from *either* message.
295
+ class TaskNotificationMessage < SystemMessage
296
+ attr_accessor :task_id, :status, :output_file, :summary, :uuid, :session_id, :tool_use_id, :usage
297
+
298
+ # Machine-readable cause, set only when the task did not end through an
299
+ # ordinary completion, failure, or stop. The one known value is
300
+ # `'worker_restart'` (the worker process restarted and the resumed process
301
+ # found the task orphaned; always with status `'stopped'`). Documentation,
302
+ # not validation: newer CLIs may add values.
303
+ #
304
+ # @return [String, nil]
305
+ attr_accessor :reason
306
+
307
+ # For a backgrounded MCP task (`task_type: 'mcp_task'`) that completed: the
308
+ # `resource_link` content blocks of its final result — the files it
309
+ # returned by reference. A backgrounded task's tool_result is placeholder
310
+ # text, so this is where a host learns which files the call produced; join
311
+ # to the originating call via `tool_use_id`. `nil` when the result had
312
+ # none or the task is any other type.
313
+ #
314
+ # Passed through from the CLI untouched, so each element is a Hash whose
315
+ # keys are **Symbols with the wire spelling preserved**: `:uri` and `:name`
316
+ # (Strings, always present), and optionally `:title`, `:description`,
317
+ # `:mimeType` (camelCase — a `:mime_type` lookup returns nil), `:size` (a
318
+ # Number, not necessarily an Integer), `:annotations` (a Hash of arbitrary
319
+ # values). Elements carry no `type: 'resource_link'` discriminator. The CLI
320
+ # describes its own output as at most 50 links / 64 KiB serialized; that is
321
+ # a producer-side note, and the SDK neither enforces nor truncates.
322
+ #
323
+ # @return [Array<Hash{Symbol => Object}>, nil]
324
+ attr_accessor :resource_links
325
+
326
+ # Display flags with the same meaning as on {TaskStartedMessage}:
327
+ # `skip_transcript` (hide from the inline transcript) and `ambient` (not
328
+ # activity — exclude from activity indicators). Optional Booleans: `nil`
329
+ # when absent, an explicit `false` preserved. The SDK does not act on them.
330
+ #
331
+ # @return [Boolean, nil]
332
+ attr_accessor :skip_transcript, :ambient
333
+ end
334
+
335
+ # Task updated system message (background task lifecycle state change).
336
+ #
337
+ # The CLI emits system/task_updated events as a task moves through its
338
+ # lifecycle. `patch` carries the changed fields (e.g. status, end_time); when
339
+ # patch["status"] is terminal (see TERMINAL_TASK_STATUSES) the task has
340
+ # finished. A background task's terminal state can arrive *only* as a
341
+ # TaskUpdatedMessage with no accompanying TaskNotificationMessage — e.g. a task
342
+ # stopped via TaskStop reports status "killed" here and the matching
343
+ # notification is sometimes suppressed. Consumers tracking active task IDs
344
+ # should clear them on a terminal status from *either* message.
345
+ #
346
+ # Parsed defensively in the constructor — a lifecycle event must never raise:
347
+ # `status` is derived from patch["status"] (not a top-level field); a non-Hash
348
+ # or absent patch falls back to {}; and `task_id` defaults to "" (never nil,
349
+ # matching the Python SDK) so consumers can rely on it always being a String.
350
+ # The full patch is preserved on `#patch` for callers that need more than the
351
+ # derived readers.
352
+ #
353
+ # A patch carries only the fields that changed, so every derived reader is
354
+ # `nil` when its field is absent. That matters most for `is_backgrounded`:
355
+ # `true` means the task just moved to the background (e.g. after
356
+ # {Client#background_tasks}), while `nil` means "this patch does not mention
357
+ # it" — not "foreground". Merge patches into your own task map rather than
358
+ # reading any single one as the task's full state.
359
+ class TaskUpdatedMessage < SystemMessage
360
+ attr_accessor :task_id, :patch, :status, :uuid, :session_id,
361
+ :description, # patch[:description] — String, nil when unchanged
362
+ :error, # patch[:error] — String, nil when unchanged
363
+ :end_time, # patch[:end_time] — Integer (epoch ms), nil when unchanged
364
+ :total_paused_ms, # patch[:total_paused_ms] — Integer, nil when unchanged
365
+ :is_backgrounded # patch[:is_backgrounded] — true/false, nil when unchanged
366
+
367
+ def initialize(attributes = {})
368
+ super
369
+ @task_id ||= ''
370
+ @patch = {} unless @patch.is_a?(Hash)
371
+ @status = patch_value(:status)
372
+ @description = patch_value(:description)
373
+ @error = patch_value(:error)
374
+ @end_time = patch_value(:end_time)
375
+ @total_paused_ms = patch_value(:total_paused_ms)
376
+ @is_backgrounded = patch_value(:is_backgrounded)
377
+ end
378
+
379
+ private
380
+
381
+ # The parser always hands over a symbol-keyed patch; a hand-built message
382
+ # may use string keys. `fetch` with a block (not `||`) keeps an explicit
383
+ # `false` from falling through to the string-key lookup's nil.
384
+ def patch_value(key)
385
+ @patch.fetch(key) { @patch[key.to_s] }
386
+ end
387
+ end
388
+
389
+ # Background tasks changed system message: the full set of live background
390
+ # tasks, emitted whenever membership changes (start, completion, kill, a
391
+ # foreground agent being backgrounded) or an entry's `ambient` flag flips.
392
+ #
393
+ # A **level** signal with **REPLACE semantics** — `tasks` is every live
394
+ # background task after the change, so swap your set for each payload rather
395
+ # than pairing task_started / task_notification edges; a missed edge then
396
+ # cannot wedge a stale "running" indicator. Per the CLI's contract:
397
+ #
398
+ # - Ordering relative to the edge frames for the same transition is
399
+ # unspecified, and the payload carries ids only — do not correlate it
400
+ # with the edge stream.
401
+ # - The level is per-process: nothing is emitted at startup, so reset to the
402
+ # empty set whenever the session's CLI process (re)starts.
403
+ # - `tasks: []` is an authoritative empty snapshot for that process, not a
404
+ # missing value.
405
+ # - A repeated `initialize` on an already-running process is answered with a
406
+ # snapshot of the current set (even an empty one) right behind its success
407
+ # response; older CLIs send nothing there. This SDK initializes once per
408
+ # connection, so that only matters to custom transports that reconnect.
409
+ # - It covers *background* tasks only. A foreground subagent (the spawning
410
+ # tool call still blocking) is not listed until it is backgrounded.
411
+ #
412
+ # `tasks` is passed through untouched: an Array of symbol-keyed Hashes
413
+ # `{ task_id:, task_type:, description:, ambient: }`. `:ambient` is optional;
414
+ # true marks tasks that are not activity (housekeeping, live-update
415
+ # watchers), which hosts should exclude from activity indicators.
416
+ #
417
+ # The SDK itself deliberately does not consume this frame for its own
418
+ # stdin-close bookkeeping; it is typed purely for consumers.
419
+ class BackgroundTasksChangedMessage < SystemMessage
420
+ attr_accessor :tasks, :uuid, :session_id
421
+ end
422
+
423
+ # Permission denied system message: a tool call was auto-denied without an
424
+ # interactive permission prompt (auto-mode classifier, dontAsk mode,
425
+ # headless-agent auto-deny, a deny rule, or — with no can_use_tool callback —
426
+ # an "ask" decision that nobody can answer). The "ask" path with a callback
427
+ # surfaces through can_use_tool instead.
428
+ #
429
+ # **Best-effort advisory, not a complete denial feed**:
430
+ # {ResultMessage#permission_denials} is the authoritative record. In rare
431
+ # races a booked denial has no frame, or a frame has no booked denial — so do
432
+ # not derive counts or permission state from this stream. Not covered at all:
433
+ # PreToolUse hook denies, deny-rule overrides of a hook's allow/ask decision,
434
+ # Read/Edit/Write calls refused by a path-scoped deny rule (all resolve before
435
+ # the permission check), and the MCP `--permission-prompt-tool` surface.
436
+ #
437
+ # `agent_id` is a subagent id for host-side routing; it is NOT a permission
438
+ # `request_id`, and this message is not a pending permission request.
439
+ # `decision_reason_type` is an open String (the values below are examples,
440
+ # not an enum). The CLI's `decision_reason_code` is marked internal and is
441
+ # left to `#data` with no stability promise.
442
+ class PermissionDeniedMessage < SystemMessage
443
+ attr_accessor :uuid, :session_id, :tool_name, :tool_use_id,
444
+ :agent_id, # Subagent ID when the denied call originated inside a subagent; nil otherwise
445
+ :decision_reason_type, # Open String ("classifier", "asyncAgent", "mode", "rule"); nil if not reported
446
+ :decision_reason, # Human-readable reason from the deciding component; nil when not reported
447
+ :message # The rejection message returned to the model in the tool_result
448
+ end
449
+
450
+ # Result message with cost and usage information
451
+ class ResultMessage < Type
452
+ # model_usage maps model name => per-model usage Hash, passed through
453
+ # verbatim from the CLI's modelUsage field, so its keys are camelCase
454
+ # (matches the TypeScript/Python SDKs' ModelUsage shape): inputTokens,
455
+ # outputTokens, cacheReadInputTokens, cacheCreationInputTokens,
456
+ # webSearchRequests, costUSD, contextWindow, maxOutputTokens, plus
457
+ # optional canonicalModel (canonical id used for the pricing lookup —
458
+ # may differ from the raw model-string key for provider-specific
459
+ # ids/aliases) and provider ('firstParty', 'bedrock', 'vertex', ...).
460
+ #
461
+ # terminal_reason says why the query loop ended ("completed",
462
+ # "max_turns", "aborted_streaming", ...). "aborted_streaming" /
463
+ # "aborted_tools" mean the turn was cancelled via Client#interrupt (an
464
+ # interrupt control request). nil when the CLI did not report one
465
+ # (older CLI versions, or a result that bypassed the query loop such
466
+ # as a local slash command).
467
+ attr_accessor :subtype, :duration_ms, :duration_api_ms, :is_error,
468
+ :num_turns, :session_id, :stop_reason, :total_cost_usd, :usage,
469
+ :result, :structured_output,
470
+ :model_usage, # Hash of { model_name => usage_data }, see above
471
+ :permission_denials, # Array of { tool_name:, tool_use_id:, tool_input: }
472
+ :errors, # Array of error strings (present on error subtypes)
473
+ :uuid,
474
+ :fast_mode_state, # "off", "cooldown", or "on"
475
+ :api_error_status, # Integer HTTP status (429, 500, 529) on api_error subtype (CLI 2.1.110+)
476
+ :terminal_reason # why the query loop ended, see above
477
+
478
+ attr_reader :deferred_tool_use # DeferredToolUse, populated when a PreToolUse hook deferred
479
+
480
+ def deferred_tool_use=(value)
481
+ @deferred_tool_use = value.is_a?(Hash) ? DeferredToolUse.from_hash(value) : value
482
+ end
483
+
484
+ # Provenance of the user message that triggered this turn — `nil` when the
485
+ # CLI did not attribute it. Lets a streaming-input consumer distinguish the
486
+ # result of its own prompt from the result of a turn the session injected
487
+ # on its own:
488
+ #
489
+ # if result.origin.nil? || result.origin[:kind] == 'human'
490
+ # # a turn this application submitted
491
+ # elsif result.origin[:kind] == 'task-notification'
492
+ # # follow-up turn driven by a background task
493
+ # end
494
+ #
495
+ # **Key form.** A plain Hash passed through from the CLI untouched, so its
496
+ # keys are **Symbols with the wire spelling preserved** — camelCase stays
497
+ # camelCase (`origin[:kind]`, `origin[:fromSession]`,
498
+ # `origin[:verifiedPeerPid]`), unlike the snake_case attributes elsewhere
499
+ # in this SDK and unlike the Python SDK's string keys. Indexing with
500
+ # `origin["kind"]` silently returns nil and makes every attributed turn
501
+ # look unattributed.
502
+ #
503
+ # See {UserMessage#origin} for the full list of known `:kind` values and
504
+ # their per-kind keys.
505
+ #
506
+ # @return [Hash{Symbol => Object}, nil]
507
+ # @see UserMessage#origin
508
+ attr_accessor :origin
509
+
510
+ # One human-readable line, e.g. `[result: success, 3 turns, 4.2s, $0.0120]`
511
+ # (parts the CLI did not report are left out). An error result appends its
512
+ # `errors`. Use #inspect for every field.
513
+ def to_s
514
+ parts = [subtype].compact
515
+ parts << "#{num_turns} #{num_turns == 1 ? 'turn' : 'turns'}" unless num_turns.nil?
516
+ parts << format('%.1fs', duration_ms / 1000.0) if duration_ms.is_a?(Numeric)
517
+ parts << format('$%.4f', total_cost_usd) if total_cost_usd.is_a?(Numeric)
518
+ line = parts.empty? ? '[result]' : "[result: #{parts.join(', ')}]"
519
+ line += " - #{Array(errors).join('; ')}" if is_error && !Array(errors).empty?
520
+ line
521
+ end
522
+ end
523
+
524
+ # Stream event for partial message updates
525
+ class StreamEvent < Type
526
+ attr_accessor :uuid, :session_id, :event, :parent_tool_use_id
527
+ end
528
+
529
+ # Tool progress message (type: 'tool_progress')
530
+ class ToolProgressMessage < Type
531
+ attr_accessor :uuid, :session_id, :tool_use_id, :tool_name, :parent_tool_use_id,
532
+ :elapsed_time_seconds, :task_id
533
+ end
534
+
535
+ # Auth status message (type: 'auth_status')
536
+ class AuthStatusMessage < Type
537
+ attr_accessor :uuid, :session_id, :is_authenticating, :output, :error
538
+ end
539
+
540
+ # Tool use summary message (type: 'tool_use_summary')
541
+ class ToolUseSummaryMessage < Type
542
+ attr_accessor :uuid, :session_id, :summary, :preceding_tool_use_ids
543
+ end
544
+
545
+ # Prompt suggestion message (type: 'prompt_suggestion')
546
+ class PromptSuggestionMessage < Type
547
+ attr_accessor :uuid, :session_id, :suggestion
548
+ end
549
+
550
+ # Type constants for rate limit statuses
551
+ RATE_LIMIT_STATUSES = %w[allowed allowed_warning rejected].freeze
552
+
553
+ # Type constants for rate limit types
554
+ RATE_LIMIT_TYPES = %w[five_hour seven_day seven_day_opus seven_day_sonnet overage].freeze
555
+
556
+ # Rate limit info with typed fields
557
+ class RateLimitInfo < Type
558
+ attr_accessor :status, :resets_at, :rate_limit_type, :utilization,
559
+ :overage_status, :overage_resets_at, :overage_disabled_reason, :raw
560
+
561
+ def initialize(attributes = {})
562
+ super
563
+ @raw ||= {}
564
+ end
565
+ end
566
+
567
+ # Rate limit event emitted when rate limit info changes
568
+ class RateLimitEvent < Type
569
+ attr_accessor :uuid, :session_id, :raw_data
570
+ attr_reader :rate_limit_info
571
+
572
+ def initialize(attributes = {})
573
+ super
574
+ @rate_limit_info ||= RateLimitInfo.new
575
+ end
576
+
577
+ def rate_limit_info=(value)
578
+ @rate_limit_info = value.is_a?(Hash) ? RateLimitInfo.new(value.merge(raw: value)) : value
579
+ end
580
+
581
+ # Backward-compatible accessor returning the full raw event payload
582
+ declare_attributes :data
583
+ def data
584
+ @raw_data || {}
585
+ end
586
+ end
587
+
588
+ # Emitted when the session's conversation is replaced without ending the
589
+ # connection — e.g. after `/clear` or any other flow that discards the
590
+ # transcript mid-session (type: 'conversation_reset').
591
+ #
592
+ # In streaming-input mode a single connection carries many user turns, and a
593
+ # reset clears the conversation history *and* zeroes the running totals
594
+ # reported on subsequent {ResultMessage} objects (e.g. `total_cost_usd`). If
595
+ # you accumulate those totals across a long-lived session, snapshot them when
596
+ # this message arrives.
597
+ #
598
+ # @!attribute [rw] new_conversation_id
599
+ # Opaque identifier for the fresh conversation, for UIs to key an empty
600
+ # transcript on (and to discard any cached session title). This is *not*
601
+ # the `session_id` of subsequent messages — read that from the next
602
+ # message.
603
+ # @return [String]
604
+ # @!attribute [rw] uuid
605
+ # Unique ID of this message.
606
+ # @return [String]
607
+ # @!attribute [rw] session_id
608
+ # ID of the session that was reset (the outgoing session; messages after
609
+ # the reset carry a new `session_id`).
610
+ # @return [String]
611
+ class ConversationResetMessage < Type
612
+ attr_accessor :new_conversation_id, :uuid, :session_id
613
+ end
614
+ end