//! Standalone accumulator for streamed tool-call deltas. //! //! Flow: `ToolCallAccumulator::add_delta` is fed incremental `(index, id, //! name, arguments_delta)` chunks as they arrive over SSE → grows its //! internal `Vec` as needed → `is_complete` reports once //! every accumulated call has both a name and arguments. //! //! Why: mirrors the accumulation logic built into `StreamedTurn::apply_event` //! but as an independent, reusable type for callers that want to track //! tool-call deltas without a full `StreamedTurn` (e.g. a lighter-weight //! preview). Currently unused (`#[allow(dead_code)]`), kept for that future //! use case. use super::turn::ParsedToolCall; use serde_json::{json, Value}; /// Standalone tool-call delta accumulator, functionally equivalent to the accumulation /// logic built into `StreamedTurn::apply_event`. Reserved for callers that want to track /// tool-call deltas independently of a full `StreamedTurn` (e.g. a lighter-weight preview). #[allow(dead_code)] pub struct ToolCallAccumulator { calls: Vec, } #[allow(dead_code)] impl ToolCallAccumulator { /// Construct an empty accumulator with no tool calls tracked yet. /// /// Return: a fresh `ToolCallAccumulator`. pub fn new() -> Self { ToolCallAccumulator { calls: Vec::new() } } /// Append a delta to the tool call at the given index, growing the /// calls vector if needed. pub fn add_delta( &mut self, index: usize, id: Option<&str>, name: Option<&str>, arguments_delta: &str, ) { while self.calls.len() <= index { self.calls.push(ParsedToolCall { id: String::new(), name: String::new(), arguments: String::new(), is_complete: false, }); } let tc = &mut self.calls[index]; if let Some(new_id) = id { if !new_id.is_empty() { tc.id = new_id.to_string(); } } if let Some(new_name) = name { if !new_name.is_empty() { tc.name = new_name.to_string(); } } tc.arguments.push_str(arguments_delta); } /// Borrow the accumulated tool calls. pub fn calls(&self) -> &[ParsedToolCall] { &self.calls } /// Return true once all tool calls have both a name and arguments. pub fn is_complete(&self) -> bool { !self.calls.is_empty() && self.calls.iter().all(|tc| !tc.name.is_empty() && !tc.arguments.is_empty()) } /// Clear all accumulated calls (starting a fresh turn). pub fn reset(&mut self) { self.calls.clear(); } /// Build a JSON-serialisable `Vec` of pending (non-empty-name) /// tool calls, suitable for downstream inspection or replay. pub fn pending_args(&self) -> Vec { self.calls .iter() .filter(|tc| !tc.name.is_empty()) .map(|tc| { json!({ "tool_call_id": tc.id, "name": tc.name, "arguments": tc.arguments, }) }) .collect() } } impl Default for ToolCallAccumulator { fn default() -> Self { Self::new() } }