DEV Community

hefty
hefty

Posted on

Your Coding Agent UI Needs an Event Model, Not a Chat Transcript

Imagine a coding agent has been running for 25 minutes. Its transcript is busy. It searched the repository, announced a plan, edited three files, ran a command, apologized for an error, and said it was making one final adjustment.

The cursor is still moving. What you cannot see is more important:

  • Which phase is active?
  • Is the agent blocked or still making progress?
  • What was the last durable artifact?
  • Did the required checks pass?
  • What action is safe now?

A chat transcript can contain every sentence the agent produced and still fail to answer all five.

Chat is useful for instructions and explanations. It is a bad state model. Once an agent runs tools, waits for approval, creates artifacts, and validates work over several minutes, its operator UI needs a structured event stream underneath the conversation.

Chat is an explanation layer

Chat became the default agent interface for an obvious reason: models produce text, and people know how to type into a box. That works well when the exchange is short and the output is the product.

The shape changes when the output is a patch, pull request, deployment plan, or test result. Now the text describes work happening elsewhere. A sentence such as "I'm checking the failing tests" may describe intent, an active process, a retry, or stale narration left behind after a crash.

The frontend should not have to parse that sentence to decide whether to show a spinner, an approval button, or a recovery action. Prose changes between models and versions. It can arrive late, contradict an earlier message, or sound finished before the acceptance checks run.

Keep the conversation. Let it carry goals, questions, tradeoffs, and rationale. Put operational truth on a separate channel with stable semantics.

Current tools are already exposing pieces of this distinction. Microsoft's Foundry Toolkit release notes describe richer model profiling and clearer Agent Inspector handling for events and latency. A recent DEV.to comparison evaluates coding agents through practical concerns such as repository permissions, tests, and knowing when to stop. These are project and author descriptions, not a shared standard, but they point at the same product problem: useful agent interfaces must represent the workflow around generation.

Give the run a small event contract

You do not need to invent a universal agent protocol. Start with the information your interface must render without guessing.

Here is an illustrative TypeScript model:

type Phase = "planning" | "editing" | "validation";

type EventBase = {
  runId: string;
  eventId: string;
  sequence: number;
  timestamp: string;
  phase?: Phase;
  correlationId?: string;
  summary: string;
};

type AgentEvent = EventBase & (
  | { type: "run_queued" }
  | { type: "phase_started"; phase: Phase }
  | { type: "tool_started"; operation: string }
  | {
      type: "tool_finished";
      operation: string;
      outcome: "succeeded" | "failed";
      durationMs?: number;
    }
  | { type: "artifact_created"; artifactRef: string }
  | {
      type: "approval_required";
      requiresAction: true;
      approvalId: string;
      scope: string[];
    }
  | {
      type: "approval_resolved";
      approvalId: string;
      decision: "approved" | "denied";
    }
  | {
      type: "check_finished";
      checkRef: string;
      outcome: "passed" | "failed";
      durationMs?: number;
    }
  | {
      type: "run_failed";
      errorCode: string;
      recoverable: boolean;
    }
  | { type: "run_completed"; checkRefs: string[] }
);
Enter fullscreen mode Exit fullscreen mode

Treat the names as placeholders; the boundaries are what matter.

runId, eventId, and sequence give the frontend identity and ordering. A correlation ID can join a tool start to its finish or connect a retry to the operation it replaced. The summary stays short enough for a timeline. Large outputs live behind artifactRef or checkRef instead of flooding the event payload.

Durations belong only on operations the runtime can measure. If the system cannot separate model time from network delay, it should not manufacture that distinction for a polished chart.

An append-only stream is often a good fit because it preserves transitions. The UI can rebuild a projection after reconnecting instead of trusting one giant mutable session object whose history has vanished. That is an implementation option, not a requirement. The contract matters more than whether you call the storage pattern event sourcing.

Model the states people care about

"Streaming," "running," and "done" are too coarse for an agent that can pause, ask permission, validate a patch, and fail in ways that may or may not be recoverable.

A practical UI might project events into these states:

  • queued
  • running
  • waiting_for_approval
  • validating
  • failed_recoverable
  • failed_terminal
  • completed

This is a proposed state set, not an industry standard. Your workflow may need fewer states or more precise ones. What matters is that each displayed state follows from an event rather than a mood inferred from the latest message.

waiting_for_approval deserves special treatment. The run is not progressing, but it has not failed. The interface should preserve what action was requested, the scope of that request, and the decision that resumed or stopped the run. A modal that disappears after one click leaves the timeline unable to explain why the agent gained permission to continue.

Failure needs similar honesty. A failed test may permit a bounded retry. A revoked credential may require operator action. A corrupted workspace may end the run. Showing the same red "failed" badge for all three forces the user back into logs to discover the available next step.

Completion should be strict. If the task contract names tests, review, or another acceptance check, run_completed should reference that evidence. The agent's final "done" message is commentary. It is not an acceptance event.

