Recommended preparation

Try first

Frame your answer before reading the reference

Senior20 min first pass

Candidate prompt

Design an AI agent run inspector for nested spans, live tool calls, approval races, resilient stream state, virtualized navigation, redaction, and accessibility.

Constraints

  • Keep client, server, and rendering ownership explicit.
  • Cover loading, failure, recovery, and accessible interaction states.

Helpful before you start

  • Client-side state management
  • Accessible UI fundamentals

Make these three decisions explicit

  1. State and ownership boundaries
  2. API and event contracts
  3. Performance and failure tradeoffs

Requirements and interview framing

Design the frontend for an AI agent run inspector used by developers and operators to understand a live or completed execution. This is an AI agent observability UI: a run can contain nested agent turns, model generations, retrievals, tool calls, handoffs, guardrails, approval requests, retries, and errors. The page must stream updates, expose a navigable trace tree, filter and search spans, inspect redacted payloads, and coordinate stop, retry, approve, or deny actions. Treat the agent runtime, trace database, authorization service, and tool execution as backend black boxes. The interview scope is the browser architecture, state model, rendering strategy, accessibility, resilience, and abstract client contracts.
A strong 60-second answer
Fetch a versioned run snapshot, normalize run, span, and approval entities, and resume a server stream from the snapshot cursor. A deterministic reducer deduplicates immutable event IDs, applies each field only from a newer sequence, tracks gaps, and tolerates a child arriving before its parent. Selectors turn the normalized hierarchy into only the expanded, filtered rows needed by a virtualized trace tree. Large payloads load into a separate cache when the inspector opens, so streaming deltas do not rerender JSON. Stop and approval controls use idempotency keys and remain pending until authoritative events confirm the outcome. Reconnect state, redaction, keyboard navigation, focus return, mobile containment, and restrained live-region announcements are first-class requirements.

A compact mental model

Six concepts to keep separate
ConceptMeaning in this designFrontend responsibility
RunOne end-to-end attempt with lifecycle, aggregates, and capabilitiesOwn the route, summary, freshness, and run-level actions
TraceThe hierarchy available for inspection for one agent execution tracePresent a coherent tree and tool call inspector without inventing missing facts
SpanOne agent, model, retrieval, tool, handoff, guardrail, or approval operationStore a small normalized summary and lazy payload references
EventAn immutable streamed fact about a run, span, or approvalValidate, deduplicate, order, and merge it through one reducer
CursorAn opaque resume position for contiguous historyReconnect from the last safely reduced position, never the largest observed sequence
ApprovalA versioned request that pauses a sensitive action for a human decisionShow safe context and pending intent while the server remains authoritative

AI agent trace viewer vs chat transcript

A chat transcript is mostly linear. An agent run is a hierarchical and sometimes concurrent execution document. One parent can start two tools in parallel, a span can stream partial output while a sibling waits for approval, and a child event can reach the browser before its parent. The default view should answer four questions quickly: Is the run still active? Where did it fail or pause? Which operation consumed time or tokens? What input and output may this user safely inspect?
Functional requirements
  • Open a route-addressable live or completed run and show status, freshness, duration, span counts, errors, and authorized usage totals.
  • Navigate parent-child spans, expand or collapse branches, search labels and safe summaries, and filter by span kind or lifecycle status.
  • Inspect timing, attributes, errors, and redacted input or output without placing large payloads in every trace row.
  • Stop an active run, retry an eligible failure, and approve or deny a pending request with explicit confirmation and progress states.
  • Resume after disconnection, expose stale or gap states, and keep already loaded evidence visible during recoverable errors.
Quality requirements
  • Keep selection, expansion, and scrolling responsive for hundreds of spans and usable for runs with ten thousand or more.
  • Merge duplicate and out-of-order events without changing an already applied result instead of treating network arrival order as authority.
  • Prevent secrets, unsafe HTML, or inaccessible private reasoning from leaking through payload rendering or client logs.
  • Support keyboard tree navigation, predictable focus, readable zoom, meaningful status announcements, and reduced motion.
  • Contain tables, long identifiers, and payload blocks on a 390-pixel viewport without widening the document.

Clarifying questions that change the design

Questions, assumptions, and consequences
QuestionMVP assumptionFrontend consequence
Who uses it?An authenticated developer inspecting runs in one authorized projectPermission-aware controls and payload capabilities arrive with the snapshot; the client never derives access from role labels
How large is a run?Usually hundreds of spans, occasionally tens of thousandsNormalize summaries, virtualize visible rows, lazy-load payloads, and bound caches
How live is live?Visible lifecycle changes appear within a few hundred milliseconds of receiptBuffer bursts until an animation frame, but do not delay approval or terminal states behind a long batch
Can events repeat or reorder?Yes; each envelope has event ID, run ID, sequence, timestamp, and resume cursorDeduplicate, merge by sequence, track gaps, and hold children whose parent is not yet known
Which actions exist?Stop, retry, approve, and deny only when advertised by server capabilitiesKeep actions in a separate state slice and reconcile conflicts with authoritative stream events
What is searchable?Safe summaries, names, IDs, kinds, status, and redacted attributesDo not silently download every private payload to power browser search

