Skip to main content

ToolLoopConfig

Struct ToolLoopConfig 

Source
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: u32

Maximum number of generate-execute iterations. Default: 10.

§parallel_tool_execution: bool

Whether 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_answer tool 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 >= n
  • None: 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()
};

Trait Implementations§

Source§

impl Clone for ToolLoopConfig

Source§

fn clone(&self) -> Self

Returns a duplicate of the value. Read more
1.0.0 · Source§

fn clone_from(&mut self, source: &Self)

Performs copy-assignment from source. Read more
Source§

impl Debug for ToolLoopConfig

Source§

fn fmt(&self, f: &mut Formatter<'_>) -> Result

Formats the value using the given formatter. Read more
Source§

impl Default for ToolLoopConfig

Source§

fn default() -> Self

Returns the “default value” for a type. Read more

Auto Trait Implementations§

Blanket Implementations§

Source§

impl<T> Any for T
where T: 'static + ?Sized,

Source§

fn type_id(&self) -> TypeId

Gets the TypeId of self. Read more
Source§

impl<T> Borrow<T> for T
where T: ?Sized,

Source§

fn borrow(&self) -> &T

Immutably borrows from an owned value. Read more
Source§

impl<T> BorrowMut<T> for T
where T: ?Sized,

Source§

fn borrow_mut(&mut self) -> &mut T

Mutably borrows from an owned value. Read more
Source§

impl<T> CloneToUninit for T
where T: Clone,

Source§

unsafe fn clone_to_uninit(&self, dest: *mut u8)

🔬This is a nightly-only experimental API. (clone_to_uninit)
Performs copy-assignment from self to dest. Read more
Source§

impl<T> DynClone for T
where T: Clone,

Source§

fn __clone_box(&self, _: Private) -> *mut ()

Source§

impl<T> From<T> for T

Source§

fn from(t: T) -> T

Returns the argument unchanged.

Source§

impl<T> Instrument for T

Source§

fn instrument(self, span: Span) -> Instrumented<Self>

Instruments this type with the provided Span, returning an Instrumented wrapper. Read more
Source§

fn in_current_span(self) -> Instrumented<Self>

Instruments this type with the current Span, returning an Instrumented wrapper. Read more
Source§

impl<T, U> Into<U> for T
where U: From<T>,

Source§

fn into(self) -> U

Calls U::from(self).

That is, this conversion is whatever the implementation of From<T> for U chooses to do.

Source§

impl<T> PolicyExt for T
where T: ?Sized,

Source§

fn and<P, B, E>(self, other: P) -> And<T, P>
where T: Policy<B, E>, P: Policy<B, E>,

Create a new Policy that returns Action::Follow only if self and other return Action::Follow. Read more
Source§

fn or<P, B, E>(self, other: P) -> Or<T, P>
where T: Policy<B, E>, P: Policy<B, E>,

Create a new Policy that returns Action::Follow if either self or other returns Action::Follow. Read more
Source§

impl<T> ToOwned for T
where T: Clone,

Source§

type Owned = T

The resulting type after obtaining ownership.
Source§

fn to_owned(&self) -> T

Creates owned data from borrowed data, usually by cloning. Read more
Source§

fn clone_into(&self, target: &mut T)

Uses borrowed data to replace owned data, usually by cloning. Read more
Source§

impl<T, U> TryFrom<U> for T
where U: Into<T>,

Source§

type Error = Infallible

The type returned in the event of a conversion error.
Source§

fn try_from(value: U) -> Result<T, <T as TryFrom<U>>::Error>

Performs the conversion.
Source§

impl<T, U> TryInto<U> for T
where U: TryFrom<T>,

Source§

type Error = <U as TryFrom<T>>::Error

The type returned in the event of a conversion error.
Source§

fn try_into(self) -> Result<U, <U as TryFrom<T>>::Error>

Performs the conversion.
Source§

impl<V, T> VZip<V> for T
where V: MultiLane<T>,

Source§

fn vzip(self) -> V

Source§

impl<T> WithSubscriber for T

Source§

fn with_subscriber<S>(self, subscriber: S) -> WithDispatch<Self>
where S: Into<Dispatch>,

Attaches the provided Subscriber to this type, returning a WithDispatch wrapper. Read more
Source§

fn with_current_subscriber(self) -> WithDispatch<Self>

Attaches the current default Subscriber to this type, returning a WithDispatch wrapper. Read more