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
- State and ownership boundaries
- API and event contracts
- 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
| Concept | Meaning in this design | Frontend responsibility |
|---|---|---|
| Run | One end-to-end attempt with lifecycle, aggregates, and capabilities | Own the route, summary, freshness, and run-level actions |
| Trace | The hierarchy available for inspection for one agent execution trace | Present a coherent tree and tool call inspector without inventing missing facts |
| Span | One agent, model, retrieval, tool, handoff, guardrail, or approval operation | Store a small normalized summary and lazy payload references |
| Event | An immutable streamed fact about a run, span, or approval | Validate, deduplicate, order, and merge it through one reducer |
| Cursor | An opaque resume position for contiguous history | Reconnect from the last safely reduced position, never the largest observed sequence |
| Approval | A versioned request that pauses a sensitive action for a human decision | Show 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
| Question | MVP assumption | Frontend consequence |
|---|---|---|
| Who uses it? | An authenticated developer inspecting runs in one authorized project | Permission-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 thousands | Normalize summaries, virtualize visible rows, lazy-load payloads, and bound caches |
| How live is live? | Visible lifecycle changes appear within a few hundred milliseconds of receipt | Buffer 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 cursor | Deduplicate, 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 capabilities | Keep actions in a separate state slice and reconcile conflicts with authoritative stream events |
| What is searchable? | Safe summaries, names, IDs, kinds, status, and redacted attributes | Do 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 | Expected experience | Invariant |
|---|---|---|
| Initial loading | Run shell and labeled skeletons appear; retry is available if the snapshot fails | No stream connects before the snapshot establishes version and cursor |
| Live and caught up | Freshness is visible, new spans merge in place, and user selection does not jump | One logical span has one entity regardless of duplicate events |
| Reconnecting or sequence gap | Existing trace remains readable with a stale banner and manual retry | The page never claims live freshness while continuity is unknown |
| Approval pending | The relevant row and inspector expose safe context and allowed decisions | Only one local decision is in flight; another tab's resolution supersedes it |
| Run terminal | Streaming stops after final reconciliation and terminal status is announced once | A late stop response cannot change completed back to stopped |
| Huge or unavailable payload | The inspector shows metadata, truncation, download permission, or a recoverable error | Payload size cannot block trace navigation or widen the page |
Continue the frontend system design path
Frontend system design questions Browse the full set of frontend architecture cases. RADIO framework Structure requirements, architecture, data, interfaces, and optimizations during an interview. Frontend performance guide Build explicit rendering, memory, network, and measurement budgets. AI chat textarea design Compare a linear streamed conversation with this hierarchical trace document. Real-time model training dashboard Compare high-frequency metric rendering with entity-based run inspection.
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 coordinatorPage composition and ownership
| Module | Owns | Must not own |
|---|---|---|
| RunRoute | Route ID, snapshot lifecycle, document title, and teardown | Span merge logic or individual row state |
| RunHeader | Summary, freshness, elapsed time, terminal state, and allowed run actions | Transport retries or optimistic authority |
| TraceToolbar | Search, kind and status filters, errors-only mode, and expansion commands | A copied filtered span collection |
| TraceTree | Virtual window, active row, tree semantics, expansion intent, and selection intent | Payload JSON or a nested mutable run object |
| SpanInspector | Selected span view, inspector tab, payload request, and approval form | Global stream subscription |
| ConnectionStatus | Caught-up, reconnecting, stale, gap, and manual recovery presentation | Hidden automatic recovery without user feedback |
| LiveAnnouncements | Deduplicated meaningful lifecycle messages | Raw token, timestamp, or span-delta announcements |
Snapshot first, resumable stream second
Opening a run
- Load one coherent baseFetch 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.
- Normalize before renderingInsert 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.
- Resume after the cursorOpen the chosen stream transport with the snapshot cursor. The adapter parses frames and validates envelope shape, run identity, size, and supported version before dispatch.
- Reconcile and acknowledgeThe 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.
- Recover explicitlyOn 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 | Store update | Visible UI result | Invariant preserved |
|---|---|---|---|
| Snapshot through seq 40, cursor c40 | Normalize run_42 and span_agent_10; set contiguousSequence to 40 | The header and known root render immediately | The stream starts from one coherent base |
| seq 42: span.started for span_tool_17, parent span_handoff_12 | Insert the tool once under orphanIdsByParentId and record a gap at 41 | Show an unresolved item and a catching-up status | Arrival order does not become tree authority |
| seq 41: span.started for span_handoff_12 | Insert the parent, attach the orphan, and advance the contiguous cursor through c42 | The tool moves under its parent without losing selection | One span ID still maps to one entity |
| Reconnect repeats the seq 42 event ID | seenEventIds rejects it without appending another child | The tree and counts do not change | Replay is idempotent |
| seq 43: approval.requested v3; local Approve becomes pending | Store approval_9 separately from the pending command and its idempotency key | The inspector says Submitting, not Approved | Optimistic feedback is not server truth |
| seq 44: approval.resolved denied v4 from another tab | The newer server version wins and clears the stale local command | Controls disable, focus moves to the resolved message, and the outcome is announced once | All 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
| Invariant | Why it matters | Enforcement |
|---|---|---|
| One entity per run, span, or approval ID | Duplicate replay cannot create duplicate rows or actions | Maps keyed by immutable ID and idempotent event IDs |
| Terminal lifecycle never moves backward | A late started event cannot revive a completed or failed span | Field sequence or entity revision guards |
| Child identity is preserved without a parent | Out-of-order delivery remains available for inspection and recoverable | Orphan index keyed by the expected parent ID |
| Acknowledged cursor represents contiguous history | Reconnect does not skip a missing event because a later one arrived | Gap tracker advances only when all earlier sequences exist |
| Payload content is not part of the row entity | Large JSON cannot make every stream update expensive | Lazy payload cache keyed by ID, kind, redaction, and version |
| Pending commands do not become server truth | Stop or approval races converge on the authoritative outcome | Separate action slice plus expected version |
| Waiting for approval is a derived display state | A run can have pending approvals without inventing a second authoritative lifecycle | Derive 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
| Request | Returns | Frontend use |
|---|---|---|
| GET /agent-runs?projectId=&cursor=&status= | Cursor page of compact RunSummary objects | Run list, stable pagination, empty and partial-error states |
| GET /agent-runs/:runId | RunSnapshot with spans, approvals, capabilities, version, sequence, and cursor | Coherent base for the detail route |
| GET /agent-runs/:runId/events?after=:cursor | Resumable event stream or bounded delta page | Live updates and reconnect catch-up |
| GET /agent-runs/:runId/spans/:spanId/payload?kind=input | Redacted bounded payload with content version and truncation metadata | Lazy 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
| Event | Minimum payload | Merge behavior |
|---|---|---|
| run.snapshot | Versioned run, summaries, approvals, sequence, and cursor | Use only for initial load or explicit recovery; atomically rebase the document and reset stream continuity before later deltas |
| span.started | Span identity, parent ID, kind, name, start time, revision | Insert or update newer start fields; register an orphan if needed |
| span.delta | Span ID, field or channel, chunk index, safe delta, revision | Append unseen chunks without replacing newer lifecycle fields |
| span.completed | Span ID, terminal status, end time, duration, summary, payload references, error | Apply newer terminal fields and invalidate matching payload cache versions |
| approval.requested | Approval ID, span ID, safe prompt, allowed decisions, version | Create or refresh the pending approval and surface one meaningful announcement |
| approval.resolved | Approval ID, outcome, resolver summary, resolved time, version | Supersede local pending commands and explain cross-tab conflicts |
| run.completed, run.failed, or run.stopped | Run terminal status, ended time, counts, version | Move 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
| Request | Body | Success and conflict |
|---|---|---|
| POST /agent-runs/:runId/stop | idempotencyKey, expectedRunVersion, optional reason | 202 accepted with command ID; 409 returns current terminal or newer run state |
| POST /agent-runs/:runId/retry | idempotencyKey, expectedRunVersion, optional failedSpanId | 202 with new or resumed run reference; 409 explains ineligible current state |
| POST /agent-runs/:runId/approvals/:approvalId/decision | decision, idempotencyKey, expectedApprovalVersion | 202 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
| Option | Strength | Frontend caveat |
|---|---|---|
| Server-sent events | Simple ordered server-to-client updates and native reconnect behavior | Headers and cursor behavior may require a fetch-based implementation; browser connection limits matter |
| Fetch response stream | Works with request headers, AbortController, and custom framing | The adapter must parse frames, reconnect, and detect truncated records |
| WebSocket | Useful when one channel also carries interactive bidirectional events | More connection lifecycle and application-level resume work than this read-heavy page needs |
| Polling | Universal fallback and easy operational behavior | Higher 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 | Use it when | Tradeoff and guardrail |
|---|---|---|
| Virtualized flat tree | Visible and expanded rows can reach thousands | Use stable row keys, overscan, and active-row pinning so focus is never silently unmounted |
| Animation-frame event batches | A stream burst creates many ordinary deltas | Flush important approval and terminal transitions promptly; measure event-to-visible latency |
| Entity-level subscriptions | One changing span rerenders unrelated rows | Keep selectors stable and avoid passing the entire map through every component |
| Lazy payload cache | Inputs or outputs are large and rarely opened | Bound by bytes, evict least-recently-used closed entries, and include redaction version in the key |
| Web Worker parsing or search | Profiling shows JSON parsing or safe-summary indexing blocks the main thread | Structured cloning has a cost; do not add a worker before measurement |
| Fixed or estimated row height | Trace rows have predictable compact summaries | Keep 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 | User experience | Correctness response |
|---|---|---|
| Connection drops | Keep the trace visible, show reconnecting and last-updated time, offer manual retry | Abort the old reader, back off with jitter, and resume from the last contiguous cursor |
| Cursor expired | Show catching up without clearing selection | Request a fresh snapshot, rebase view state by stable IDs, and discard obsolete transport state |
| Sequence gap persists | Mark data as incomplete rather than live | Fetch a bounded delta range or replace the snapshot; never guess that missing events were irrelevant |
| Child arrives before parent | Optionally show an unresolved root group so the span remains available for inspection | Store the child once under an orphan index and attach it when the parent appears |
| Completed event arrives after newer failure | Keep the newer terminal evidence | Field sequence or revision rejects lifecycle regression |
| Malformed or unknown event | Continue known history and surface degraded freshness if continuity is affected | Validate envelope, log schema-safe diagnostics, and request recovery when required |
Human-in-the-loop approval UI races
| Race | Resolution |
|---|---|
| User stops while the run completes | Completion is authoritative. Clear stopping, show that the run completed before stop took effect, and do not rewrite it as stopped. |
| Two tabs decide one approval | The 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 succeeded | Retry with the same idempotency key or await the stream; duplicate confirmation converges without executing twice. |
| Approval becomes unavailable while the dialog is open | Disable the stale form after the newer event, preserve any typed reason only locally, and move focus to the outcome message. |
| Route changes during a command | Do 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
- Static completed runsShip normalized snapshots, tree navigation, lazy redacted payloads, responsive layout, and accessibility before adding live mutation.
- Read-only live runsAdd resumable streaming behind a flag, shadow-check event reduction against refreshed snapshots, and instrument gaps and memory.
- Stop and retryIntroduce idempotent run commands with visible pending and conflict states, then test completion races and navigation.
- Human approvalsGate 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.
Technical references
OpenAI Agents tracing model Reference terminology for traces and spans while keeping the UI provider-neutral. OpenAI Agents human-in-the-loop Ground approval, rejection, interruption, and resume behavior in a concrete agent run model. OpenTelemetry GenAI spans Review standardized generative AI attribute names and their evolving stability. WAI-ARIA treegrid pattern Use the keyboard and semantic expectations for an interactive hierarchical grid. WAI-ARIA log role technique Understand live-region behavior before announcing meaningful appended status messages. MDN AbortController Cancel obsolete snapshot, stream, search, and payload work when route or selection changes.
Use the Question Library for baseline coverage, then move into a Study Plan before targeted Company Prep.