RFC-0008: Run Output Stream Adapter
Generated from the repository source of truth. Source file:
/docs/RFC-0008-run-stream-consumer-utilities.md. Status:Accepted.
- Status: Accepted
- Date: 2026-05-11
- Owners: aioc maintainers
Context
Section titled “Context”run(..., { stream: true }) exposes a public RunStreamEvent union.
That low-level contract is intentionally explicit and should remain available to advanced consumers.
However, many host applications have a more specific and common need:
- stream text deltas to a UI while the model is responding,
- keep the final assistant message,
- access the final run history,
- access paired tool calls and tool outputs after the run completes,
- persist the last active agent.
Those applications often do not need to process tool outputs live while deltas are still being emitted. They need the tool outputs after completion, usually to derive application metadata such as references, citations, audit summaries, or persistence records.
The common pattern is therefore not a generic event callback problem. It is a run-output adaptation problem:
- deltas are useful during the stream,
- final output and tool outputs are useful at completion.
Callback-based stream consumers also do not compose well with application APIs implemented as async generators, because yield must happen in the generator body rather than inside callback handlers.
Decision
Section titled “Decision”aioc adds a small optional adapter for streamed run results.
The adapter should consume a StreamedRunResult<TContext> and produce a simpler async iterable:
text_deltaevents during the run,- one final
completedevent containing final output, history, last agent, and paired tool calls.
By default, the adapter should emit only:
text_delta,completed.
Additional live events may be enabled explicitly for applications that need them.
This adapter should sit above RunStreamEvent and should not replace the low-level stream contract.
The goals are:
- support UI streaming without forcing applications to branch on low-level event strings,
- expose final run data in one completion event,
- preserve access to tool outputs after completion,
- keep the default event stream small,
- allow opt-in live events for agent updates, tool calls, or tool outputs,
- preserve streaming order for deltas,
- preserve the one-shot nature of
StreamedRunResult.toStream(), - keep raw stream events available for advanced consumers.
In scope:
- consuming a
StreamedRunResult<TContext>, - yielding text deltas while streaming,
- tracking the final assistant message,
- exposing
historyandlastAgentafter stream completion, - exposing paired tool calls and tool outputs after stream completion,
- optionally yielding live agent updates,
- optionally yielding live tool calls and tool outputs,
- preserving provider neutrality.
Out of scope:
- transport adapters for HTTP, SSE, WebSocket, or framework-specific APIs,
- retry semantics,
- resumable streaming,
- event persistence,
- frontend rendering primitives,
- a fully lossless replacement for
RunStreamEvent, - replacement of the public
RunStreamEventunion.
Proposed Helper
Section titled “Proposed Helper”export type RunOutputEvent<TContext = unknown> = | { type: "text_delta"; delta: string; } | { type: "completed"; finalOutput: string; history: AgentInputItem[]; lastAgent: Agent<TContext>; toolCalls: ExtractedToolCall[]; } | { type: "agent_updated"; agent: Agent<TContext>; } | { type: "tool_call"; item: ToolCallItem; } | { type: "tool_output"; item: ToolCallOutputItem; output: unknown; toolCall?: ToolCallItem; };
export async function* toRunOutputEvents<TContext = unknown>( result: StreamedRunResult<TContext>, options?: { emitAgentUpdates?: boolean; emitToolCalls?: boolean; emitToolOutputs?: boolean; },): AsyncIterable<RunOutputEvent<TContext>>;ExtractedToolCall is the existing run-record utility shape produced by extractToolCalls(...).
This keeps tool_call_item and tool_call_output_item pairing inside aioc, where the runtime history contract is known.
Semantics
Section titled “Semantics”- The helper consumes
result.toStream()exactly once. - A
text_deltaevent is yielded for eachraw_model_stream_event.data.deltawhen present. - By default, only
text_deltaandcompletedevents are yielded. - An
agent_updatedevent is yielded when the active agent changes only ifemitAgentUpdatesistrue. - The initial agent activation is not yielded as
agent_updated;completed.lastAgentstill exposes the final active agent. - A
tool_callevent is yielded for live tool calls only ifemitToolCallsistrue. - A
tool_outputevent is yielded for live tool outputs only ifemitToolOutputsistrue. - Live
tool_outputevents include the matchingtool_callitem when it has already been observed in the stream. - The final assistant message is read from
message_output_item. - After the underlying stream completes, the helper yields exactly one
completedevent. completed.finalOutputis the final assistant message content, or an empty string if no final message was emitted.completed.historyis a shallow copy ofresult.history.completed.lastAgentisresult.lastAgent.completed.toolCallsis computed fromresult.historywithextractToolCalls(result.history).- Tool outputs are not discarded; they remain available through
completed.toolCalls[].outputeven whenemitToolOutputsis not enabled. - Options affect only additional live events. They do not change the content of
completed. - Errors from the underlying stream are propagated.
Example
Section titled “Example”const streamed = await run(agent, input, { stream: true });
for await (const event of toRunOutputEvents(streamed)) { if (event.type === "text_delta") { yield new TextDelta({ delta: event.delta }); continue; }
if (event.type === "completed") { yield new TextResponse({ text: event.finalOutput });
const ragOutput = event.toolCalls.find( (call) => call.name === "find_chunks" && call.hasOutput, );
const references = buildReferences(ragOutput?.output, event.finalOutput);
if (references.length > 0) { yield { references }; }
yield { items: event.history, agent: event.lastAgent, }; }}Optional Live Events
Section titled “Optional Live Events”Applications that need live telemetry or progress can opt into additional events without changing the default stream shape.
for await (const event of toRunOutputEvents(streamed, { emitAgentUpdates: true, emitToolCalls: true, emitToolOutputs: true,})) { if (event.type === "agent_updated") { logger.info({ agent: event.agent.name }, "Agent updated"); }
if (event.type === "tool_call") { logger.info({ tool: event.item.name }, "Tool called"); }
if (event.type === "tool_output") { logger.info({ callId: event.item.callId }, "Tool output received"); }}These live events are convenience signals.
The authoritative completed view remains completed.toolCalls, because it is derived from the final run history and includes paired tool call/output information.
Relation To Tool Outputs And References
Section titled “Relation To Tool Outputs And References”Applications that stream answer text often do not need structured tool outputs until the answer is complete.
For example, a retrieval tool may return chunks to the model. The model may emit inline citation markers such as [1] and [2] during the text stream. The application can then resolve those markers into structured references after completion by combining:
completed.finalOutput,completed.toolCalls[].output.
This preserves the live text stream while avoiding live coupling to every tool-output event.
Applications that need live tool progress or live tool-output handling can still consume result.toStream() directly.
Applications that only need lightweight live progress can enable emitToolCalls or emitToolOutputs.
Relation To RFC-0007
Section titled “Relation To RFC-0007”RFC-0007 covers thread history utilities.
This RFC covers streamed run output adaptation.
The two utilities may be used together, but they solve different problems:
- RFC-0007 reduces boilerplate around application-owned conversation state,
- RFC-0008 reduces boilerplate around UI streaming and final output collection.
Security and Privacy Notes
Section titled “Security and Privacy Notes”- Streamed output, tool arguments, tool outputs, and history may contain sensitive data.
- The helper must not redact, persist, or transform sensitive data.
- Applications remain responsible for transport security, persistence, redaction, and access control.
- Tool outputs exposed in
completed.toolCallsshould be treated as application-sensitive data.
Alternatives Considered
Section titled “Alternatives Considered”Callback-based stream consumer
Section titled “Callback-based stream consumer”This was the first shape considered.
It is not the primary API because callbacks do not compose well with async-generator APIs that need to yield application events.
It may still be useful later as a convenience wrapper around toRunOutputEvents(...).
Text-only stream adapter
Section titled “Text-only stream adapter”This would keep the API very small, but it would hide tool outputs that applications often need after completion for references, citations, audit metadata, or persistence.
Always emit every normalized live item
Section titled “Always emit every normalized live item”This would preserve more live detail, but it makes the default adapter noisy and mostly renames low-level branches without materially reducing application code.
The selected design keeps the default small and makes live agent/tool events opt-in.
Advanced consumers that need full item-by-item control can use the existing RunStreamEvent contract directly.
Implementation Notes
Section titled “Implementation Notes”This RFC is intentionally separate from RFC-0007 implementation.
The implemented surface is:
src/run-output-events.ts- exported from
src/index.ts
The implementation can reuse extractToolCalls(...) from src/run-record-utils.ts after the stream completes.
Minimal Test Matrix
Section titled “Minimal Test Matrix”- Yields
text_deltaevents for raw model deltas. - Does not yield
agent_updated,tool_call, ortool_outputevents by default. - Yields
agent_updatedevents whenemitAgentUpdatesistrue. - Yields
tool_callevents whenemitToolCallsistrue. - Yields
tool_outputevents whenemitToolOutputsistrue. - Yields exactly one
completedevent after stream completion. - Sets
completed.finalOutputfrom the final message output item. - Sets
completed.historyto a shallow copy ofStreamedRunResult.history. - Sets
completed.lastAgentfromStreamedRunResult.lastAgent. - Includes paired tool calls and outputs in
completed.toolCalls. - Preserves the one-shot consumption behavior of
StreamedRunResult.toStream(). - Propagates stream errors.
Status
Section titled “Status”Accepted. Implemented in src/run-output-events.ts.