pub struct AgentExecutor { /* private fields */ }Expand description
Agent executor.
Responsible for executing the agent’s decision loop: Plan -> Act -> Observe.
Implementations§
Source§impl AgentExecutor
impl AgentExecutor
Sourcepub fn new(agent: Arc<dyn BaseAgent>, tools: Vec<Arc<dyn BaseTool>>) -> Self
pub fn new(agent: Arc<dyn BaseAgent>, tools: Vec<Arc<dyn BaseTool>>) -> Self
Creates a new AgentExecutor.
Sourcepub fn with_max_iterations(self, max_iterations: usize) -> Self
pub fn with_max_iterations(self, max_iterations: usize) -> Self
Sets max iterations, clamped to [1, 100].
Sourcepub fn with_on_max_iterations(self, policy: MaxIterationsPolicy) -> Self
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 withAgentError::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.
Sourcepub fn with_rule_of_two(self, on: bool) -> Self
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).
Sourcepub fn with_tool_spotlight(self, on: bool) -> Self
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.
Sourcepub fn with_memory_tool(
self,
root: impl Into<PathBuf>,
) -> Result<Self, FileMemoryError>
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.
Sourcepub fn with_tool_timeout(self, timeout: Duration) -> Self
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.
Sourcepub fn with_max_concurrency(self, max_concurrency: usize) -> Self
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.
Sourcepub fn with_response_cache(self, cache: Arc<dyn ResponseCache>) -> Self
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);Sourcepub fn with_tool_policy(self, policy: ToolPolicy) -> Self
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);Sourcepub fn with_approval(self, handler: Arc<dyn ApprovalHandler>) -> Self
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):
ApprovalDecision::Allow: run as-is;ApprovalDecision::Deny: skip the tool, feed the reason back as an observation, and re-plan next round;ApprovalDecision::Modify: run with the new arguments substituted.
§Example
let executor = AgentExecutor::new(agent, tools)
.with_approval(Arc::new(AllowAll));Sourcepub fn with_budget(self, budget: BudgetConfig) -> Self
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 returnsAgentError::BudgetExceededand stops immediately;stream: any limit hit sendsErr(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);Sourcepub fn with_cost_tracker(self, tracker: Arc<CostTracker>) -> Self
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.
Sourcepub fn with_compaction(self, compaction: CompactionConfig) -> Self
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);Sourcepub fn with_resume_store(self, store: Arc<dyn ResumeStore>) -> Self
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));Sourcepub fn validate_tool_registration(&self) -> Result<(), AgentError>
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.
Sourcepub fn with_verbose(self, verbose: bool) -> Self
pub fn with_verbose(self, verbose: bool) -> Self
Sets verbose output.
Sourcepub fn with_memory(self, memory: Arc<Mutex<dyn BaseMemory>>) -> Self
pub fn with_memory(self, memory: Arc<Mutex<dyn BaseMemory>>) -> Self
Sets memory.
Sourcepub fn with_semantic_memory(
self,
store: Arc<TwoTierMemory>,
namespace: impl Into<String>,
extractor: Arc<dyn MemoryExtractor + Send + Sync>,
) -> Self
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 sharedTwoTierMemory(safe to share across executors; thenamespaceisolates this executor’s facts).namespace— e.g. a user/session id; recall and extraction never cross it.extractor— turn-to-facts extractor, typicallycrate::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.
Sourcepub fn with_callbacks(self, callbacks: Arc<CallbackManager>) -> Self
pub fn with_callbacks(self, callbacks: Arc<CallbackManager>) -> Self
Sets callback manager.
Sourcepub fn with_metrics_sink(self, sink: Arc<dyn MetricsSink>) -> Self
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.
Sourcepub fn last_metrics(&self) -> Option<AgentMetrics>
pub fn last_metrics(&self) -> Option<AgentMetrics>
Returns metrics from the most recent invocation, if any.
Sourcepub async fn pending_approval(
&self,
) -> Result<Option<PendingApproval>, AgentError>
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.
Sourcepub async fn resume(
&self,
decision: ApprovalDecision,
) -> Result<Option<String>, AgentError>
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
ResumeStoreconfigured 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, andmax_durationrestarts 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.
Sourcepub async fn invoke_with_config(
&self,
input: String,
config: Option<RunnableConfig>,
) -> Result<String, AgentError>
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.
Sourcepub fn stream(
&self,
input: String,
) -> Pin<Box<dyn Stream<Item = Result<AgentStreamEvent, AgentError>> + Send>>
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
Textevents 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 emptyTextis 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
Textevent immediately beforeFinalAnswer.
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 */ }
_ => {}
}
}