agent_sdk_tools/tools.rs
1//! Tool definition and registry.
2//!
3//! Tools allow the LLM to perform actions in the real world. This module provides:
4//!
5//! - [`Tool`] trait - Define custom tools the LLM can call
6//! - [`ToolName`] trait - Marker trait for strongly-typed tool names
7//! - [`PrimitiveToolName`] - Tool names for SDK's built-in tools
8//! - [`DynamicToolName`] - Tool names created at runtime (MCP bridges)
9//! - [`ToolRegistry`] - Collection of available tools
10//! - [`ToolContext`] - Context passed to tool execution
11//! - [`ListenExecuteTool`] - Tools that listen for updates, then execute later
12//!
13//! # Implementing a Tool
14//!
15//! ```ignore
16//! use agent_sdk::{Tool, ToolContext, ToolResult, ToolTier, PrimitiveToolName};
17//!
18//! struct MyTool;
19//!
20//! // No #[async_trait] needed - Rust 1.75+ supports native async traits
21//! impl Tool<MyContext> for MyTool {
22//! type Name = PrimitiveToolName;
23//!
24//! fn name(&self) -> PrimitiveToolName { PrimitiveToolName::Read }
25//! fn display_name(&self) -> &'static str { "My Tool" }
26//! fn description(&self) -> &'static str { "Does something useful" }
27//! fn input_schema(&self) -> Value { json!({ "type": "object" }) }
28//! fn tier(&self) -> ToolTier { ToolTier::Observe }
29//!
30//! async fn execute(&self, ctx: &ToolContext<MyContext>, input: Value) -> Result<ToolResult> {
31//! Ok(ToolResult::success("Done!"))
32//! }
33//! }
34//! ```
35
36use crate::authority::{EventAuthority, LocalEventAuthority};
37use crate::seed::{HostDependencies, ToolContextSeed};
38use crate::stores::EventStore;
39use agent_sdk_foundation::events::AgentEvent;
40use agent_sdk_foundation::llm;
41use agent_sdk_foundation::types::{ToolOutcome, ToolResult, ToolTier};
42use anyhow::Result;
43use async_trait::async_trait;
44use futures::Stream;
45use serde::{Deserialize, Serialize, de::DeserializeOwned};
46use serde_json::Value;
47use std::collections::HashMap;
48use std::future::Future;
49use std::marker::PhantomData;
50use std::pin::Pin;
51use std::sync::Arc;
52use time::OffsetDateTime;
53use tokio_util::sync::CancellationToken;
54
55// ============================================================================
56// Tool Name Types
57// ============================================================================
58
59/// Marker trait for tool names.
60///
61/// Tool names must be serializable (for storage/logging) and deserializable
62/// (for parsing from LLM responses). The string representation is derived
63/// from serde serialization.
64///
65/// # Example
66///
67/// ```ignore
68/// #[derive(Serialize, Deserialize)]
69/// #[serde(rename_all = "snake_case")]
70/// pub enum MyToolName {
71/// Read,
72/// Write,
73/// }
74///
75/// impl ToolName for MyToolName {}
76/// ```
77pub trait ToolName: Send + Sync + Serialize + DeserializeOwned + 'static {}
78
79/// Helper to get string representation of a tool name via serde.
80///
81/// Returns `"<unknown_tool>"` if serialization fails (should never happen
82/// with properly implemented `ToolName` types that use `#[derive(Serialize)]`).
83#[must_use]
84pub fn tool_name_to_string<N: ToolName>(name: &N) -> String {
85 serde_json::to_string(name)
86 .unwrap_or_else(|_| "\"<unknown_tool>\"".to_string())
87 .trim_matches('"')
88 .to_string()
89}
90
91/// Parse a tool name from string via serde.
92///
93/// # Errors
94/// Returns error if the string doesn't match a valid tool name.
95pub fn tool_name_from_str<N: ToolName>(s: &str) -> Result<N, serde_json::Error> {
96 serde_json::from_str(&format!("\"{s}\""))
97}
98
99/// Tool names for SDK's built-in primitive tools.
100#[derive(Clone, Copy, Debug, PartialEq, Eq, Hash, Serialize, Deserialize)]
101#[serde(rename_all = "snake_case")]
102pub enum PrimitiveToolName {
103 Read,
104 Write,
105 Edit,
106 MultiEdit,
107 Bash,
108 Glob,
109 Grep,
110 NotebookRead,
111 NotebookEdit,
112 TodoRead,
113 TodoWrite,
114 AskUser,
115 LinkFetch,
116 WebSearch,
117}
118
119impl ToolName for PrimitiveToolName {}
120
121/// Dynamic tool name for runtime-created tools (MCP bridges, subagents).
122#[derive(Clone, Debug, PartialEq, Eq, Hash, Serialize, Deserialize)]
123#[serde(transparent)]
124pub struct DynamicToolName(String);
125
126impl DynamicToolName {
127 #[must_use]
128 pub fn new(name: impl Into<String>) -> Self {
129 Self(name.into())
130 }
131
132 #[must_use]
133 pub fn as_str(&self) -> &str {
134 &self.0
135 }
136}
137
138impl ToolName for DynamicToolName {}
139
140// ============================================================================
141// Progress Stage Types (for AsyncTool)
142// ============================================================================
143
144/// Marker trait for tool progress stages (type-safe, like [`ToolName`]).
145///
146/// Progress stages are used by async tools to indicate the current phase
147/// of a long-running operation. They must be serializable for event streaming.
148///
149/// # Example
150///
151/// ```ignore
152/// #[derive(Clone, Debug, Serialize, Deserialize)]
153/// #[serde(rename_all = "snake_case")]
154/// pub enum PixTransferStage {
155/// Initiated,
156/// Processing,
157/// SentToBank,
158/// }
159///
160/// impl ProgressStage for PixTransferStage {}
161/// ```
162pub trait ProgressStage: Clone + Send + Sync + Serialize + DeserializeOwned + 'static {}
163
164/// Helper to get string representation of a progress stage via serde.
165///
166/// # Panics
167///
168/// Panics if the stage cannot be serialized to a string. This should
169/// never happen with properly implemented `ProgressStage` types.
170#[must_use]
171pub fn stage_to_string<S: ProgressStage>(stage: &S) -> String {
172 serde_json::to_string(stage)
173 .expect("ProgressStage must serialize to string")
174 .trim_matches('"')
175 .to_string()
176}
177
178/// Status update from an async tool operation.
179#[derive(Clone, Debug, Serialize)]
180pub enum ToolStatus<S: ProgressStage> {
181 /// Operation is making progress
182 Progress {
183 stage: S,
184 message: String,
185 data: Option<serde_json::Value>,
186 },
187
188 /// Operation completed successfully
189 Completed(ToolResult),
190
191 /// Operation failed
192 Failed(ToolResult),
193}
194
195/// Type-erased status for the agent loop.
196#[derive(Clone, Debug, Serialize, Deserialize)]
197pub enum ErasedToolStatus {
198 /// Operation is making progress
199 Progress {
200 stage: String,
201 message: String,
202 data: Option<serde_json::Value>,
203 },
204 /// Operation completed successfully
205 Completed(ToolResult),
206 /// Operation failed
207 Failed(ToolResult),
208}
209
210/// Update emitted from a `listen()` stream.
211///
212/// This models workflows where a runtime prepares an operation over time, and
213/// execution happens later using an operation identifier and revision.
214#[derive(Clone, Debug, Serialize, Deserialize)]
215pub enum ListenToolUpdate {
216 /// Preparation is still running and should keep listening.
217 Listening {
218 /// Opaque operation identifier used for later execute/cancel calls.
219 operation_id: String,
220 /// Monotonic revision number for optimistic concurrency.
221 revision: u64,
222 /// Human-readable status message.
223 message: String,
224 /// Optional current snapshot for UI rendering.
225 snapshot: Option<serde_json::Value>,
226 /// Optional expiration timestamp (RFC3339).
227 #[serde(with = "time::serde::rfc3339::option")]
228 expires_at: Option<OffsetDateTime>,
229 },
230
231 /// Preparation is complete and execution can be confirmed.
232 Ready {
233 /// Opaque operation identifier used for later execute/cancel calls.
234 operation_id: String,
235 /// Monotonic revision number for optimistic concurrency.
236 revision: u64,
237 /// Human-readable status message.
238 message: String,
239 /// Snapshot shown in confirmation UI.
240 snapshot: serde_json::Value,
241 /// Optional expiration timestamp (RFC3339).
242 #[serde(with = "time::serde::rfc3339::option")]
243 expires_at: Option<OffsetDateTime>,
244 },
245
246 /// Operation is no longer valid.
247 Invalidated {
248 /// Opaque operation identifier.
249 operation_id: String,
250 /// Human-readable reason.
251 message: String,
252 /// Whether caller may recover by starting a new listen operation.
253 recoverable: bool,
254 },
255}
256
257/// Reason for stopping a listen session.
258#[derive(Clone, Copy, Debug, Serialize, Deserialize)]
259pub enum ListenStopReason {
260 /// User explicitly rejected confirmation.
261 UserRejected,
262 /// Agent policy/hook blocked execution before confirmation.
263 Blocked,
264 /// Consumer disconnected while listen stream was active.
265 StreamDisconnected,
266 /// Listen stream ended unexpectedly before terminal state.
267 StreamEnded,
268}
269
270impl<S: ProgressStage> From<ToolStatus<S>> for ErasedToolStatus {
271 fn from(status: ToolStatus<S>) -> Self {
272 match status {
273 ToolStatus::Progress {
274 stage,
275 message,
276 data,
277 } => Self::Progress {
278 stage: stage_to_string(&stage),
279 message,
280 data,
281 },
282 ToolStatus::Completed(r) => Self::Completed(r),
283 ToolStatus::Failed(r) => Self::Failed(r),
284 }
285 }
286}
287
288/// Context passed to tool execution
289#[derive(Clone)]
290pub struct ToolContext<Ctx> {
291 /// Application-specific context (e.g., `user_id`, db connection)
292 pub app: Ctx,
293 /// Tool-specific metadata
294 pub metadata: HashMap<String, Value>,
295 /// Optional event store for tools to emit turn-scoped events.
296 event_store: Option<Arc<dyn EventStore>>,
297 /// Thread associated with the bound event store.
298 event_thread_id: Option<agent_sdk_foundation::types::ThreadId>,
299 /// Turn associated with the bound event store.
300 event_turn: Option<usize>,
301 /// Optional event authority for wrapping events in envelopes
302 event_authority: Option<Arc<dyn EventAuthority>>,
303 /// Optional cancellation token for propagating cancellation to subtasks
304 cancel_token: Option<CancellationToken>,
305 /// Optional semaphore for limiting concurrent subagent threads.
306 subagent_semaphore: Option<Arc<tokio::sync::Semaphore>>,
307 /// Optional per-tool execution timeout enforced at the SDK boundary.
308 ///
309 /// When set, the agent loop races each tool's `execute()` future
310 /// against this duration. A tool that does not finish within the
311 /// budget is stopped at the boundary and reported with a synthetic
312 /// timeout [`ToolResult`] so the `tool_use` / `tool_result` pair stays
313 /// balanced. Tools that hold OS resources (subprocesses, sockets) must
314 /// observe the [cooperative-cancel contract](Tool#cooperative-cancellation)
315 /// so the timeout actually reclaims them.
316 tool_timeout: Option<std::time::Duration>,
317}
318
319impl<Ctx> ToolContext<Ctx> {
320 #[must_use]
321 pub fn new(app: Ctx) -> Self {
322 Self {
323 app,
324 metadata: HashMap::new(),
325 event_store: None,
326 event_thread_id: None,
327 event_turn: None,
328 event_authority: None,
329 cancel_token: None,
330 subagent_semaphore: None,
331 tool_timeout: None,
332 }
333 }
334
335 /// Reconstruct a `ToolContext` from a durable seed and host-provided
336 /// runtime dependencies.
337 ///
338 /// This is the authoritative reconstruction path. Workers should use
339 /// this (or a host's [`crate::seed::ExecutionContextFactory`]) instead
340 /// of chaining builder methods, so that the context shape is
341 /// deterministic and auditable.
342 ///
343 /// The event authority is constructed internally from
344 /// [`ToolContextSeed::sequence_offset`] to guarantee monotonic
345 /// sequencing — callers cannot accidentally supply a misaligned
346 /// authority.
347 #[must_use]
348 pub fn from_seed(seed: &ToolContextSeed, app: Ctx, deps: HostDependencies) -> Self {
349 let authority: Arc<dyn EventAuthority> =
350 Arc::new(LocalEventAuthority::with_offset(seed.sequence_offset));
351 Self {
352 app,
353 metadata: seed.metadata.clone(),
354 event_store: Some(deps.event_store),
355 event_thread_id: Some(seed.thread_id.clone()),
356 event_turn: Some(seed.turn),
357 event_authority: Some(authority),
358 cancel_token: Some(deps.cancel_token),
359 subagent_semaphore: deps.subagent_semaphore,
360 tool_timeout: None,
361 }
362 }
363
364 #[must_use]
365 pub fn with_metadata(mut self, key: impl Into<String>, value: Value) -> Self {
366 self.metadata.insert(key.into(), value);
367 self
368 }
369
370 /// Bind the tool context to the event store for a specific thread/turn.
371 #[must_use]
372 pub fn with_event_store(
373 mut self,
374 store: Arc<dyn EventStore>,
375 thread_id: agent_sdk_foundation::types::ThreadId,
376 turn: usize,
377 authority: Arc<dyn EventAuthority>,
378 ) -> Self {
379 self.event_store = Some(store);
380 self.event_thread_id = Some(thread_id);
381 self.event_turn = Some(turn);
382 self.event_authority = Some(authority);
383 self
384 }
385
386 /// Emit an event through the configured event store (if set).
387 ///
388 /// The event is wrapped in an [`agent_sdk_foundation::AgentEventEnvelope`] with a unique ID,
389 /// sequence number, and timestamp before publishing.
390 ///
391 /// # Errors
392 /// Returns an error if the configured event store cannot persist the event.
393 pub async fn emit_event(&self, event: AgentEvent) -> Result<()>
394 where
395 Ctx: Sync,
396 {
397 if let Some((store, authority, thread_id, turn)) = self
398 .event_store
399 .as_ref()
400 .zip(self.event_authority.as_ref())
401 .zip(self.event_thread_id.as_ref())
402 .zip(self.event_turn)
403 .map(|(((store, authority), thread_id), turn)| (store, authority, thread_id, turn))
404 {
405 let envelope = authority.wrap(event);
406 store.append(thread_id, turn, envelope).await?;
407 }
408 Ok(())
409 }
410
411 /// Get a clone of the event authority (if set).
412 ///
413 /// This is useful for tools that spawn subprocesses (like subagents)
414 /// and need to wrap events with the same sequencing authority as the
415 /// parent's turn log.
416 #[must_use]
417 pub fn event_authority(&self) -> Option<Arc<dyn EventAuthority>> {
418 self.event_authority.clone()
419 }
420
421 /// Set the cancellation token for propagating cancellation to subtasks.
422 #[must_use]
423 pub fn with_cancel_token(mut self, token: CancellationToken) -> Self {
424 self.cancel_token = Some(token);
425 self
426 }
427
428 /// Get the cancellation token (if set).
429 ///
430 /// Used by tools that spawn long-running subtasks (like subagents)
431 /// to propagate cancellation from the parent.
432 #[must_use]
433 pub fn cancel_token(&self) -> Option<CancellationToken> {
434 self.cancel_token.clone()
435 }
436
437 /// Set the per-tool execution timeout enforced at the SDK boundary.
438 ///
439 /// The agent loop populates this from `AgentConfig::tool_timeout_ms`;
440 /// callers can also set it directly when constructing a context.
441 #[must_use]
442 pub const fn with_tool_timeout(mut self, timeout: std::time::Duration) -> Self {
443 self.tool_timeout = Some(timeout);
444 self
445 }
446
447 /// Get the per-tool execution timeout (if set).
448 ///
449 /// Read by the agent loop's SDK-boundary execution race; tools do not
450 /// normally need to consult this themselves.
451 #[must_use]
452 pub const fn tool_timeout(&self) -> Option<std::time::Duration> {
453 self.tool_timeout
454 }
455
456 /// Set a shared semaphore for limiting concurrent subagent threads.
457 #[must_use]
458 pub fn with_subagent_semaphore(mut self, semaphore: Arc<tokio::sync::Semaphore>) -> Self {
459 self.subagent_semaphore = Some(semaphore);
460 self
461 }
462
463 /// Get the subagent thread-limiting semaphore (if set).
464 #[must_use]
465 pub fn subagent_semaphore(&self) -> Option<Arc<tokio::sync::Semaphore>> {
466 self.subagent_semaphore.clone()
467 }
468}
469
470// ============================================================================
471// Tool Trait
472// ============================================================================
473
474/// Definition of a tool that can be called by the agent.
475///
476/// Tools have a strongly-typed `Name` associated type that determines
477/// how the tool name is serialized for LLM communication.
478///
479/// # Native Async Support
480///
481/// This trait uses Rust's native async functions in traits (stabilized in Rust 1.75).
482/// You do NOT need the `async_trait` crate to implement this trait.
483///
484/// # Cooperative cancellation
485///
486/// The agent loop races every tool's `execute()` future against the run's
487/// [`ToolContext::cancel_token`] and, when configured, against
488/// [`ToolContext::tool_timeout`]. If either fires the SDK drops the
489/// in-flight `execute()` future and synthesises a balanced `tool_result`
490/// (`"Cancelled by user"` or a timeout message). Dropping a future runs
491/// its destructors but cannot, on its own, reclaim OS resources a tool
492/// has handed to the kernel.
493///
494/// **Subprocess contract:** a tool that spawns a child process MUST make
495/// the process die when its `execute()` future is dropped. The two
496/// supported ways to satisfy this are:
497///
498/// * Build the command with `tokio::process::Command::kill_on_drop(true)`,
499/// so the child is killed when the `Child` handle is dropped together
500/// with the cancelled future (this is what the SDK's MCP stdio transport
501/// does), or
502/// * Observe [`ToolContext::cancel_token`] directly and `kill()` the child
503/// when it fires.
504///
505/// A tool that holds a subprocess open without either of these will leak
506/// the process when cancelled or timed out — the synthesised `tool_result`
507/// keeps the conversation balanced, but the orphaned OS process is the
508/// tool author's bug, not the SDK's.
509pub trait Tool<Ctx>: Send + Sync {
510 /// The type of name for this tool.
511 type Name: ToolName;
512
513 /// Returns the tool's strongly-typed name.
514 fn name(&self) -> Self::Name;
515
516 /// Human-readable display name for UI (e.g., "Read File" vs "read").
517 ///
518 /// Defaults to empty string. Override for better UX.
519 fn display_name(&self) -> &'static str;
520
521 /// Human-readable description of what the tool does.
522 fn description(&self) -> &'static str;
523
524 /// JSON schema for the tool's input parameters.
525 fn input_schema(&self) -> Value;
526
527 /// Permission tier for this tool.
528 fn tier(&self) -> ToolTier {
529 ToolTier::Observe
530 }
531
532 /// Execute the tool with the given input.
533 ///
534 /// # Errors
535 /// Returns an error if tool execution fails.
536 fn execute(
537 &self,
538 ctx: &ToolContext<Ctx>,
539 input: Value,
540 ) -> impl Future<Output = Result<ToolResult>> + Send;
541}
542
543// ============================================================================
544// TypedTool Trait (typed input + runtime validation / self-correction)
545// ============================================================================
546
547/// A tool whose model-emitted arguments are validated against a typed,
548/// deserializable [`Input`](TypedTool::Input) **before** [`execute`](TypedTool::execute)
549/// runs.
550///
551/// Today a raw [`serde_json::Value`] is handed straight to [`Tool::execute`],
552/// so a malformed tool call reaches tool code unvalidated. `TypedTool` closes
553/// that gap: you declare a `Serialize` / `Deserialize` argument struct as
554/// [`Input`](TypedTool::Input), and the runtime deserializes the model's args
555/// into it at the dispatch boundary. On a deserialization/validation failure
556/// the runtime synthesises a structured error [`ToolResult`] (carrying the
557/// serde error message) so the model can self-correct on its next turn —
558/// `execute` is **never** called with invalid arguments.
559///
560/// # Relationship to [`Tool`]
561///
562/// `TypedTool` is the typed, opt-in *sugar* layer; [`Tool`] remains the
563/// untyped baseline. A [`TypedTool`] becomes a full [`Tool`] through
564/// [`TypedToolAdapter`] (mirroring how [`SimpleTool`] becomes a [`Tool`] via
565/// [`SimpleToolAdapter`]). Register one with
566/// [`ToolRegistry::register_typed`], which wraps it in the adapter for you;
567/// the adapter performs the deserialize-then-dispatch (or
568/// deserialize-then-synthesise-error) described above.
569///
570/// # Back-compat / migration
571///
572/// Existing [`Tool`] impls (and [`SimpleTool`] / [`DynamicToolName`] tools)
573/// keep compiling and running unchanged — they stay on the `Value`-in
574/// baseline, which is the identity passthrough (a `Value` always
575/// "deserializes" into a `Value`). Migrate a tool to typed args by moving its
576/// `impl Tool<Ctx>` to `impl TypedTool<Ctx>`, setting `type Input = MyArgs`,
577/// and changing `execute`'s signature from `input: Value` to `input: MyArgs`.
578/// The hand-written [`input_schema`](TypedTool::input_schema) JSON stays
579/// user-declared; this trait does **not** auto-derive a schema from `Input`.
580///
581/// # Example
582///
583/// ```
584/// use agent_sdk_tools::tools::{TypedTool, ToolContext};
585/// use agent_sdk_foundation::types::ToolResult;
586/// use serde::{Deserialize, Serialize};
587/// use serde_json::{json, Value};
588/// use std::future::Future;
589///
590/// #[derive(Debug, Serialize, Deserialize)]
591/// struct WeatherArgs {
592/// city: String,
593/// }
594///
595/// struct WeatherTool;
596///
597/// impl TypedTool<()> for WeatherTool {
598/// type Input = WeatherArgs;
599///
600/// fn name(&self) -> &'static str { "get_weather" }
601/// fn description(&self) -> &'static str { "Get current weather for a city" }
602/// fn input_schema(&self) -> Value {
603/// json!({
604/// "type": "object",
605/// "properties": { "city": { "type": "string" } },
606/// "required": ["city"]
607/// })
608/// }
609///
610/// fn execute(
611/// &self,
612/// _ctx: &ToolContext<()>,
613/// input: WeatherArgs,
614/// ) -> impl Future<Output = anyhow::Result<ToolResult>> + Send {
615/// async move { Ok(ToolResult::success(format!("Weather in {}: Sunny", input.city))) }
616/// }
617/// }
618/// ```
619///
620/// Like [`SimpleTool`], a `TypedTool` has a single fixed `&'static str`
621/// [`name`](TypedTool::name) (mapping to [`DynamicToolName`] via
622/// [`TypedToolAdapter`]). Reach for a hand-written [`Tool`] with a
623/// strongly-typed [`ToolName`] when the name must be computed at runtime or
624/// constrained to an enum.
625pub trait TypedTool<Ctx>: Send + Sync {
626 /// The typed input the model's arguments are deserialized into before
627 /// [`execute`](TypedTool::execute) runs.
628 ///
629 /// Must be [`DeserializeOwned`] (to parse model args), [`Serialize`] (so
630 /// the typed value round-trips for logging/storage), and `Send + 'static`
631 /// (to cross the async dispatch boundary).
632 type Input: DeserializeOwned + Serialize + Send + 'static;
633
634 /// The tool's name as sent to (and parsed from) the model.
635 fn name(&self) -> &'static str;
636
637 /// Human-readable display name for UI. Defaults to an empty string.
638 fn display_name(&self) -> &'static str {
639 ""
640 }
641
642 /// Human-readable description of what the tool does.
643 fn description(&self) -> &'static str;
644
645 /// User-declared JSON schema for the tool's input parameters.
646 ///
647 /// This stays hand-written JSON — it is **not** auto-derived from
648 /// [`Input`](TypedTool::Input). Keeping the schema explicit lets the
649 /// declared provider-facing contract diverge from the Rust type when that
650 /// is useful (descriptions, examples, provider-specific keywords).
651 fn input_schema(&self) -> Value;
652
653 /// Permission tier for this tool. Defaults to [`ToolTier::Observe`].
654 fn tier(&self) -> ToolTier {
655 ToolTier::Observe
656 }
657
658 /// Execute the tool with the already-validated, typed input.
659 ///
660 /// The runtime guarantees `input` deserialized cleanly from the model's
661 /// arguments; a malformed call is turned into a structured error
662 /// [`ToolResult`] before this method is reached, so implementations never
663 /// see invalid arguments.
664 ///
665 /// # Errors
666 /// Returns an error if tool execution fails.
667 fn execute(
668 &self,
669 ctx: &ToolContext<Ctx>,
670 input: Self::Input,
671 ) -> impl Future<Output = Result<ToolResult>> + Send;
672}
673
674/// Synthesise the structured validation-error [`ToolResult`] returned to the
675/// model when its arguments fail to deserialize into a [`TypedTool::Input`].
676///
677/// Factored out (and `pub`) so the exact self-correction wording is
678/// consistent with [`TypedToolAdapter`] and is directly unit-testable. The
679/// error is an *error* [`ToolResult`] (not a thrown `anyhow::Error`): it flows
680/// through the normal balanced `tool_use` / `tool_result` path so history
681/// stays balanced and the model gets a concrete, machine-actionable hint on
682/// its next turn.
683#[must_use]
684pub fn invalid_tool_input_result(tool_name: &str, error: &serde_json::Error) -> ToolResult {
685 ToolResult::error(format!(
686 "Invalid arguments for tool `{tool_name}`: {error}. \
687 The arguments did not match the tool's input schema — \
688 re-read the schema and call the tool again with corrected arguments."
689 ))
690}
691
692/// Deserialize raw model args into a typed `Input`, or synthesise the
693/// structured validation-error result.
694///
695/// Returns `Ok(typed)` for the happy path and `Err(result)` carrying the
696/// balanced error [`ToolResult`] for the self-correction path.
697/// [`TypedToolAdapter`] uses this to ensure [`TypedTool::execute`] is never
698/// reached with invalid arguments.
699///
700/// # Errors
701/// Returns the synthesised error [`ToolResult`] when `raw` does not
702/// deserialize into `Input`.
703pub fn validate_tool_input<Input>(tool_name: &str, raw: Value) -> Result<Input, ToolResult>
704where
705 Input: DeserializeOwned,
706{
707 serde_json::from_value(raw).map_err(|error| invalid_tool_input_result(tool_name, &error))
708}
709
710/// Adapter that turns any [`TypedTool`] into a full [`Tool`].
711///
712/// It gives the wrapped tool `Name = DynamicToolName`, deserializes the
713/// model's `Value` arguments into [`TypedTool::Input`] before dispatching, and
714/// synthesises a structured validation-error [`ToolResult`] when that fails.
715///
716/// You rarely name this type directly — register a [`TypedTool`] with
717/// [`ToolRegistry::register_typed`], which wraps it for you. The adapter
718/// pattern (rather than a blanket `impl Tool for T: TypedTool`) is required
719/// for coherence: a blanket impl would conflict with the existing
720/// [`SimpleToolAdapter`] impl, because the compiler cannot rule out a
721/// downstream `TypedTool` impl for `SimpleToolAdapter`.
722///
723/// This adapter is also where the typed `Input` is threaded through the
724/// erased-tool machinery without leaking the generic into trait objects: the
725/// registry's [`ErasedTool`] wrapper still only ever sees `Value`, while the
726/// concrete `Input` type (and the deserialize) live here, inside the adapter's
727/// concrete `T`.
728pub struct TypedToolAdapter<T> {
729 inner: T,
730}
731
732impl<T> TypedToolAdapter<T> {
733 /// Wrap a [`TypedTool`] so it can be used anywhere a [`Tool`] is expected.
734 pub const fn new(tool: T) -> Self {
735 Self { inner: tool }
736 }
737
738 /// Unwrap the inner [`TypedTool`].
739 pub fn into_inner(self) -> T {
740 self.inner
741 }
742}
743
744impl<Ctx, T> Tool<Ctx> for TypedToolAdapter<T>
745where
746 T: TypedTool<Ctx>,
747 Ctx: Send + Sync,
748{
749 type Name = DynamicToolName;
750
751 fn name(&self) -> DynamicToolName {
752 DynamicToolName::new(TypedTool::name(&self.inner))
753 }
754
755 fn display_name(&self) -> &'static str {
756 TypedTool::display_name(&self.inner)
757 }
758
759 fn description(&self) -> &'static str {
760 TypedTool::description(&self.inner)
761 }
762
763 fn input_schema(&self) -> Value {
764 TypedTool::input_schema(&self.inner)
765 }
766
767 fn tier(&self) -> ToolTier {
768 TypedTool::tier(&self.inner)
769 }
770
771 async fn execute(&self, ctx: &ToolContext<Ctx>, input: Value) -> Result<ToolResult> {
772 match validate_tool_input::<<T as TypedTool<Ctx>>::Input>(
773 TypedTool::name(&self.inner),
774 input,
775 ) {
776 Ok(typed) => TypedTool::execute(&self.inner, ctx, typed).await,
777 // A validation failure is returned as an error `ToolResult`,
778 // never `?`-bailed: it must reach the model as a balanced
779 // `tool_result` for self-correction. `execute` is not called.
780 Err(result) => Ok(result),
781 }
782 }
783}
784
785// ============================================================================
786// ToolLogic Trait (execute-only companion for the derive macros)
787// ============================================================================
788
789/// The `execute`-only half of a tool, used as the target of the
790/// `#[derive(Tool)]` / `#[derive(TypedTool)]` ergonomics macros.
791///
792/// The derives generate everything *except* the behaviour — `name`,
793/// `description`, `input_schema`, `tier` come from `#[tool(...)]` attributes —
794/// and delegate execution to this trait. You implement `ToolLogic` to supply
795/// the one thing a macro cannot: the `execute` body.
796///
797/// It is deliberately a **trait** (not an inherent method): a trait-method
798/// `async fn` that performs no `await` is fine, whereas an inherent one trips
799/// `clippy::unused_async`. Writing the body here keeps trivial, fully
800/// synchronous tools lint-clean without an `#[allow]`.
801///
802/// You rarely name this trait in prose — the derive docs show it in context —
803/// but the shape is:
804///
805/// ```
806/// use agent_sdk_tools::tools::{ToolLogic, ToolContext};
807/// use agent_sdk_foundation::types::ToolResult;
808/// use serde_json::Value;
809///
810/// struct MyTool;
811///
812/// impl ToolLogic<()> for MyTool {
813/// type Input = Value; // typed tools set this to their `Input` struct
814///
815/// async fn execute(&self, _ctx: &ToolContext<()>, input: Value) -> anyhow::Result<ToolResult> {
816/// Ok(ToolResult::success(format!("got {input}")))
817/// }
818/// }
819/// ```
820pub trait ToolLogic<Ctx>: Send + Sync {
821 /// The input the tool's `execute` receives. For `#[derive(Tool)]` this is
822 /// [`serde_json::Value`]; for `#[derive(TypedTool)]` it is the typed
823 /// `Input` (validated before `execute` runs).
824 type Input;
825
826 /// The tool's behaviour. Receives the (already-validated, for typed tools)
827 /// input.
828 ///
829 /// # Errors
830 /// Returns an error if tool execution fails.
831 fn execute(
832 &self,
833 ctx: &ToolContext<Ctx>,
834 input: Self::Input,
835 ) -> impl Future<Output = Result<ToolResult>> + Send;
836}
837
838// ============================================================================
839// SimpleTool Trait
840// ============================================================================
841
842/// An ergonomic [`Tool`] whose name is a plain string.
843///
844/// Most custom tools don't need a strongly-typed [`ToolName`] enum — they have
845/// a single, fixed name. `SimpleTool` lets you write a tool by returning a
846/// `&str` from [`name`](SimpleTool::name) instead of defining a `ToolName`
847/// type and an associated [`Tool::Name`].
848///
849/// Any `SimpleTool` is automatically a [`Tool`] (via a blanket impl) with
850/// `Name = DynamicToolName`, so it can be registered and used exactly like a
851/// hand-written `Tool`.
852///
853/// # Example
854///
855/// ```
856/// use agent_sdk_tools::tools::{SimpleTool, ToolContext};
857/// use agent_sdk_foundation::types::ToolResult;
858/// use serde_json::{json, Value};
859/// use std::future::Future;
860///
861/// struct WeatherTool;
862///
863/// impl SimpleTool<()> for WeatherTool {
864/// fn name(&self) -> &'static str { "get_weather" }
865/// fn description(&self) -> &'static str { "Get current weather for a city" }
866/// fn input_schema(&self) -> Value {
867/// json!({ "type": "object", "properties": { "city": { "type": "string" } } })
868/// }
869///
870/// fn execute(
871/// &self,
872/// _ctx: &ToolContext<()>,
873/// input: Value,
874/// ) -> impl Future<Output = anyhow::Result<ToolResult>> + Send {
875/// async move {
876/// let city = input["city"].as_str().unwrap_or("Unknown");
877/// Ok(ToolResult::success(format!("Weather in {city}: Sunny")))
878/// }
879/// }
880/// }
881/// ```
882pub trait SimpleTool<Ctx>: Send + Sync {
883 /// The tool's name as sent to (and parsed from) the LLM.
884 ///
885 /// Returns `&'static str` because a simple tool has one fixed name; reach
886 /// for the full [`Tool`] trait with a [`DynamicToolName`] when the name is
887 /// computed at runtime.
888 fn name(&self) -> &'static str;
889
890 /// Human-readable display name for UI.
891 ///
892 /// Defaults to an empty string; override for a friendlier label.
893 fn display_name(&self) -> &'static str {
894 ""
895 }
896
897 /// Human-readable description of what the tool does.
898 fn description(&self) -> &'static str;
899
900 /// JSON schema for the tool's input parameters.
901 fn input_schema(&self) -> Value;
902
903 /// Permission tier for this tool. Defaults to [`ToolTier::Observe`].
904 fn tier(&self) -> ToolTier {
905 ToolTier::Observe
906 }
907
908 /// Execute the tool with the given input.
909 ///
910 /// # Errors
911 /// Returns an error if tool execution fails.
912 fn execute(
913 &self,
914 ctx: &ToolContext<Ctx>,
915 input: Value,
916 ) -> impl Future<Output = Result<ToolResult>> + Send;
917}
918
919/// Adapter that turns any [`SimpleTool`] into a full [`Tool`] with
920/// `Name = DynamicToolName`.
921///
922/// You rarely name this type directly — register a [`SimpleTool`] with
923/// [`ToolRegistry::register_simple`], which wraps it for you. Use this adapter
924/// explicitly only when you need a `Tool` value (e.g. to pass to code that is
925/// generic over [`Tool`]).
926pub struct SimpleToolAdapter<T> {
927 inner: T,
928}
929
930impl<T> SimpleToolAdapter<T> {
931 /// Wrap a [`SimpleTool`] so it can be used anywhere a [`Tool`] is expected.
932 pub const fn new(tool: T) -> Self {
933 Self { inner: tool }
934 }
935
936 /// Unwrap the inner [`SimpleTool`].
937 pub fn into_inner(self) -> T {
938 self.inner
939 }
940}
941
942impl<Ctx, T> Tool<Ctx> for SimpleToolAdapter<T>
943where
944 T: SimpleTool<Ctx>,
945{
946 type Name = DynamicToolName;
947
948 fn name(&self) -> DynamicToolName {
949 DynamicToolName::new(SimpleTool::name(&self.inner))
950 }
951
952 fn display_name(&self) -> &'static str {
953 SimpleTool::display_name(&self.inner)
954 }
955
956 fn description(&self) -> &'static str {
957 SimpleTool::description(&self.inner)
958 }
959
960 fn input_schema(&self) -> Value {
961 SimpleTool::input_schema(&self.inner)
962 }
963
964 fn tier(&self) -> ToolTier {
965 SimpleTool::tier(&self.inner)
966 }
967
968 fn execute(
969 &self,
970 ctx: &ToolContext<Ctx>,
971 input: Value,
972 ) -> impl Future<Output = Result<ToolResult>> + Send {
973 SimpleTool::execute(&self.inner, ctx, input)
974 }
975}
976
977// ============================================================================
978// AsyncTool Trait
979// ============================================================================
980
981/// A tool that performs long-running async operations.
982///
983/// `AsyncTool`s have two phases:
984/// 1. `execute()` - Start the operation (lightweight, returns quickly)
985/// 2. `check_status()` - Stream progress until completion
986///
987/// The actual work should happen externally (background task, external service)
988/// and persist results to a durable store. The tool is just an orchestrator.
989///
990/// # Example
991///
992/// ```ignore
993/// impl AsyncTool<MyCtx> for ExecutePixTransferTool {
994/// type Name = PixToolName;
995/// type Stage = PixTransferStage;
996///
997/// async fn execute(&self, ctx: &ToolContext<MyCtx>, input: Value) -> Result<ToolOutcome> {
998/// let params = parse_input(&input)?;
999/// let operation_id = ctx.app.pix_service.start_transfer(params).await?;
1000/// Ok(ToolOutcome::in_progress(
1001/// operation_id,
1002/// format!("PIX transfer of {} initiated", params.amount),
1003/// ))
1004/// }
1005///
1006/// fn check_status(&self, ctx: &ToolContext<MyCtx>, operation_id: &str)
1007/// -> impl Stream<Item = ToolStatus<PixTransferStage>> + Send
1008/// {
1009/// async_stream::stream! {
1010/// loop {
1011/// let status = ctx.app.pix_service.get_status(operation_id).await;
1012/// match status {
1013/// PixStatus::Success { id } => {
1014/// yield ToolStatus::Completed(ToolResult::success(id));
1015/// break;
1016/// }
1017/// _ => yield ToolStatus::Progress { ... };
1018/// }
1019/// tokio::time::sleep(Duration::from_millis(500)).await;
1020/// }
1021/// }
1022/// }
1023/// }
1024/// ```
1025pub trait AsyncTool<Ctx>: Send + Sync {
1026 /// The type of name for this tool.
1027 type Name: ToolName;
1028 /// The type of progress stages for this tool.
1029 type Stage: ProgressStage;
1030
1031 /// Returns the tool's strongly-typed name.
1032 fn name(&self) -> Self::Name;
1033
1034 /// Human-readable display name for UI.
1035 fn display_name(&self) -> &'static str;
1036
1037 /// Human-readable description of what the tool does.
1038 fn description(&self) -> &'static str;
1039
1040 /// JSON schema for the tool's input parameters.
1041 fn input_schema(&self) -> Value;
1042
1043 /// Permission tier for this tool.
1044 fn tier(&self) -> ToolTier {
1045 ToolTier::Observe
1046 }
1047
1048 /// Execute the tool. Returns immediately with one of:
1049 /// - Success/Failed: Operation completed synchronously
1050 /// - `InProgress`: Operation started, use `check_status()` to stream updates
1051 ///
1052 /// # Errors
1053 /// Returns an error if tool execution fails.
1054 fn execute(
1055 &self,
1056 ctx: &ToolContext<Ctx>,
1057 input: Value,
1058 ) -> impl Future<Output = Result<ToolOutcome>> + Send;
1059
1060 /// Stream status updates for an in-progress operation.
1061 /// Must yield until Completed or Failed.
1062 fn check_status(
1063 &self,
1064 ctx: &ToolContext<Ctx>,
1065 operation_id: &str,
1066 ) -> impl Stream<Item = ToolStatus<Self::Stage>> + Send;
1067}
1068
1069// ============================================================================
1070// ListenExecuteTool Trait
1071// ============================================================================
1072
1073/// A tool whose runtime has two phases:
1074/// 1. `listen()` - starts preparation and streams updates
1075/// 2. `execute()` - performs final execution after confirmation
1076///
1077/// This abstraction is useful when runtime state can expire or evolve before
1078/// execution (quotes, challenge windows, leases, approvals).
1079///
1080/// Ordering note: the agent loop consumes `listen()` updates before
1081/// `AgentHooks::pre_tool_use()` runs. Hooks can therefore block `execute()`, but
1082/// any side effects done during `listen()` have already happened.
1083pub trait ListenExecuteTool<Ctx>: Send + Sync {
1084 /// The type of name for this tool.
1085 type Name: ToolName;
1086
1087 /// Returns the tool's strongly-typed name.
1088 fn name(&self) -> Self::Name;
1089
1090 /// Human-readable display name for UI.
1091 fn display_name(&self) -> &'static str;
1092
1093 /// Human-readable description of what the tool does.
1094 fn description(&self) -> &'static str;
1095
1096 /// JSON schema for the tool's input parameters.
1097 fn input_schema(&self) -> Value;
1098
1099 /// Permission tier for this tool.
1100 fn tier(&self) -> ToolTier {
1101 ToolTier::Confirm
1102 }
1103
1104 /// Start and stream runtime preparation updates.
1105 fn listen(
1106 &self,
1107 ctx: &ToolContext<Ctx>,
1108 input: Value,
1109 ) -> impl Stream<Item = ListenToolUpdate> + Send;
1110
1111 /// Execute using operation ID and optimistic concurrency revision.
1112 ///
1113 /// # Errors
1114 /// Returns an error if execution fails or revision is stale.
1115 fn execute(
1116 &self,
1117 ctx: &ToolContext<Ctx>,
1118 operation_id: &str,
1119 expected_revision: u64,
1120 ) -> impl Future<Output = Result<ToolResult>> + Send;
1121
1122 /// Stop a listen operation (best effort).
1123 ///
1124 /// # Errors
1125 /// Returns an error if cancellation fails.
1126 fn cancel(
1127 &self,
1128 _ctx: &ToolContext<Ctx>,
1129 _operation_id: &str,
1130 _reason: ListenStopReason,
1131 ) -> impl Future<Output = Result<()>> + Send {
1132 async { Ok(()) }
1133 }
1134}
1135
1136// ============================================================================
1137// Type-Erased Tool (for Registry)
1138// ============================================================================
1139
1140/// Type-erased tool trait for registry storage.
1141///
1142/// This allows tools with different `Name` associated types to be stored
1143/// in the same registry by erasing the type information.
1144///
1145/// # Example
1146///
1147/// ```ignore
1148/// for tool in registry.all() {
1149/// println!("Tool: {} - {}", tool.name_str(), tool.description());
1150/// }
1151/// ```
1152#[async_trait]
1153pub trait ErasedTool<Ctx>: Send + Sync {
1154 /// Get the tool name as a string.
1155 fn name_str(&self) -> &str;
1156 /// Get a human-friendly display name for the tool.
1157 fn display_name(&self) -> &'static str;
1158 /// Get the tool description.
1159 fn description(&self) -> &'static str;
1160 /// Get the JSON schema for tool inputs.
1161 fn input_schema(&self) -> Value;
1162 /// Get the tool's permission tier.
1163 fn tier(&self) -> ToolTier;
1164 /// Execute the tool with the given input.
1165 async fn execute(&self, ctx: &ToolContext<Ctx>, input: Value) -> Result<ToolResult>;
1166}
1167
1168/// Wrapper that erases the Name associated type from a Tool.
1169struct ToolWrapper<T, Ctx>
1170where
1171 T: Tool<Ctx>,
1172{
1173 inner: T,
1174 name_cache: String,
1175 _marker: PhantomData<Ctx>,
1176}
1177
1178impl<T, Ctx> ToolWrapper<T, Ctx>
1179where
1180 T: Tool<Ctx>,
1181{
1182 fn new(tool: T) -> Self {
1183 let name_cache = tool_name_to_string(&tool.name());
1184 Self {
1185 inner: tool,
1186 name_cache,
1187 _marker: PhantomData,
1188 }
1189 }
1190}
1191
1192#[async_trait]
1193impl<T, Ctx> ErasedTool<Ctx> for ToolWrapper<T, Ctx>
1194where
1195 T: Tool<Ctx> + 'static,
1196 Ctx: Send + Sync + 'static,
1197{
1198 fn name_str(&self) -> &str {
1199 &self.name_cache
1200 }
1201
1202 fn display_name(&self) -> &'static str {
1203 self.inner.display_name()
1204 }
1205
1206 fn description(&self) -> &'static str {
1207 self.inner.description()
1208 }
1209
1210 fn input_schema(&self) -> Value {
1211 self.inner.input_schema()
1212 }
1213
1214 fn tier(&self) -> ToolTier {
1215 self.inner.tier()
1216 }
1217
1218 async fn execute(&self, ctx: &ToolContext<Ctx>, input: Value) -> Result<ToolResult> {
1219 self.inner.execute(ctx, input).await
1220 }
1221}
1222
1223// ============================================================================
1224// Type-Erased AsyncTool (for Registry)
1225// ============================================================================
1226
1227/// Type-erased async tool trait for registry storage.
1228///
1229/// This allows async tools with different `Name` and `Stage` associated types
1230/// to be stored in the same registry by erasing the type information.
1231#[async_trait]
1232pub trait ErasedAsyncTool<Ctx>: Send + Sync {
1233 /// Get the tool name as a string.
1234 fn name_str(&self) -> &str;
1235 /// Get a human-friendly display name for the tool.
1236 fn display_name(&self) -> &'static str;
1237 /// Get the tool description.
1238 fn description(&self) -> &'static str;
1239 /// Get the JSON schema for tool inputs.
1240 fn input_schema(&self) -> Value;
1241 /// Get the tool's permission tier.
1242 fn tier(&self) -> ToolTier;
1243 /// Execute the tool with the given input.
1244 async fn execute(&self, ctx: &ToolContext<Ctx>, input: Value) -> Result<ToolOutcome>;
1245 /// Stream status updates for an in-progress operation (type-erased).
1246 fn check_status_stream<'a>(
1247 &'a self,
1248 ctx: &'a ToolContext<Ctx>,
1249 operation_id: &'a str,
1250 ) -> Pin<Box<dyn Stream<Item = ErasedToolStatus> + Send + 'a>>;
1251}
1252
1253/// Wrapper that erases the Name and Stage associated types from an [`AsyncTool`].
1254struct AsyncToolWrapper<T, Ctx>
1255where
1256 T: AsyncTool<Ctx>,
1257{
1258 inner: T,
1259 name_cache: String,
1260 _marker: PhantomData<Ctx>,
1261}
1262
1263impl<T, Ctx> AsyncToolWrapper<T, Ctx>
1264where
1265 T: AsyncTool<Ctx>,
1266{
1267 fn new(tool: T) -> Self {
1268 let name_cache = tool_name_to_string(&tool.name());
1269 Self {
1270 inner: tool,
1271 name_cache,
1272 _marker: PhantomData,
1273 }
1274 }
1275}
1276
1277#[async_trait]
1278impl<T, Ctx> ErasedAsyncTool<Ctx> for AsyncToolWrapper<T, Ctx>
1279where
1280 T: AsyncTool<Ctx> + 'static,
1281 Ctx: Send + Sync + 'static,
1282{
1283 fn name_str(&self) -> &str {
1284 &self.name_cache
1285 }
1286
1287 fn display_name(&self) -> &'static str {
1288 self.inner.display_name()
1289 }
1290
1291 fn description(&self) -> &'static str {
1292 self.inner.description()
1293 }
1294
1295 fn input_schema(&self) -> Value {
1296 self.inner.input_schema()
1297 }
1298
1299 fn tier(&self) -> ToolTier {
1300 self.inner.tier()
1301 }
1302
1303 async fn execute(&self, ctx: &ToolContext<Ctx>, input: Value) -> Result<ToolOutcome> {
1304 self.inner.execute(ctx, input).await
1305 }
1306
1307 fn check_status_stream<'a>(
1308 &'a self,
1309 ctx: &'a ToolContext<Ctx>,
1310 operation_id: &'a str,
1311 ) -> Pin<Box<dyn Stream<Item = ErasedToolStatus> + Send + 'a>> {
1312 use futures::StreamExt;
1313 let stream = self.inner.check_status(ctx, operation_id);
1314 Box::pin(stream.map(ErasedToolStatus::from))
1315 }
1316}
1317
1318// ============================================================================
1319// Type-Erased ListenExecuteTool (for Registry)
1320// ============================================================================
1321
1322/// Type-erased listen/execute tool trait for registry storage.
1323#[async_trait]
1324pub trait ErasedListenTool<Ctx>: Send + Sync {
1325 /// Get the tool name as a string.
1326 fn name_str(&self) -> &str;
1327 /// Get a human-friendly display name for the tool.
1328 fn display_name(&self) -> &'static str;
1329 /// Get the tool description.
1330 fn description(&self) -> &'static str;
1331 /// Get the JSON schema for tool inputs.
1332 fn input_schema(&self) -> Value;
1333 /// Get the tool's permission tier.
1334 fn tier(&self) -> ToolTier;
1335 /// Start listen stream.
1336 fn listen_stream<'a>(
1337 &'a self,
1338 ctx: &'a ToolContext<Ctx>,
1339 input: Value,
1340 ) -> Pin<Box<dyn Stream<Item = ListenToolUpdate> + Send + 'a>>;
1341 /// Execute using a prepared operation.
1342 async fn execute(
1343 &self,
1344 ctx: &ToolContext<Ctx>,
1345 operation_id: &str,
1346 expected_revision: u64,
1347 ) -> Result<ToolResult>;
1348 /// Cancel operation.
1349 async fn cancel(
1350 &self,
1351 ctx: &ToolContext<Ctx>,
1352 operation_id: &str,
1353 reason: ListenStopReason,
1354 ) -> Result<()>;
1355}
1356
1357/// Wrapper that erases the Name associated type from a [`ListenExecuteTool`].
1358struct ListenToolWrapper<T, Ctx>
1359where
1360 T: ListenExecuteTool<Ctx>,
1361{
1362 inner: T,
1363 name_cache: String,
1364 _marker: PhantomData<Ctx>,
1365}
1366
1367impl<T, Ctx> ListenToolWrapper<T, Ctx>
1368where
1369 T: ListenExecuteTool<Ctx>,
1370{
1371 fn new(tool: T) -> Self {
1372 let name_cache = tool_name_to_string(&tool.name());
1373 Self {
1374 inner: tool,
1375 name_cache,
1376 _marker: PhantomData,
1377 }
1378 }
1379}
1380
1381#[async_trait]
1382impl<T, Ctx> ErasedListenTool<Ctx> for ListenToolWrapper<T, Ctx>
1383where
1384 T: ListenExecuteTool<Ctx> + 'static,
1385 Ctx: Send + Sync + 'static,
1386{
1387 fn name_str(&self) -> &str {
1388 &self.name_cache
1389 }
1390
1391 fn display_name(&self) -> &'static str {
1392 self.inner.display_name()
1393 }
1394
1395 fn description(&self) -> &'static str {
1396 self.inner.description()
1397 }
1398
1399 fn input_schema(&self) -> Value {
1400 self.inner.input_schema()
1401 }
1402
1403 fn tier(&self) -> ToolTier {
1404 self.inner.tier()
1405 }
1406
1407 fn listen_stream<'a>(
1408 &'a self,
1409 ctx: &'a ToolContext<Ctx>,
1410 input: Value,
1411 ) -> Pin<Box<dyn Stream<Item = ListenToolUpdate> + Send + 'a>> {
1412 let stream = self.inner.listen(ctx, input);
1413 Box::pin(stream)
1414 }
1415
1416 async fn execute(
1417 &self,
1418 ctx: &ToolContext<Ctx>,
1419 operation_id: &str,
1420 expected_revision: u64,
1421 ) -> Result<ToolResult> {
1422 self.inner
1423 .execute(ctx, operation_id, expected_revision)
1424 .await
1425 }
1426
1427 async fn cancel(
1428 &self,
1429 ctx: &ToolContext<Ctx>,
1430 operation_id: &str,
1431 reason: ListenStopReason,
1432 ) -> Result<()> {
1433 self.inner.cancel(ctx, operation_id, reason).await
1434 }
1435}
1436
1437/// Registry of available tools.
1438///
1439/// Tools are stored with their names erased to allow different `Name` types
1440/// in the same registry. The registry uses string-based lookup for LLM
1441/// compatibility.
1442///
1443/// Supports both synchronous [`Tool`]s and asynchronous [`AsyncTool`]s.
1444pub struct ToolRegistry<Ctx> {
1445 tools: HashMap<String, Arc<dyn ErasedTool<Ctx>>>,
1446 async_tools: HashMap<String, Arc<dyn ErasedAsyncTool<Ctx>>>,
1447 listen_tools: HashMap<String, Arc<dyn ErasedListenTool<Ctx>>>,
1448}
1449
1450impl<Ctx> Clone for ToolRegistry<Ctx> {
1451 fn clone(&self) -> Self {
1452 Self {
1453 tools: self.tools.clone(),
1454 async_tools: self.async_tools.clone(),
1455 listen_tools: self.listen_tools.clone(),
1456 }
1457 }
1458}
1459
1460impl<Ctx: Send + Sync + 'static> Default for ToolRegistry<Ctx> {
1461 fn default() -> Self {
1462 Self::new()
1463 }
1464}
1465
1466impl<Ctx: Send + Sync + 'static> ToolRegistry<Ctx> {
1467 #[must_use]
1468 pub fn new() -> Self {
1469 Self {
1470 tools: HashMap::new(),
1471 async_tools: HashMap::new(),
1472 listen_tools: HashMap::new(),
1473 }
1474 }
1475
1476 /// Register a synchronous tool in the registry.
1477 ///
1478 /// The tool's name is converted to a string via serde serialization
1479 /// and used as the lookup key.
1480 pub fn register<T>(&mut self, tool: T) -> &mut Self
1481 where
1482 T: Tool<Ctx> + 'static,
1483 {
1484 let wrapper = ToolWrapper::new(tool);
1485 let name = wrapper.name_str().to_string();
1486 self.tools.insert(name, Arc::new(wrapper));
1487 self
1488 }
1489
1490 /// Register a [`SimpleTool`] — a tool whose name is a plain `&str` and
1491 /// which needs no [`ToolName`] type.
1492 ///
1493 /// The tool is wrapped in a [`SimpleToolAdapter`] (giving it
1494 /// `Name = DynamicToolName`) and registered like any other [`Tool`].
1495 /// This is the lowest-ceremony way to add a first custom tool.
1496 pub fn register_simple<T>(&mut self, tool: T) -> &mut Self
1497 where
1498 T: SimpleTool<Ctx> + 'static,
1499 {
1500 self.register(SimpleToolAdapter::new(tool))
1501 }
1502
1503 /// Register a [`TypedTool`] — a tool whose model-emitted arguments are
1504 /// deserialized into a typed [`TypedTool::Input`] and validated **before**
1505 /// `execute` runs.
1506 ///
1507 /// The tool is wrapped in a [`TypedToolAdapter`] (giving it
1508 /// `Name = DynamicToolName`) and registered like any other [`Tool`]. A
1509 /// malformed tool call is turned into a structured validation-error
1510 /// [`ToolResult`] at the dispatch boundary so the model can self-correct;
1511 /// `execute` is never reached with invalid arguments.
1512 pub fn register_typed<T>(&mut self, tool: T) -> &mut Self
1513 where
1514 T: TypedTool<Ctx> + 'static,
1515 {
1516 self.register(TypedToolAdapter::new(tool))
1517 }
1518
1519 /// Register an async tool in the registry.
1520 ///
1521 /// Async tools have two phases: execute (lightweight, starts operation)
1522 /// and `check_status` (streams progress until completion).
1523 pub fn register_async<T>(&mut self, tool: T) -> &mut Self
1524 where
1525 T: AsyncTool<Ctx> + 'static,
1526 {
1527 let wrapper = AsyncToolWrapper::new(tool);
1528 let name = wrapper.name_str().to_string();
1529 self.async_tools.insert(name, Arc::new(wrapper));
1530 self
1531 }
1532
1533 /// Register a listen/execute tool in the registry.
1534 ///
1535 /// Listen/execute tools start by streaming updates via `listen()`, then run
1536 /// final execution with `execute()` once confirmed.
1537 pub fn register_listen<T>(&mut self, tool: T) -> &mut Self
1538 where
1539 T: ListenExecuteTool<Ctx> + 'static,
1540 {
1541 let wrapper = ListenToolWrapper::new(tool);
1542 let name = wrapper.name_str().to_string();
1543 self.listen_tools.insert(name, Arc::new(wrapper));
1544 self
1545 }
1546
1547 /// Get a synchronous tool by name.
1548 #[must_use]
1549 pub fn get(&self, name: &str) -> Option<&Arc<dyn ErasedTool<Ctx>>> {
1550 self.tools.get(name)
1551 }
1552
1553 /// Get an async tool by name.
1554 #[must_use]
1555 pub fn get_async(&self, name: &str) -> Option<&Arc<dyn ErasedAsyncTool<Ctx>>> {
1556 self.async_tools.get(name)
1557 }
1558
1559 /// Get a listen/execute tool by name.
1560 #[must_use]
1561 pub fn get_listen(&self, name: &str) -> Option<&Arc<dyn ErasedListenTool<Ctx>>> {
1562 self.listen_tools.get(name)
1563 }
1564
1565 /// Check if a tool name refers to an async tool.
1566 #[must_use]
1567 pub fn is_async(&self, name: &str) -> bool {
1568 self.async_tools.contains_key(name)
1569 }
1570
1571 /// Check if a tool name refers to a listen/execute tool.
1572 #[must_use]
1573 pub fn is_listen(&self, name: &str) -> bool {
1574 self.listen_tools.contains_key(name)
1575 }
1576
1577 /// Get all registered synchronous tools.
1578 pub fn all(&self) -> impl Iterator<Item = &Arc<dyn ErasedTool<Ctx>>> {
1579 self.tools.values()
1580 }
1581
1582 /// Get all registered async tools.
1583 pub fn all_async(&self) -> impl Iterator<Item = &Arc<dyn ErasedAsyncTool<Ctx>>> {
1584 self.async_tools.values()
1585 }
1586
1587 /// Get all registered listen/execute tools.
1588 pub fn all_listen(&self) -> impl Iterator<Item = &Arc<dyn ErasedListenTool<Ctx>>> {
1589 self.listen_tools.values()
1590 }
1591
1592 /// Get the number of registered tools (sync + async).
1593 #[must_use]
1594 pub fn len(&self) -> usize {
1595 self.tools.len() + self.async_tools.len() + self.listen_tools.len()
1596 }
1597
1598 /// Check if the registry is empty.
1599 #[must_use]
1600 pub fn is_empty(&self) -> bool {
1601 self.tools.is_empty() && self.async_tools.is_empty() && self.listen_tools.is_empty()
1602 }
1603
1604 /// Filter tools by a predicate.
1605 ///
1606 /// Removes tools for which the predicate returns false.
1607 /// The predicate receives the tool name.
1608 /// Applies to both sync and async tools.
1609 ///
1610 /// # Example
1611 ///
1612 /// ```ignore
1613 /// registry.filter(|name| name != "bash");
1614 /// ```
1615 pub fn filter<F>(&mut self, predicate: F)
1616 where
1617 F: Fn(&str) -> bool,
1618 {
1619 self.tools.retain(|name, _| predicate(name));
1620 self.async_tools.retain(|name, _| predicate(name));
1621 self.listen_tools.retain(|name, _| predicate(name));
1622 }
1623
1624 /// Convert all tools (sync + async + listen) to LLM tool
1625 /// definitions. The output is sorted by tool name so the order
1626 /// is deterministic across builds and across calls.
1627 ///
1628 /// Determinism matters for **prompt caching**. Anthropic's
1629 /// `cache_control: ephemeral` keys on the byte content of the
1630 /// system + tool list. Anything that perturbs the order of the
1631 /// tool list invalidates the cache. The three backing maps are
1632 /// `HashMap`s, whose `values()` order is randomized (DoS-safe
1633 /// `RandomState` by default), so two consecutive turns with the
1634 /// same registered tool set were producing different orderings
1635 /// and silently zeroing the cache hit rate.
1636 ///
1637 /// Sorting by name is the cheapest fix that holds across
1638 /// insertion order, internal map type changes, and concurrent
1639 /// builds. The tool count is small (tens, not thousands) so the
1640 /// sort cost is negligible compared to a single LLM call.
1641 #[must_use]
1642 pub fn to_llm_tools(&self) -> Vec<llm::Tool> {
1643 let mut tools: Vec<_> = self
1644 .tools
1645 .values()
1646 .map(|tool| llm::Tool {
1647 name: tool.name_str().to_string(),
1648 description: tool.description().to_string(),
1649 input_schema: tool.input_schema(),
1650 display_name: tool.display_name().to_string(),
1651 tier: tool.tier(),
1652 })
1653 .collect();
1654
1655 tools.extend(self.async_tools.values().map(|tool| llm::Tool {
1656 name: tool.name_str().to_string(),
1657 description: tool.description().to_string(),
1658 input_schema: tool.input_schema(),
1659 display_name: tool.display_name().to_string(),
1660 tier: tool.tier(),
1661 }));
1662
1663 tools.extend(self.listen_tools.values().map(|tool| llm::Tool {
1664 name: tool.name_str().to_string(),
1665 description: tool.description().to_string(),
1666 input_schema: tool.input_schema(),
1667 display_name: tool.display_name().to_string(),
1668 tier: tool.tier(),
1669 }));
1670
1671 tools.sort_by(|a, b| a.name.cmp(&b.name));
1672 tools
1673 }
1674}
1675
1676#[cfg(test)]
1677mod tests {
1678 use super::*;
1679 use anyhow::Context;
1680
1681 // Test tool name enum for tests
1682 #[derive(Clone, Copy, Debug, PartialEq, Eq, Hash, Serialize, Deserialize)]
1683 #[serde(rename_all = "snake_case")]
1684 enum TestToolName {
1685 MockTool,
1686 AnotherTool,
1687 }
1688
1689 impl ToolName for TestToolName {}
1690
1691 struct MockTool;
1692
1693 impl Tool<()> for MockTool {
1694 type Name = TestToolName;
1695
1696 fn name(&self) -> TestToolName {
1697 TestToolName::MockTool
1698 }
1699
1700 fn display_name(&self) -> &'static str {
1701 "Mock Tool"
1702 }
1703
1704 fn description(&self) -> &'static str {
1705 "A mock tool for testing"
1706 }
1707
1708 fn input_schema(&self) -> Value {
1709 serde_json::json!({
1710 "type": "object",
1711 "properties": {
1712 "message": { "type": "string" }
1713 }
1714 })
1715 }
1716
1717 async fn execute(&self, _ctx: &ToolContext<()>, input: Value) -> Result<ToolResult> {
1718 let message = input
1719 .get("message")
1720 .and_then(|v| v.as_str())
1721 .unwrap_or("no message");
1722 Ok(ToolResult::success(format!("Received: {message}")))
1723 }
1724 }
1725
1726 #[test]
1727 fn test_tool_name_serialization() {
1728 let name = TestToolName::MockTool;
1729 assert_eq!(tool_name_to_string(&name), "mock_tool");
1730
1731 let parsed: TestToolName = tool_name_from_str("mock_tool").unwrap();
1732 assert_eq!(parsed, TestToolName::MockTool);
1733 }
1734
1735 #[test]
1736 fn test_dynamic_tool_name() {
1737 let name = DynamicToolName::new("my_mcp_tool");
1738 assert_eq!(tool_name_to_string(&name), "my_mcp_tool");
1739 assert_eq!(name.as_str(), "my_mcp_tool");
1740 }
1741
1742 #[test]
1743 fn test_tool_registry() {
1744 let mut registry = ToolRegistry::new();
1745 registry.register(MockTool);
1746
1747 assert_eq!(registry.len(), 1);
1748 assert!(registry.get("mock_tool").is_some());
1749 assert!(registry.get("nonexistent").is_none());
1750 }
1751
1752 #[test]
1753 fn test_to_llm_tools() {
1754 let mut registry = ToolRegistry::new();
1755 registry.register(MockTool);
1756
1757 let llm_tools = registry.to_llm_tools();
1758 assert_eq!(llm_tools.len(), 1);
1759 assert_eq!(llm_tools[0].name, "mock_tool");
1760 }
1761
1762 #[test]
1763 fn to_llm_tools_returns_alphabetical_order() {
1764 let mut registry = ToolRegistry::new();
1765 // Register in non-alphabetical order so the assertion would
1766 // fail if we ever returned insertion order again.
1767 registry.register(MockTool); // "mock_tool"
1768 registry.register(AnotherTool); // "another_tool"
1769
1770 let names: Vec<String> = registry
1771 .to_llm_tools()
1772 .into_iter()
1773 .map(|t| t.name)
1774 .collect();
1775 assert_eq!(names, vec!["another_tool", "mock_tool"]);
1776 }
1777
1778 #[test]
1779 fn to_llm_tools_is_deterministic_across_calls() {
1780 // Regression: prompt caching depends on byte-stable tool list
1781 // ordering. The `HashMap` behind the registry randomizes its
1782 // `values()` order, so without an explicit sort two consecutive
1783 // builds with the same registered set could ship different
1784 // tool orderings to the LLM and silently invalidate the cache.
1785 let mut registry = ToolRegistry::new();
1786 registry.register(MockTool);
1787 registry.register(AnotherTool);
1788
1789 let first: Vec<String> = registry
1790 .to_llm_tools()
1791 .into_iter()
1792 .map(|t| t.name)
1793 .collect();
1794
1795 for _ in 0..32 {
1796 let next: Vec<String> = registry
1797 .to_llm_tools()
1798 .into_iter()
1799 .map(|t| t.name)
1800 .collect();
1801 assert_eq!(next, first, "tool ordering must be stable across calls");
1802 }
1803 }
1804
1805 struct AnotherTool;
1806
1807 impl Tool<()> for AnotherTool {
1808 type Name = TestToolName;
1809
1810 fn name(&self) -> TestToolName {
1811 TestToolName::AnotherTool
1812 }
1813
1814 fn display_name(&self) -> &'static str {
1815 "Another Tool"
1816 }
1817
1818 fn description(&self) -> &'static str {
1819 "Another tool for testing"
1820 }
1821
1822 fn input_schema(&self) -> Value {
1823 serde_json::json!({ "type": "object" })
1824 }
1825
1826 async fn execute(&self, _ctx: &ToolContext<()>, _input: Value) -> Result<ToolResult> {
1827 Ok(ToolResult::success("Done"))
1828 }
1829 }
1830
1831 #[test]
1832 fn test_filter_tools() {
1833 let mut registry = ToolRegistry::new();
1834 registry.register(MockTool);
1835 registry.register(AnotherTool);
1836
1837 assert_eq!(registry.len(), 2);
1838
1839 // Filter out mock_tool
1840 registry.filter(|name| name != "mock_tool");
1841
1842 assert_eq!(registry.len(), 1);
1843 assert!(registry.get("mock_tool").is_none());
1844 assert!(registry.get("another_tool").is_some());
1845 }
1846
1847 #[test]
1848 fn test_filter_tools_keep_all() {
1849 let mut registry = ToolRegistry::new();
1850 registry.register(MockTool);
1851 registry.register(AnotherTool);
1852
1853 registry.filter(|_| true);
1854
1855 assert_eq!(registry.len(), 2);
1856 }
1857
1858 #[test]
1859 fn test_filter_tools_remove_all() {
1860 let mut registry = ToolRegistry::new();
1861 registry.register(MockTool);
1862 registry.register(AnotherTool);
1863
1864 registry.filter(|_| false);
1865
1866 assert!(registry.is_empty());
1867 }
1868
1869 #[test]
1870 fn test_display_name() {
1871 let mut registry = ToolRegistry::new();
1872 registry.register(MockTool);
1873
1874 let tool = registry.get("mock_tool").unwrap();
1875 assert_eq!(tool.display_name(), "Mock Tool");
1876 }
1877
1878 struct ListenMockTool;
1879
1880 impl ListenExecuteTool<()> for ListenMockTool {
1881 type Name = TestToolName;
1882
1883 fn name(&self) -> TestToolName {
1884 TestToolName::MockTool
1885 }
1886
1887 fn display_name(&self) -> &'static str {
1888 "Listen Mock Tool"
1889 }
1890
1891 fn description(&self) -> &'static str {
1892 "A listen/execute mock tool for testing"
1893 }
1894
1895 fn input_schema(&self) -> Value {
1896 serde_json::json!({ "type": "object" })
1897 }
1898
1899 fn listen(
1900 &self,
1901 _ctx: &ToolContext<()>,
1902 _input: Value,
1903 ) -> impl futures::Stream<Item = ListenToolUpdate> + Send {
1904 futures::stream::iter(vec![ListenToolUpdate::Ready {
1905 operation_id: "op_1".to_string(),
1906 revision: 1,
1907 message: "ready".to_string(),
1908 snapshot: serde_json::json!({"ok": true}),
1909 expires_at: None,
1910 }])
1911 }
1912
1913 async fn execute(
1914 &self,
1915 _ctx: &ToolContext<()>,
1916 _operation_id: &str,
1917 _expected_revision: u64,
1918 ) -> Result<ToolResult> {
1919 Ok(ToolResult::success("Executed"))
1920 }
1921 }
1922
1923 #[test]
1924 fn test_listen_tool_registry() {
1925 let mut registry = ToolRegistry::new();
1926 registry.register_listen(ListenMockTool);
1927
1928 assert_eq!(registry.len(), 1);
1929 assert!(registry.get_listen("mock_tool").is_some());
1930 assert!(registry.is_listen("mock_tool"));
1931 }
1932
1933 // ── TypedTool: typed input + validation / self-correction ───────────
1934
1935 use std::sync::atomic::{AtomicBool, Ordering};
1936
1937 #[derive(Debug, Serialize, Deserialize)]
1938 struct GreetArgs {
1939 name: String,
1940 // Required so a missing/typo'd field is a hard validation error.
1941 greeting: String,
1942 }
1943
1944 /// A typed tool that records whether `execute` was reached, so tests can
1945 /// assert the validation boundary never calls `execute` with bad args.
1946 struct GreetTool {
1947 executed: Arc<AtomicBool>,
1948 }
1949
1950 impl TypedTool<()> for GreetTool {
1951 type Input = GreetArgs;
1952
1953 fn name(&self) -> &'static str {
1954 "greet"
1955 }
1956
1957 fn description(&self) -> &'static str {
1958 "Greet someone by name"
1959 }
1960
1961 fn input_schema(&self) -> Value {
1962 serde_json::json!({
1963 "type": "object",
1964 "properties": {
1965 "name": { "type": "string" },
1966 "greeting": { "type": "string" }
1967 },
1968 "required": ["name", "greeting"]
1969 })
1970 }
1971
1972 async fn execute(&self, _ctx: &ToolContext<()>, input: GreetArgs) -> Result<ToolResult> {
1973 self.executed.store(true, Ordering::SeqCst);
1974 Ok(ToolResult::success(format!(
1975 "{}, {}!",
1976 input.greeting, input.name
1977 )))
1978 }
1979 }
1980
1981 #[tokio::test]
1982 async fn typed_tool_happy_path_receives_typed_input() -> Result<()> {
1983 let executed = Arc::new(AtomicBool::new(false));
1984 let adapter = TypedToolAdapter::new(GreetTool {
1985 executed: executed.clone(),
1986 });
1987 let ctx = ToolContext::new(());
1988
1989 let result = Tool::execute(
1990 &adapter,
1991 &ctx,
1992 serde_json::json!({ "name": "Ada", "greeting": "Hello" }),
1993 )
1994 .await?;
1995
1996 assert!(executed.load(Ordering::SeqCst), "execute must be called");
1997 assert!(result.success);
1998 assert_eq!(result.output, "Hello, Ada!");
1999 Ok(())
2000 }
2001
2002 #[tokio::test]
2003 async fn typed_tool_invalid_args_self_correct_without_executing() -> Result<()> {
2004 let executed = Arc::new(AtomicBool::new(false));
2005 let adapter = TypedToolAdapter::new(GreetTool {
2006 executed: executed.clone(),
2007 });
2008 let ctx = ToolContext::new(());
2009
2010 // `greeting` is missing — must not deserialize into `GreetArgs`.
2011 let result = Tool::execute(&adapter, &ctx, serde_json::json!({ "name": "Ada" })).await?;
2012
2013 assert!(
2014 !executed.load(Ordering::SeqCst),
2015 "execute must NOT be called with invalid arguments"
2016 );
2017 assert!(!result.success, "validation failure is an error result");
2018 assert!(
2019 result.output.contains("Invalid arguments for tool `greet`"),
2020 "error must identify the tool: {}",
2021 result.output
2022 );
2023 assert!(
2024 result.output.contains("greeting"),
2025 "error must surface the serde message naming the bad field: {}",
2026 result.output
2027 );
2028 Ok(())
2029 }
2030
2031 #[tokio::test]
2032 async fn typed_tool_wrong_type_self_corrects() -> Result<()> {
2033 let executed = Arc::new(AtomicBool::new(false));
2034 let adapter = TypedToolAdapter::new(GreetTool {
2035 executed: executed.clone(),
2036 });
2037 let ctx = ToolContext::new(());
2038
2039 // `name` is a number, not a string.
2040 let result = Tool::execute(
2041 &adapter,
2042 &ctx,
2043 serde_json::json!({ "name": 42, "greeting": "Hi" }),
2044 )
2045 .await?;
2046
2047 assert!(!executed.load(Ordering::SeqCst));
2048 assert!(!result.success);
2049 Ok(())
2050 }
2051
2052 /// Back-compat: a `TypedTool` whose `Input = Value` is the identity
2053 /// passthrough — any JSON deserializes, mirroring today's untyped tools.
2054 struct ValueTypedTool;
2055
2056 impl TypedTool<()> for ValueTypedTool {
2057 type Input = Value;
2058
2059 fn name(&self) -> &'static str {
2060 "value_typed"
2061 }
2062
2063 fn description(&self) -> &'static str {
2064 "Accepts any JSON, like an untyped tool"
2065 }
2066
2067 fn input_schema(&self) -> Value {
2068 serde_json::json!({ "type": "object" })
2069 }
2070
2071 async fn execute(&self, _ctx: &ToolContext<()>, input: Value) -> Result<ToolResult> {
2072 Ok(ToolResult::success(input.to_string()))
2073 }
2074 }
2075
2076 #[tokio::test]
2077 async fn typed_tool_value_input_is_identity_passthrough() -> Result<()> {
2078 let adapter = TypedToolAdapter::new(ValueTypedTool);
2079 let ctx = ToolContext::new(());
2080
2081 // Arbitrary shape — Value always "deserializes".
2082 let result = Tool::execute(
2083 &adapter,
2084 &ctx,
2085 serde_json::json!({ "anything": [1, 2, 3], "nested": { "ok": true } }),
2086 )
2087 .await?;
2088
2089 assert!(result.success);
2090 Ok(())
2091 }
2092
2093 #[test]
2094 fn register_typed_exposes_tool_via_registry() -> Result<()> {
2095 let mut registry = ToolRegistry::new();
2096 registry.register_typed(GreetTool {
2097 executed: Arc::new(AtomicBool::new(false)),
2098 });
2099
2100 assert_eq!(registry.len(), 1);
2101 let tool = registry.get("greet").context("typed tool registered")?;
2102 // The user-declared schema flows through unchanged.
2103 assert_eq!(tool.input_schema()["required"][0], "name");
2104 Ok(())
2105 }
2106
2107 #[test]
2108 fn invalid_tool_input_result_is_balanced_error() -> Result<()> {
2109 let Err(err) = serde_json::from_str::<GreetArgs>("{}") else {
2110 anyhow::bail!("empty object must fail to deserialize GreetArgs");
2111 };
2112 let result = invalid_tool_input_result("greet", &err);
2113
2114 assert!(!result.success);
2115 assert!(result.output.contains("greet"));
2116 assert!(result.output.contains("call the tool again"));
2117 Ok(())
2118 }
2119}