Activity is not progress

A transcript looks productive whenever text keeps arriving. An event timeline can make the same mistake if it rewards volume.

Ten tool calls might represent ten useful steps. They might also be the same failed search repeated with slightly different wording. Counting events does not tell you which one happened.

Progress indicators should follow durable milestones: a phase transition, a new diff, an approval decision, or a named check result. If the workflow has no defensible percentage, skip the percentage. "Validation: 2 of 4 checks finished" tells the truth. "87% complete" usually does not.

The same rule applies to status copy. Prefer "waiting for approval to modify deployment files" over "the agent is thinking." One describes an observable condition and gives the operator a decision. The other decorates uncertainty.

Latency needs semantics

One timer cannot explain a long-running agent.

A run can spend time in a queue, waiting on a model, executing a tool, waiting for a person, or validating an artifact. Those delays call for different responses. Faster model inference will not fix a five-hour approval wait. A quicker shell command will not help a run that keeps choosing the wrong tool.

Recent launch discussions make this measurement gap visible. Makers describe fewer round trips, bounded tool output, and lower cost, while community replies ask for stronger evidence. Rather than adding another "fast" badge, give operators a latency view that connects measurable phases to outcomes.

For each correlated operation, record the start and finish when the runtime owns both boundaries. Then show where elapsed time accumulated and whether the operation produced an artifact, a check result, a retry, or a failure. Keep unobserved time labeled as unobserved rather than assigning it to the model by subtraction.

That makes performance work concrete. It also prevents approval delay and validation time from being misreported as generation latency.

Project one stream into several views

The event stream is infrastructure, not the interface itself. Different users need different projections of the same run.

A compact status panel can show the current phase, blocker, last durable artifact, acceptance state, and allowed next action. A timeline can preserve transitions for diagnosis. An approval inbox can collect unresolved requests across runs. Artifact and check views can connect a run to its diff, worktree, pull request, logs, and validation results. A latency view can group measured duration by phase and operation.

This shape matches what is appearing around current multi-agent products. Project discussions connect agent runs with documents and human approval gates. Product descriptions connect them with worktrees, pull requests, artifacts, remote access, and recovery. Those descriptions do not prove reliability or productivity. They do show why a transcript-only UI runs out of room: the work has relationships that paragraphs cannot represent cleanly.

Keep the event payload bounded. A test log belongs in controlled artifact storage, with the event pointing to it. The same goes for a diff or pull request. References let each view fetch the evidence it needs without turning the timeline into a second database of duplicated blobs.

Recovery starts with an honest last state

An event model alone cannot make recovery safe, but it gives the interface enough information to stop pretending.

After a disconnect or crash, the UI should be able to show the last accepted sequence, the last durable artifact, whether the failure was marked recoverable, and which action the runtime currently permits. It should also expose uncertainty. A stale connection should look stale, not "running."

The projection code must expect duplicates, delayed delivery, and reconnects. Event IDs support idempotent handling. Per-run sequence numbers expose gaps and out-of-order delivery. If event 42 arrives before event 41, the UI can wait, refresh, or mark the view incomplete instead of quietly building a fictional timeline.

This does not replace checkpointing or idempotent tools. It keeps the frontend focused on the information needed to present recovery without guessing at backend state.

Do not turn telemetry into a data leak

Structured events are easier to query and display. They are also easier to retain forever by accident.

Tool arguments, file paths, prompts, and logs can expose source code or secrets. Capture the smallest summaries and references required to operate the run. Redact deliberately, restrict access, and set retention rules for event and artifact stores. A beautiful inspector is not worth copying sensitive payloads into five projections.

Small synchronous assistants may not need any of this. If a tool answers in seconds, produces no external artifact, and never waits for permission, a conversation can be enough. The event model earns its complexity when the workflow becomes long-running, tool-using, approval-bearing, or recoverable.

The transcript is not the control surface

A transcript tells the story of a run. An event model lets the product operate it.

With explicit events, the interface can answer the questions that matter: what phase is active, what is blocked, which artifact exists, what passed, and what action is available. Chat can then do what it is good at: helping a person steer the work and understand why the agent made a choice.

If your frontend has to parse reassuring prose to decide whether the work is complete, it does not have a run model yet.

Source notes

Top comments (1)

Collapse
 
raknaos profile image
Raknaos • Edited

The 'each displayed state follows from an event rather than a mood inferred from the latest message' line is the correct bar, and most agent UIs fail it. From running agents day to day, two things I'd add to your contract. First, waiting_for_approval needs a timeout semantics too: an approval request that no one answers is neither running nor failed, and operators want to distinguish 'nobody looked yet' from 'this has been parked for six hours'. Second, keep the event log durable independently of the UI — half of our postmortems came from replaying the append-only stream after the transcript was already gone, and that only works if the stream was written before anyone rendered it.