Scope boundaries

Deliberately out of scope
  • Model selection, prompt execution, planning loops, tool scheduling, queues, and distributed agent orchestration.
  • Trace storage schema, ingestion pipelines, cross-service sampling, and backend authorization implementation.
  • Editing workflows, executing arbitrary tools in the browser, or exposing hidden chain-of-thought.
  • Cross-run analytics, evaluation dashboards, and trace comparison beyond links from a paginated run list.
  • Inventing a universal transport: the design works with a resumable fetch stream, server-sent events, or WebSocket adapter.

Critical states and acceptance criteria

State coverage
StateExpected experienceInvariant
Initial loadingRun shell and labeled skeletons appear; retry is available if the snapshot failsNo stream connects before the snapshot establishes version and cursor
Live and caught upFreshness is visible, new spans merge in place, and user selection does not jumpOne logical span has one entity regardless of duplicate events
Reconnecting or sequence gapExisting trace remains readable with a stale banner and manual retryThe page never claims live freshness while continuity is unknown
Approval pendingThe relevant row and inspector expose safe context and allowed decisionsOnly one local decision is in flight; another tab's resolution supersedes it
Run terminalStreaming stops after final reconciliation and terminal status is announced onceA late stop response cannot change completed back to stopped
Huge or unavailable payloadThe inspector shows metadata, truncation, download permission, or a recoverable errorPayload size cannot block trace navigation or widen the page

Architecture and live trace flow

Treat real-time trace streaming as a one-way read pipeline: route snapshot, transport adapter, validated event envelopes, normalized store, memoized selectors, and focused components. Components dispatch view intent or action commands; they do not own sockets, mutate span objects, or rebuild the trace recursively. This separation lets transport recovery, document reconciliation, and rendering evolve independently.
Run route
  -> snapshot loader
  -> normalized entity store
  -> stream adapter(after = snapshot.cursor)
  -> envelope validation + idempotent reducer
  -> visible flat-tree selector
  -> virtualized treegrid
  -> span inspector + action coordinator

Page composition and ownership

Frontend modules
ModuleOwnsMust not own
RunRouteRoute ID, snapshot lifecycle, document title, and teardownSpan merge logic or individual row state
RunHeaderSummary, freshness, elapsed time, terminal state, and allowed run actionsTransport retries or optimistic authority
TraceToolbarSearch, kind and status filters, errors-only mode, and expansion commandsA copied filtered span collection
TraceTreeVirtual window, active row, tree semantics, expansion intent, and selection intentPayload JSON or a nested mutable run object
SpanInspectorSelected span view, inspector tab, payload request, and approval formGlobal stream subscription
ConnectionStatusCaught-up, reconnecting, stale, gap, and manual recovery presentationHidden automatic recovery without user feedback
LiveAnnouncementsDeduplicated meaningful lifecycle messagesRaw token, timestamp, or span-delta announcements

Snapshot first, resumable stream second

Opening a run
  1. Load one coherent base
    Fetch the run summary, span summaries, pending approvals, server capabilities, snapshot version, latest contiguous sequence, and resume cursor. An AbortController cancels the request if the route changes.
  2. Normalize before rendering
    Insert entities into maps, build parent-to-child indexes, register unresolved parents, and choose a valid initial selection. The route can now render completed history even if the live connection is slow.
  3. Resume after the cursor
    Open the chosen stream transport with the snapshot cursor. The adapter parses frames and validates envelope shape, run identity, size, and supported version before dispatch.
  4. Reconcile and acknowledge
    The reducer ignores seen event IDs, applies newer field versions, advances only a contiguous sequence watermark, records the latest safe cursor, and exposes gaps without discarding useful later entities.
  5. Recover explicitly
    On interruption, reconnect with bounded exponential backoff and jitter. Resume from the last acknowledged cursor; request a delta or replacement snapshot when the server reports cursor expiry or an unresolved gap.
Do not subscribe before establishing a base
A stream-only boot requires replaying an unknown amount of history and complicates permissions, payload policies, and loading UX. A snapshot-only poll makes live approvals and failures feel stale. Snapshot plus resumable deltas provides a coherent base and bounded recovery contract.

Reducer responsibilities

Deterministic event application
  • Reject an envelope for another run and quarantine unsupported schema versions rather than partially applying them.
  • Ignore an immutable event ID already seen, even if reconnect delivers it with a different cursor.
  • Record a sequence per mutable field or entity revision; a late started event cannot overwrite a completed span.
  • Append streamed text or structured deltas only when their delta index is new, and compact chunks outside the hot render path.
  • Keep a child entity when its parent is missing and index it under the unresolved parent ID until that parent appears.
  • Advance the acknowledged cursor only through a contiguous range; a larger observed sequence is not proof that the gap is safe.
  • Preserve unknown span kinds as generic custom rows with escaped labels so an older client remains usable.

