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, ®istry, 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, ®istry, 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;