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