Skip to content

05. Streaming, Cancellation, And Events

This chapter explains how an agent run moves from a one-shot JSON response to a live process. Streaming, cancellation, and runtime events are the foundation of a real agent experience.

After reading this chapter, you should understand:

  • how a streaming route differs from a normal JSON route
  • why AbortSignal crosses runtime boundaries
  • why agent events and frontend events are separate
  • how projection turns internal events into UI-facing events

Background

A synchronous agent endpoint is hard to use. Real agent work takes time and has observable phases:

  • prompt built
  • model started
  • assistant text streaming
  • tool requested
  • tool started
  • tool finished
  • run succeeded or failed

An agent run needs to expose progress while it is happening.

Streaming Route

The project added:

text
app/api/agent/stream/route.ts

The route returns Server-Sent Events. It is still a route boundary, not the agent runtime.

The backend emits internal AgentEvents. The route projects them to frontend SSE events.

Cancellation Boundary

Cancellation was designed as a real abort chain, not a prompt instruction:

text
React AbortController
  -> fetch signal
  -> NextRequest.signal
  -> AgentRunContext.signal
  -> OpenAI SDK request option
  -> stream chunk guard
  -> tool runtime checks

The key file is:

text
lib/agent-run-context.ts

The runtime calls assertAgentRunNotAborted(...) at important checkpoints.

Agent Events

lib/agent-events.ts introduced the internal event model.

Examples:

text
run_started
model_started
assistant_delta
tool_requested
tool_started
tool_finished
step_created
run_succeeded
run_failed

Later phases added:

text
model_requested
model_completed
tool_permission_decided
approval_requested

Projection Boundary

lib/agent-stream-projection.ts maps internal events to frontend events.

This keeps the browser from becoming the runtime. The frontend observes; the server owns model calls and tools.

Data Flow

Git Evidence

Relevant commits:

text
d5f8ad8 Stream agent progress and answer
edf8405 Add cancellable agent runtime boundaries
72bed76 Add agent harness event state
6fb0b86 Project agent events to stream responses

Tradeoff

Streaming introduced more event names, but it preserved a clean direction:

text
runtime event -> projection -> frontend event

That decision made the later Debug Console possible.

Common Misunderstandings

Misunderstanding 1: Streaming Only Splits The Final Answer Into Chunks

Agent streaming includes more than answer chunks. It includes process text, tool starts, tool finishes, errors, cancellation, and final commits.

Misunderstanding 2: Cancellation Only Stops The Frontend

Cancellation must reach the runtime. Otherwise the UI can stop rendering while the backend continues calling models or running tools.

Misunderstanding 3: Internal Events Can Go Directly To The UI

Internal events often contain runtime details. A projection layer stabilizes the frontend protocol and preserves richer internal semantics for future telemetry.

Chapter Summary

This chapter turns an agent run into a live process: the frontend receives events, the runtime reacts to abort, and internal events are projected into a frontend protocol.

Chapter Checkpoint

Verify two things: validation failure never opens the SSE stream, and terminal run events are locked down by deterministic tests.

  1. POST an empty body to /api/agent/stream (no key required). Measured: the response is a plain JSON 400 (content-type: application/json), not an SSE error event — validation completes before the stream opens:
bash
curl -s -i -X POST http://localhost:3000/api/agent/stream \
  -H 'Content-Type: application/json' -d '{}'
text
HTTP/1.1 400 Bad Request
content-type: application/json

{"ok":false,"error":"Request body validation failed.","validationErrors":{"formErrors":[],"fieldErrors":{"task":["Field `task` is required."]}}}
  1. Terminal-event tests (no key, fake gateway):
bash
npx tsx --test tests/agent-run-terminal-events.test.ts
text
✔ an aborted run emits run_cancelled as its terminal event
✔ a failed run emits run_failed as its terminal event
ℹ pass 2