use super::budget::{budget_iteration_gate, budget_token_gate, budget_tool_gate};
use super::engine::AgentExecutor;
use super::tools::{run_tool_with_timeout, tool_error_observation};
use super::AgentError;
use crate::approval::ApprovalDecision;
use crate::hooks::{ToolCallAction, ToolCallContext, ToolResultContext};
use crate::metrics::AgentMetrics;
use crate::resume::{PendingApproval, ResumeStore};
use crate::types::{AgentAction, AgentOutput, AgentStep, ToolInput};
use lc_callbacks::{RunTree, RunType};
use serde_json::json;
use std::collections::HashMap;
use std::sync::Arc;
use std::time::Instant;
pub(crate) struct ResumeContext<'a> {
pending: &'a PendingApproval,
store: &'a Arc<dyn ResumeStore>,
}
fn apply_approval_decision(
decision: ApprovalDecision,
tool_ctx: &mut ToolCallContext,
) -> Option<String> {
match decision {
ApprovalDecision::Allow => None,
ApprovalDecision::Deny { reason } => {
log::info!(
target: "lc_agents::approval",
"tool_call denied by approval handler name={} reason={}",
tool_ctx.name,
reason
);
Some(reason)
}
ApprovalDecision::Modify { arguments, note } => {
log::info!(
target: "lc_agents::approval",
"tool_call arguments modified by approval handler name={} note={}",
tool_ctx.name,
note
);
tool_ctx.arguments = arguments;
None
}
}
}
impl AgentExecutor {
pub(crate) async fn run_agent_loop(
&self,
inputs: HashMap<String, String>,
intermediate_steps: Vec<AgentStep>,
root_run: &mut RunTree,
metrics: &mut AgentMetrics,
) -> Result<String, AgentError> {
self.run_agent_loop_from(inputs, intermediate_steps, 0, root_run, metrics)
.await
}
pub(crate) async fn run_agent_loop_from(
&self,
inputs: HashMap<String, String>,
mut intermediate_steps: Vec<AgentStep>,
start_iteration: usize,
root_run: &mut RunTree,
metrics: &mut AgentMetrics,
) -> Result<String, AgentError> {
let loop_start = Instant::now();
for iteration in start_iteration..self.max_iterations {
if let Some(err) = budget_iteration_gate(
self.budget.as_ref(),
self.max_iterations,
iteration,
loop_start,
) {
return Err(err);
}
if self.verbose {
log::info!("=== Iteration {} ===", iteration + 1);
}
let output = self
.plan_cached(&intermediate_steps, &inputs, metrics)
.await?;
if let Some(err) = budget_token_gate(self.budget.as_ref(), metrics) {
return Err(err);
}
match output {
AgentOutput::Finish(finish) => {
if self.verbose {
log::info!("Final answer: {:?}", finish.return_values);
}
return Ok(finish.output().unwrap_or("").to_string());
}
AgentOutput::Action(action) => {
metrics.tool_calls += 1;
if self.verbose {
log::info!("Action: {}({})", action.tool, action.tool_input);
}
if let Some(err) = budget_tool_gate(self.budget.as_ref(), metrics, loop_start) {
return Err(err);
}
let pending = PendingApproval {
tool_name: action.tool.clone(),
arguments: serde_json::Value::Null,
tool_id: String::new(),
inputs: inputs.clone(),
steps: intermediate_steps.clone(),
iteration,
tool_calls_consumed: metrics.tool_calls,
tokens_consumed: metrics.total_tokens,
trace_id: root_run.trace_id.map(|id| id.to_string()),
};
let resume_ctx = self.resume_store.as_ref().map(|store| ResumeContext {
pending: &pending,
store,
});
let observation = self
.execute_tool_inner(&action, root_run, resume_ctx.as_ref(), None)
.await?;
if self.verbose {
log::info!("Observation: {}", observation);
}
intermediate_steps.push(AgentStep::new(action, observation));
}
AgentOutput::Actions(actions) => {
metrics.tool_calls += actions.len();
if self.verbose {
log::info!("Parallel actions: {} count", actions.len());
for action in &actions {
log::info!(" - {}({})", action.tool, action.tool_input);
}
}
if let Some(err) = budget_tool_gate(self.budget.as_ref(), metrics, loop_start) {
return Err(err);
}
let observations = self.execute_tools_parallel(&actions, root_run).await?;
if self.verbose {
for (i, obs) in observations.iter().enumerate() {
log::info!("Observation {}: {}", i + 1, obs);
}
}
for (action, observation) in actions.into_iter().zip(observations.into_iter()) {
intermediate_steps.push(AgentStep::new(action, observation));
}
}
}
}
log::warn!(
"agent reached max iterations {} without returning a final answer; returning a placeholder result (not the real final answer)",
self.max_iterations
);
let finish = self.agent.return_stopped_response(&intermediate_steps);
Ok(finish.output().unwrap_or("").to_string())
}
async fn execute_tools_parallel(
&self,
actions: &[AgentAction],
root_run: &RunTree,
) -> Result<Vec<String>, AgentError> {
use futures_util::future::join_all;
let sem = self.concurrency_sem.clone();
let futures = actions.iter().map(|action| {
let sem = sem.clone();
async move {
let _permit = sem
.acquire_owned()
.await
.map_err(|e| AgentError::Other(format!("concurrency semaphore closed: {e}")))?;
self.execute_tool(action, root_run).await
}
});
let results = join_all(futures).await;
let mut observations = Vec::with_capacity(results.len());
for result in results {
match result {
Ok(output) => observations.push(output),
Err(e) => observations.push(tool_error_observation(&e)),
}
}
Ok(observations)
}
async fn execute_tool(
&self,
action: &AgentAction,
root_run: &RunTree,
) -> Result<String, AgentError> {
self.execute_tool_inner(action, root_run, None, None).await
}
pub(crate) async fn execute_tool_inner(
&self,
action: &AgentAction,
root_run: &RunTree,
resume_ctx: Option<&ResumeContext<'_>>,
pre_decided: Option<ApprovalDecision>,
) -> Result<String, AgentError> {
let tool = self
.tools
.iter()
.find(|t| t.name() == action.tool)
.ok_or_else(|| AgentError::ToolNotFound(action.tool.clone()))?;
let _input_str = match &action.tool_input {
ToolInput::String { value: s } => s.clone(),
ToolInput::Object { value: v } => serde_json::to_string(v)
.map_err(|e| AgentError::Other(format!("Failed to serialize tool input: {}", e)))?,
};
let mut tool_ctx = ToolCallContext {
name: action.tool.clone(),
arguments: match &action.tool_input {
ToolInput::String { value: s } => {
serde_json::from_str::<serde_json::Value>(s)
.unwrap_or(serde_json::Value::String(s.clone()))
}
ToolInput::Object { value: v } => v.clone(),
},
tool_id: String::new(),
};
for hook in &self.hooks {
match hook.on_before_tool_call(&mut tool_ctx) {
ToolCallAction::Continue => {}
ToolCallAction::Modify { name, arguments } => {
tool_ctx.name = name;
tool_ctx.arguments = arguments;
}
ToolCallAction::Reject { reason } => {
return Err(AgentError::Other(format!(
"Tool call rejected by hook: {}",
reason
)));
}
ToolCallAction::Skip => {
return Ok("[Skipped by hook]".to_string());
}
}
}
let deny_reason: Option<String> = if let Some(pre) = pre_decided {
apply_approval_decision(pre, &mut tool_ctx)
} else if let Some(handler) = &self.approval {
if let Some(ctx) = resume_ctx {
let mut pending = ctx.pending.clone();
pending.tool_name = tool_ctx.name.clone();
pending.arguments = tool_ctx.arguments.clone();
pending.tool_id = tool_ctx.tool_id.clone();
if let Err(e) = ctx.store.save_pending(&pending).await {
log::warn!(
target: "lc_agents::resume",
"failed to persist pending approval: {}",
e
);
}
}
apply_approval_decision(handler.approve(&tool_ctx).await, &mut tool_ctx)
} else {
None
};
if let Some(ctx) = resume_ctx {
if let Err(e) = ctx.store.clear_pending().await {
log::warn!(
target: "lc_agents::resume",
"failed to clear pending approval: {}",
e
);
}
}
if let Some(reason) = deny_reason {
return Ok(format!("[DENIED by approval: {reason}]"));
}
let tool_name = tool_ctx.name.clone();
if let Some(policy) = &self.tool_policy {
policy.check(&tool_name)?;
}
let input_for_tool = serde_json::to_string(&tool_ctx.arguments)
.unwrap_or_else(|_| tool_ctx.arguments.to_string());
let mut tool_run = root_run.create_child(
&tool_name,
RunType::Tool,
json!({"input": input_for_tool.clone()}),
);
if let Some(ref callbacks) = self.callbacks {
for handler in callbacks.handlers() {
handler
.on_tool_start(&tool_run, &tool_name, &input_for_tool)
.await;
}
}
let tool_started = std::time::Instant::now();
let result = run_tool_with_timeout(tool, input_for_tool.clone(), self.tool_timeout).await;
let tool_duration_ms = tool_started.elapsed().as_millis();
let trace = root_run
.trace_id
.map(|id| id.to_string())
.unwrap_or_default();
match result {
Ok(output) => {
log::info!(
target: "lc_agents::audit",
"tool_call trace_id={} name={} input={} duration_ms={} outcome=ok",
trace,
tool_name,
input_for_tool,
tool_duration_ms
);
tool_run.end(json!({"output": output.clone()}));
if let Some(ref callbacks) = self.callbacks {
for handler in callbacks.handlers() {
handler.on_tool_end(&tool_run, &output).await;
}
}
let mut result_ctx = ToolResultContext {
name: tool_name,
result: output.clone(),
tool_id: String::new(),
};
for hook in &self.hooks {
if let Err(e) = hook.on_after_tool_call(&mut result_ctx) {
log::warn!("Hook on_after_tool_call error: {}", e);
}
}
Ok(result_ctx.result)
}
Err(e) => {
log::info!(
target: "lc_agents::audit",
"tool_call trace_id={} name={} input={} duration_ms={} outcome=error:{}",
trace,
tool_name,
input_for_tool,
tool_duration_ms,
e
);
tool_run.end_with_error(e.to_string());
if let Some(ref callbacks) = self.callbacks {
for handler in callbacks.handlers() {
handler.on_tool_error(&tool_run, &e.to_string()).await;
}
}
Err(AgentError::ToolExecutionError(e.to_string()))
}
}
}
}