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}