Skip to main content

tkach/
tool.rs

1use std::path::PathBuf;
2use std::sync::Arc;
3
4use async_trait::async_trait;
5use serde_json::Value;
6use tokio_util::sync::CancellationToken;
7
8use crate::error::ToolError;
9use crate::executor::ToolExecutor;
10
11/// Context passed to every tool execution.
12///
13/// Intentionally slim — holds only primitives the runtime actually owns
14/// and wants to share with tools:
15///
16/// - `working_dir`: file-system base for path resolution.
17/// - `cancel`: cooperative cancellation. Long-running tools should
18///   `tokio::select!` on `cancel.cancelled()` and return
19///   [`ToolError::Cancelled`] promptly.
20/// - `depth` / `max_depth`: current nesting level of the agent; used by
21///   `SubAgent` to prevent unbounded recursion.
22/// - `executor`: the parent agent's [`ToolExecutor`], letting tools that
23///   spawn nested work (e.g. `SubAgent`) inherit the full toolset
24///   automatically — no explicit layering required.
25pub struct ToolContext {
26    pub working_dir: PathBuf,
27    pub cancel: CancellationToken,
28    pub depth: usize,
29    pub max_depth: usize,
30    pub executor: Arc<ToolExecutor>,
31}
32
33impl ToolContext {
34    pub(crate) fn with_cancel(&self, cancel: CancellationToken) -> Self {
35        Self {
36            working_dir: self.working_dir.clone(),
37            cancel,
38            depth: self.depth,
39            max_depth: self.max_depth,
40            executor: Arc::clone(&self.executor),
41        }
42    }
43}
44
45/// Result of a tool execution.
46pub enum ToolOutput {
47    Text(String),
48    Error(String),
49}
50
51impl ToolOutput {
52    pub fn text(s: impl Into<String>) -> Self {
53        ToolOutput::Text(s.into())
54    }
55
56    pub fn error(s: impl Into<String>) -> Self {
57        ToolOutput::Error(s.into())
58    }
59
60    pub fn is_error(&self) -> bool {
61        matches!(self, ToolOutput::Error(_))
62    }
63
64    pub fn content(&self) -> &str {
65        match self {
66            ToolOutput::Text(s) | ToolOutput::Error(s) => s,
67        }
68    }
69}
70
71/// Side-effect class of a tool.
72///
73/// Used by the executor to safely parallelise consecutive read-only calls
74/// in a single batch while keeping mutating calls sequential. Mutating is
75/// the default because misclassifying a side-effectful tool as `ReadOnly`
76/// can lead to subtle ordering bugs (two "mutating" writes racing against
77/// each other); the reverse is merely a missed optimisation.
78#[derive(Debug, Clone, Copy, PartialEq, Eq)]
79pub enum ToolClass {
80    /// No observable side effects. Safe to run concurrently with other
81    /// `ReadOnly` tools. Examples: `Read`, `Glob`, `Grep`, `WebFetch`.
82    ReadOnly,
83    /// Changes state — file system, external services, processes, or
84    /// nested agents. Must run sequentially to preserve ordering.
85    /// Examples: `Write`, `Edit`, `Bash`, `SubAgent`.
86    Mutating,
87}
88
89/// Trait for tools that the agent can use.
90///
91/// Implement this trait to create custom tools.
92///
93/// # Example
94///
95/// ```ignore
96/// use tkach::{Tool, ToolContext, ToolOutput, ToolError};
97/// use serde_json::{json, Value};
98///
99/// struct MyTool;
100///
101/// #[async_trait::async_trait]
102/// impl Tool for MyTool {
103///     fn name(&self) -> &str { "my_tool" }
104///     fn description(&self) -> &str { "Does something useful" }
105///     fn input_schema(&self) -> Value {
106///         json!({
107///             "type": "object",
108///             "properties": {
109///                 "query": { "type": "string" }
110///             },
111///             "required": ["query"]
112///         })
113///     }
114///     async fn execute(&self, input: Value, ctx: &ToolContext) -> Result<ToolOutput, ToolError> {
115///         let query = input["query"].as_str().unwrap_or_default();
116///         Ok(ToolOutput::text(format!("Result for: {query}")))
117///     }
118/// }
119/// ```
120#[async_trait]
121pub trait Tool: Send + Sync {
122    /// Unique name of the tool (used by the LLM to invoke it).
123    fn name(&self) -> &str;
124
125    /// Human-readable description of what the tool does.
126    fn description(&self) -> &str;
127
128    /// JSON Schema describing the tool's input parameters.
129    fn input_schema(&self) -> Value;
130
131    /// Side-effect class. Defaults to `Mutating` — the safe choice for
132    /// tools that are not explicitly marked read-only. Override to return
133    /// [`ToolClass::ReadOnly`] only when you are certain the tool has no
134    /// observable side effects.
135    fn class(&self) -> ToolClass {
136        ToolClass::Mutating
137    }
138
139    /// Whether this tool itself drives nested executor work — i.e.
140    /// the tool's `execute` body holds the executor task open while
141    /// running another `Agent::run` underneath. The canonical example
142    /// is `SubAgent`.
143    ///
144    /// The executor uses this to admit recursive tools through the
145    /// concurrent-mutator pool *regardless of explicit promotion*,
146    /// because non-recursive admission classes (the width-1
147    /// `serial_mut` pool, in particular) are shared across the
148    /// agent tree and would deadlock when a parent's permit is
149    /// pinned during the child's nested `execute`. Routing recursive
150    /// tools through `concurrent_mut` — which the executor forks
151    /// per nesting level — keeps nested fan-out free of pool
152    /// contention while still bounding it by the
153    /// `max_concurrent_mutations` cap of the *current* level.
154    ///
155    /// Defaults to `false`. Override to `true` for tools whose
156    /// `execute` body drives nested executor work.
157    ///
158    /// When you override this, the recursive call site **must** run
159    /// against an executor obtained from
160    /// `ctx.executor.fork_for_subagent()` — not directly against
161    /// `ctx.executor`. The fork is what gives the nested level its
162    /// own `concurrent_mut` and per-tool semaphores; without it, a
163    /// parent saturating those pools would deadlock the moment any
164    /// child tried to acquire a permit from the same shared
165    /// semaphore. The canonical pattern is to construct an
166    /// `Agent::builder().executor(ctx.executor.fork_for_subagent())`
167    /// and `agent.run(...)` from inside `execute`; `SubAgent`
168    /// implements exactly this shape.
169    fn is_recursive(&self) -> bool {
170        false
171    }
172
173    /// Execute the tool with the given input.
174    async fn execute(&self, input: Value, ctx: &ToolContext) -> Result<ToolOutput, ToolError>;
175}