use serde_json::Value;
#[derive(Debug, Clone)]
pub struct PendingCall {
pub id: usize,
pub tool: String,
pub args: Value,
pub priority: i32,
}
#[derive(Debug, Default)]
pub struct CallBatcher {
queue: Vec<PendingCall>,
next_id: usize,
}
impl CallBatcher {
pub fn new() -> Self { Self::default() }
pub fn enqueue(&mut self, tool: impl Into<String>, args: Value) -> usize {
self.enqueue_with_priority(tool, args, 0)
}
pub fn enqueue_with_priority(&mut self, tool: impl Into<String>, args: Value, priority: i32) -> usize {
let id = self.next_id;
self.next_id += 1;
self.queue.push(PendingCall { id, tool: tool.into(), args, priority });
id
}
pub fn flush(&mut self) -> Vec<PendingCall> {
std::mem::take(&mut self.queue)
}
pub fn flush_by_priority(&mut self) -> Vec<PendingCall> {
let mut calls = std::mem::take(&mut self.queue);
calls.sort_by(|a, b| b.priority.cmp(&a.priority));
calls
}
pub fn peek(&self) -> &[PendingCall] { &self.queue }
pub fn cancel(&mut self, id: usize) -> bool {
let before = self.queue.len();
self.queue.retain(|c| c.id != id);
self.queue.len() < before
}
pub fn calls_for(&self, tool: &str) -> Vec<&PendingCall> {
self.queue.iter().filter(|c| c.tool == tool).collect()
}
pub fn len(&self) -> usize { self.queue.len() }
pub fn is_empty(&self) -> bool { self.queue.is_empty() }
pub fn clear(&mut self) { self.queue.clear(); }
pub fn to_tool_use_blocks(&self) -> Vec<Value> {
self.queue.iter().map(|c| {
serde_json::json!({
"type": "tool_use",
"id": format!("call_{}", c.id),
"name": c.tool,
"input": c.args,
})
}).collect()
}
}
#[cfg(test)]
mod tests {
use super::*;
use serde_json::json;
#[test]
fn enqueue_and_flush() {
let mut b = CallBatcher::new();
b.enqueue("search", json!({}));
b.enqueue("fetch", json!({}));
let calls = b.flush();
assert_eq!(calls.len(), 2);
assert!(b.is_empty());
}
#[test]
fn flush_clears_queue() {
let mut b = CallBatcher::new();
b.enqueue("t", json!(null));
b.flush();
assert!(b.is_empty());
}
#[test]
fn flush_by_priority() {
let mut b = CallBatcher::new();
b.enqueue_with_priority("low", json!({}), 1);
b.enqueue_with_priority("high", json!({}), 10);
let calls = b.flush_by_priority();
assert_eq!(calls[0].tool, "high");
}
#[test]
fn cancel_removes_call() {
let mut b = CallBatcher::new();
let id = b.enqueue("x", json!({}));
assert!(b.cancel(id));
assert!(b.is_empty());
}
#[test]
fn cancel_nonexistent_returns_false() {
let mut b = CallBatcher::new();
assert!(!b.cancel(999));
}
#[test]
fn calls_for_filter() {
let mut b = CallBatcher::new();
b.enqueue("search", json!({}));
b.enqueue("fetch", json!({}));
b.enqueue("search", json!({}));
assert_eq!(b.calls_for("search").len(), 2);
}
#[test]
fn ids_are_unique() {
let mut b = CallBatcher::new();
let a = b.enqueue("t", json!({}));
let c = b.enqueue("t", json!({}));
assert_ne!(a, c);
}
#[test]
fn peek_does_not_remove() {
let mut b = CallBatcher::new();
b.enqueue("t", json!({}));
let _ = b.peek();
assert_eq!(b.len(), 1);
}
#[test]
fn to_tool_use_blocks() {
let mut b = CallBatcher::new();
b.enqueue("search", json!({"q": "rust"}));
let blocks = b.to_tool_use_blocks();
assert_eq!(blocks.len(), 1);
assert_eq!(blocks[0]["type"], "tool_use");
assert_eq!(blocks[0]["name"], "search");
}
#[test]
fn clear_empties_queue() {
let mut b = CallBatcher::new();
b.enqueue("t", json!({}));
b.clear();
assert!(b.is_empty());
}
#[test]
fn flush_preserves_order() {
let mut b = CallBatcher::new();
b.enqueue("a", json!({}));
b.enqueue("b", json!({}));
b.enqueue("c", json!({}));
let calls = b.flush();
let tools: Vec<&str> = calls.iter().map(|c| c.tool.as_str()).collect();
assert_eq!(tools, vec!["a", "b", "c"]);
}
}