Skip to main content

AgentExecutor

Struct AgentExecutor 

Source
pub struct AgentExecutor { /* private fields */ }
Expand description

Agent executor.

Responsible for executing the agent’s decision loop: Plan -> Act -> Observe.

Implementations§

Source§

impl AgentExecutor

Source

pub fn new(agent: Arc<dyn BaseAgent>, tools: Vec<Arc<dyn BaseTool>>) -> Self

Creates a new AgentExecutor.

Source

pub fn with_max_iterations(self, max_iterations: usize) -> Self

Sets max iterations, clamped to [1, 100].

Source

pub fn with_on_max_iterations(self, policy: MaxIterationsPolicy) -> Self

0.22.0 C4 fix: chooses what happens when the loop exhausts max_iterations without a final answer.

  • MaxIterationsPolicy::Error (default): the run fails with AgentError::MaxIterationsReached — the caller can distinguish “did not converge” from a real answer, and PlanExecute treats the step as failed instead of completed-with-garbage.
  • MaxIterationsPolicy::Placeholder: legacy ≤ 0.21.x behavior — return the stopped-response placeholder string.
Source

pub fn with_rule_of_two(self, on: bool) -> Self

A2 Rule of Two (v0.22.1 §S8): when enabled, a tool whose declared risk profile arms all three properties (untrusted-input + sensitive-access + state-changing) is blocked before execution and the loop receives a rejection observation. Default off — an undeclared tool has an all-false profile and is never intercepted (zero behavior change).

Source

pub fn with_tool_spotlight(self, on: bool) -> Self

A1 tool-output spotlighting (v0.22.1 §S8): when enabled, tool observations are wrapped in <untrusted_data>…</untrusted_data> before entering intermediate steps. Default off — tool output passes through unchanged.

Source

pub fn with_memory_tool( self, root: impl Into<PathBuf>, ) -> Result<Self, FileMemoryError>

C1: mounts a file-memory tool (v0.22.1 §S8).

Pushes the crate::executor::FileMemoryTool adapter over a fresh lc_memory::file_memory::FileMemoryStore rooted at root into the executor’s tool set, letting the agent explicitly view / create / write / append / delete / list named memories during a run. Default off — this is explicit opt-in; the tools are only registered when this builder is used. Err is returned when the root directory cannot be set up.

Source

pub fn with_tool_timeout(self, timeout: Duration) -> Self

Sets the tool execution timeout.

A tool that exceeds the timeout returns an error instead of hanging the whole agent loop. None (the default) disables the timeout.

Source

pub fn with_max_concurrency(self, max_concurrency: usize) -> Self

Sets the maximum number of tools executed concurrently.

Clamped to at least 1. The default is 8.

Source

pub fn with_response_cache(self, cache: Arc<dyn ResponseCache>) -> Self

Enables the LLM result cache (P2-1).

For deterministic prompts, plan() results with the same (inputs, intermediate_steps) are reused directly, skipping the LLM round-trip — suited to cost-sensitive / repeatedly-evaluated deterministic tasks. Tool execution results enter the cache key; tools themselves are not cached; the cache applies to the non-streaming invoke path.

§Example
let cache = Arc::new(MemoryCache::with_capacity(256));
let executor = AgentExecutor::new(agent, tools).with_response_cache(cache);
Source

pub fn with_tool_policy(self, policy: ToolPolicy) -> Self

Tool permission policy (permission tiering + sandbox gate, P2-9).

Checked before every tool execution: tools whose risk exceeds max_permitted are rejected; high-risk tools that are not declared sandboxed (ToolPolicy::sandboxed) are also rejected. Unconfigured = everything allowed.

§Example
let policy = ToolPolicy::new()
    .risk("code_interpreter", ToolRisk::Dangerous)
    .sandboxed("code_interpreter"); // moved into a restricted environment, allowed to run
let executor = AgentExecutor::new(agent, tools).with_tool_policy(policy);
Source

pub fn with_approval(self, handler: Arc<dyn ApprovalHandler>) -> Self

Approval gate (§4.2): async approval before each tool execution.

Default None = no interception; existing behavior unchanged. Approval decisions (implemented by the caller via ApprovalHandler):

§Example
let executor = AgentExecutor::new(agent, tools)
    .with_approval(Arc::new(AllowAll));
Source

pub fn with_budget(self, budget: BudgetConfig) -> Self

