use std::collections::BTreeMap;
use locode_protocol::ContentBlock;
use serde_json::Value;
use thiserror::Error;
#[derive(Debug, Default)]
pub struct ToolCallAssembler {
partials: BTreeMap<usize, Partial>,
}
#[derive(Debug)]
struct Partial {
id: String,
name: String,
json: String,
}
#[derive(Debug, Error)]
pub enum AssembleError {
#[error("no tool call started at content-block index {0}")]
MissingStart(usize),
#[error("invalid tool-call arguments JSON at index {index}: {source}")]
InvalidJson {
index: usize,
source: serde_json::Error,
},
}
impl ToolCallAssembler {
#[must_use]
pub fn new() -> Self {
Self::default()
}
pub fn begin(&mut self, index: usize, id: impl Into<String>, name: impl Into<String>) {
self.partials.insert(
index,
Partial {
id: id.into(),
name: name.into(),
json: String::new(),
},
);
}
pub fn push_json(&mut self, index: usize, fragment: &str) -> Result<(), AssembleError> {
match self.partials.get_mut(&index) {
Some(partial) => {
partial.json.push_str(fragment);
Ok(())
}
None => Err(AssembleError::MissingStart(index)),
}
}
pub fn finish(self) -> Result<Vec<ContentBlock>, AssembleError> {
let mut blocks = Vec::with_capacity(self.partials.len());
for (index, partial) in self.partials {
let input: Value = if partial.json.trim().is_empty() {
Value::Object(serde_json::Map::new())
} else {
serde_json::from_str(&partial.json)
.map_err(|source| AssembleError::InvalidJson { index, source })?
};
blocks.push(ContentBlock::ToolUse {
id: partial.id,
name: partial.name,
input,
});
}
Ok(blocks)
}
}