Skip to main content

harness_core/
tool.rs

1use crate::{World, error::ToolError};
2use async_trait::async_trait;
3use serde::{Deserialize, Serialize};
4use std::sync::Arc;
5
6/// Risk class for a tool — drives sandbox / permission decisions.
7///
8/// Names are MCP-aligned (readOnlyHint, destructiveHint, idempotentHint).
9#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash, Serialize, Deserialize)]
10#[serde(rename_all = "kebab-case")]
11#[non_exhaustive]
12pub enum ToolRisk {
13    /// No side effects.
14    ReadOnly,
15    /// Side effects exist but repeating is safe (e.g. `cargo fmt`).
16    Idempotent,
17    /// Modifies state in a way that may not be safely repeated.
18    Destructive,
19    /// Talks to network. Independent of read/write risk.
20    Network,
21}
22
23#[derive(Debug, Clone, Serialize, Deserialize)]
24pub struct ToolSchema {
25    pub name: String,
26    pub description: String,
27    /// JSON Schema for the tool input.
28    pub input: serde_json::Value,
29}
30
31#[derive(Debug, Clone, Serialize, Deserialize)]
32pub struct ToolResult {
33    pub ok: bool,
34    pub content: serde_json::Value,
35    /// Output sized for `tracing` / replay logs but not necessarily for the model.
36    pub trace: Option<String>,
37}
38
39#[async_trait]
40pub trait Tool: Send + Sync + 'static {
41    fn name(&self) -> &str;
42    fn schema(&self) -> &ToolSchema;
43    fn risk(&self) -> ToolRisk;
44    async fn invoke(
45        &self,
46        args: serde_json::Value,
47        world: &mut World,
48    ) -> Result<ToolResult, ToolError>;
49}
50
51/// `inventory` slot for compile-time tool registration via `#[tool]`.
52pub struct ToolEntry {
53    pub factory: fn() -> Arc<dyn Tool>,
54}
55
56inventory::collect!(ToolEntry);
57
58pub fn iter_macro_tools() -> impl Iterator<Item = Arc<dyn Tool>> {
59    inventory::iter::<ToolEntry>().map(|e| (e.factory)())
60}