Budget gate (§4.2): hard limits, effective on both the invoke and stream paths.

  • invoke: any limit hit returns AgentError::BudgetExceeded and stops immediately;
  • stream: any limit hit sends Err(AgentError::BudgetExceeded) on the channel and stops.

The caller can catch this error to distinguish a “budget stop” from “the model did not converge”. Default None = unlimited.

§Example
let budget = BudgetConfig {
    max_tool_calls: Some(3),
    max_tokens: Some(10_000),
    max_duration: Some(Duration::from_secs(60)),
    max_iterations: Some(5),
    max_cost_usd: Some(1.0),
};
let executor = AgentExecutor::new(agent, tools).with_budget(budget);
Source

pub fn with_cost_tracker(self, tracker: Arc<CostTracker>) -> Self

B3 (0.22.4): attaches a shared CostTracker used by the max_cost_usd budget gate.

Pass the same Arc that records the agent’s LLM calls — typically via TokenTrackingLLM::with_cost_tracker (or the equivalent tracked model wrapper). After every planning call the executor reads CostTracker::total_cost_usd and hard-stops with AgentError::BudgetExceeded / super::budget::BudgetExceeded::Cost once the configured spend is reached. Attaching a tracker without a max_cost_usd limit only measures; setting a limit without a tracker never trips.

Source

pub fn with_compaction(self, compaction: CompactionConfig) -> Self

Context compaction (0.21.0 S6.1): drop the oldest intermediate steps (whole steps — action + observation stay paired) when the trigger fires.

Checked before every plan() round in both the invoke and stream paths. Off by default (None = unlimited history, zero behavior change).

§Example
let config = CompactionConfig::new(
    CompactionTrigger::TurnCount(20),
    CompactionStrategy::SlidingWindow { keep_recent_turns: 8 },
);
let executor = AgentExecutor::new(agent, tools).with_compaction(config);
Source

pub fn with_resume_store(self, store: Arc<dyn ResumeStore>) -> Self

Cross-process resume (§4.2): checkpoint store.

When enabled, before each tool call enters the approval gate to await approval, the framework writes the pending tool + the context needed to resume the agent loop (PendingApproval) into the store; it is cleared once the approval decision lands. If the process crashes, the checkpoint stays on disk; a new process rebuilding an executor with the same configuration calls pending_approval / resume to continue instead of replaying the whole conversation from scratch.

Applies only to the non-streaming invoke path (the streaming path has no approval gate); only meaningful together with with_approval. Parallel tool execution (multiple tools approved concurrently) does not participate in cross-process persistence — the in-process approval still works.

§Example
let store = Arc::new(FileResumeStore::new("/var/checkpoints/app")?);
let executor = AgentExecutor::new(agent, tools)
    .with_resume_store(store)
    .with_approval(Arc::new(MyHandler));
Source

pub fn validate_tool_registration(&self) -> Result<(), AgentError>

Tool registration validation (P2-2).

The Agent declares the tool names it may call via get_allowed_tools(); when it declares some, every one must be present in this executor’s tools, otherwise an error is returned listing all missing tools. When the Agent declares nothing (returns None, e.g. a base Agent with no tools), validation is skipped.

Called before each invoke / stream: startup fail-fast, turning a mid-loop ToolNotFound into a one-shot, all-configuration-errors-at-once report before first execution.

Source

pub fn with_verbose(self, verbose: bool) -> Self

Sets verbose output.

Source

pub fn with_memory(self, memory: Arc<Mutex<dyn BaseMemory>>) -> Self

Sets memory.

Source

pub fn with_semantic_memory( self, store: Arc<TwoTierMemory>, namespace: impl Into<String>, extractor: Arc<dyn MemoryExtractor + Send + Sync>, ) -> Self

B4 (v0.22.4): mounts two-tier semantic memory.

  • store — the shared TwoTierMemory (safe to share across executors; the namespace isolates this executor’s facts).
  • namespace — e.g. a user/session id; recall and extraction never cross it.
  • extractor — turn-to-facts extractor, typically crate::LlmMemoryExtractor.

On every run the executor recalls relevant facts and injects them into the prompt inputs under semantic_memory; after a successful answer it extracts durable facts in a detached background task and promotes hot/important facts from the short tier to the weighted-decay long tier. Off by default.

Source

pub fn with_callbacks(self, callbacks: Arc<CallbackManager>) -> Self

Sets callback manager.

Source

