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