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.
- checksums.yaml +4 -4
- data/CHANGELOG.md +17 -0
- data/README.md +2 -2
- data/docs/client.md +26 -1
- data/docs/hooks-and-permissions.md +5 -3
- data/docs/mcp-servers.md +1 -2
- data/docs/sessions.md +81 -2
- data/docs/types.md +106 -4
- data/lib/claude_agent_sdk/cli_installer.rb +30 -3
- data/lib/claude_agent_sdk/command_builder.rb +109 -98
- data/lib/claude_agent_sdk/deprecation.rb +39 -0
- data/lib/claude_agent_sdk/fiber_boundary.rb +2 -0
- data/lib/claude_agent_sdk/instrumentation/otel.rb +15 -7
- data/lib/claude_agent_sdk/message_parser.rb +23 -9
- data/lib/claude_agent_sdk/observer.rb +2 -1
- data/lib/claude_agent_sdk/option_warnings.rb +2 -0
- data/lib/claude_agent_sdk/query.rb +50 -43
- data/lib/claude_agent_sdk/sdk_mcp_server.rb +22 -13
- data/lib/claude_agent_sdk/session_mutations.rb +20 -8
- data/lib/claude_agent_sdk/session_resume.rb +20 -11
- data/lib/claude_agent_sdk/session_store.rb +7 -3
- data/lib/claude_agent_sdk/session_summary.rb +4 -2
- data/lib/claude_agent_sdk/sessions.rb +8 -6
- data/lib/claude_agent_sdk/streaming.rb +1 -1
- data/lib/claude_agent_sdk/subprocess_cli_transport.rb +72 -40
- data/lib/claude_agent_sdk/testing/session_store_conformance.rb +14 -10
- data/lib/claude_agent_sdk/transcript_mirror_batcher.rb +2 -0
- data/lib/claude_agent_sdk/types/attributes.rb +271 -0
- data/lib/claude_agent_sdk/types/base.rb +320 -0
- data/lib/claude_agent_sdk/types/content_blocks.rb +57 -0
- data/lib/claude_agent_sdk/types/hooks.rb +640 -0
- data/lib/claude_agent_sdk/types/mcp.rb +232 -0
- data/lib/claude_agent_sdk/types/messages.rb +614 -0
- data/lib/claude_agent_sdk/types/option_values.rb +302 -0
- data/lib/claude_agent_sdk/types/options.rb +352 -0
- data/lib/claude_agent_sdk/types/permissions.rb +107 -0
- data/lib/claude_agent_sdk/types/sessions.rb +10 -0
- data/lib/claude_agent_sdk/types.rb +13 -2534
- data/lib/claude_agent_sdk/version.rb +1 -1
- data/lib/claude_agent_sdk.rb +51 -17
- 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
|