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