pub enum Message {
Show 27 variants
Assistant {
message: AssistantEnvelope,
session_id: String,
parent_tool_use_id: Option<String>,
error: Option<AssistantMessageError>,
uuid: Option<String>,
timestamp: Option<String>,
extras: Extras,
},
User {
message: UserEnvelope,
session_id: String,
parent_tool_use_id: Option<String>,
uuid: Option<String>,
tool_use_result: Option<Value>,
timestamp: Option<String>,
synthetic: bool,
extras: Extras,
},
System {
subtype: String,
session_id: Option<String>,
data: Value,
},
TaskStarted {
task_id: String,
description: String,
uuid: String,
session_id: String,
tool_use_id: Option<String>,
task_type: Option<String>,
extras: Extras,
},
TaskUpdated {
task_id: String,
patch: TaskUpdatePatch,
uuid: String,
session_id: String,
extras: Extras,
},
TaskProgress {
task_id: String,
description: String,
usage: TaskUsage,
uuid: String,
session_id: String,
tool_use_id: Option<String>,
last_tool_name: Option<String>,
workflow_progress: Vec<WorkflowProgressEvent>,
extras: Extras,
},
TaskNotification {
task_id: String,
status: TaskNotificationStatus,
output_file: String,
summary: String,
uuid: String,
session_id: String,
tool_use_id: Option<String>,
usage: Option<TaskUsage>,
extras: Extras,
},
ToolProgress {
tool_use_id: String,
tool_name: String,
elapsed_time_seconds: f64,
heartbeat: bool,
parent_tool_use_id: Option<String>,
session_id: Option<String>,
uuid: Option<String>,
extras: Extras,
},
ThinkingTokens {
estimated_tokens: u64,
estimated_tokens_delta: i64,
uuid: String,
session_id: String,
extras: Extras,
},
TurnDuration {
ms: u64,
message_count: Option<u64>,
parent_tool_use_id: Option<String>,
session_id: String,
uuid: String,
extras: Extras,
},
StopHookSummary {
actions: u32,
hook_infos: Vec<StopHookInfo>,
hook_errors: Vec<String>,
has_output: bool,
level: String,
prevented_continuation: bool,
stop_reason: String,
tool_use_id: String,
parent_tool_use_id: Option<String>,
session_id: String,
uuid: String,
extras: Extras,
},
BackgroundTasksChanged {
tasks: Vec<Value>,
uuid: String,
session_id: String,
extras: Extras,
},
CommandsChanged {
commands: Vec<Value>,
uuid: String,
session_id: String,
extras: Extras,
},
HookStarted {
hook_id: String,
hook_name: String,
hook_event: String,
uuid: String,
session_id: String,
extras: Extras,
},
HookResponse {
hook_id: String,
hook_name: String,
hook_event: String,
outcome: String,
exit_code: i64,
output: String,
stdout: String,
stderr: String,
uuid: String,
session_id: String,
extras: Extras,
},
HookProgress {
hook_id: String,
hook_name: String,
hook_event: String,
stdout: String,
stderr: String,
output: String,
uuid: String,
session_id: String,
extras: Extras,
},
Notification {
key: Option<String>,
text: String,
priority: Option<String>,
uuid: String,
session_id: String,
extras: Extras,
},
PermissionDenied {
tool_name: String,
tool_use_id: String,
decision_reason_type: Option<String>,
decision_reason: Option<String>,
message: String,
uuid: String,
session_id: String,
extras: Extras,
},
CompactBoundary {
trigger: String,
pre_tokens: u64,
post_tokens: u64,
uuid: String,
session_id: String,
metadata_extras: Extras,
extras: Extras,
},
VcsStateChanged {
kind: String,
branch: Option<String>,
cwd: String,
uuid: String,
session_id: String,
extras: Extras,
},
CodeChangePublished {
provider: String,
url: String,
repo: String,
identifier: String,
action: Option<String>,
branch: Option<String>,
uuid: String,
session_id: String,
extras: Extras,
},
RateLimitEvent {
rate_limit_info: RateLimitInfo,
uuid: String,
session_id: String,
extras: Extras,
},
CommandLifecycle {
command_uuid: String,
state: String,
uuid: String,
session_id: String,
extras: Extras,
},
Result {Show 17 fields
subtype: String,
session_id: String,
is_error: bool,
num_turns: u64,
duration_ms: u64,
duration_api_ms: u64,
stop_reason: Option<String>,
total_cost_usd: Option<f64>,
usage: Option<Usage>,
result: Option<String>,
structured_output: Option<Value>,
model_usage: Option<Value>,
permission_denials: Option<Vec<Value>>,
errors: Option<Vec<String>>,
uuid: Option<String>,
terminal_reason: Option<TerminalReason>,
extras: Extras,
},
StreamEvent {
uuid: String,
session_id: String,
event: Value,
parent_tool_use_id: Option<String>,
extras: Extras,
},
Error {
error: String,
extras: Extras,
},
Unknown {
type_str: String,
raw: Value,
},
}Expand description
One stream-json message.
Wire-level dispatch on type and, for type="system", on subtype is
handled by a private shim - users never see it. Every variant here is
the user-facing shape.
Variants§
Assistant
An assistant turn (may be a partial chunk during streaming).
Fields
message: AssistantEnvelopeThe nested Anthropic-API-shaped message envelope.
parent_tool_use_id: Option<String>Parent tool-use id when this turn is a sub-agent spawned via Task.
error: Option<AssistantMessageError>Classification of a failure the CLI attributes to this turn
(e.g. rate_limit, billing_error). None for successful
turns. AssistantMessage.error
User
A user turn - user prompts or tool-result envelopes.
Fields
message: UserEnvelopeThe nested user-message envelope.
uuid: Option<String>Stable identifier for this user turn - the CLI
UserMessage.uuid. None unless the CLI is configured to
emit them (extra_args={"replay-user-messages": None}).
tool_use_result: Option<Value>Raw tool-result payload the CLI attaches when this user turn
reports a tool’s output. The CLI UserMessage.tool_use_result;
forge-sdk passes it through as a
Value since the upstream type is dict[str, Any].
timestamp: Option<String>When the CLI wrote this frame, RFC 3339. The only record of when a turn ran that a transcript holds: no result frame reaches one.
synthetic: boolThe CLI’s own mark that nobody typed this turn, folded from every
spelling the CLI gives it: the wire’s isSynthetic (itself the
union of isMeta, isVisibleInTranscriptOnly and
isCompactSummary), and the transcript row’s isMeta,
isCompactSummary, isVisibleInTranscriptOnly or
turnCompanion. A view that reads it draws the harness speaking
rather than the reader.
System
Out-of-band system event - subtype discriminates (e.g. "init").
Known task-lifecycle and mirror-error subtypes get their own typed
variants below; everything else lands here with the raw payload.
Fields
TaskStarted
A sub-agent Task has started running. Subtype "task_started".
TaskStartedMessage.
Fields
TaskUpdated
Incremental lifecycle update for any long-running tool task
(backgrounded Bash, Monitor, sub-agent Task). Subtype
"task_updated"; it carries a patch object with status /
end_time deltas. Without a typed variant, the reducer can’t
transition a backgrounded Bash from running to completed.
It rides alongside task_notification, not instead of it. The
wire baseline (baselines/sdk/2.1.280/backgrounded_bash_lifecycle.jsonl
in forge-test-harness) carries task_started, this and the ending
task_notification together for one local-bash command, and it is
that ending frame which names the output file.
Fields
task_id: StringStable identifier for this task instance - same id surface
as task_started / task_progress / task_notification.
patch: TaskUpdatePatchIncremental patch applied to the task’s state. Each field is optional because patches may update only some fields.
TaskProgress
Periodic progress update while a sub-agent Task is in flight.
Subtype "task_progress". v0.1.64
TaskProgressMessage.
Fields
workflow_progress: Vec<WorkflowProgressEvent>Workflow tool’s per-event snapshot of the workflow’s phase + agent state. Empty for non-Workflow task_progress events. Each event carries the FULL snapshot (not a delta), so the renderer can rebuild the per-phase tree from a single most-recent event.
TaskNotification
Terminal notification when a task completes, fails, or is stopped:
a sub-agent Task, a backgrounded Bash or a Monitor. Subtype
"task_notification". v0.1.64 TaskNotificationMessage.
Every task flavour fires it, not only the sub-agent tool. The
wire baselines record it ending a backgrounded command
(baselines/sdk/2.1.280/backgrounded_bash_lifecycle.jsonl in
forge-test-harness), carrying the output_file its output went to;
monitor.jsonl under the claude-cli-upgrade skill records the same
for a Monitor. The task_started / task_updated pair (see
Self::TaskStarted, Self::TaskUpdated) rides alongside while
the task runs rather than instead of this. Monitor stream events
also arrive as Result frames with
origin: {kind: "task-notification"}.
Fields
status: TaskNotificationStatusHow the task ended.
ToolProgress
The CLI’s heartbeat for a long-running tool call, emitted every
30 seconds while the call is in flight. Top-level tool_progress.
tool_use_id appends -heartbeat-<n> to the running call’s own
id, and parent_tool_use_id names that call. An informational
frame the SDK used to drop at the reader; it crosses like any
other so whoever draws the raw stream can surface liveness on the
call it belongs to.
Fields
ThinkingTokens
CLI-side estimated thinking-token count, fires repeatedly
during model thinking. Subtype "thinking_tokens" (#273).
Both counters are scoped to one thinking block, not to the turn: the CLI restarts them at each agentic iteration, so a turn with two thinking blocks emits a second run beginning at 50 again. A per-turn total is therefore the sum of the deltas, which is what the turn info row renders.
Fields
estimated_tokens: u64Estimated reasoning tokens so far in the current thinking block. Restarts at each block within a turn, so this is not a turn total and reading it as one understates a multi-block turn.
TurnDuration
Assistant-turn wall-clock + message-count summary emitted at
the end of each turn. Subtype "turn_duration" (#273); decoded
for wire conformance, and no renderer composes a chip from ms.
Fields
ms: u64Total wall-clock duration of the turn in milliseconds.
Wire field is durationMs (camelCase).
StopHookSummary
Stop-hook execution summary surfaced at end-of-turn. Subtype
"stop_hook_summary" (#273). The renderer composes a
collapsed 1-liner ↳ hook summary · N actions [▶ expand]
from actions (wire hookCount); expanded view enumerates
hook_infos.
Fields
actions: u32Number of hooks that fired. Wire field is hookCount.
Renderer hides the surface entirely when this is 0.
hook_infos: Vec<StopHookInfo>Per-hook breakdown. Wire field is hookInfos. Each entry
carries the command string + its duration in ms.
hook_errors: Vec<String>Errors the hook batch reported, empty when nothing failed.
Wire field is hookErrors; the client’s hooks chip draws
these against the batch rather than against one entry.
has_output: boolWhether any hook produced output. Wire field is
hasOutput. Forwarded to the renderer so a zero-actions
- has-output edge case can be surfaced if needed.
BackgroundTasksChanged
Full background-task set after it changed. Subtype
"background_tasks_changed" (2.1.204); carries the whole list,
not a delta.
Fields
CommandsChanged
Available slash-command set after it changed. Subtype
"commands_changed" (2.1.204), emitted on plugin reload.
Fields
HookStarted
A hook began executing. Subtype "hook_started" (2.1.204).
Fields
hook_id: StringStable id for this hook run, paired with Self::HookResponse.
HookResponse
A hook finished executing. Subtype "hook_response" (2.1.204).
Fields
hook_id: StringStable id tying this back to its Self::HookStarted.
HookProgress
A long-running hook emitted interim output. Subtype
"hook_progress" (2.1.263).
Fields
hook_id: StringStable id for this hook run, paired with Self::HookResponse.
Notification
The CLI surfaced a notification to the SDK host. Subtype
"notification" (2.1.263).
Fields
PermissionDenied
The CLI refused a tool call on permission grounds. Subtype
"permission_denied" (first observed on 2.1.280).
Fields
decision_reason_type: Option<String>Machine-readable reason category (e.g. "other"). Optional in the
2.1.280 schema, beside agent_id and decision_reason; no
measured frame omits it.
CompactBoundary
A compaction finished and the transcript was replaced. Subtype
"compact_boundary".
The wire nests the counts and the trigger under compact_metadata,
next to preserved-message uuids nothing reads. Only what forge
reacts to is modelled: every field modelled here is one a later
CLI can drop the whole frame to the generic bucket over.
Fields
trigger: StringWhat started the compaction: "manual" for /compact,
"auto" when the context window forced it.
VcsStateChanged
A foreground shell command mutated repository state. Subtype
"vcs_state_changed".
An event, not state: beyond the kind it names at most the branch the mutation landed on, so a reader still re-reads the tree for what it now holds.
Fields
kind: StringWhat class of mutation was observed: commit, push, merge
or rebase today. The CLI calls this an open set and asks
consumers to treat an unrecognized kind as “something changed”,
so it stays a string.
branch: Option<String>The branch a commit landed on or a push updated. Absent on merge and rebase frames, and on a push whose branch could not be attributed.
CodeChangePublished
A change from this session went out for review, or linked the
session to one. Subtype "code_change_published".
The URL fields are scraped from the command’s output and the CLI calls them unverified, so forge uses the frame as a refresh trigger and lets its own lookup fill what a PR row states.
Fields
provider: StringForge classification from the URL’s shape: github,
github-enterprise, gitlab, bitbucket, gerrit, and values
for other review systems. Open set.
action: Option<String>What the session did that produced the announcement: created,
edited, merged, commented, closed, reopened, ready,
draft, auto-merge-enabled, auto-merge-disabled, pushed,
checked-out, started. Absent only from older producers, and
free-form so a new verb decodes.
RateLimitEvent
Rate-limit state transition. The CLI emits this when the current
rate-limit window changes state (e.g. allowed → allowed_warning).
Wire shape mirrors +
.
Fields
rate_limit_info: RateLimitInfoRate-limit snapshot at the moment of the transition.
CommandLifecycle
The CLI’s own lifecycle for one prompt, keyed by the uuid forge
stamped on the frame that carried it. Top-level command_lifecycle.
Emitted only for prompts that carried a uuid when written (measured:
zero frames without one), and it is what lets a view say queued versus
taken: queued lands 1-5 ms after the write, started 2-3 ms after
the turn boundary that delivers the prompt.
state stays a free-form string so a state this build has not seen
decodes without a primitives bump; the known set is queued,
started, completed, cancelled, discarded, refused.
Fields
Result
End-of-turn or end-of-session summary with cost and usage.
Only six fields are required on the wire (subtype,
session_id, is_error, num_turns, duration_ms,
duration_api_ms); every other field is Option<...> because
the CLI omits them silently when not applicable.
Fields
stop_reason: Option<String>Why the turn ended, if the CLI reported a stop reason
(e.g. "end_turn", "max_turns", "error").
total_cost_usd: Option<f64>Total cost so far in USD. None when the CLI can’t compute
or doesn’t report (free-tier sessions, error-path results).
usage: Option<Usage>Aggregate token usage for the turn. Optional - the CLI omits the field on error-path frames.
result: Option<String>Plain-text result body when the turn produced one (e.g. the assistant’s final output).
structured_output: Option<Value>Structured output when
forge_sdk::Options::output_format was set to a JSON
schema. Passed through verbatim.
model_usage: Option<Value>Per-model usage breakdown. Wire key is camelCase
modelUsage (matches the CLI’s data.get("modelUsage")).
permission_denials: Option<Vec<Value>>Permissions denied during the turn, surfaced so callers can
audit can_use_tool outcomes.
terminal_reason: Option<TerminalReason>Why the turn ended at the runtime layer (completed,
aborted_streaming, max_turns, etc.). The CLI reports
this on result frames; surfaced here so consumers don’t
have to re-parse the wire JSON to read it.
StreamEvent
Streaming partial-message event emitted when
forge_sdk::Options::include_partial_messages is set. The
CLI forwards raw Anthropic-API stream events (message_start,
content_block_delta, message_delta, message_stop) without
coalescing them into complete turns. SDK’s
StreamEvent
).
Fields
Error
Fatal transport error injected into the message stream when the
CLI’s read loop fails. the CLI emits this at
as a last-gasp signal before teardown - emitted by the
CLI’s read loop. forge-sdk surfaces it via
the events stream returned by forge_sdk::Client::spawn so callers
see the failure on the iterator rather than via a side
channel.
Fields
Unknown
Forward-compat fallback: a frame whose top-level type value
forge-sdk doesn’t recognise. Surfaced through
the events stream returned by forge_sdk::Client::spawn when the codec
produces a the SDK’s DecodedLine::Unknown.
Library consumers can match this variant to detect upstream CLI
drift programmatically (telemetry, structured alerts) instead of
relying on tracing::warn! log scraping.
Mirrors the Unknown pattern already used by
ContentBlock and forge_sdk::control::ControlRequestKind.
Never produced by deserialization - decode_dispatch filters
unknown types into the SDK’s DecodedLine::Unknown
before they reach serde - but Serialize round-trips raw
verbatim so logs / replay capture the original bytes.
Implementations§
Source§impl Message
impl Message
Sourcepub fn session_id(&self) -> Option<&str>
pub fn session_id(&self) -> Option<&str>
Extract the session_id a message is tagged with, when present.
Used by the events stream returned by forge_sdk::Client::spawn to bind
the client’s session_id field on the first frame that carries
one - the CLI in stream-json interactive mode only emits
system/init (the canonical session-id source) AFTER both an
initialize control_request AND a user message have been seen,
so the session id isn’t known at spawn time.
Returns None for the two variants that aren’t session-scoped
(Error and RateLimitEvent, which carries no session id of its
own).
Sourcepub fn display_only_user(text: String, uuid: String) -> Self
pub fn display_only_user(text: String, uuid: String) -> Self
A user turn forged rather than read off the wire, for prose the model received and the CLI does not echo back.
The id is the prompt’s own, the one its command_lifecycle frames
carry: it is what lets a view hold this row while the prompt waits in
the queue, and pair it with the page’s later copy of the same message -
which carries the same id, the CLI writing the client’s uuid into the
transcript it delivers. Nothing routes on the empty session_id.
Sourcepub fn display_only_user_with_blocks(
text: String,
images: Vec<ContentBlock>,
uuid: String,
) -> Self
pub fn display_only_user_with_blocks( text: String, images: Vec<ContentBlock>, uuid: String, ) -> Self
Self::display_only_user with the image blocks a prompt’s
attachments draw as, placed before the words: the caller builds them
from the attachments it holds, so a dispatch never clones a payload it
will not draw.
Sourcepub fn stamp_if_absent(&mut self, stamp: &str)
pub fn stamp_if_absent(&mut self, stamp: &str)
Attach stamp to a frame that carries none.
Each kind takes it in the slot the wire’s own stamp occupies, so a
stamped frame reaches a reader shaped exactly like one the CLI stamped
itself: the typed field on Message::Assistant and Message::User,
the verbatim payload on Message::System and Message::Unknown,
and the extras every other variant re-emits at the frame’s top level -
which is where a wire stamp on those already lands.
A frame that carries one is left alone: a receipt is a fallback for a row the CLI did not stamp, never a correction of one it did.