Skip to main content

llm_stack/tool/
mod.rs

1//! Tool execution engine.
2//!
3//! This module provides the runtime layer for executing tools that LLMs
4//! invoke during generation. It builds on the foundational types from
5//! [`chat`](crate::chat) ([`ToolCall`](crate::chat::ToolCall), [`ToolResult`](crate::chat::ToolResult)) and
6//! [`provider`](crate::provider) ([`ToolDefinition`](crate::provider::ToolDefinition), [`JsonSchema`](crate::JsonSchema)).
7//!
8//! # Architecture
9//!
10//! ```text
11//!   ToolHandler        — defines a single tool (schema + execute fn)
12//!       │
13//!   ToolRegistry       — stores handlers by name, validates & dispatches
14//!       │
15//!   tool_loop()        — automates generate → execute → feedback cycle
16//!   tool_loop_stream() — streaming variant
17//! ```
18//!
19//! # Example
20//!
21//! ```rust,no_run
22//! use llm_stack::tool::{ToolRegistry, tool_fn, ToolLoopConfig, tool_loop};
23//! use llm_stack::{ChatParams, ChatMessage, JsonSchema, ToolDefinition};
24//! use serde_json::{json, Value};
25//!
26//! # async fn example(provider: &dyn llm_stack::DynProvider) -> Result<(), llm_stack::LlmError> {
27//! let mut registry: ToolRegistry<()> = ToolRegistry::new();
28//! registry.register(tool_fn(
29//!     ToolDefinition {
30//!         name: "add".into(),
31//!         description: "Add two numbers".into(),
32//!         parameters: JsonSchema::new(json!({
33//!             "type": "object",
34//!             "properties": {
35//!                 "a": {"type": "number"},
36//!                 "b": {"type": "number"}
37//!             },
38//!             "required": ["a", "b"]
39//!         })),
40//!         retry: None,
41//!     },
42//!     |input: Value| async move {
43//!         let a = input["a"].as_f64().unwrap_or(0.0);
44//!         let b = input["b"].as_f64().unwrap_or(0.0);
45//!         Ok(format!("{}", a + b))
46//!     },
47//! ));
48//!
49//! let params = ChatParams {
50//!     messages: vec![ChatMessage::user("What is 2 + 3?")],
51//!     tools: Some(registry.definitions()),
52//!     ..Default::default()
53//! };
54//!
55//! let result = tool_loop(provider, &registry, params, ToolLoopConfig::default(), &()).await?;
56//! println!("Final answer: {:?}", result.response.text());
57//! # Ok(())
58//! # }
59//! ```
60//!
61//! # Using Context
62//!
63//! Tools often need access to shared state like database connections, user identity,
64//! or configuration. Use [`tool_fn_with_ctx`] to create tools that receive context:
65//!
66//! ```rust,no_run
67//! use llm_stack::tool::{tool_fn_with_ctx, ToolRegistry, ToolError, ToolOutput, tool_loop, ToolLoopConfig, LoopDepth};
68//! use llm_stack::{ToolDefinition, JsonSchema, ChatParams, ChatMessage};
69//! use serde_json::{json, Value};
70//!
71//! // Your application context - must implement Clone for LoopDepth
72//! #[derive(Clone)]
73//! struct AppContext {
74//!     user_id: String,
75//!     api_key: String,
76//!     depth: u32,
77//! }
78//!
79//! // Implement LoopDepth for automatic depth tracking in nested loops
80//! impl LoopDepth for AppContext {
81//!     fn loop_depth(&self) -> u32 { self.depth }
82//!     fn with_depth(&self, depth: u32) -> Self {
83//!         Self { depth, ..self.clone() }
84//!     }
85//! }
86//!
87//! # async fn example(provider: &dyn llm_stack::DynProvider) -> Result<(), llm_stack::LlmError> {
88//! // Create a tool that uses context
89//! let handler = tool_fn_with_ctx(
90//!     ToolDefinition {
91//!         name: "get_user_data".into(),
92//!         description: "Fetch data for the current user".into(),
93//!         parameters: JsonSchema::new(json!({"type": "object"})),
94//!         retry: None,
95//!     },
96//!     |_input: Value, ctx: &AppContext| {
97//!         // Clone data from context before the async block
98//!         let user_id = ctx.user_id.clone();
99//!         async move {
100//!             // Use the cloned data in the async block
101//!             Ok(ToolOutput::new(format!("Data for user: {}", user_id)))
102//!         }
103//!     },
104//! );
105//!
106//! // Register with a typed registry
107//! let mut registry: ToolRegistry<AppContext> = ToolRegistry::new();
108//! registry.register(handler);
109//!
110//! // Create context and run
111//! let ctx = AppContext {
112//!     user_id: "user123".into(),
113//!     api_key: "secret".into(),
114//!     depth: 0,
115//! };
116//!
117//! let params = ChatParams {
118//!     messages: vec![ChatMessage::user("Get my data")],
119//!     tools: Some(registry.definitions()),
120//!     ..Default::default()
121//! };
122//!
123//! let result = tool_loop(provider, &registry, params, ToolLoopConfig::default(), &ctx).await?;
124//! # Ok(())
125//! # }
126//! ```
127//!
128//! **Note on lifetimes**: The closure passed to `tool_fn_with_ctx` uses higher-ranked
129//! trait bounds (`for<'c> Fn(Value, &'c Ctx) -> Fut`). This means the future returned
130//! by your closure must be `'static` — it cannot borrow from the context reference.
131//! Clone any data you need from the context before creating the async block.
132
133mod approval;
134mod config;
135mod depth;
136mod error;
137mod execution;
138mod handler;
139mod helpers;
140mod loop_channel;
141mod loop_detection;
142mod loop_stream;
143mod loop_sync;
144mod output;
145mod registry;
146
147// Re-export all public types
148pub use config::{
149    LoopAction, LoopDetectionConfig, StopConditionFn, StopContext, StopDecision, TerminationReason,
150    ToolApproval, ToolApprovalFn, ToolLoopConfig, ToolLoopEvent, ToolLoopEventFn, ToolLoopResult,
151};
152pub use depth::LoopDepth;
153pub use error::ToolError;
154pub use handler::{FnToolHandler, NoCtxToolHandler, ToolHandler};
155pub use helpers::{tool_fn, tool_fn_with_ctx};
156pub use loop_channel::tool_loop_channel;
157pub use loop_stream::tool_loop_stream;
158pub use loop_sync::tool_loop;
159pub use output::ToolOutput;
160pub use registry::ToolRegistry;
161
162#[cfg(test)]
163mod tests;