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