Skip to main content

lash_tool_support/
static_provider.rs

1//! [`StaticToolProvider`] — a reusable [`ToolProvider`] for the common case of
2//! serving a *fixed* set of [`ToolDefinition`]s.
3//!
4//! Almost every single- (or fixed-multi-) tool provider in the workspace used
5//! to hand-roll the same idiom: `tool_manifests()` rebuilt `def.manifest()` and
6//! `resolve_contract()` rebuilt `def.contract()` on *every* call, re-running
7//! schema and doc generation each time. `StaticToolProvider` derives the
8//! manifests and contracts **once** in its constructor and serves them from a
9//! cache, delegating only `execute` (and, by default, the identity
10//! `prepare_tool_call`) to a small [`StaticToolExecute`] implementation that
11//! holds the tool's runtime state and behavior.
12
13use std::collections::HashMap;
14use std::sync::Arc;
15
16use lash_core::{
17    ToolCall, ToolContract, ToolDefinition, ToolManifest, ToolPrepareCall, ToolPrepareContext,
18    ToolProvider, ToolResult, sansio::PendingToolCall,
19};
20
21/// Per-call execution behavior for a [`StaticToolProvider`].
22///
23/// Implement this on the struct that owns the tool's runtime state (HTTP
24/// clients, shared mutable state, configuration flags, ...). The provider's
25/// manifests and contracts come from the [`ToolDefinition`]s passed to
26/// [`StaticToolProvider::new`]; this trait supplies only the dynamic behavior.
27#[async_trait::async_trait]
28pub trait StaticToolExecute: Send + Sync + 'static {
29    /// Execute a resolved tool call. Dispatch on `call.name` when serving more
30    /// than one tool.
31    async fn execute(&self, call: ToolCall<'_>) -> ToolResult;
32
33    /// Optional argument-preparation hook, mirroring
34    /// [`ToolProvider::prepare_tool_call`]. Defaults to the identity transform.
35    async fn prepare_tool_call(
36        &self,
37        pending: PendingToolCall,
38        _context: &ToolPrepareContext,
39    ) -> Result<lash_core::PreparedToolCall, ToolResult> {
40        Ok(lash_core::PreparedToolCall::identity(pending))
41    }
42}
43
44/// A [`ToolProvider`] that serves a fixed set of [`ToolDefinition`]s from a
45/// cache, delegating execution to an [`StaticToolExecute`].
46pub struct StaticToolProvider<E: StaticToolExecute> {
47    manifests: Vec<ToolManifest>,
48    contracts: HashMap<String, Arc<ToolContract>>,
49    executor: E,
50}
51
52impl<E: StaticToolExecute> StaticToolProvider<E> {
53    /// Build a provider from a fixed set of definitions and an executor.
54    ///
55    /// Manifests and contracts are derived once, here, and reused for the life
56    /// of the provider.
57    pub fn new(definitions: Vec<ToolDefinition>, executor: E) -> Self {
58        let mut manifests = Vec::with_capacity(definitions.len());
59        let mut contracts = HashMap::with_capacity(definitions.len());
60        for def in &definitions {
61            let manifest = def.manifest();
62            contracts.insert(manifest.name.clone(), Arc::new(def.contract()));
63            manifests.push(manifest);
64        }
65        Self {
66            manifests,
67            contracts,
68            executor,
69        }
70    }
71
72    /// Borrow the underlying executor. Useful for tests that need to inspect
73    /// the executor's internal state.
74    pub fn executor(&self) -> &E {
75        &self.executor
76    }
77}
78
79#[async_trait::async_trait]
80impl<E: StaticToolExecute> ToolProvider for StaticToolProvider<E> {
81    fn tool_manifests(&self) -> Vec<ToolManifest> {
82        self.manifests.clone()
83    }
84
85    fn resolve_manifest(&self, name: &str) -> Option<ToolManifest> {
86        self.manifests
87            .iter()
88            .find(|manifest| manifest.name == name)
89            .cloned()
90    }
91
92    fn resolve_contract(&self, name: &str) -> Option<Arc<ToolContract>> {
93        self.contracts.get(name).cloned()
94    }
95
96    async fn prepare_tool_call(
97        &self,
98        call: ToolPrepareCall<'_>,
99    ) -> Result<lash_core::PreparedToolCall, ToolResult> {
100        self.executor
101            .prepare_tool_call(call.pending, call.context)
102            .await
103    }
104
105    async fn execute(&self, call: ToolCall<'_>) -> ToolResult {
106        self.executor.execute(call).await
107    }
108}