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}