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