Skip to content

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 ​

typescript
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().

typescript
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 ​

typescript
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 timestamps

TurnPart ​

typescript
type TurnPart =
  | TextPart
  | CitationPart
  | FilePart
  | ThinkingPart
  | ActionPart
  | CompactionPart;

Every part has id, type, optional annotations, and optional timing.

PartAdditional fields
TextParttext: string, citations?: Citation[], providerMetadata?
CitationPartcitations: Citation[], providerMetadata?
FilePartfile: FileInfo
ThinkingPartsummary?, raw?, continuity?, providerMetadata?
CompactionPartstatus: "running" | "complete" | "error", summary?, progress?, error?

ThinkingPart ​

typescript
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 ​

typescript
type ActionPart = ToolAction | SubagentAction | ProviderToolAction;

All share status: "pending" | "running" | "complete" | "cancelled" | "error" and discriminate on kind.

kinddetail
"tool"{ name, parameters, pendingArgs?, result? }
"agent"{ name, config?, children: Turn[], result? }
"provider-tool"{ name, input?, result? }
typescript
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 ​

typescript
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 ​

EventFields
turn:userturn — the complete user turn
turn:startturnId, timing?
turn:endturnId, status, usage, timing?

Part streaming ​

EventFields
part:startturnId, part — the whole part object
text:deltaturnId, partId, delta
text:citationturnId, partId, citation
thinking:raw-deltaturnId, partId, delta
thinking:summary-deltaturnId, partId, delta
thinking:updateturnId, partId, continuity?, providerMetadata?
part:endturnId, partId, timing?

Openings carry the full part; deltas carry only ids and the delta.

Actions ​

EventFields
action:args-deltaturnId, partId, delta, accumulated
action:runningturnId, partId, parameters?
action:inputturnId, partId, input — what the provider tool was asked to do (added in 0.33.0)
action:progressturnId, partId, chunk
action:completeturnId, partId, result, timing?
action:errorturnId, partId, error, timing?
action:child-eventturnId, partId, event — a nested TurnEvent

Compaction ​

EventFields
compaction:updateturnId, partId, update: { summary?; progress? }
compaction:completeturnId, partId, summary?, timing?
compaction:errorturnId, partId, error, timing?

The part itself arrives through part:start with status: "running". compaction:complete sets progress: 1.

Annotations and errors ​

EventFields
annotation:starttarget, annotation
annotation:updatetarget, annotation
annotation:endtarget, annotation
errorturnId?, error: { type, message }
typescript
type AnnotationTarget =
  | { type: "turn"; turnId: string }
  | { type: "part"; turnId: string; partId: string };

TurnEventBuilder ​

typescript
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.