use serde_json::json;
use std::collections::HashSet;
use zagens_tools::{ToolError, ToolResult};
use crate::chat::{ContentBlock, Message, Tool};
use crate::engine::context::{compact_tool_result_for_context, summarize_text};
use crate::engine::dispatch::{
caller_allowed_for_tool, caller_type_for_tool_use, format_tool_error,
should_stop_after_plan_tool,
};
use crate::engine::emit_tool_audit;
use crate::engine::kernel_event::{KernelEvent, PolicyDecision, ToolOutcome as KernelToolOutcome};
use crate::engine::loop_guard::{AttemptDecision, LoopGuard, OutcomeDecision};
use crate::engine::streaming::ToolUseState;
use crate::engine::tool_catalog::{
CODE_EXECUTION_TOOL_NAME, REQUEST_USER_INPUT_NAME, is_audit_scratchpad_bind_tool,
is_tool_search_tool, missing_tool_error_message, scratchpad_defer_set_area_batch_error,
};
use crate::engine::tool_effects::tool_writes_state;
use crate::engine::turn_loop::control::TurnLoopToolPhaseOutcome;
use crate::engine::turn_loop::exec::ToolExecutionPlan;
use crate::engine::turn_loop::inner_step_host::InnerStepHost;
use crate::engine::turn_machine::emit_kernel_event;
use crate::error_taxonomy::ErrorEnvelope;
use crate::turn::{TurnContext, TurnLoopMode, TurnToolCall};
#[allow(clippy::too_many_arguments)]
pub async fn run_tool_execution_phase<H: InnerStepHost>(
host: &mut H,
turn: &mut TurnContext,
mode: TurnLoopMode,
tool_uses: &mut [ToolUseState],
tool_catalog: &mut [Tool],
active_tool_names: &mut HashSet<String>,
loop_guard: &mut LoopGuard,
consecutive_tool_error_steps: u32,
tool_registry: Option<&H::ToolRegistry>,
) -> TurnLoopToolPhaseOutcome {
let tool_exec_lock = host.tool_exec_lock();
let mcp_pool = host.ensure_mcp_pool_for_tools(tool_uses).await;
let defer_set_area_batch_count = tool_uses
.iter()
.filter(|tool| {
tool.name == "scratchpad_set_area"
&& tool.input.get("status").and_then(serde_json::Value::as_str) == Some("deferred")
})
.count();
let mut plans: Vec<ToolExecutionPlan> = Vec::with_capacity(tool_uses.len());
for (index, tool) in tool_uses.iter_mut().enumerate() {
let tool_id = tool.id.clone();
let mut tool_name = tool.name.clone();
let tool_input = tool.input.clone();
let tool_caller = tool.caller.clone();
tracing::info!("Planning tool '{}' with input: {:?}", tool_name, tool_input);
let interactive = (tool_name == "exec_shell"
&& tool_input
.get("interactive")
.and_then(serde_json::Value::as_bool)
== Some(true))
|| tool_name == REQUEST_USER_INPUT_NAME;
let mut approval_required = false;
let mut approval_description = "Tool execution requires approval".to_string();
let mut supports_parallel = false;
let mut read_only = false;
let mut blocked_error: Option<ToolError> = None;
let mut guard_result: Option<ToolResult> = None;
if host.maybe_activate_deferred_tool(&tool_name, tool_catalog, active_tool_names) {
let _ = host
.tx_event()
.send(crate::events::Event::status(format!(
"Auto-loaded deferred tool '{tool_name}' after model request."
)))
.await;
emit_kernel_event(
host,
KernelEvent::DeferredToolActivated {
turn_id: turn.id.clone(),
step_idx: turn.step,
tool_name: tool_name.clone(),
},
);
}
let mut tool_def = tool_catalog.iter().find(|def| def.name == tool_name);
if tool_def.is_none()
&& let Some(canonical) =
host.resolve_hallucinated_tool_name(&tool_name, tool_catalog, tool_registry)
{
tracing::info!(
"Resolved hallucinated tool name '{}' -> '{}'",
tool_name,
canonical
);
tool_def = tool_catalog.iter().find(|d| d.name == canonical);
if tool_def.is_some() {
tool_name = canonical;
tool.name = tool_name.clone();
if host.maybe_activate_deferred_tool(&tool_name, tool_catalog, active_tool_names) {
emit_kernel_event(
host,
KernelEvent::DeferredToolActivated {
turn_id: turn.id.clone(),
step_idx: turn.step,
tool_name: tool_name.clone(),
},
);
let _ = host
.tx_event()
.send(crate::events::Event::status(format!(
"Auto-loaded deferred tool '{}' after resolving '{}'.",
tool_name, tool_name
)))
.await;
}
}
}
if !caller_allowed_for_tool(tool_caller.as_ref(), tool_def) {
blocked_error = Some(ToolError::permission_denied(format!(
"Tool '{tool_name}' does not allow caller '{}'",
caller_type_for_tool_use(tool_caller.as_ref())
)));
}
if blocked_error.is_none()
&& tool_def.is_none()
&& !host.is_mcp_tool_name(&tool_name)
&& tool_name != CODE_EXECUTION_TOOL_NAME
&& !is_tool_search_tool(&tool_name)
{
blocked_error = Some(ToolError::not_available(missing_tool_error_message(
&tool_name,
tool_catalog,
)));
}
if blocked_error.is_none() {
let meta = host.tool_plan_approval_meta(&tool_name, &tool_input, tool_registry);
approval_required = meta.approval_required;
approval_description = meta.approval_description;
supports_parallel = meta.supports_parallel;
read_only = meta.read_only;
}
if blocked_error.is_none()
&& tool_name == "scratchpad_set_area"
&& tool_input.get("status").and_then(serde_json::Value::as_str) == Some("deferred")
&& let Some(err) = scratchpad_defer_set_area_batch_error(defer_set_area_batch_count)
{
blocked_error = Some(err);
emit_kernel_event(
host,
KernelEvent::LoopGuardTriggered {
turn_id: turn.id.clone(),
call_id: tool_id.clone(),
reason: "deferred_set_area_batch".into(),
},
);
}
if blocked_error.is_none()
&& let AttemptDecision::Block(message) =
loop_guard.record_attempt(&tool_name, &tool_input)
{
tracing::warn!("{}", message);
emit_kernel_event(
host,
KernelEvent::LoopGuardTriggered {
turn_id: turn.id.clone(),
call_id: tool_id.clone(),
reason: "identical_tool_call".into(),
},
);
guard_result = Some(
ToolResult::success(message)
.with_metadata(json!({"loop_guard": "identical_tool_call"})),
);
}
plans.push(ToolExecutionPlan {
index,
id: tool_id,
name: tool_name,
input: tool_input,
caller: tool_caller,
interactive,
approval_required,
approval_description,
supports_parallel,
read_only,
blocked_error,
guard_result,
});
}
for plan in &plans {
emit_kernel_event(
host,
KernelEvent::ToolCallPlanned {
turn_id: turn.id.clone(),
step_idx: turn.step,
call_id: plan.id.clone(),
tool_name: plan.name.clone(),
input_json: serde_json::to_string(&plan.input).unwrap_or_else(|_| "{}".into()),
decision: PolicyDecision::new(
plan.approval_required,
plan.supports_parallel,
plan.read_only,
),
},
);
emit_kernel_event(
host,
KernelEvent::ToolCallStarted {
turn_id: turn.id.clone(),
call_id: plan.id.clone(),
wave_idx: 0,
},
);
}
let outcomes = host
.execute_tool_plans(
mode,
plans,
tool_catalog,
active_tool_names,
tool_registry,
mcp_pool.clone(),
tool_exec_lock.clone(),
)
.await;
let mut step_error_count = 0usize;
let mut step_error_categories = Vec::new();
let mut stop_after_plan_tool = false;
let mut loop_guard_halt: Option<String> = None;
for outcome in outcomes {
let duration = outcome.started_at.elapsed();
let tool_input = outcome.input.clone();
let tool_name_for_ws = outcome.name.clone();
let mut tool_call =
TurnToolCall::new(outcome.id.clone(), outcome.name.clone(), outcome.input);
let should_stop_this_turn =
should_stop_after_plan_tool(mode == TurnLoopMode::Plan, &outcome.name, &outcome.result);
let session_content_for_log;
match outcome.result {
Ok(output) => {
host.record_scratchpad_tool_outcome(&outcome.name, output.success);
let mut result_text = output.content.clone();
host.record_long_horizon_tool_outcome(
&outcome.name,
&tool_input,
&result_text,
output.success,
)
.await;
if let Some(suffix) = host.take_long_horizon_tool_suffix() {
result_text.push_str(&suffix);
}
if output.success && is_audit_scratchpad_bind_tool(&outcome.name) {
host.on_audit_scratchpad_bind_success(
mode,
&outcome.name,
tool_catalog,
active_tool_names,
);
}
match loop_guard.record_outcome(&outcome.name, output.success) {
OutcomeDecision::Continue => {}
OutcomeDecision::Warn(message) => {
tracing::warn!("{}", message);
let _ = host
.tx_event()
.send(crate::events::Event::status(message))
.await;
}
OutcomeDecision::Halt(message) => {
emit_kernel_event(
host,
KernelEvent::LoopGuardTriggered {
turn_id: turn.id.clone(),
call_id: outcome.id.clone(),
reason: "failure_halt".into(),
},
);
loop_guard_halt.get_or_insert(message);
}
}
if output.success && LoopGuard::is_state_mutating_tool(&outcome.name) {
loop_guard.note_state_changed();
}
emit_tool_audit(json!({
"event": "tool.result",
"tool_id": outcome.id.clone(),
"tool_name": outcome.name.clone(),
"success": output.success,
}));
let workspace = host.workspace().to_path_buf();
let session_model = host.session_mut().model.clone();
let mut output_for_model = output.clone();
output_for_model.content = result_text.clone();
let output_for_context = compact_tool_result_for_context(
&session_model,
&outcome.name,
&output_for_model,
);
let output_content = result_text;
tool_call.set_result(output_content.clone(), duration);
host.session_mut().working_set.observe_tool_call(
&tool_name_for_ws,
&tool_input,
Some(&output_for_context),
&workspace,
);
if output.success {
host.run_post_edit_lsp_hook(&outcome.name, &tool_input)
.await;
}
session_content_for_log = output_for_context.clone();
host.add_session_message(Message {
role: "user".to_string(),
content: vec![ContentBlock::ToolResult {
tool_use_id: outcome.id.clone(),
content: output_for_context.clone(),
is_error: None,
content_blocks: None,
}],
})
.await;
}
Err(e) => {
match loop_guard.record_outcome(&outcome.name, false) {
OutcomeDecision::Continue => {}
OutcomeDecision::Warn(message) => {
tracing::warn!("{}", message);
let _ = host
.tx_event()
.send(crate::events::Event::status(message))
.await;
}
OutcomeDecision::Halt(message) => {
emit_kernel_event(
host,
KernelEvent::LoopGuardTriggered {
turn_id: turn.id.clone(),
call_id: outcome.id.clone(),
reason: "failure_halt".into(),
},
);
loop_guard_halt.get_or_insert(message);
}
}
let envelope: ErrorEnvelope = e.clone().into();
emit_tool_audit(json!({
"event": "tool.result",
"tool_id": outcome.id.clone(),
"tool_name": outcome.name.clone(),
"success": false,
"error": e.to_string(),
"category": envelope.category.to_string(),
"severity": envelope.severity.to_string(),
}));
step_error_count += 1;
step_error_categories.push(envelope.category);
let error = format_tool_error(&e, &outcome.name);
tool_call.set_error(error.clone(), duration);
let session_error_content = format!("Error: {error}");
let workspace = host.workspace().to_path_buf();
host.session_mut().working_set.observe_tool_call(
&tool_name_for_ws,
&tool_input,
Some(&error),
&workspace,
);
session_content_for_log = session_error_content.clone();
host.add_session_message(Message {
role: "user".to_string(),
content: vec![ContentBlock::ToolResult {
tool_use_id: outcome.id.clone(),
content: session_error_content,
is_error: Some(true),
content_blocks: None,
}],
})
.await;
}
}
{
let wrote = tool_writes_state(&tool_call.name);
let kernel_outcome = if tool_call.result.is_some() {
KernelToolOutcome::Success
} else {
KernelToolOutcome::ToolError {
message: tool_call.error.clone().unwrap_or_default(),
}
};
let result_preview = tool_call
.result
.as_deref()
.or(tool_call.error.as_deref())
.map(|text| summarize_text(text, 512))
.unwrap_or_default();
emit_kernel_event(
host,
KernelEvent::ToolCallFinished {
turn_id: turn.id.clone(),
call_id: tool_call.id.clone(),
tool_name: tool_call.name.clone(),
outcome: kernel_outcome,
duration_ms: tool_call
.duration
.map(|d| d.as_millis() as u32)
.unwrap_or(0),
wrote_state: wrote,
result_preview,
session_content: session_content_for_log,
},
);
}
turn.record_tool_call(tool_call);
stop_after_plan_tool |= should_stop_this_turn;
}
let mut outcome = TurnLoopToolPhaseOutcome {
step_error_count,
step_error_categories,
break_outer_loop: stop_after_plan_tool || loop_guard_halt.is_some(),
loop_guard_halted: loop_guard_halt.is_some(),
continue_outer_loop: false,
};
if let Some(message) = loop_guard_halt {
tracing::warn!("{}", message);
let _ = host
.tx_event()
.send(crate::events::Event::status(message))
.await;
}
outcome.continue_outer_loop = host
.run_capacity_post_tool_checkpoint(
turn,
mode,
tool_registry,
tool_exec_lock,
mcp_pool,
step_error_count,
consecutive_tool_error_steps,
)
.await;
outcome
}