pub struct ToolLoopConfig {
pub max_iterations: u32,
pub parallel_tool_execution: bool,
pub on_tool_call: Option<ToolApprovalFn>,
pub stop_when: Option<StopConditionFn>,
pub loop_detection: Option<LoopDetectionConfig>,
pub timeout: Option<Duration>,
pub result_processor: Option<Arc<dyn ToolResultProcessor>>,
pub result_extractor: Option<Arc<dyn ToolResultExtractor>>,
pub result_cacher: Option<Arc<dyn ToolResultCacher>>,
pub masking: Option<ObservationMaskingConfig>,
pub force_mask_iterations: Option<Arc<Mutex<HashSet<u32>>>>,
pub max_depth: Option<u32>,
}Expand description
Configuration for tool_loop and tool_loop_stream.
Fields§
§max_iterations: u32Maximum number of generate-execute iterations. Default: 10.
parallel_tool_execution: boolWhether to execute multiple tool calls in parallel. Default: true.
on_tool_call: Option<ToolApprovalFn>Optional callback to approve, deny, or modify each tool call before execution.
Called once per tool call in the LLM response, after the response
is assembled but before any tool is executed. Receives the
ToolCall as parsed from the LLM output.
Modified arguments are re-validated against the tool’s schema.
Panics in the callback propagate and terminate the loop.
stop_when: Option<StopConditionFn>Optional stop condition checked after each LLM response.
Called after the LLM response is received but before tools
are executed. If the callback returns StopDecision::Stop or
StopDecision::StopWithReason, the loop terminates immediately
without executing the requested tool calls.
Receives a StopContext with information about the current
iteration and returns a StopDecision. Use this to implement:
final_answertool patterns (stop when a specific tool is called)- Token budget enforcement
- Total tool call limits
- Content pattern matching
§Example
use llm_stack::tool::{ToolLoopConfig, StopDecision};
use std::sync::Arc;
let config = ToolLoopConfig {
stop_when: Some(Arc::new(|ctx| {
// Stop if we've executed 5 or more tool calls
if ctx.tool_calls_executed >= 5 {
StopDecision::StopWithReason("Tool call limit reached".into())
} else {
StopDecision::Continue
}
})),
..Default::default()
};loop_detection: Option<LoopDetectionConfig>Optional loop detection to catch stuck agents.
When enabled, tracks consecutive identical tool calls (same name and arguments) and takes action when the threshold is reached.
§Example
use llm_stack::tool::{ToolLoopConfig, LoopDetectionConfig, LoopAction};
let config = ToolLoopConfig {
loop_detection: Some(LoopDetectionConfig {
threshold: 3,
action: LoopAction::InjectWarning,
}),
..Default::default()
};timeout: Option<Duration>Maximum wall-clock time for the entire tool loop.
If exceeded, returns with TerminationReason::Timeout.
This is useful for enforcing time budgets in production systems.
§Example
use llm_stack::tool::ToolLoopConfig;
use std::time::Duration;
let config = ToolLoopConfig {
timeout: Some(Duration::from_secs(30)),
..Default::default()
};result_processor: Option<Arc<dyn ToolResultProcessor>>Optional processor that runs on tool results before they enter the conversation context.
When set, the processor’s process
method is called on each tool result after execution. If it modifies
the content, a LoopEvent::ToolResultProcessed event is emitted
for observability.
Default: None (no processing — results pass through unmodified).
§Example
use llm_stack::tool::{ToolLoopConfig, ToolResultProcessor, ProcessedResult};
use std::sync::Arc;
struct TruncateProcessor;
impl ToolResultProcessor for TruncateProcessor {
fn process(&self, _tool_name: &str, output: &str) -> ProcessedResult {
if output.len() > 10_000 {
ProcessedResult {
content: output[..10_000].to_string(),
was_processed: true,
original_tokens_est: (output.len() as u32) / 4,
processed_tokens_est: 2500,
}
} else {
ProcessedResult::unchanged()
}
}
}
let config = ToolLoopConfig {
result_processor: Some(Arc::new(TruncateProcessor)),
..Default::default()
};result_extractor: Option<Arc<dyn ToolResultExtractor>>Async semantic extractor for large tool results.
After the result_processor runs, if the
result still exceeds the extractor’s extraction_threshold,
the extractor condenses it using async work (e.g., a fast LLM call).
The extractor receives the last user message for relevance-guided extraction. Results below the threshold skip this stage entirely.
Default: None (no semantic extraction).
result_cacher: Option<Arc<dyn ToolResultCacher>>Out-of-context cacher for oversized tool results.
After the result_processor and optional
result_extractor run, if the result still
exceeds the cacher’s inline_threshold,
the cacher stores the full content externally and returns a compact
summary for the conversation.
The caller decides how to store (disk, memory, KV, …). llm-stack only provides the hook and the threshold check.
Default: None (no caching — oversized results stay inline).
masking: Option<ObservationMaskingConfig>Observation masking: replace old tool results with compact placeholders to reduce context size between iterations.
When enabled, LoopCore scans the message history before each
LLM call and masks tool results from old iterations. Masking
preserves the tool call / result structure (so the LLM knows a
tool was called) but replaces the content with a short placeholder.
Default: None (no masking — all tool results stay in context).
force_mask_iterations: Option<Arc<Mutex<HashSet<u32>>>>Agent-directed force-mask set for observation masking.
When set, tool results from iterations listed in this set are
masked regardless of age. This enables tools like context_release
to mark specific iterations as stale during execution.
The set is shared between the tool loop config and the tool that
writes to it (e.g., via Arc::clone). Thread-safe via Mutex.
Default: None (only age-based masking applies).
max_depth: Option<u32>Maximum allowed nesting depth for recursive tool loops.
When a tool calls tool_loop internally (e.g., spawning a sub-agent),
the depth is tracked via the context’s LoopDepth
implementation. If ctx.loop_depth() >= max_depth at entry,
returns Err(LlmError::MaxDepthExceeded).
Some(n): Error if depth >= nNone: No limit (dangerous, use with caution)
Default: Some(3) (allows master → worker → one more level)
§Example
use llm_stack::tool::ToolLoopConfig;
// Master/Worker pattern: master=0, worker=1, no grandchildren
let config = ToolLoopConfig {
max_depth: Some(2),
..Default::default()
};