Skip to main content

harness_loop/
registry.rs

1//! A tiny name-keyed tool registry used by `AgentLoop`.
2
3use harness_core::{Action, Tool, ToolError, ToolResult, ToolRisk, ToolSchema, World};
4use std::collections::HashMap;
5use std::sync::Arc;
6
7#[derive(Default)]
8pub struct ToolRegistry {
9    tools: HashMap<String, Arc<dyn Tool>>,
10}
11
12impl ToolRegistry {
13    pub fn new() -> Self {
14        Self::default()
15    }
16
17    pub fn insert(&mut self, t: Arc<dyn Tool>) {
18        self.tools.insert(t.name().to_string(), t);
19    }
20
21    /// Tool schemas in a **stable, name-sorted order**. Deterministic ordering
22    /// keeps the request's `tools` block byte-identical across turns, which is
23    /// what lets a provider's prefix cache (e.g. DeepSeek) hit — a `HashMap`'s
24    /// arbitrary iteration order would silently break it.
25    pub fn schemas(&self) -> Vec<ToolSchema> {
26        let mut v: Vec<ToolSchema> = self.tools.values().map(|t| t.schema().clone()).collect();
27        v.sort_by(|a, b| a.name.cmp(&b.name));
28        v
29    }
30
31    pub async fn dispatch(
32        &self,
33        action: &Action,
34        world: &mut World,
35    ) -> Result<ToolResult, ToolError> {
36        let tool = self
37            .tools
38            .get(&action.tool)
39            .ok_or_else(|| ToolError::NotFound {
40                name: action.tool.clone(),
41            })?
42            .clone();
43        tool.invoke(action.args.clone(), world).await
44    }
45
46    /// The risk class of a tool by name (used to decide parallel-safe dispatch).
47    pub fn risk(&self, name: &str) -> Option<ToolRisk> {
48        self.tools.get(name).map(|t| t.risk())
49    }
50
51    pub fn len(&self) -> usize {
52        self.tools.len()
53    }
54    pub fn is_empty(&self) -> bool {
55        self.tools.is_empty()
56    }
57}