Worked example: follow one run through the reducer

Start with run_42 at contiguous sequence 40. The snapshot contains root span_agent_10 and cursor c40. The next network frame is sequence 42 for tool span_tool_17, whose parent span_handoff_12 is not known yet. The frontend may show useful partial evidence, but it must not claim that the stream is caught up. Then sequence 41 arrives, reconnect repeats sequence 42, and a human decision races with another tab. One reducer and one version rule make every view converge.
run_42 [running]
└─ span_agent_10 [running]
   └─ span_handoff_12 [running]
      └─ span_tool_17 [waiting for approval]
         └─ approval_9 [denied in another tab]
Event-by-event reconciliation
EventStore updateVisible UI resultInvariant preserved
Snapshot through seq 40, cursor c40Normalize run_42 and span_agent_10; set contiguousSequence to 40The header and known root render immediatelyThe stream starts from one coherent base
seq 42: span.started for span_tool_17, parent span_handoff_12Insert the tool once under orphanIdsByParentId and record a gap at 41Show an unresolved item and a catching-up statusArrival order does not become tree authority
seq 41: span.started for span_handoff_12Insert the parent, attach the orphan, and advance the contiguous cursor through c42The tool moves under its parent without losing selectionOne span ID still maps to one entity
Reconnect repeats the seq 42 event IDseenEventIds rejects it without appending another childThe tree and counts do not changeReplay is idempotent
seq 43: approval.requested v3; local Approve becomes pendingStore approval_9 separately from the pending command and its idempotency keyThe inspector says Submitting, not ApprovedOptimistic feedback is not server truth
seq 44: approval.resolved denied v4 from another tabThe newer server version wins and clears the stale local commandControls disable, focus moves to the resolved message, and the outcome is announced onceAll tabs converge on one authoritative decision
Normalized trace projection
Event identity prevents duplicate work, sequence protects field freshness, the contiguous cursor protects resume correctness, normalized entities preserve partial evidence, and separate action state keeps a responsive control from fabricating an outcome.

Derived visible tree, not a second source of truth

The store keeps normalized entities and stable child ID arrays. A memoized selector performs a depth-first walk over roots, expansion state, filters, and match ancestry to create flat row descriptors such as span ID, depth, position, sibling count, and expansion state. Virtualization consumes that flat array. The rows are derived and disposable: no component writes status or selection back into them. When search matches a descendant, the selector may include its ancestors as context without permanently changing the user's expansion set.
type VisibleTraceRow = {
  spanId: string;
  depth: number;
  posInSet: number;
  setSize: number;
  expanded: boolean | undefined;
  matched: boolean;
};

const rows = selectVisibleRows({
  spansById,
  childIdsByParentId,
  rootIds,
  expandedIds,
  searchQuery,
  filters
});

Keep the streaming hot path small

The adapter can receive many small deltas in one network turn. Queue ordinary updates and flush them once per animation frame, while terminal run states and approval resolutions can take a higher-priority path. Entity-specific selectors ensure that changing span A does not rerender every row or the open inspector for span B. Duration labels for running spans derive from a shared low-frequency clock rather than one interval per row. Large input and output bodies live in a payload cache keyed by span ID, payload kind, redaction version, and content version.

Action coordination is a separate write path

Render a command only when the latest entity advertises the capability, then submit an idempotency key and expected version. Show stopping or submitting locally without rewriting the authoritative outcome. The command response and matching stream event may arrive in either order; both merge into the same entity version. A conflict clears pending intent, preserves the newer server fact, and explains that the run completed or the approval was resolved elsewhere.

Responsive composition

On a wide screen, the trace and inspector can share a split view with sensible minimum widths. On tablet, collapse optional columns before squeezing labels. On mobile, keep the trace as the primary view and open the selected span in a full-height drawer or nested route. Focus moves to the inspector heading on open and returns to the triggering trace row on close. Payloads and comparison tables scroll inside bounded containers; the document itself must not gain horizontal overflow.

Normalized data and event reconciliation

Model the browser state as a live trace document plus independent view and command state. Span summaries belong in normalized maps because the same entity appears in the tree, search results, aggregate counts, breadcrumbs, and inspector. Payload bodies are references, not fields on every row. The exact backend schema is abstract; these types describe the minimum stable contract the frontend needs.

Core entities

type RunStatus =
  | 'queued' | 'running'
  | 'completed' | 'failed' | 'stopped';

type RunViewStatus = RunStatus | 'waiting_approval';

type SpanKind =
  | 'agent' | 'model' | 'tool' | 'retrieval'
  | 'handoff' | 'guardrail' | 'approval' | 'custom';