pub fn with_metrics_sink(self, sink: Arc<dyn MetricsSink>) -> Self

Sets an observability sink: each run (invoke / stream / resume) exports one AgentMetrics event at the end (v0.20.2). Failures are logged, never propagated.

Source

pub fn hook(self, hook: impl AgentHook + 'static) -> Self

Adds an agent hook.

Source

pub fn last_metrics(&self) -> Option<AgentMetrics>

Returns metrics from the most recent invocation, if any.

Source

pub async fn pending_approval( &self, ) -> Result<Option<PendingApproval>, AgentError>

Reads the currently pending approval checkpoint (cross-process resume).

Returns Ok(None) when no ResumeStore is configured or the store is empty. After getting a PendingApproval, the caller shows tool_name / arguments to an operator, collects the approval decision, then calls resume to continue.

Source

pub async fn resume( &self, decision: ApprovalDecision, ) -> Result<Option<String>, AgentError>

Resumes from a checkpoint (cross-process resume): processes the pending tool with the given decision, then continues the agent loop from the suspended iteration and returns the final answer.

  • No ResumeStore configured or no checkpoint → Ok(None) (no-op).
  • A checkpoint exists → first claims it (clears it) to prevent duplicate approval, executes the pending tool, then continues the loop from iteration + 1; budgets (tool / token / iteration) keep counting from the checkpoint’s accumulated amounts, and max_duration restarts its timer at the resume moment (a cross-process monotonic clock is not portable — an honest approximation).

The resuming executor must be constructed identically to the one before the crash (same agent / tools / store directory) to resume correctly; the approval decision is injected by the caller and ApprovalHandler is not re-run.

Source

pub async fn invoke(&self, input: String) -> Result<String, AgentError>

Executes the agent.

Source

pub async fn invoke_with_config( &self, input: String, config: Option<RunnableConfig>, ) -> Result<String, AgentError>

Execute the agent with a RunnableConfig, merging config callbacks with the executor’s own callbacks.

This is the entry point used by AgentRunnable (LCEL adapter). Config callbacks take precedence over the executor’s callbacks.

Source

pub fn stream( &self, input: String, ) -> Pin<Box<dyn Stream<Item = Result<AgentStreamEvent, AgentError>> + Send>>

Stream agent execution as a true async stream of events.

Each step of the agent loop (tool calls, observations, final answer) is emitted as an AgentStreamEvent as soon as it occurs.

§Error semantics (A9, unified)

The stream item is Result<AgentStreamEvent, AgentError>. A terminal failure — a permission-policy rejection, a tool timeout, a guarded-tool abort, or budget exhaustion (A-S2 / A-H1) — is delivered as an Err(AgentError), which terminates the stream. There is no successful Ok(AgentStreamEvent::Error { .. }); that variant exists for infallible streams (e.g. crate::StreamingFunctionCallingAgent) and in-band errors.

§Text event granularity (F3, honest)

Text events carry model text, but their granularity depends on the agent’s BaseAgent::plan_stream implementation:

  • ReAct and FunctionCalling agents stream from the model’s chat API, so Text events arrive per token — concat them as they come for a live word-stream. A function-calling step that calls a tool streams back empty model text (tool calls aren’t carried in stream chunks); such steps fall back to the non-streaming path internally, so no phantom empty Text is emitted.
  • Other agents (plan-and-execute without a streaming inner agent, …) use the non-streaming default, so the whole final answer arrives as a single Text event immediately before FinalAnswer.

ToolStart/ToolEnd events are always emitted per tool call.

§Example
let mut stream = executor.stream("What is Rust?".to_string());
while let Some(event) = stream.next().await {
    match event {
        Ok(AgentStreamEvent::ToolStart { name, input }) => { /* show tool call */ }
        Ok(AgentStreamEvent::ToolEnd { name, output }) => { /* show result */ }
        Ok(AgentStreamEvent::Text { content }) => { print!("{}", content); } /* model text */
        Ok(AgentStreamEvent::FinalAnswer { content }) => { /* show answer */ }
        Err(e) => { /* terminal failure — the loop has ended */ }
        _ => {}
    }
}

Trait Implementations§

Source§

impl Debug for AgentExecutor

Source§

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

Formats the value using the given formatter. 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> 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: Sized + 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: Sized + 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> Same for T

Source§

type Output = T

Should always be Self
Source§

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

Source§

type Error = !

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

fn try_from(value: U) -> Result<T, !>

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<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