use std::collections::HashMap;
use std::sync::{Arc, Mutex};
use std::time::Instant;
use yapi_types::message::{NestedToolCall, NestedToolCallStatus, NestedToolCalls, ToolCall, Usage};
use yapi_types::sync::lock;
const MAX_CALLS: usize = 256;
const MAX_ARGUMENT_BYTES_PER_CALL: usize = 8 * 1024;
const MAX_ARGUMENT_BYTES_TOTAL: usize = 32 * 1024;
const MAX_ERROR_CHARS: usize = 500;
#[derive(Default)]
struct Recorder {
calls: Vec<(NestedToolCall, Option<Instant>)>,
incomplete: bool,
argument_bytes: usize,
usage: Option<Usage>,
}
impl Recorder {
fn start(&mut self, call: &ToolCall) -> Option<usize> {
if self.calls.len() >= MAX_CALLS {
self.incomplete = true;
return None;
}
let json = yapi_types::json::stringify(&call.arguments);
let bytes = json.len();
let mut record = NestedToolCall {
id: call.id.clone(),
name: call.name.clone(),
arguments: None,
arguments_bytes: None,
status: NestedToolCallStatus::Unfinished,
duration_ms: None,
error: None,
};
if bytes > MAX_ARGUMENT_BYTES_PER_CALL
|| self.argument_bytes + bytes > MAX_ARGUMENT_BYTES_TOTAL
{
record.arguments_bytes = Some(bytes as u64);
self.incomplete = true;
} else {
record.arguments = Some(call.arguments.clone());
self.argument_bytes += bytes;
}
self.calls.push((record, Some(Instant::now())));
Some(self.calls.len() - 1)
}
fn finish(&mut self, index: Option<usize>, is_error: bool, error: &str) {
let Some((record, started)) = index.and_then(|index| self.calls.get_mut(index)) else {
return;
};
record.status = if is_error {
NestedToolCallStatus::Error
} else {
NestedToolCallStatus::Ok
};
let elapsed = started
.take()
.map_or(0.0, |started| started.elapsed().as_secs_f64() * 1000.0);
record.duration_ms = Some(yapi_types::js::round(elapsed) as u64);
if is_error && !error.is_empty() {
record.error = Some(yapi_types::js::slice(error, 0, MAX_ERROR_CHARS));
}
}
fn snapshot(&self) -> Option<NestedToolCalls> {
if self.calls.is_empty() && !self.incomplete {
return None;
}
let calls: Vec<NestedToolCall> = self.calls.iter().map(|(call, _)| call.clone()).collect();
let complete = !self.incomplete
&& calls
.iter()
.all(|call| call.status != NestedToolCallStatus::Unfinished);
Some(NestedToolCalls { calls, complete })
}
}
struct Scope {
recorder: Arc<Mutex<Recorder>>,
next_id: u64,
holds_queue: bool,
}
pub(crate) struct Started {
pub id: String,
recorder: Arc<Mutex<Recorder>>,
record: Option<usize>,
pub holds_queue: bool,
}
pub(crate) struct Summary {
pub calls: Option<NestedToolCalls>,
pub usage: Option<Usage>,
}
#[derive(Default)]
pub(crate) struct NestedCalls {
scopes: Mutex<HashMap<String, Scope>>,
pub queue: Arc<tokio::sync::Mutex<()>>,
}
impl NestedCalls {
pub fn start(&self, caller: &str, call: &mut ToolCall) -> Started {
let mut scopes = lock(&self.scopes);
let scope = scopes.entry(caller.to_owned()).or_insert_with(|| Scope {
recorder: Arc::default(),
next_id: 1,
holds_queue: false,
});
call.id = format!("{caller}/{}", scope.next_id);
scope.next_id += 1;
let recorder = scope.recorder.clone();
let holds_queue = scope.holds_queue;
let record = lock(&recorder).start(call);
Started {
id: call.id.clone(),
recorder,
record,
holds_queue,
}
}
pub fn enter(&self, started: &Started, holds_queue: bool) {
lock(&self.scopes).insert(
started.id.clone(),
Scope {
recorder: started.recorder.clone(),
next_id: 1,
holds_queue,
},
);
}
pub fn finish(&self, started: Started, is_error: bool, error: &str, usage: Option<&Usage>) {
lock(&self.scopes).remove(&started.id);
let mut recorder = lock(&started.recorder);
recorder.finish(started.record, is_error, error);
if let Some(usage) = usage {
recorder.usage = Some(match &recorder.usage {
Some(total) => total.combine(usage),
None => usage.clone(),
});
}
}
pub fn take(&self, tool_call_id: &str) -> Option<Summary> {
let scope = lock(&self.scopes).remove(tool_call_id)?;
let recorder = lock(&scope.recorder);
Some(Summary {
calls: recorder.snapshot(),
usage: recorder.usage.clone(),
})
}
pub fn clear(&self) {
lock(&self.scopes).clear();
}
}
#[cfg(test)]
mod tests {
use serde_json::{Map, Value, json};
use super::*;
fn call(name: &str, arguments: serde_json::Value) -> ToolCall {
ToolCall {
id: String::new(),
name: name.into(),
arguments: arguments.as_object().cloned().unwrap_or_default(),
thought_signature: None,
namespace: None,
}
}
#[test]
fn numbers_calls_per_caller() {
let nested = NestedCalls::default();
let mut first = call("read", json!({"path": "a.txt"}));
let started = nested.start("toolu_1", &mut first);
assert_eq!(first.id, "toolu_1/1");
nested.enter(&started, false);
let mut inner = call("ls", json!({}));
let inner_started = nested.start("toolu_1/1", &mut inner);
assert_eq!(inner.id, "toolu_1/1/1");
nested.finish(inner_started, true, &"x".repeat(600), None);
nested.finish(started, false, "", None);
let mut second = call("read", Value::Object(Map::new()));
let started = nested.start("toolu_1", &mut second);
assert_eq!(second.id, "toolu_1/2");
let summary = nested.take("toolu_1").unwrap().calls.unwrap();
assert!(!summary.complete);
let statuses: Vec<_> = summary.calls.iter().map(|call| call.status).collect();
assert_eq!(
statuses,
[
NestedToolCallStatus::Ok,
NestedToolCallStatus::Error,
NestedToolCallStatus::Unfinished
]
);
assert_eq!(summary.calls[1].error.as_ref().unwrap().len(), 500);
assert_eq!(summary.calls[0].arguments, Some(first.arguments));
drop(started);
}
}