Skip to main content

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}