type AgentRun = {
  id: string;
  workflowName: string;
  status: RunStatus;
  startedAt: string;
  endedAt?: string;
  version: number;
  rootSpanIds: string[];
  counts: { spans: number; errors: number; pendingApprovals: number };
  capabilities: { canStop: boolean; canRetry: boolean };
};

type Span = {
  id: string;
  runId: string;
  parentSpanId?: string;
  kind: SpanKind;
  originalKind?: string;
  name: string;
  status: 'pending' | 'running' | 'completed' | 'failed' | 'cancelled';
  startedAt?: string;
  endedAt?: string;
  durationMs?: number;
  safeSummary?: string;
  attributes: Record<string, string | number | boolean>;
  inputRef?: PayloadRef;
  outputRef?: PayloadRef;
  error?: { code?: string; message: string; retryable: boolean };
  revision: number;
};
type ApprovalRequest = {
  id: string;
  runId: string;
  spanId: string;
  status: 'pending' | 'approved' | 'denied' | 'expired';
  prompt: string;
  safeActionSummary: string;
  requestedAt: string;
  resolvedAt?: string;
  version: number;
  capabilities: { canApprove: boolean; canDeny: boolean };
};

type PayloadRef = {
  payloadId: string;
  contentType: 'application/json' | 'text/plain';
  byteSize: number;
  redaction: 'redacted' | 'partial' | 'unavailable';
  contentVersion: number;
  truncated: boolean;
};

type PayloadCacheEntry = {
  state: 'loading' | 'ready' | 'truncated' | 'error';
  content?: unknown;
  byteSize: number;
  contentVersion: number;
  redactionVersion: number;
};

type VisibleTraceRow = {
  spanId: string;
  depth: number;
  posInSet: number;
  setSize: number;
  expanded: boolean | undefined;
  matched: boolean;
};

type RunSnapshotPayload = {
  run: AgentRun;
  spans: Span[];
  approvals: ApprovalRequest[];
  snapshotVersion: number;
  latestContiguousSequence: number;
  resumeCursor: string;
};

type SpanDelta = {
  spanId: string;
  revision: number;
  fields: Partial<Pick<
    Span,
    | 'name' | 'status' | 'startedAt' | 'endedAt' | 'durationMs'
    | 'safeSummary' | 'attributes' | 'inputRef' | 'outputRef' | 'error'
  >>;
};

type TraceEventDataByType = {
  'run.snapshot': RunSnapshotPayload;
  'span.started': Span & { status: 'pending' | 'running' };
  'span.delta': SpanDelta;
  'span.completed': Span & { status: 'completed' | 'failed' | 'cancelled' };
  'approval.requested': ApprovalRequest & { status: 'pending' };
  'approval.resolved': ApprovalRequest & { status: 'approved' | 'denied' | 'expired' };
  'run.completed': AgentRun & { status: 'completed' };
  'run.failed': AgentRun & { status: 'failed' };
  'run.stopped': AgentRun & { status: 'stopped' };
};

type TraceEventType = keyof TraceEventDataByType;

type TraceEventBase = {
  schemaVersion: 1;
  eventId: string;
  runId: string;
  sequence: number;
  cursor: string;
  occurredAt: string;
};

type TraceEventEnvelope = {
  [Type in TraceEventType]: TraceEventBase & {
    type: Type;
    data: TraceEventDataByType[Type];
  }
}[TraceEventType];

Store shape

type TraceStore = {
  document: {
    run?: AgentRun;
    spansById: Record<string, Span>;
    childIdsByParentId: Record<string, string[]>;
    orphanIdsByParentId: Record<string, string[]>;
    approvalsById: Record<string, ApprovalRequest>;
    seenEventIds: Set<string>;
    fieldSequenceByKey: Record<string, number>;
    contiguousSequence: number;
    observedSequence: number;
    acknowledgedCursor?: string;
    connection: 'offline' | 'connecting' | 'live' | 'stale' | 'gap';
  };
  view: {
    selectedSpanId?: string;
    expandedIds: Set<string>;
    query: string;
    kinds: Set<SpanKind>;
    statuses: Set<Span['status']>;
    inspectorTab: 'overview' | 'input' | 'output' | 'error';
  };
  actions: Record<string, {
    command: 'stop' | 'retry' | 'approve' | 'deny';
    idempotencyKey: string;
    expectedVersion: number;
    status: 'submitting' | 'awaiting_event' | 'conflict' | 'failed';
  }>;
  payloadCache: Map<string, PayloadCacheEntry>;
};

Invariants that prevent corrupt UI

