Skip to main content

agent_engine/tools/
mod.rs

1//! Tool system — trait, registry, and built-in tool implementations.
2//!
3//! All tools implement the `Tool` trait and are registered in `ToolRegistry`.
4//! Subagents get `ToolRegistry::without_subagent()` to prevent recursion.
5use crate::Result;
6use serde_json::Value;
7use std::path::PathBuf;
8use std::sync::{Arc, Mutex};
9
10// ── Module declarations ──────────────────────────────────────────────────────────
11
12mod bash;
13mod edit;
14mod extension;
15mod find;
16mod grep;
17mod ls;
18pub mod memory;
19mod memory_context;
20mod powershell;
21mod read;
22mod secret_prompt;
23mod subagent;
24mod write;
25
26pub mod activation;
27mod agent;
28pub mod catalog;
29pub mod discovery;
30pub mod ledger;
31pub mod output;
32mod registry;
33pub mod respond;
34pub mod send_channel;
35pub mod shell;
36pub(crate) mod util;
37pub mod watcher_exit;
38
39// ── Re-exports ──────────────────────────────────────────────────────────────────
40
41pub use crate::runtime::subagent::{
42    SubagentDisplayRow, SubagentHandle, SubagentRegistry, SubagentResult, SubagentState,
43    SubagentStatus,
44};
45pub use agent::resolve_agent_prompt;
46pub use bash::{bash_intermediary_snapshot, BashIntermediarySnapshot, BashTool};
47pub use discovery::{ActivateToolsTool, SearchToolsTool};
48pub use edit::EditTool;
49pub use extension::ExtensionTool;
50pub use find::FindTool;
51pub use grep::GrepTool;
52pub use ls::LsTool;
53pub use memory_context::MemoryContextTool;
54pub use powershell::PowerShellTool;
55pub use read::ReadTool;
56pub use registry::{DroppedSessionMember, SessionSchemaProjection, ToolRegistry};
57pub use respond::RespondTool;
58pub use secret_prompt::SecretPromptQueue;
59pub use secret_prompt::{SecretPromptHandle, SecretPromptRequest};
60pub use send_channel::SendChannelTool;
61pub use shell::{ShellEndTool, ShellSendTool, ShellStartTool};
62pub use subagent::{
63    SubagentCollectTool, SubagentModelAuthorizeTool, SubagentModelsTool, SubagentResumeTool,
64    SubagentStartTool, SubagentStatusTool, SubagentSteerTool, SubagentTool,
65};
66pub use watcher_exit::WatcherExitTool;
67pub use write::WriteTool;
68
69// Re-export util items used by sibling tool modules via `super::`
70pub(crate) use util::{expand_path, strip_ansi, NEXT_SUBAGENT_ID};
71
72// Facade: expose finalize internals for integration tests without making the
73// subagent module pub. Tests import `agent_engine::tools::{build_completion_event, finalize_subagent}`.
74#[doc(hidden)]
75pub use subagent::finalize::{build_completion_event, finalize_subagent};
76
77// ── Tool Trait ──────────────────────────────────────────────────────────────────
78
79/// Streaming channels — carry partial tool output and stream events.
80///
81/// `tx_delta` is the Task 26 bounded delta lane (spec §8.4): a non-blocking
82/// coalesce-then-drop producer handle whose consumer enforces the UI-preview
83/// byte budget. It replaced the previous unbounded `mpsc` sender so a fast
84/// producer with a slow consumer can no longer grow memory without bound.
85pub struct ToolChannels {
86    pub tx_delta: Option<crate::tools::output::DeltaSender>,
87    pub tx_events: Option<tokio::sync::mpsc::UnboundedSender<crate::StreamEvent>>,
88}
89
90/// Runtime capability handles — shared services a tool may require.
91pub struct ToolCapabilities {
92    pub watcher_exit_path: Option<PathBuf>,
93    pub tool_register_tx: Option<tokio::sync::mpsc::UnboundedSender<Vec<Arc<dyn Tool>>>>,
94    pub session_manager: Option<std::sync::Arc<crate::tools::shell::SessionManager>>,
95    pub subagent_registry: Option<Arc<Mutex<SubagentRegistry>>>,
96    pub event_queue: Option<Arc<crate::events::EventQueue>>,
97    /// Current worker handle when this context belongs to a delegated
98    /// runtime. `None` denotes the foreground root.
99    pub delegation_parent: Option<String>,
100    pub secret_prompt: Option<SecretPromptHandle>,
101    /// Runtime-enforced delegation/lifecycle policy. When present, every spawn
102    /// path must authorize before creating channels, threads, or provider runtimes.
103    pub orchestration: Option<Arc<crate::orchestration::OrchestrationRuntime>>,
104    /// Discovery/activation capability context (Task 17): passive catalog
105    /// snapshot + retained session-set handle + host-supplied activation
106    /// authority. `None` (default for manual fixtures and non-stream
107    /// contexts) means the discovery/activation builtins fail typed and no
108    /// model-initiated activation is possible.
109    pub tool_activation: Option<crate::tools::discovery::ActivationCapability>,
110    /// Session-scoped exact MCP lease capability (Task 19): exact session
111    /// identity + shared runtime manager. `None` (default for non-stream
112    /// contexts) means deferred MCP tools fail typed and start nothing.
113    pub mcp_leases: Option<crate::mcp::McpLeaseCapability>,
114    /// Session-scoped exact EXTENSION lease capability (Task 20): exact
115    /// session identity + shared extension runtime manager. `None`
116    /// (default for non-stream contexts) means deferred extension tools
117    /// fail typed and start nothing.
118    pub extension_leases: Option<crate::extensions::lease::ExtensionLeaseCapability>,
119    /// Session-scoped continuous-memory context capability (task A4, spec
120    /// §7.2): shared per-session memory state + host-owned grant scope.
121    /// `None` (default for non-stream contexts) means the `memory_context`
122    /// builtin can commit nothing — lease-granting actions fail typed,
123    /// while `status`/`disable` still answer deterministically `Off`
124    /// (memory off requires no infrastructure).
125    pub memory_context: Option<crate::runtime::memory_context::MemoryContextCapability>,
126}
127
128/// Configuration limits and timeouts.
129pub struct ToolLimits {
130    /// Context-budget ceiling — final tool output entering history is truncated
131    /// to this. Applied AFTER the `after_tool_call` hook so compression
132    /// extensions see the FULL buffered output and can decide what to keep.
133    pub max_tool_output: usize,
134    /// Memory/pipe safety bound — the maximum bytes a tool may buffer
135    /// internally before truncating with a marker. Sized well above
136    /// `max_tool_output` so transform extensions are never starved.
137    pub max_tool_buffer: usize,
138    pub bash_timeout: u64,
139    pub bash_max_timeout: u64,
140    pub subagent_timeout: u64,
141}
142
143/// Context passed to tool execution — composition of channels, capabilities, and limits.
144pub struct ToolContext {
145    pub channels: ToolChannels,
146    pub capabilities: ToolCapabilities,
147    pub limits: ToolLimits,
148}
149
150/// Explicit runtime-origin identity of a registered tool implementation.
151///
152/// This is the metadata boundary the capability catalog trusts for
153/// provenance. The default is conservative: a tool that declares nothing is
154/// [`ToolOrigin::Unknown`] (or [`ToolOrigin::Extension`] when it already
155/// declares an owning extension via [`Tool::extension_id`]) — never builtin.
156#[derive(Clone, Debug, PartialEq, Eq)]
157pub enum ToolOrigin {
158    /// Compiled into this binary.
159    Builtin,
160    /// Registered by a locally installed extension (`<extension_id>:<tool>`
161    /// runtime names).
162    Extension { extension_id: String },
163    /// Bridged from an MCP server; identifies the server and the tool name
164    /// as the server knows it (not the prefixed runtime name).
165    Mcp {
166        server_id: String,
167        server_tool_name: String,
168    },
169    /// Declared by a plugin definition.
170    Plugin {
171        plugin_id: String,
172        tool_name: String,
173    },
174    /// No declared origin. Cataloged conservatively, never as builtin.
175    Unknown,
176}
177
178#[derive(Debug, Clone, PartialEq, Eq)]
179pub enum ConcurrencyKey {
180    Key(String),
181    Serialize,
182}
183
184/// Structured tool output. `Text` is the legacy contract (what `execute`
185/// returns). `Blocks` carries Anthropic-shaped content blocks destined for
186/// `tool_result.content`, plus a plain-text `summary` used everywhere a
187/// `String` is expected today: UI preview, `after_tool_call` hooks, trace
188/// ledger byte counts, headless logs, and providers that cannot carry the
189/// blocks. INVARIANT: `blocks[0]` MUST be a `{"type":"text"}` block whose
190/// `text` == `summary` (compaction reads `blocks[0].text`; non-Anthropic
191/// translators join only text sub-blocks).
192#[derive(Debug, Clone)]
193pub enum ToolOutput {
194    Text(String),
195    Blocks { blocks: Vec<Value>, summary: String },
196}
197
198impl ToolOutput {
199    pub fn summary(&self) -> &str {
200        match self {
201            Self::Text(s) => s,
202            Self::Blocks { summary, .. } => summary,
203        }
204    }
205
206    /// `(summary, Some(blocks))` for rich output, `(text, None)` for plain.
207    /// This is the one choke point every rich result passes through, so the
208    /// `blocks[0]` text-first invariant is ENFORCED here in every build: a
209    /// `Blocks` value whose first block is not `{"type":"text"}` gets a text
210    /// block built from `summary` prepended. Debug builds additionally
211    /// assert that an existing leading text block matches `summary`.
212    pub fn into_parts(self) -> (String, Option<Vec<Value>>) {
213        match self {
214            Self::Text(s) => (s, None),
215            Self::Blocks {
216                mut blocks,
217                summary,
218            } => {
219                let text_first = blocks.first().is_some_and(|b| b["type"] == "text");
220                if !text_first {
221                    blocks.insert(0, serde_json::json!({"type": "text", "text": summary}));
222                }
223                debug_assert!(
224                    blocks[0]["text"] == summary,
225                    "ToolOutput::Blocks invariant: blocks[0] text must equal summary"
226                );
227                (summary, Some(blocks))
228            }
229        }
230    }
231
232    pub fn into_summary(self) -> String {
233        self.into_parts().0
234    }
235}
236
237/// The core trait for all tools. Implement this to add a new tool.
238#[async_trait::async_trait]
239pub trait Tool: Send + Sync {
240    /// Tool name as it appears in the API (e.g. "bash", "read").
241    fn name(&self) -> &str;
242
243    /// Human-readable description sent to the model.
244    fn description(&self) -> &str;
245
246    /// JSON Schema for the tool's parameters.
247    fn parameters(&self) -> Value;
248
249    /// Execute the tool with the given parameters.
250    async fn execute(&self, params: Value, ctx: ToolContext) -> Result<String>;
251
252    /// Structured variant of [`Tool::execute`]. DEFAULTED — existing tools
253    /// keep returning `String` via `execute` and never see this. Tools that
254    /// need to place non-text content blocks (images) into the model history
255    /// override this and make `execute` delegate to it.
256    ///
257    /// RECURSION TRAP: the default delegates to `execute`. If you make
258    /// `execute` delegate here (as `ReadTool` does) you MUST override this
259    /// method too, or the pair recurses until the stack blows.
260    async fn execute_rich(&self, params: Value, ctx: ToolContext) -> Result<ToolOutput> {
261        self.execute(params, ctx).await.map(ToolOutput::Text)
262    }
263
264    /// Owning extension id for tools registered by an extension. Built-in tools return `None`.
265    fn extension_id(&self) -> Option<&str> {
266        None
267    }
268
269    /// Runtime-origin identity used for catalog provenance. Conservative by
270    /// default: tools that declare nothing are `Unknown` (fail-closed source
271    /// trust), and tools that declare an owning extension classify as
272    /// extension-provided. Built-in implementations override this explicitly.
273    fn origin(&self) -> ToolOrigin {
274        match self.extension_id() {
275            Some(extension_id) => ToolOrigin::Extension {
276                extension_id: extension_id.to_string(),
277            },
278            None => ToolOrigin::Unknown,
279        }
280    }
281
282    /// Conservative effect class (Task 24, spec §8.2). The default keeps
283    /// unknown/dynamic tools `NonIdempotent` — serialized execution;
284    /// implementations opt IN to weaker classes explicitly.
285    fn effect(&self) -> crate::tools::catalog::ToolEffect {
286        crate::tools::catalog::ToolEffect::NonIdempotent
287    }
288
289    /// Key resolution derived from validated input. `Serialize` explicitly
290    /// fails closed into the global mutation lane; `None` is the conservative
291    /// default for tools with no key support.
292    fn concurrency_key(&self, _validated_input: &Value) -> Option<ConcurrencyKey> {
293        None
294    }
295}
296
297#[cfg(test)]
298mod test_helpers;
299
300#[cfg(test)]
301mod tool_output_tests {
302    use super::ToolOutput;
303    use serde_json::json;
304
305    #[test]
306    fn into_parts_prepends_summary_text_when_blocks_not_text_first() {
307        let img = json!({"type": "image", "source": {"type": "base64", "media_type": "image/png", "data": "AA=="}});
308        let out = ToolOutput::Blocks {
309            blocks: vec![img.clone()],
310            summary: "[image 1x1]".into(),
311        };
312        let (summary, blocks) = out.into_parts();
313        let blocks = blocks.unwrap();
314        assert_eq!(summary, "[image 1x1]");
315        assert_eq!(blocks.len(), 2);
316        assert_eq!(blocks[0], json!({"type": "text", "text": "[image 1x1]"}));
317        assert_eq!(blocks[1], img);
318    }
319
320    #[test]
321    fn into_parts_prepends_summary_text_when_blocks_empty() {
322        let out = ToolOutput::Blocks {
323            blocks: vec![],
324            summary: "s".into(),
325        };
326        let (_, blocks) = out.into_parts();
327        assert_eq!(blocks.unwrap(), vec![json!({"type": "text", "text": "s"})]);
328    }
329
330    #[test]
331    fn into_parts_leaves_well_formed_blocks_alone() {
332        let blocks = vec![
333            json!({"type": "text", "text": "ok"}),
334            json!({"type": "image"}),
335        ];
336        let out = ToolOutput::Blocks {
337            blocks: blocks.clone(),
338            summary: "ok".into(),
339        };
340        let (_, got) = out.into_parts();
341        assert_eq!(got.unwrap(), blocks);
342    }
343}