use lellm_core::{LlmError, ToolCall};
#[derive(Debug)]
pub struct ToolCallDelta {
pub index: usize,
pub id: Option<String>,
pub name: Option<String>,
pub arguments_delta: Option<String>,
}
pub struct ToolCallAccumulator {
current: std::collections::HashMap<usize, PendingToolCall>,
}
struct PendingToolCall {
id: Option<String>,
name: Option<String>,
arguments: String,
}
impl ToolCallAccumulator {
pub fn new() -> Self {
Self {
current: std::collections::HashMap::new(),
}
}
pub fn push(&mut self, delta: &ToolCallDelta) {
let entry = self
.current
.entry(delta.index)
.or_insert_with(|| PendingToolCall {
id: None,
name: None,
arguments: String::new(),
});
if let Some(ref id) = delta.id {
entry.id = Some(id.clone());
}
if let Some(ref name) = delta.name {
entry.name = Some(name.clone());
}
if let Some(ref d) = delta.arguments_delta {
entry.arguments.push_str(d);
}
}
pub fn finalize(self) -> Result<Vec<ToolCall>, LlmError> {
let mut entries: Vec<_> = self.current.into_iter().collect();
entries.sort_by_key(|&(idx, _)| idx);
let mut result = Vec::new();
for (_index, pending) in entries {
let id = pending.id.unwrap_or_else(|| "unknown".to_string());
let name = pending.name.unwrap_or_else(|| "unknown".to_string());
let arguments: serde_json::Value = if pending.arguments.is_empty() {
serde_json::Value::Object(Default::default())
} else {
serde_json::from_str(&pending.arguments)
.unwrap_or(serde_json::Value::String(pending.arguments))
};
result.push(ToolCall {
id,
name,
arguments,
});
}
Ok(result)
}
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn accumulates_delta_into_tool_call() {
let mut acc = ToolCallAccumulator::new();
acc.push(&ToolCallDelta {
index: 0,
id: None,
name: Some("read_file".into()),
arguments_delta: None,
});
acc.push(&ToolCallDelta {
index: 0,
id: Some("tc_123".into()),
name: None,
arguments_delta: Some(r#""{"path""#.into()),
});
acc.push(&ToolCallDelta {
index: 0,
id: None,
name: None,
arguments_delta: Some(r#": "/test"}}"#.into()),
});
let calls = acc.finalize().unwrap();
assert_eq!(calls.len(), 1);
assert_eq!(calls[0].id, "tc_123");
assert_eq!(calls[0].name, "read_file");
}
#[test]
fn empty_accumulator_returns_empty() {
let acc = ToolCallAccumulator::new();
let calls = acc.finalize().unwrap();
assert!(calls.is_empty());
}
}