State invariants
InvariantWhy it mattersEnforcement
One entity per run, span, or approval IDDuplicate replay cannot create duplicate rows or actionsMaps keyed by immutable ID and idempotent event IDs
Terminal lifecycle never moves backwardA late started event cannot revive a completed or failed spanField sequence or entity revision guards
Child identity is preserved without a parentOut-of-order delivery remains available for inspection and recoverableOrphan index keyed by the expected parent ID
Acknowledged cursor represents contiguous historyReconnect does not skip a missing event because a later one arrivedGap tracker advances only when all earlier sequences exist
Payload content is not part of the row entityLarge JSON cannot make every stream update expensiveLazy payload cache keyed by ID, kind, redaction, and version
Pending commands do not become server truthStop or approval races converge on the authoritative outcomeSeparate action slice plus expected version
Waiting for approval is a derived display stateA run can have pending approvals without inventing a second authoritative lifecycleDerive the label from a running run plus pending ApprovalRequest entities

Idempotent merge algorithm

type SnapshotTraceEvent = Extract<TraceEventEnvelope, { type: 'run.snapshot' }>;
type SpanTraceEvent = Extract<
  TraceEventEnvelope,
  { type: 'span.started' | 'span.delta' | 'span.completed' }
>;
type ApprovalTraceEvent = Extract<
  TraceEventEnvelope,
  { type: 'approval.requested' | 'approval.resolved' }
>;
type TerminalRunTraceEvent = Extract<
  TraceEventEnvelope,
  { type: 'run.completed' | 'run.failed' | 'run.stopped' }
>;
type IncrementalTraceEvent = Exclude<TraceEventEnvelope, SnapshotTraceEvent>;

declare function rebaseFromInitialOrRecoverySnapshot(
  store: TraceStore,
  event: SnapshotTraceEvent,
): void;
declare function recordEventIdentityAndObservedSequence(
  store: TraceStore,
  event: IncrementalTraceEvent,
): void;
declare function mergeSpanFieldsBySequence(store: TraceStore, event: SpanTraceEvent): void;
declare function attachKnownParentOrRecordOrphan(store: TraceStore, event: SpanTraceEvent): void;
declare function mergeApprovalByVersion(store: TraceStore, event: ApprovalTraceEvent): void;
declare function mergeRunTerminalState(store: TraceStore, event: TerminalRunTraceEvent): void;
declare function recordReceivedSequence(
  store: TraceStore,
  sequence: number,
  cursor: string,
): void;
declare function advanceContiguousCursor(store: TraceStore): void;

function assertNever(value: never): never {
  throw new Error(`Unsupported trace event: ${String(value)}`);
}

function applyTraceEvent(
  store: TraceStore,
  event: TraceEventEnvelope,
  routeRunId: string,
): void {
  if (event.runId !== routeRunId) return;

  if (event.type === 'run.snapshot') {
    rebaseFromInitialOrRecoverySnapshot(store, event);
    return;
  }

  if (event.runId !== store.document.run?.id) return;
  if (store.document.seenEventIds.has(event.eventId)) return;
  recordEventIdentityAndObservedSequence(store, event);

  switch (event.type) {
    case 'span.started':
    case 'span.delta':
    case 'span.completed':
      mergeSpanFieldsBySequence(store, event);
      attachKnownParentOrRecordOrphan(store, event);
      break;
    case 'approval.requested':
    case 'approval.resolved':
      mergeApprovalByVersion(store, event);
      break;
    case 'run.completed':
    case 'run.failed':
    case 'run.stopped':
      mergeRunTerminalState(store, event);
      break;
    default:
      assertNever(event);
  }

  recordReceivedSequence(store, event.sequence, event.cursor);
  advanceContiguousCursor(store);
}
The pseudocode separates deduplication from field freshness. Event ID answers whether this exact fact was already processed; sequence or revision answers whether the fact is newer than the value currently shown. If an event is beyond a gap, the reducer may retain its entity update for visibility but cannot advance the safe resume cursor past missing history. A timed gap policy asks for missing deltas or replaces the snapshot rather than guessing.

Streaming deltas and bounded memory

Delta rules
  • Give each appendable text or structured delta a stable chunk index; ignore a repeated index after reconnect.
  • Compact many small chunks into a larger immutable buffer during idle work, not on every token.
  • Retain complete summaries for all spans but evict closed payload bodies with a least-recently-used byte budget.
  • Keep search indexing limited to safe summary fields unless the user explicitly loads an authorized payload.
  • Persist no sensitive payload in local storage, analytics events, error reporting breadcrumbs, or URL parameters.
  • Represent truncation and unavailable content explicitly so absence is not mistaken for an empty tool result.

Selectors and URL state

The selected span ID may live in the query string so a support link can reopen a specific operation. Validate it against the loaded run and fall back to the failed span or first root when it is missing. Search, filters, and expansion are local view state because indexing every combination would create noisy URLs. The flattened row selector returns depth, sibling position, child count, match state, and expansion state for treegrid semantics. Aggregate counts should derive incrementally from entity transitions rather than scanning ten thousand spans on every event.
Redaction is data modeling, not decoration
A masked CSS layer over a secret still sends the secret to the browser. Payload references must describe server-side redaction or omission, and caches must be partitioned by authorization and redaction version. The frontend escapes text and masks defensively, but it cannot turn delivered private data into secure data.

