MusePi

Provider streaming internals

This document explains how token/tool streaming is normalized in @musepi/pi-ai, then propagated through @musepi/pi-agent-core and coding-agent session events.

End-to-end flow

  1. streamSimple() (packages/ai/src/stream.ts) maps generic options and dispatches to a provider stream function.
  2. Provider stream functions translate provider-native stream events into the unified AssistantMessageEvent sequence. Current built-ins include Anthropic, OpenAI Responses/Completions/Codex/Azure Responses, Google Gemini/Gemini CLI/Vertex, Bedrock Converse, Ollama, Cursor, pi-native gateway transport, plus GitLab Duo/Kimi/Synthetic/xAI-Grok-Responses wrappers and extension-registered custom APIs.
  3. Each provider pushes events into AssistantMessageEventStream (packages/ai/src/utils/event-stream.ts), which exposes:
    • async iteration for incremental updates
    • result() for final AssistantMessage
  4. agentLoop (packages/agent/src/agent-loop.ts) consumes those events, mutates in-flight assistant state, and emits message_update events carrying the raw assistantMessageEvent.
  5. AgentSession (packages/coding-agent/src/session/agent-session.ts) subscribes to agent events, persists messages, drives extension hooks, and applies session behaviors (retry, compaction, TTSR, streaming-edit abort checks).

Unified stream contract in @musepi/pi-ai

All providers emit the same shape (AssistantMessageEvent in packages/ai/src/types.ts):

AssistantMessageEventStream guarantees:

Delta throttling behavior

AssistantMessageEventStream itself no longer throttles or merges delta events — every provider event is delivered as pushed. The per-delta cost control moved into tool-call argument parsing: providers accumulate partial JSON and re-parse it via parseStreamingJsonThrottled() (packages/ai/src/utils/json-parse.ts), which skips the re-parse until at least STREAMING_JSON_PARSE_MIN_GROWTH (256) new bytes have arrived, bounding mid-stream parse cost from quadratic to linear. The final toolcall_end parse is always unconditional and authoritative.

There is no provider backpressure: providers still produce at full speed, while the local stream queues.

Provider normalization details

Anthropic (anthropic-messages)

Source: packages/ai/src/providers/anthropic.ts

Normalization points:

Tool-call argument streaming:

OpenAI Responses family (openai-responses, openai-codex-responses, azure-openai-responses)

Sources: packages/ai/src/providers/openai-responses.ts, openai-codex-responses.ts, and azure-openai-responses.ts

Normalization points:

Tool-call argument streaming:

Google Generative AI (google-generative-ai)

Source: packages/ai/src/providers/google.ts (thin request wrapper) and google-shared.ts (streamGoogleGenAI, shared chunk-to-block translation)

Normalization points:

Tool-call argument streaming:

Partial tool-call JSON accumulation and recovery

Shared behavior for Anthropic/OpenAI Responses uses parseStreamingJson() / parseStreamingJsonThrottled() (packages/ai/src/utils/json-parse.ts):

  1. try JSON.parse
  2. fallback to the in-house RelaxedJson parser (relaxed/repairing) for incomplete fragments
  3. if both fail, return {}

Implications:

Stop reasons vs transport/runtime errors

Provider stop reasons are mapped to normalized stopReason:

Error semantics are split in two stages:

  1. Model completion semantics (provider reported finish reason/status)
  2. Transport/runtime failure (network/client/parser/abort exceptions)

If provider stream throws or signals failure, each provider wrapper catches and emits terminal error event with:

Malformed chunk / SSE parse failure behavior

The OpenAI Completions/Responses paths use the in-repo HTTP+SSE transport postOpenAIStream() (packages/ai/src/utils/openai-http.ts), which decodes frames with readSseJson() and replaced the openai SDK client. Anthropic uses the in-repo AnthropicMessagesClient (packages/ai/src/providers/anthropic-client.ts); the Google paths and the Codex SSE fallback read SSE via readSseJson() directly, and websocket Codex frames are normalized through the same event handler.

Observed behavior in current implementation:

Cancellation boundaries

Cancellation is layered:

Tool execution cancellation is separate from model stream cancellation:

Backpressure boundaries

There is no hard backpressure mechanism between provider SDK stream and downstream consumers:

Current design favors responsiveness and simple ordering over bounded-buffer flow control.

How stream events surface as agent/session events

agentLoop.streamAssistantResponse() bridges AssistantMessageEvent to AgentEvent:

AgentSession then consumes those events for session-level behaviors:

Unified vs provider-specific responsibilities

Unified (common contract):

Provider-specific (not fully abstracted):

Implementation files