Transcript & turn events
Conceptual guide: Turns & Transcripts.
Everything on this page is also available type-only from @fifthrevision/axle/ui, which doesn't pull in the provider SDKs — use that import in UI code.
Transcript
class Transcript<TAnnotation extends Annotation = Annotation, THostEvent extends UnknownEvent = UnknownEvent> {
constructor(turns?: readonly Turn<TAnnotation>[]);
get turns(): readonly Turn<TAnnotation>[];
getTurn(turnId: string): Turn<TAnnotation> | undefined;
apply(event: TranscriptInput<TAnnotation, THostEvent>): TranscriptApplyResult<TAnnotation, THostEvent>;
}The constructor shallow-copies the array. turns is readonly; all structural change goes through apply().
type TranscriptApplyResult<TAnnotation extends Annotation, THostEvent extends UnknownEvent> =
| { handled: true; event: TurnEvent<TAnnotation> }
| { handled: false; event: THostEvent };UnknownEvent and TimingInfo are exported from @fifthrevision/axle/ui only, not from the package root.
Unrecognized event types pass through with handled: false, so a single dispatcher can carry both Axle events and your own.
Turn
interface Turn<TAnnotation extends Annotation = Annotation> {
id: string;
owner: "user" | "agent";
parts: TurnPart<TAnnotation>[];
status: TurnStatus;
annotations?: TAnnotation[];
metadata?: Record<string, unknown>;
timing?: TimingInfo;
usage?: Stats;
error?: { type: string; message: string };
}
type TurnStatus = "streaming" | "complete" | "cancelled" | "error";
interface TimingInfo { start: string; end?: string } // ISO timestampsTurnPart
type TurnPart =
| TextPart
| CitationPart
| FilePart
| ThinkingPart
| ActionPart
| CompactionPart;Every part has id, type, optional annotations, and optional timing.
| Part | Additional fields |
|---|---|
TextPart | text: string, citations?: Citation[], providerMetadata? |
CitationPart | citations: Citation[], providerMetadata? |
FilePart | file: FileInfo |
ThinkingPart | summary?, raw?, continuity?, providerMetadata? |
CompactionPart | status: "running" | "complete" | "error", summary?, progress?, error? |
ThinkingPart
interface ThinkingPart {
id: string;
type: "thinking";
summary?: string; // the provider's condensed account of its reasoning
raw?: string; // the chain of thought itself; open-weight models only
continuity?: ThinkingContinuity;
providerMetadata?: Record<string, unknown>;
}Each content field is named for what the provider handed back; neither present is the withheld state. Render summary ?? raw. A field appears only once a delta wrote it, so never test for "". (text / redacted on the turn part were removed in 0.32.0 — redacted now lives only on the message layer; see Messages & parts and Upgrading.)
ActionPart
type ActionPart = ToolAction | SubagentAction | ProviderToolAction;All share status: "pending" | "running" | "complete" | "cancelled" | "error" and discriminate on kind.
kind | detail |
|---|---|
"tool" | { name, parameters, pendingArgs?, result? } |
"agent" | { name, config?, children: Turn[], result? } |
"provider-tool" | { name, input?, result? } |
type ActionResult =
| { type: "in-progress"; content: string }
| { type: "success"; content?: string | ConsoleOutput | ToolResultPart[] }
| { type: "error"; error: { type: string; message: string } };
interface ConsoleOutput {
stdout: string;
stderr?: string;
exitCode?: number;
}ConsoleOutput (added in 0.33.0) arrives only from provider code execution that separates its streams (Anthropic today); OpenAI and Gemini report one stream, so their output arrives as a string. A renderer that narrows content with typeof content === "string" and treats the rest as parts must add the object case. Local tools still return strings or parts.
pendingArgs holds accumulated argument JSON before it can be parsed into parameters. Render it during streaming so there's something on screen.
SubagentAction.detail.children is a nested Turn[], so subagent rendering is recursive.
Annotations
interface Annotation<TData = unknown, TKind extends string = string> {
id: string;
kind: TKind;
label: string;
placement?: "before" | "after"; // defaults to "after" when accumulated
status?: "running" | "complete" | "cancelled" | "error";
data?: TData;
timing?: TimingInfo;
}Your own UI state, attached to a turn or a part and never sent to a provider. Omit status for static annotations that have no lifecycle.
TurnEvent
Turn lifecycle
| Event | Fields |
|---|---|
turn:user | turn — the complete user turn |
turn:start | turnId, timing? |
turn:end | turnId, status, usage, timing? |
Part streaming
| Event | Fields |
|---|---|
part:start | turnId, part — the whole part object |
text:delta | turnId, partId, delta |
text:citation | turnId, partId, citation |
thinking:raw-delta | turnId, partId, delta |
thinking:summary-delta | turnId, partId, delta |
thinking:update | turnId, partId, continuity?, providerMetadata? |
part:end | turnId, partId, timing? |
Openings carry the full part; deltas carry only ids and the delta.
Actions
| Event | Fields |
|---|---|
action:args-delta | turnId, partId, delta, accumulated |
action:running | turnId, partId, parameters? |
action:input | turnId, partId, input — what the provider tool was asked to do (added in 0.33.0) |
action:progress | turnId, partId, chunk |
action:complete | turnId, partId, result, timing? |
action:error | turnId, partId, error, timing? |
action:child-event | turnId, partId, event — a nested TurnEvent |
Compaction
| Event | Fields |
|---|---|
compaction:update | turnId, partId, update: { summary?; progress? } |
compaction:complete | turnId, partId, summary?, timing? |
compaction:error | turnId, partId, error, timing? |
The part itself arrives through part:start with status: "running". compaction:complete sets progress: 1.
Annotations and errors
| Event | Fields |
|---|---|
annotation:start | target, annotation |
annotation:update | target, annotation |
annotation:end | target, annotation |
error | turnId?, error: { type, message } |
type AnnotationTarget =
| { type: "turn"; turnId: string }
| { type: "part"; turnId: string; partId: string };TurnEventBuilder
import { TurnEventBuilder } from "@fifthrevision/axle";Converts StreamEvents into TurnEvents — the same translation Agent does internally. You'd only need this if you're driving stream() directly but still want render-layer events.