Interface contracts for UI, API, stream, and actions

The endpoints below are illustrative contracts between the browser and an abstract trace service. They do not prescribe trace storage, agent execution, queues, or orchestration. The important frontend properties are versioned snapshots, resumable ordered envelopes, stable entity IDs, explicit capabilities, idempotent commands, bounded payload delivery, and errors that distinguish retry from conflict.

Read contracts

Illustrative endpoints
RequestReturnsFrontend use
GET /agent-runs?projectId=&cursor=&status=Cursor page of compact RunSummary objectsRun list, stable pagination, empty and partial-error states
GET /agent-runs/:runIdRunSnapshot with spans, approvals, capabilities, version, sequence, and cursorCoherent base for the detail route
GET /agent-runs/:runId/events?after=:cursorResumable event stream or bounded delta pageLive updates and reconnect catch-up
GET /agent-runs/:runId/spans/:spanId/payload?kind=inputRedacted bounded payload with content version and truncation metadataLazy inspector content outside the row hot path
type RunSnapshot = RunSnapshotPayload & {
  schemaVersion: 1;
  serverTime: string;
};

type PayloadResponse = {
  payloadId: string;
  spanId: string;
  kind: 'input' | 'output';
  contentType: 'application/json' | 'text/plain';
  content: unknown;
  byteSize: number;
  contentVersion: number;
  redactionVersion: number;
  truncated: boolean;
  nextCursor?: string;
};

Stream event contract

Required event semantics
EventMinimum payloadMerge behavior
run.snapshotVersioned run, summaries, approvals, sequence, and cursorUse only for initial load or explicit recovery; atomically rebase the document and reset stream continuity before later deltas
span.startedSpan identity, parent ID, kind, name, start time, revisionInsert or update newer start fields; register an orphan if needed
span.deltaSpan ID, field or channel, chunk index, safe delta, revisionAppend unseen chunks without replacing newer lifecycle fields
span.completedSpan ID, terminal status, end time, duration, summary, payload references, errorApply newer terminal fields and invalidate matching payload cache versions
approval.requestedApproval ID, span ID, safe prompt, allowed decisions, versionCreate or refresh the pending approval and surface one meaningful announcement
approval.resolvedApproval ID, outcome, resolver summary, resolved time, versionSupersede local pending commands and explain cross-tab conflicts
run.completed, run.failed, or run.stoppedRun terminal status, ended time, counts, versionMove forward to terminal, stop reconnecting after the final contiguous state
{
  "schemaVersion": 1,
  "eventId": "evt_01J...",
  "runId": "run_42",
  "sequence": 184,
  "cursor": "opaque_resume_cursor",
  "occurredAt": "2026-07-28T10:15:30.120Z",
  "type": "approval.requested",
  "data": {
    "id": "approval_9",
    "runId": "run_42",
    "spanId": "span_tool_17",
    "status": "pending",
    "prompt": "Approve the prepared support reply?",
    "safeActionSummary": "Send the prepared support reply",
    "requestedAt": "2026-07-28T10:15:30.120Z",
    "version": 3,
    "capabilities": { "canApprove": true, "canDeny": true }
  }
}

Command contracts

Mutating requests
RequestBodySuccess and conflict
POST /agent-runs/:runId/stopidempotencyKey, expectedRunVersion, optional reason202 accepted with command ID; 409 returns current terminal or newer run state
POST /agent-runs/:runId/retryidempotencyKey, expectedRunVersion, optional failedSpanId202 with new or resumed run reference; 409 explains ineligible current state
POST /agent-runs/:runId/approvals/:approvalId/decisiondecision, idempotencyKey, expectedApprovalVersion202 pending confirmation; 409 returns already resolved approval
type ApprovalDecisionCommand = {
  decision: 'approve' | 'deny';
  idempotencyKey: string;
  expectedApprovalVersion: number;
};

type CommandAccepted = {
  status: 'accepted';
  commandId: string;
  acceptedAt: string;
};

type CommandConflict = {
  status: 'conflict';
  code: 'RUN_TERMINAL' | 'APPROVAL_RESOLVED' | 'VERSION_MISMATCH';
  currentRun?: AgentRun;
  currentApproval?: ApprovalRequest;
};

Component contracts

type SpanRowView = {
  id: string;
  name: string;
  kind: SpanKind;
  status: Span['status'];
  durationMs?: number;
  hasChildren: boolean;
};

type TraceTreeProps = {
  rows: readonly VisibleTraceRow[];
  activeSpanId?: string;
  selectedSpanId?: string;
  getSpanSummary: (spanId: string) => SpanRowView;
  onToggle: (spanId: string) => void;
  onSelect: (spanId: string) => void;
  onActiveChange: (spanId: string) => void;
};

