| title | Transcript Schema |
|---|
Reference for the agent exchange transcript JSONL format produced at storage/transcripts/<run-id>.jsonl. Each line is a complete ExchangeEvent. See the Agent Exchange Transcript guide for usage.
All lines share the same envelope. Event-specific data lives in payload.
{
"seq": 1,
"run_id": "550e8400-e29b-41d4-a716-446655440000",
"type": "run.started",
"path": "",
"iteration": 0,
"timestamp": "2026-06-08T08:14:42.123Z",
"payload": null
}| Field | Type | Required | Notes |
|---|---|---|---|
seq |
uint64 | yes | Strictly monotonic per file, starting at 1. Allocated atomically by the recorder before write. |
run_id |
string | yes | UUID v4 identifying the workflow run. Reused from ExecutionContext.WorkflowID — same identifier as the audit trail. |
parent_run_id |
string | no | UUID v4 of the enclosing parent run. Present on child lifecycle events when the run was invoked via call_workflow. Omitted via omitempty when empty. |
child_run_id |
string | no | UUID v4 of a spawned sub-workflow run. Present on step.call_workflow.started / step.call_workflow.completed envelopes in the parent file. Omitted via omitempty when empty. |
type |
string | yes | One of the closed event types. |
path |
string | yes | Dot-separated step path within the workflow definition (e.g., analyze, parallel_block.branch_a). Empty string at the run-level events. |
iteration |
int | yes | Zero-based loop/retry iteration counter. 0 outside loops. |
timestamp |
string | yes | RFC 3339 / ISO 8601 with timezone, UTC preferred. Set at emission time. |
payload |
any | yes | Concrete shape determined by type. May be null for run lifecycle events. See Payload Dispatch. |
The vocabulary is closed. The complete set of valid type values is exactly:
type |
Payload | Description |
|---|---|---|
run.started |
*StepPayload or null |
Workflow run begins. |
run.completed |
*StepPayload or null |
Workflow run ends. |
step.started |
*StepPayload |
A step begins. Covers agent, command, operation, terminal, parallel, for_each, while, and generic custom step types. |
step.completed |
*StepPayload |
A step ends (success or failure). |
step.call_workflow.started |
*StepPayload |
Parent emits a sub-workflow invocation; the envelope carries child_run_id. |
step.call_workflow.completed |
*StepPayload |
Parent observes sub-workflow completion. |
message.user |
*MessagePayload |
Agent seam — resolved user prompt + composed system_prompt. |
message.assistant |
*MessagePayload |
Agent reply, normalized into ContentBlocks. |
tool.call |
*ToolPayload |
Tool invocation begins. Captured at the tools.Router.CallTool seam (fidelity:"router") or from agent NDJSON (fidelity:"agent_emitted"). |
tool.result |
*ToolPayload |
Tool invocation completes. |
Writers never emit values outside this set. Readers must handle unknown type values forward-compatibly — see Forward Compatibility.
Carried by step.* and run.* events.
| Field | Type | Required | Notes |
|---|---|---|---|
name |
string | yes | Step name from the workflow YAML, or workflow name for run.* events. |
kind |
string | yes | Step type discriminator (agent, command, operation, terminal, parallel, for_each, while, call_workflow, or a custom type name). Reflects the actual step.Type field. |
error |
string | no | Failure description on *.completed events; omitted on success via omitempty. |
result |
any | no | Step result for *.completed events. Shape depends on the step kind (e.g., agent output, command stdout, custom step return value). Omitted via omitempty when nil. |
{
"type": "step.started",
"payload": {
"name": "analyze",
"kind": "agent"
}
}Carried by message.user and message.assistant.
| Field | Type | Required | Notes |
|---|---|---|---|
role |
string | yes | "user" or "assistant". Discriminates payload dispatch in tolerant decoders. |
blocks |
array of ContentBlock |
yes | Ordered content blocks. For message.user, contains the resolved prompt and (when non-empty) the composed system_prompt. For message.assistant, contains the normalized provider output. |
{
"type": "message.user",
"payload": {
"role": "user",
"blocks": [
{"type": "text", "fidelity": "router", "text": "Review main.go for bugs."}
]
}
}Carried by tool.call and tool.result.
| Field | Type | Required | Notes |
|---|---|---|---|
name |
string | yes | Tool name (Read, Bash, plugin operation name, …). |
call_id |
string | yes | Opaque correlation token linking a tool.call to its tool.result. |
input |
any | yes (on tool.call) |
Tool arguments as provided. |
output |
any | yes (on tool.result) |
Tool return value. |
error |
string | no | Failure description on tool.result; omitted on success via omitempty. |
fidelity |
string | yes | "router" (synthesized at the AWF router seam) or "agent_emitted" (reported by the agent over stdio NDJSON). |
{
"type": "tool.call",
"payload": {
"name": "Read",
"call_id": "tool_01H...",
"input": {"path": "main.go"},
"output": null,
"fidelity": "router"
}
}Building block inside MessagePayload.blocks. The vocabulary is closed; field population depends on type. Unused fields are omitted via omitempty.
type |
Carries | Active Fields |
|---|---|---|
text |
Plain assistant text | text |
thinking |
Reasoning output (e.g., Claude extended thinking) | thinking |
tool_use |
Tool invocation block | tool_name, tool_id, tool_input |
tool_result |
Tool output block | tool_id, tool_content |
command |
Shell command executed by a step | command |
stream |
Provider stream chunk for partial-output replay | chunk (in text field per the wire format) |
Every block carries:
| Field | Type | Required | Notes |
|---|---|---|---|
type |
string | yes | One of the six block types above. |
fidelity |
string | yes | "router" or "agent_emitted". |
Block-specific fields (only populated for the relevant type; others omitted via omitempty):
| Field | Type | Used By | Notes |
|---|---|---|---|
text |
string | text, stream |
Verbatim assistant text. NUL bytes preserved. |
thinking |
string | thinking |
Verbatim thinking output. |
tool_name |
string | tool_use |
Tool name as advertised by the provider. |
tool_id |
string | tool_use, tool_result |
Provider-supplied correlation token; links a tool_use block to its tool_result block within the same message. |
tool_input |
any | tool_use |
Arguments as provided by the agent. |
tool_content |
any | tool_result |
Output as observed. May be a string, structured object, or null for errors. |
command |
string | command |
Resolved shell command string after template interpolation. |
chunk |
string | stream |
Single stream chunk from the provider. |
{"type": "text", "fidelity": "agent_emitted", "text": "Found 2 issues."}
{"type": "thinking", "fidelity": "agent_emitted", "thinking": "First I should..."}
{"type": "tool_use", "fidelity": "agent_emitted", "tool_name": "Read", "tool_id": "toolu_01", "tool_input": {"path": "main.go"}}
{"type": "tool_result", "fidelity": "router", "tool_id": "toolu_01", "tool_content": "package main\n..."}
{"type": "command", "fidelity": "router", "command": "go test ./..."}The fidelity field on every ContentBlock and ToolPayload distinguishes two provenances:
| Value | Meaning |
|---|---|
"router" |
Block synthesized by AWF's tools.Router.CallTool seam. Authoritative for in-process tool calls (builtin + plugin). |
"agent_emitted" |
Block emitted directly by the agent (e.g., stdio proxy NDJSON tool_use). Provenance is the agent, not the router. |
Consumers can use this marker to avoid double-counting tool calls reported on multiple channels.
The decoder selects the concrete payload type from the envelope type combined with a tolerant probe of the JSON shape:
- If the raw payload is a JSON array, decode into
[]ContentBlock. - If the raw payload is an object containing
"role", decode into*MessagePayload. - Else if it contains
"call_id", decode into*ToolPayload. - Else if it contains
"kind", decode into*StepPayload. - Otherwise, decode into a generic
any(forward-compatibility fallback).
A null or missing payload yields payload: nil — valid for run.* lifecycle events.
| Property | Value |
|---|---|
| Path | storage/transcripts/<run-id>.jsonl |
| Mode | 0o600 |
| Open flags | O_APPEND | O_CREATE | O_WRONLY |
| Writes | Serialized via sync.Mutex (always held — also covers payloads beyond POSIX PIPE_BUF) |
| Encoding | UTF-8, one JSON object per line, LF line endings |
| Ordering | Strictly monotonic seq per file, starting at 1 |
seqis allocated from a singleatomic.AddUint64inside the recorder before the write lock is taken.- All writes are serialized; no torn lines even when a payload exceeds
PIPE_BUF. - Multiple goroutines emitting concurrently produce a strictly monotonic
seqseries with no gaps.
O_APPENDguarantees that each successfulwrite()lands at the file end.- Process kill mid-write cannot tear a line written under
PIPE_BUF; the mutex covers the larger case.
A call_workflow step produces a new file for the child run. Linkage is bidirectional:
- Parent file —
step.call_workflow.startedenvelope setschild_run_idto the new run's UUID. - Child file — every envelope sets
parent_run_idto the invoking run's UUID.
Each file's seq series is independent and starts at 1. Reconstruction walks child_run_id links from the parent to locate every child file and assembles a connected tree.
Nesting deeper than one level produces one file per level with consistent parent linkage at every depth.
The reader is tolerant; the writer is strict:
- Writers only emit values from the closed
EventTypeandBlockTypevocabularies. - Readers that encounter an unknown
EventTypesurface it througherrors.Is(err, transcript.ErrUnknownEventType)so callers may treat it as a warning rather than a parse failure. Likewisetranscript.ErrUnknownBlockTypefor unknownBlockTypevalues. - Adding new envelope fields is safe — older readers ignore unknown fields per the JSON standard.
- Adding new event or block types requires every consumer to handle the unknown-type path explicitly. Removing or renaming values requires a coordinated schema bump.
The transcript is a pure addition:
audit.jsonloutput is byte-identical whether the transcript recorder is wired or not.DisplayEventstreams (used by streaming display and TUI) remain unchanged.- Plugin behavior is unaffected — instrumentation happens at AWF boundaries, not inside plugins.
Subscribers connect via the Recorder.Subscribe() port and receive every event written to disk, in order.
| Property | Value |
|---|---|
| Per-subscriber buffer | 256 events (default) |
| Drop policy | drop-newest |
| Ordering | Write-then-broadcast: disk first, subscribers second |
| Back-pressure | Slow subscriber drops events; disk write never blocks |
| Drop visibility | FanOut.Stats() exposes a drop counter; rate-limited WARN log (1/s per subscriber) when drops occur |
Close() |
Idempotent on both recorder and subscriber |
- No default secret masking. Prompts, system prompts, and tool inputs are recorded verbatim. The design preserves an opt-in masking hook for future use, but no masking ships in this version.
awf mcp-servesubprocess capture is not yet instrumented. Thefidelity:"agent_emitted"marker documents this gap and enables a future transition without changing the wire format.- Token-by-token streaming deltas are not captured. The
streamblock type exists in the vocabulary but is reserved for future use.
jq '.' storage/transcripts/<run-id>.jsonljq -s '
[.[] | select(.type == "tool.call" or .type == "tool.result")]
| group_by(.payload.call_id)
| map({
call_id: .[0].payload.call_id,
name: .[0].payload.name,
input: (map(select(.type == "tool.call"))[0].payload.input),
output: (map(select(.type == "tool.result"))[0].payload.output)
})
' storage/transcripts/<run-id>.jsonljq -r 'select(.type == "message.assistant")
| .payload.blocks[]
| select(.type == "text")
| .text' storage/transcripts/<run-id>.jsonljq -s '[.[].seq] | . == (sort)' storage/transcripts/<run-id>.jsonl
# truejq -r 'select(.type == "step.started")
| "\(.seq)\t\(.path)\t\(.payload.kind)\t\(.payload.name)"' \
storage/transcripts/<run-id>.jsonl# List every child run spawned by this parent
jq -r 'select(.type == "step.call_workflow.started") | .child_run_id' \
storage/transcripts/<parent-run-id>.jsonljq -s '
[.[] | select(.type == "tool.call")]
| group_by(.payload.fidelity)
| map({fidelity: .[0].payload.fidelity, count: length})
' storage/transcripts/<run-id>.jsonl- Agent Exchange Transcript Guide — Conceptual overview, file location, fan-out, and security notes
- Audit Trail Schema — The separate paired-event audit format that coexists with the transcript
- Package documentation: run
go doc github.com/awf-project/cli/internal/domain/transcriptfor the in-tree reference