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