type SpanInspectorProps = {
  span?: Span;
  approval?: ApprovalRequest;
  payloadState: 'idle' | 'loading' | 'ready' | 'truncated' | 'error';
  pendingCommand?: TraceStore['actions'][string];
  onLoadPayload: (kind: 'input' | 'output', signal: AbortSignal) => void;
  onDecision: (decision: 'approve' | 'deny') => void;
  onClose: () => void;
};
Rows receive small view models and stable callbacks, not the whole store. The inspector aborts obsolete payload work when selection, payload tab, authorization version, or route changes. Action callbacks contain intent only; the coordinator reads the latest entity version before submitting. This prevents a component rendered from stale props from approving an already replaced request.

Transport choice is an adapter detail

Stream options
OptionStrengthFrontend caveat
Server-sent eventsSimple ordered server-to-client updates and native reconnect behaviorHeaders and cursor behavior may require a fetch-based implementation; browser connection limits matter
Fetch response streamWorks with request headers, AbortController, and custom framingThe adapter must parse frames, reconnect, and detect truncated records
WebSocketUseful when one channel also carries interactive bidirectional eventsMore connection lifecycle and application-level resume work than this read-heavy page needs
PollingUniversal fallback and easy operational behaviorHigher staleness and repeated transfer; use visibility-aware intervals and conditional requests
Preferred interview choice
Choose server-sent events or a fetch stream for the read-heavy trace, while keeping stop and approval commands as ordinary idempotent HTTP requests. Name the resume and gap contract. The quality of reconciliation matters more than claiming one transport is universally best.

Optimizations for performance, resilience, security, and accessibility

Optimize from measured bottlenecks while protecting correctness. The dominant risks are bursts of entity updates, an unbounded trace tree, huge structured payloads, reconnect ambiguity, conflicting human actions, sensitive content, and an interaction model that works visually but fails with a keyboard or screen reader.

Rendering and memory budgets

Technique and trigger
TechniqueUse it whenTradeoff and guardrail
Virtualized flat treeVisible and expanded rows can reach thousandsUse stable row keys, overscan, and active-row pinning so focus is never silently unmounted
Animation-frame event batchesA stream burst creates many ordinary deltasFlush important approval and terminal transitions promptly; measure event-to-visible latency
Entity-level subscriptionsOne changing span rerenders unrelated rowsKeep selectors stable and avoid passing the entire map through every component
Lazy payload cacheInputs or outputs are large and rarely openedBound by bytes, evict least-recently-used closed entries, and include redaction version in the key
Web Worker parsing or searchProfiling shows JSON parsing or safe-summary indexing blocks the main threadStructured cloning has a cost; do not add a worker before measurement
Fixed or estimated row heightTrace rows have predictable compact summariesKeep expanded payloads out of rows; variable measurement complicates scroll anchoring
Performance details worth naming
  • Share one low-frequency clock for elapsed labels instead of creating a timer for each running row.
  • Do not syntax-highlight megabytes of JSON synchronously; show a capped preview and progressively render or download when allowed.
  • Debounce text search briefly, but apply kind and status filters immediately; cancel obsolete worker or payload requests.
  • Preserve scroll position by stable span identity when rows above change, expand, collapse, or attach to a late parent.
  • Pause visual batching and reduce reconnect work while the page is hidden, then catch up from the acknowledged cursor.
  • Measure DOM nodes, long tasks, frame drops, selector duration, retained payload bytes, and event-to-visible latency on a mid-range device.

Reconnect and partial-order failure modes

Failure response matrix
FailureUser experienceCorrectness response
Connection dropsKeep the trace visible, show reconnecting and last-updated time, offer manual retryAbort the old reader, back off with jitter, and resume from the last contiguous cursor
Cursor expiredShow catching up without clearing selectionRequest a fresh snapshot, rebase view state by stable IDs, and discard obsolete transport state
Sequence gap persistsMark data as incomplete rather than liveFetch a bounded delta range or replace the snapshot; never guess that missing events were irrelevant
Child arrives before parentOptionally show an unresolved root group so the span remains available for inspectionStore the child once under an orphan index and attach it when the parent appears
Completed event arrives after newer failureKeep the newer terminal evidenceField sequence or revision rejects lifecycle regression
Malformed or unknown eventContinue known history and surface degraded freshness if continuity is affectedValidate envelope, log schema-safe diagnostics, and request recovery when required

Human-in-the-loop approval UI races

Race handling
RaceResolution
User stops while the run completesCompletion is authoritative. Clear stopping, show that the run completed before stop took effect, and do not rewrite it as stopped.
Two tabs decide one approvalThe first accepted server version wins. The losing tab consumes the resolved event or 409 body and explains who or what resolved it when safe.
Command response is lost but action succeededRetry with the same idempotency key or await the stream; duplicate confirmation converges without executing twice.
Approval becomes unavailable while the dialog is openDisable the stale form after the newer event, preserve any typed reason only locally, and move focus to the outcome message.
Route changes during a commandDo not cancel a request if cancellation could hide an accepted consequence; track command identity outside the component and reconcile on return.

