Chapter 13

Stream Product Events, Not Internal Reasoning

It's useful for the user to see that the system is checking policy or preparing a calculation. They don't need the raw reasoning trace, internal tool arguments, or personal data from results. So you should stream product events, not internal reasoning - you define the public event contract yourself and filter the provider stream down to it.

TypeScript
export type PublicAgentEvent =
  | { type: "status"; message: string }
  | { type: "source_found"; title: string }
  | { type: "answer_delta"; text: string }
  | { type: "outcome"; value: AgentOutcome }
  | { type: "error"; code: string; retryable: boolean };

function toSse(event: PublicAgentEvent) {
  return `event: ${event.type}\ndata: ${JSON.stringify(event)}\n\n`;
}

Each provider has its own streaming mechanism, but the public contract stays single. OpenAI Responses streaming emits typed semantic events - filter them before sending to the client. The Anthropic Messages API delivers SSE, and the TypeScript SDK can both stream and fetch the final Message. Google Interactions supports streaming. Your job is to reduce all three to a single PublicAgentEvent at the application level.

A separate case is confirming an action, and here the stream ends not with text but with a card.

Confirmation UX. When the outcome equals approval_required, the stream ends with a card showing the exact amount, the reason, and the validity. The button calls a separate endpoint rather than sending the model the word "yes."

Events are under control. What remains is the unpleasant reality of the network and providers - errors - and you may retry them only where a retry is safe.

Links