use serde_json::Value;
use crate::error::ProviderError;
use crate::types::{StreamEvent, ToolCall};
#[derive(Debug, Default)]
pub struct ToolCallAccumulator {
calls: Vec<PendingToolCall>,
}
#[derive(Debug, Clone)]
pub struct PendingToolCall {
pub id: String,
pub function_name: String,
pub arguments_json: String,
}
impl ToolCallAccumulator {
pub fn new() -> Self {
Self { calls: Vec::new() }
}
pub fn process(&mut self, event: &StreamEvent) -> Option<&PendingToolCall> {
match event {
StreamEvent::ToolCallDelta { index, id, function_name, arguments_delta } => {
while self.calls.len() <= *index {
self.calls.push(PendingToolCall {
id: String::new(),
function_name: String::new(),
arguments_json: String::new(),
});
}
if let Some(id) = id {
self.calls[*index].id = id.clone();
}
if let Some(name) = function_name {
self.calls[*index].function_name = name.clone();
}
self.calls[*index].arguments_json.push_str(arguments_delta);
Some(&self.calls[*index])
}
_ => None,
}
}
pub fn finalize(self) -> Result<Vec<ToolCall>, ProviderError> {
self.calls
.into_iter()
.map(|pending| {
if pending.id.is_empty() {
return Err(ProviderError::Format(
"ToolCallDelta completed with empty id".into(),
));
}
if pending.function_name.is_empty() {
return Err(ProviderError::Format(format!(
"ToolCallDelta completed with empty function_name for id={}",
pending.id
)));
}
let arguments: Value = if pending.arguments_json.is_empty() {
Value::Object(Default::default())
} else {
serde_json::from_str(&pending.arguments_json).map_err(|e| {
ProviderError::Format(format!(
"Failed to parse tool call arguments JSON for id={}: {}",
pending.id, e
))
})?
};
Ok(ToolCall { id: pending.id, function_name: pending.function_name, arguments })
})
.collect()
}
pub fn pending_count(&self) -> usize {
self.calls.len()
}
pub fn clear(&mut self) {
self.calls.clear();
}
}
#[cfg(test)]
#[allow(clippy::unwrap_used)]
mod tests {
use super::*;
use crate::types::StreamEvent;
#[test]
fn test_single_tool_call() {
let mut acc = ToolCallAccumulator::new();
acc.process(&StreamEvent::ToolCallDelta {
index: 0,
id: Some("call_123".into()),
function_name: Some("search".into()),
arguments_delta: "".into(),
});
acc.process(&StreamEvent::ToolCallDelta {
index: 0,
id: None,
function_name: None,
arguments_delta: r#"{"query": "Rust 2024"}"#.into(),
});
let mut calls = acc.finalize().unwrap();
assert_eq!(calls.len(), 1);
let call = calls.remove(0);
assert_eq!(call.id, "call_123");
assert_eq!(call.function_name, "search");
assert_eq!(call.arguments["query"], "Rust 2024");
}
#[test]
fn test_multiple_tool_calls_parallel() {
let mut acc = ToolCallAccumulator::new();
acc.process(&StreamEvent::ToolCallDelta {
index: 0,
id: Some("call_0".into()),
function_name: Some("search".into()),
arguments_delta: r#"{"q": "a""#.into(),
});
acc.process(&StreamEvent::ToolCallDelta {
index: 1,
id: Some("call_1".into()),
function_name: Some("code".into()),
arguments_delta: r#"{"lang": "rust", "task": "setup"}"#.into(),
});
acc.process(&StreamEvent::ToolCallDelta {
index: 0,
id: None,
function_name: None,
arguments_delta: r#", "b": 2}"#.into(),
});
let calls = acc.finalize().unwrap();
assert_eq!(calls.len(), 2);
let call0 = calls.iter().find(|c| c.id == "call_0").unwrap();
assert_eq!(call0.arguments["q"], "a");
assert_eq!(call0.arguments["b"], 2);
let call1 = calls.iter().find(|c| c.id == "call_1").unwrap();
assert_eq!(call1.function_name, "code");
assert_eq!(call1.arguments["lang"], "rust");
}
#[test]
fn test_empty_arguments() {
let mut acc = ToolCallAccumulator::new();
acc.process(&StreamEvent::ToolCallDelta {
index: 0,
id: Some("call_empty".into()),
function_name: Some("ping".into()),
arguments_delta: "".into(),
});
let mut calls = acc.finalize().unwrap();
assert_eq!(calls.len(), 1);
let call = calls.remove(0);
assert_eq!(call.arguments, Value::Object(Default::default()));
}
#[test]
fn test_missing_id_error() {
let mut acc = ToolCallAccumulator::new();
acc.process(&StreamEvent::ToolCallDelta {
index: 0,
id: None,
function_name: Some("search".into()),
arguments_delta: "{}".into(),
});
let result = acc.finalize();
assert!(result.is_err());
}
}