Security and privacy

Defense in depth
  • Redact or omit secrets on the server before delivery. Client masking is presentation, never the access-control boundary.
  • Render tool output, prompts, attributes, and errors as escaped text or a safe structured JSON tree; never inject raw HTML.
  • Do not display hidden reasoning or imply that generated summaries are private chain-of-thought.
  • Partition caches by project, user authorization context, payload ID, content version, and redaction version; clear sensitive memory on logout or project change.
  • Keep payload values out of URLs, local storage, analytics, console logs, crash breadcrumbs, copied support diagnostics, and DOM data attributes.
  • Enforce response-size and nesting limits, detect cyclic client objects, and offer a server-prepared download only when capability allows it.
  • Treat capability flags as UI hints. Every stop, retry, approve, deny, and payload request still requires server authorization.

Accessible virtualized treegrid

Use a tree when each row is mainly a label and status; use a treegrid only if rows expose meaningful interactive columns such as kind, duration, tokens, and status. Follow the WAI-ARIA interaction model consistently. The active row supports Up and Down movement through visible rows, Right to expand or enter the first child, Left to collapse or move to the parent, Home and End for boundaries, and Enter to open details. Expose expanded state and hierarchy with aria-expanded, aria-level, aria-posinset, and aria-setsize where the chosen pattern requires them.
Focus and announcement rules
  • Keep one predictable tab stop for the tree and manage active-row focus without trapping the user.
  • Pin or overscan the focused row so virtualization does not remove it; restore by span ID after filter or hierarchy changes.
  • When a filter hides the active row, move to the nearest visible ancestor or first result and announce the change once.
  • Move focus to the inspector heading when a mobile drawer opens and return it to the triggering row when the drawer closes.
  • Use a polite live region for approval requested, run failed, run completed, run stopped, reconnecting, and caught up; deduplicate by semantic transition.
  • Do not announce token deltas, timers, every new span, or continuously changing counts. Provide a manual new-activity summary when volume is high.
  • Never use color alone for running, failed, or waiting states; retain text labels and visible focus at 200 percent zoom.

Mobile and overflow resilience

At narrow widths, hide secondary trace columns behind the inspector rather than compressing the name into one word per line. Long run and span IDs may wrap with overflow-wrap anywhere or be truncated with an accessible copy control. Tables and code samples receive their own horizontal scroll containers. JSON uses a bounded viewport with wrapping optional, and the detail drawer cannot exceed the visual viewport. Test 320 and 390 pixels, browser zoom, large text, safe-area insets, an unbroken 200-character identifier, deeply nested spans, and a payload with wide keys.

Metrics, testing, and rollout

Product and reliability metrics
  • Snapshot-to-useful-trace and stream event-to-visible latency.
  • Reconnect success, time stale, cursor expiry, and unresolved gap rate.
  • Stop and approval success, conflict, duplicate-submit, and confirmation latency.
  • Search-to-selection time and failed-span discovery time.
  • Payload error, truncation, and unauthorized request rate.
Client health metrics
  • Long tasks, interaction latency, dropped frames, and row-selector duration.
  • Mounted rows, retained entities, payload-cache bytes, and worker queue depth.
  • Schema rejection and unknown span-kind rate without sensitive payload values.
  • Accessibility regression results for keyboard navigation, focus, names, and live regions.
  • Horizontal overflow checks across trace tables, code, payloads, and long identifiers.
Safe rollout
  1. Static completed runs
    Ship normalized snapshots, tree navigation, lazy redacted payloads, responsive layout, and accessibility before adding live mutation.
  2. Read-only live runs
    Add resumable streaming behind a flag, shadow-check event reduction against refreshed snapshots, and instrument gaps and memory.
  3. Stop and retry
    Introduce idempotent run commands with visible pending and conflict states, then test completion races and navigation.
  4. Human approvals
    Gate approval controls by explicit capabilities, audit cross-tab conflicts, verify focus and announcements, and roll out per project.
Interview answer checkpoint
  • Why a snapshot plus resumable deltas gives the browser a coherent base without replaying an unbounded history.
  • Why event ID, sequence or revision, and cursor solve different duplicate, freshness, and recovery problems.
  • Why normalized span entities and a derived flat tree make orphan attachment, search, and virtualization safer than a mutable nested object.
  • Why payloads stay lazy and server-redacted while trace rows remain compact, escaped, and permission-aware.
  • Why stop and approval controls need separate pending state, idempotency keys, version conflicts, predictable focus, and restrained announcements.

Use the Question Library for baseline coverage, then move into a Study Plan before targeted Company Prep.