nav.groups.runtime
StreamAccumulator
Incrementally assembles streamed text and tool-call arguments into a complete `Message::Assistant`.
StreamAccumulator
Incrementally assemble a streamed model response into a
Message::Assistant.
StreamAccumulator consumes the chunks of a ChatStream (provider-specific) and produces a complete Message::Assistant plus a list of ToolCalls when the stream ends. It runs inside the CallingModel state of the Turn FSM.
The full file is src/runtime/accumulator.rs.
Why a dedicated accumulator
A ChatStream delivers chunks that are not independent — text deltas are concatenable, but tool-call arguments arrive as JSON fragments that must be merged. The accumulator handles the merge, validates the result, and surfaces errors early (e.g. malformed JSON in a tool-call argument).
API
pub struct StreamAccumulator {
text: String,
tool_calls: Vec<PartialToolCall>,
finish_reason: Option<FinishReason>,
usage: Option<TokenUsage>,
}
impl StreamAccumulator {
pub fn new() -> Self;
pub fn observe(&mut self, chunk: ChatStreamEvent) -> Result<(), AccumulatorError>;
pub fn finalize(self) -> Result<AccumulatedResponse, AccumulatorError>;
}
pub struct AccumulatedResponse {
pub message: Message,
pub tool_calls: Vec<ToolCall>,
pub finish_reason: FinishReason,
pub usage: Option<TokenUsage>,
}
Merge rules
TextDelta(delta)— appenddeltato the text buffer.ToolCallStart { id, name }— push a newPartialToolCallwith empty args.ToolCallArgumentDelta { id, delta }— appenddeltato the matchingPartialToolCall.args.ToolCallEnd { id }— mark the call as ready.FinishReason(r)— store; stream is done.Usage(u)— store; stream is done.
finalize runs after FinishReason is observed. It assembles the Message::Assistant, parses each PartialToolCall.args as JSON, and returns the full response.
Edge cases
- Stream ends without
FinishReason—finalizereturnsAccumulatorError::Truncated. The run loop logs the partial text and continues with what was accumulated. - Malformed tool-call JSON —
finalizereturnsAccumulatorError::InvalidToolCallJson { call_id, message }. The call is dropped; other calls in the same stream are kept. - Out-of-order chunks — most providers are in-order, but the accumulator is defensive:
ToolCallArgumentDeltafor an unknownidis logged and dropped. - Empty stream —
finalizereturns an emptyMessage::AssistantwithFinishReason::Stop. This is normal for models that return only tool calls.
See also
- AgentRuntime — the consumer.
- Message Types — the assembled
Messageshape.