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
ThinkingParttext?, summary?, redacted?, continuity?, providerMetadata?
CompactionPartstatus: "running" | "complete" | "error", summary?, progress?, error?

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: unknown }
  | { type: "error"; error: { type: string; message: string } };

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:deltaturnId, partId, delta
thinking:summary-deltaturnId, partId, delta
thinking:updateturnId, partId, redacted?, 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: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.