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