use super::budget_blocking::{DuplicateSendFileNoopCtx, ToolBlockKind, ToolBudgetBlockCtx};
use super::execution_io::ToolExecutionIoCtx;
use super::guards::LoopPatternGuardOutcome;
use super::project_dir::{
extract_project_dir_hint_with_aliases, is_file_recheck_tool,
maybe_inject_project_dir_into_tool_args, project_dir_from_tool_args,
tool_call_includes_project_path,
};
use super::result_learning::{ResultLearningEnv, ResultLearningState};
use super::run_helpers::*;
use super::types::{ToolExecutionCtx, ToolExecutionOutcome};
use crate::agent::execution_state::OutcomeEntry;
use crate::agent::loop_state::{
canonical_path_from_arguments, LineInterval, ReadDecision, ReadRequest,
};
use crate::agent::recall_guardrails::is_personal_memory_tool;
use crate::agent::*;
use crate::events::TaskOutcome;
fn format_line_intervals(intervals: &[LineInterval]) -> String {
intervals
.iter()
.map(|interval| {
if interval.start == interval.end {
interval.start.to_string()
} else {
format!("{}-{}", interval.start, interval.end)
}
})
.collect::<Vec<_>>()
.join(", ")
}
pub(in crate::agent) async fn run_tool_execution_phase(
services: &crate::agent::services::AgentServices<'_>,
ctx: &mut ToolExecutionCtx<'_>,
) -> anyhow::Result<ToolExecutionOutcome> {
let agent = services.agent;
let resp = ctx.resp;
let emitter = ctx.emitter;
let task_id = ctx.task_id;
let session_id = ctx.session_id;
let iteration = ctx.iteration;
let task_start = ctx.task_start;
let learning_ctx = &mut *ctx.learning_ctx;
let task_tokens_used = ctx.task_tokens_used;
let _user_text = ctx.user_text;
let model = ctx.model;
let restrict_to_personal_memory_tools = ctx.restrict_to_personal_memory_tools;
let active_skill_names = ctx.active_skill_names;
let active_untrusted_external_reference_skills = ctx.active_untrusted_external_reference_skills;
let restrict_untrusted_external_reference_tools =
ctx.restrict_untrusted_external_reference_tools;
let is_reaffirmation_challenge_turn = ctx.is_reaffirmation_challenge_turn;
let personal_memory_tool_call_cap = ctx.personal_memory_tool_call_cap;
let base_tool_defs = ctx.base_tool_defs;
let available_capabilities = ctx.available_capabilities;
let policy_bundle = ctx.policy_bundle;
let status_tx = ctx.status_tx.clone();
let channel_ctx = ctx.channel_ctx;
let user_role = ctx.user_role;
let heartbeat = ctx.heartbeat;
let turn_context = ctx.turn_context;
let resolved_goal_id = ctx.resolved_goal_id;
let evidence_state = &mut *ctx.evidence_state;
let validation_state = &mut *ctx.validation_state;
let read_file_tracker = &mut *ctx.read_file_tracker;
let mut tool_defs = std::mem::take(ctx.tool_defs);
let mut total_tool_calls_attempted = *ctx.total_tool_calls_attempted;
let mut total_successful_tool_calls = *ctx.total_successful_tool_calls;
let mut tool_failure_count = std::mem::take(ctx.tool_failure_count);
let mut tool_failure_signatures = std::mem::take(ctx.tool_failure_signatures);
let mut tool_transient_failure_count = std::mem::take(ctx.tool_transient_failure_count);
let mut tool_cooldown_until_iteration = std::mem::take(ctx.tool_cooldown_until_iteration);
let mut tool_call_count = std::mem::take(ctx.tool_call_count);
let mut personal_memory_tool_calls = *ctx.personal_memory_tool_calls;
let mut no_evidence_result_streak = *ctx.no_evidence_result_streak;
let mut no_evidence_tools_seen = std::mem::take(ctx.no_evidence_tools_seen);
let mut evidence_gain_count = *ctx.evidence_gain_count;
let mut pending_error_solution_ids = std::mem::take(ctx.pending_error_solution_ids);
let mut tool_error_history = std::mem::take(ctx.tool_error_history);
let mut reflection_completed = std::mem::take(ctx.reflection_completed);
let mut pending_reflection_recoveries = std::mem::take(ctx.pending_reflection_recoveries);
let mut tool_failure_patterns = std::mem::take(ctx.tool_failure_patterns);
let mut last_tool_failure = std::mem::take(ctx.last_tool_failure);
let mut in_session_learned = std::mem::take(ctx.in_session_learned);
let mut unknown_tools = std::mem::take(ctx.unknown_tools);
let mut recent_tool_calls = std::mem::take(ctx.recent_tool_calls);
let mut consecutive_same_tool = std::mem::take(ctx.consecutive_same_tool);
let mut consecutive_same_tool_arg_hashes = std::mem::take(ctx.consecutive_same_tool_arg_hashes);
let mut force_text_response = *ctx.force_text_response;
let mut pending_system_messages = std::mem::take(ctx.pending_system_messages);
let mut recent_tool_names = std::mem::take(ctx.recent_tool_names);
let mut successful_send_file_keys = std::mem::take(ctx.successful_send_file_keys);
let mut cli_agent_boundary_injected = *ctx.cli_agent_boundary_injected;
let mut pending_background_ack = std::mem::take(ctx.pending_background_ack);
let mut pending_external_action_ack: Option<String> = None;
let mut stall_count = *ctx.stall_count;
let mut deferred_no_tool_streak = *ctx.deferred_no_tool_streak;
let mut consecutive_clean_iterations = *ctx.consecutive_clean_iterations;
let mut fallback_expanded_once = *ctx.fallback_expanded_once;
let mut known_project_dir = std::mem::take(ctx.known_project_dir);
let mut dirs_with_project_inspect_file_evidence =
std::mem::take(ctx.dirs_with_project_inspect_file_evidence);
let mut dirs_with_search_no_matches = std::mem::take(ctx.dirs_with_search_no_matches);
let mut require_file_recheck_before_answer = *ctx.require_file_recheck_before_answer;
let mut completion_progress = ctx.completion_progress.clone();
let mut tool_result_cache = std::mem::take(ctx.tool_result_cache);
let execution_state = &mut *ctx.execution_state;
macro_rules! commit_state {
() => {
*ctx.tool_defs = tool_defs;
*ctx.total_tool_calls_attempted = total_tool_calls_attempted;
*ctx.total_successful_tool_calls = total_successful_tool_calls;
*ctx.tool_failure_count = tool_failure_count;
*ctx.tool_failure_signatures = tool_failure_signatures;
*ctx.tool_transient_failure_count = tool_transient_failure_count;
*ctx.tool_cooldown_until_iteration = tool_cooldown_until_iteration;
*ctx.tool_call_count = tool_call_count;
*ctx.personal_memory_tool_calls = personal_memory_tool_calls;
*ctx.no_evidence_result_streak = no_evidence_result_streak;
*ctx.no_evidence_tools_seen = no_evidence_tools_seen;
*ctx.evidence_gain_count = evidence_gain_count;
*ctx.pending_error_solution_ids = pending_error_solution_ids;
*ctx.tool_error_history = tool_error_history;
*ctx.reflection_completed = reflection_completed;
*ctx.pending_reflection_recoveries = pending_reflection_recoveries;
*ctx.tool_failure_patterns = tool_failure_patterns;
*ctx.last_tool_failure = last_tool_failure;
*ctx.in_session_learned = in_session_learned;
*ctx.unknown_tools = unknown_tools;
*ctx.recent_tool_calls = recent_tool_calls;
*ctx.consecutive_same_tool = consecutive_same_tool;
*ctx.consecutive_same_tool_arg_hashes = consecutive_same_tool_arg_hashes;
*ctx.force_text_response = force_text_response;
*ctx.pending_system_messages = pending_system_messages;
*ctx.recent_tool_names = recent_tool_names;
*ctx.successful_send_file_keys = successful_send_file_keys;
*ctx.cli_agent_boundary_injected = cli_agent_boundary_injected;
*ctx.pending_background_ack = pending_background_ack;
*ctx.pending_external_action_ack = pending_external_action_ack;
*ctx.stall_count = stall_count;
*ctx.deferred_no_tool_streak = deferred_no_tool_streak;
*ctx.consecutive_clean_iterations = consecutive_clean_iterations;
*ctx.fallback_expanded_once = fallback_expanded_once;
*ctx.known_project_dir = known_project_dir;
*ctx.dirs_with_project_inspect_file_evidence = dirs_with_project_inspect_file_evidence;
*ctx.dirs_with_search_no_matches = dirs_with_search_no_matches;
*ctx.require_file_recheck_before_answer = require_file_recheck_before_answer;
*ctx.completion_progress = completion_progress.clone();
*ctx.tool_result_cache = tool_result_cache;
};
}
if known_project_dir.is_none() {
known_project_dir =
extract_project_dir_hint_with_aliases(_user_text, &agent.path_aliases.projects);
}
let mut successful_tool_calls = 0;
let mut iteration_had_tool_failures = false;
let mut hard_block_streak: usize = 0;
let active_dialogue_scope = agent
.state
.get_dialogue_state(session_id)
.await
.ok()
.flatten()
.and_then(|state| {
state
.open_request
.and_then(|request| request.semantic_scope)
});
info!(
session_id,
iteration,
tool_count = resp.tool_calls.len(),
total_successful_tool_calls,
"Tool execution phase starting"
);
super::result_learning::expire_stale_pending_reflection_recoveries(
&mut pending_reflection_recoveries,
iteration,
);
let mut prefetched_io = if !restrict_untrusted_external_reference_tools
&& super::parallel_prefetch::batch_is_prefetch_eligible(
&resp.tool_calls,
available_capabilities,
&unknown_tools,
&tool_cooldown_until_iteration,
iteration,
) {
info!(
session_id,
iteration,
batch_size = resp.tool_calls.len(),
"Prefetching read-only tool batch concurrently"
);
let prefetch_project_scope = (!turn_context.allow_multi_project_scope)
.then_some(turn_context.primary_project_scope.as_deref())
.flatten();
super::parallel_prefetch::prefetch_read_only_batch(
agent,
&resp.tool_calls,
&super::parallel_prefetch::PrefetchCtx {
model,
idempotency_key: execution_state
.current_step
.as_ref()
.and_then(|step| step.idempotency_key.as_deref()),
project_scope: prefetch_project_scope,
session_id,
task_id,
status_tx: &status_tx,
channel_ctx,
user_role,
heartbeat,
emitter,
policy_bundle,
},
)
.await
} else {
HashMap::new()
};
for tc in &resp.tool_calls {
if let Some(limit) = execution_state.exhausted_limit(task_tokens_used, task_start.elapsed())
{
force_text_response = true;
agent
.emit_warning_decision_point(
emitter,
task_id,
iteration,
DecisionType::ExecutionStateSnapshot,
format!(
"Stopped additional tool execution because {} is exhausted",
limit.as_str()
),
json!({
"condition": "execution_budget_exhausted_mid_iteration",
"budget_limit": limit,
"execution_state": execution_state.clone(),
"next_tool_blocked": tc.name,
}),
)
.await;
break;
}
let policy_tool_budget = policy_bundle.policy.tool_budget;
if agent.policy_config.policy_enforce
&& is_hard_policy_tool_budget_reached(total_tool_calls_attempted, policy_tool_budget)
{
force_text_response = true;
pending_system_messages
.push(SystemDirective::HardPolicyToolBudgetReached { policy_tool_budget });
agent
.emit_decision_point(
emitter,
task_id,
iteration,
DecisionType::ToolBudgetBlock,
format!(
"Blocked tool {} because hard tool budget was reached",
tc.name
),
json!({
"tool": tc.name,
"policy_tool_budget": policy_tool_budget,
"total_tool_calls_attempted": total_tool_calls_attempted,
"reason": "hard_policy_tool_budget_reached"
}),
)
.await;
let result_text = ToolResultNotice::HardPolicyToolBudgetBlocked {
policy_tool_budget,
tool_name: tc.name.clone(),
}
.render();
let tool_msg = Message {
id: Uuid::new_v4().to_string(),
session_id: session_id.to_string(),
role: "tool".to_string(),
content: Some(result_text),
tool_call_id: Some(tc.id.clone()),
tool_name: Some(tc.name.clone()),
tool_calls_json: None,
created_at: Utc::now(),
importance: 0.2,
..Message::runtime_defaults()
};
agent
.append_tool_message_with_result_event(
emitter,
&tool_msg,
true,
0,
None,
Some(task_id),
)
.await?;
continue;
}
total_tool_calls_attempted = total_tool_calls_attempted.saturating_add(1);
let send_file_key = if tc.name == "send_file" {
extract_send_file_dedupe_key_from_args(&tc.arguments)
} else {
None
};
let is_personal_memory_tool_call = is_personal_memory_tool(&tc.name);
if restrict_to_personal_memory_tools {
if !is_personal_memory_tool_call {
let result_text = ToolResultNotice::PersonalMemoryToolsOnly {
tool_name: tc.name.clone(),
}
.render();
let tool_msg = Message {
id: Uuid::new_v4().to_string(),
session_id: session_id.to_string(),
role: "tool".to_string(),
content: Some(result_text),
tool_call_id: Some(tc.id.clone()),
tool_name: Some(tc.name.clone()),
tool_calls_json: None,
created_at: Utc::now(),
importance: 0.1,
..Message::runtime_defaults()
};
agent
.append_tool_message_with_result_event(
emitter,
&tool_msg,
true,
0,
None,
Some(task_id),
)
.await?;
continue;
}
if personal_memory_tool_calls >= personal_memory_tool_call_cap {
force_text_response = true;
pending_system_messages.push(SystemDirective::PersonalMemoryRecheckLimitReached);
let result_text =
"Targeted personal-memory re-check limit reached. No further tool calls are allowed for this question."
.to_string();
let tool_msg = Message {
id: Uuid::new_v4().to_string(),
session_id: session_id.to_string(),
role: "tool".to_string(),
content: Some(result_text),
tool_call_id: Some(tc.id.clone()),
tool_name: Some(tc.name.clone()),
tool_calls_json: None,
created_at: Utc::now(),
importance: 0.2,
..Message::runtime_defaults()
};
agent
.append_tool_message_with_result_event(
emitter,
&tool_msg,
true,
0,
None,
Some(task_id),
)
.await?;
continue;
}
personal_memory_tool_calls = personal_memory_tool_calls.saturating_add(1);
}
if restrict_untrusted_external_reference_tools
&& crate::agent::is_untrusted_external_reference_blocked_tool(&tc.name)
{
let result_text = blocked_for_untrusted_external_reference_message(
&tc.name,
active_untrusted_external_reference_skills,
);
let tool_msg = Message {
id: Uuid::new_v4().to_string(),
session_id: session_id.to_string(),
role: "tool".to_string(),
content: Some(result_text),
tool_call_id: Some(tc.id.clone()),
tool_name: Some(tc.name.clone()),
tool_calls_json: None,
created_at: Utc::now(),
importance: 0.15,
..Message::runtime_defaults()
};
agent
.append_tool_message_with_result_event(
emitter,
&tool_msg,
true,
0,
None,
Some(task_id),
)
.await?;
iteration_had_tool_failures = true;
continue;
}
let tool_is_known_but_hidden = !tool_is_currently_exposed(&tool_defs, &tc.name)
&& (tool_is_currently_exposed(base_tool_defs, &tc.name)
|| agent.has_registered_tool(&tc.name));
if tool_is_known_but_hidden {
*tool_call_count.entry(tc.name.clone()).or_insert(0) += 1;
agent
.emit_decision_point(
emitter,
task_id,
iteration,
DecisionType::ToolBudgetBlock,
format!(
"Blocked tool {} because it is not currently exposed",
tc.name
),
json!({
"tool": tc.name,
"reason": "tool_not_currently_exposed",
}),
)
.await;
let result_text = ToolResultNotice::ToolNotCurrentlyExposed {
tool_name: tc.name.clone(),
}
.render();
let tool_msg = Message {
id: Uuid::new_v4().to_string(),
session_id: session_id.to_string(),
role: "tool".to_string(),
content: Some(result_text),
tool_call_id: Some(tc.id.clone()),
tool_name: Some(tc.name.clone()),
tool_calls_json: None,
created_at: Utc::now(),
importance: 0.15,
..Message::runtime_defaults()
};
agent
.append_tool_message_with_result_event(
emitter,
&tool_msg,
true,
0,
None,
Some(task_id),
)
.await?;
iteration_had_tool_failures = true;
continue;
}
let tool_semantic_scope = agent
.tools
.iter()
.find(|tool| tool.name() == tc.name && tool.is_available())
.and_then(|tool| {
tool.semantic_affordances()
.map(|affordances| affordances.scope)
})
.or_else(|| fallback_tool_semantic_scope(&tc.name));
if semantic_scope_blocks_tool(active_dialogue_scope, tool_semantic_scope) {
*tool_call_count.entry(tc.name.clone()).or_insert(0) += 1;
let active_scope = active_dialogue_scope
.map(|scope| format!("{scope:?}"))
.unwrap_or_else(|| "unknown".to_string());
let tool_scope = tool_semantic_scope
.map(|scope| format!("{scope:?}"))
.unwrap_or_else(|| "unknown".to_string());
let result_text = format!(
"[SYSTEM] Semantic scope blocked `{}`: active request scope is {}, but the tool scope is {}. Continue with tools that match the active request.",
tc.name, active_scope, tool_scope
);
let tool_msg = Message {
id: Uuid::new_v4().to_string(),
session_id: session_id.to_string(),
role: "tool".to_string(),
content: Some(result_text.clone()),
tool_call_id: Some(tc.id.clone()),
tool_name: Some(tc.name.clone()),
tool_calls_json: None,
created_at: Utc::now(),
importance: 0.2,
..Message::runtime_defaults()
};
agent
.append_tool_message_with_result_event(
emitter,
&tool_msg,
true,
0,
None,
Some(task_id),
)
.await?;
agent
.emit_warning_decision_point(
emitter,
task_id,
iteration,
DecisionType::ExecutionFailureClassification,
format!(
"Blocked {} because it did not match dialogue scope",
tc.name
),
json!({
"condition": "dialogue_semantic_scope_violation",
"tool": tc.name,
"active_scope": active_scope,
"tool_scope": tool_scope,
}),
)
.await;
iteration_had_tool_failures = true;
continue;
}
let mut effective_arguments = tc.arguments.clone();
let mut injected_project_dir: Option<String> = None;
if let Some(explicit_dir) = project_dir_from_tool_args(&tc.name, &effective_arguments) {
known_project_dir = Some(explicit_dir);
}
if let Some((updated_args, injected)) = maybe_inject_project_dir_into_tool_args(
&tc.name,
&effective_arguments,
known_project_dir.as_deref(),
) {
effective_arguments = updated_args;
injected_project_dir = Some(injected);
}
let attempted_required_file_recheck = require_file_recheck_before_answer
&& is_file_recheck_tool(&tc.name)
&& tool_call_includes_project_path(&tc.name, &effective_arguments);
let internal_scope_violation =
raw_internal_scope_violation(&tc.arguments, session_id, resolved_goal_id);
let allowed_project_scope = (!turn_context.allow_multi_project_scope)
.then_some(turn_context.primary_project_scope.as_deref())
.flatten();
let call_semantics = agent
.tools
.iter()
.find(|tool| tool.name() == tc.name && tool.is_available())
.map(|tool| tool.call_semantics(&effective_arguments))
.unwrap_or_default();
let tool_caps = available_capabilities
.get(&tc.name)
.copied()
.unwrap_or_default();
let step_plan = compile_step_execution_plan(
&execution_state.execution_id,
execution_state.current_plan_version.unwrap_or(1),
iteration,
&tc.id,
&tc.name,
&effective_arguments,
&call_semantics,
tool_caps,
allowed_project_scope,
);
execution_state.begin_step(step_plan.clone());
if matches!(
step_plan.approval_requirement,
ApprovalRequirement::Required { .. }
) || call_semantics.mutates_state()
|| tool_caps.external_side_effect
{
execution_state.promote_persistence(ExecutionPersistence::Durable);
}
execution_state.mark_persisted_now();
agent
.emit_decision_point(
emitter,
task_id,
iteration,
DecisionType::ExecutionStateSnapshot,
format!("Compiled execution step for {}", tc.name),
json!({
"condition": "step_compiled",
"execution_id": execution_state.execution_id,
"current_step": step_plan,
"execution_state": execution_state.clone(),
}),
)
.await;
let step_scope_violation =
target_scope_violation_for_tool_call(&tc.name, &effective_arguments, &step_plan);
if let Some(scope_reason) = internal_scope_violation.or(step_scope_violation) {
POLICY_METRICS
.cross_scope_blocked_total
.fetch_add(1, Ordering::Relaxed);
*tool_call_count.entry(tc.name.clone()).or_insert(0) += 1;
let result_text = ToolResultNotice::ScopeLockBlockedResult {
tool_name: tc.name.clone(),
reason: scope_reason.clone(),
}
.render();
let tool_msg = Message {
id: Uuid::new_v4().to_string(),
session_id: session_id.to_string(),
role: "tool".to_string(),
content: Some(result_text.clone()),
tool_call_id: Some(tc.id.clone()),
tool_name: Some(tc.name.clone()),
tool_calls_json: None,
created_at: Utc::now(),
importance: 0.2,
..Message::runtime_defaults()
};
agent
.append_tool_message_with_result_event(
emitter,
&tool_msg,
true,
0,
None,
Some(task_id),
)
.await?;
validation_state.record_failure(ValidationFailure::ScopeViolation);
validation_state.note_replan();
learning_ctx.record_replay_note(
ReplayNoteCategory::ValidationFailure,
"target_scope_violation",
format!(
"Blocked {} because the requested target fell outside the compiled step scope.",
tc.name
),
true,
);
learning_ctx.record_replay_note(
ReplayNoteCategory::RetryReason,
"replan_required",
format!(
"Replanned because {} attempted to act outside the allowed target scope.",
tc.name
),
true,
);
pending_system_messages.push(SystemDirective::ScopeLockBlocked {
tool_name: tc.name.clone(),
reason: scope_reason,
});
agent
.emit_warning_decision_point(
emitter,
task_id,
iteration,
DecisionType::ExecutionFailureClassification,
format!("Classified scope violation for {}", tc.name),
json!({
"condition": "target_scope_violation",
"tool": tc.name,
"execution_failure_kind": ExecutionFailureKind::LogicFailure,
"failure_class": "semantic",
"key_error_line": "scope violation",
"loop_repetition_reason": validation_state.loop_repetition_reason,
}),
)
.await;
execution_state.complete_current_step(StepExecutionOutcome::NonrecoverableFailure);
execution_state.mark_persisted_now();
iteration_had_tool_failures = true;
continue;
}
if let Some(contract_violation) =
deterministic_tool_contract_violation(&tc.name, &effective_arguments)
{
*tool_call_count.entry(tc.name.clone()).or_insert(0) += 1;
let result_text = ToolResultNotice::DeterministicArgumentContractBlocked {
tool_name: tc.name.clone(),
reason: contract_violation.reason.clone(),
}
.render();
let tool_msg = Message {
id: Uuid::new_v4().to_string(),
session_id: session_id.to_string(),
role: "tool".to_string(),
content: Some(result_text.clone()),
tool_call_id: Some(tc.id.clone()),
tool_name: Some(tc.name.clone()),
tool_calls_json: None,
created_at: Utc::now(),
importance: 0.2,
..Message::runtime_defaults()
};
agent
.append_tool_message_with_result_event(
emitter,
&tool_msg,
true,
0,
None,
Some(task_id),
)
.await?;
pending_system_messages.push(SystemDirective::ArgumentContractBlocked {
tool_name: tc.name.clone(),
reason: contract_violation.reason.to_string(),
coaching: contract_violation.coaching.to_string(),
});
learning_ctx.record_replay_note(
ReplayNoteCategory::ValidationFailure,
"tool_contract_violation",
format!(
"Blocked {} because its arguments violated a deterministic contract: {}.",
tc.name, contract_violation.reason
),
true,
);
learning_ctx.record_replay_note(
ReplayNoteCategory::RetryReason,
"retry_step",
format!(
"Retried locally after deterministic contract failure on {}.",
tc.name
),
true,
);
agent
.emit_warning_decision_point(
emitter,
task_id,
iteration,
DecisionType::ExecutionFailureClassification,
format!("Classified deterministic contract failure for {}", tc.name),
json!({
"condition": "tool_contract_violation",
"tool": tc.name,
"execution_failure_kind": ExecutionFailureKind::ToolContractFailure,
"failure_class": "semantic",
"key_error_line": contract_violation.reason,
"loop_repetition_reason": "retry_step",
}),
)
.await;
execution_state.complete_current_step(StepExecutionOutcome::NonrecoverableFailure);
execution_state.mark_persisted_now();
iteration_had_tool_failures = true;
continue;
}
match super::budget_blocking::maybe_block_tool_by_budget(
agent,
tc,
&mut ToolBudgetBlockCtx {
emitter,
task_id,
session_id,
model,
iteration,
tool_failure_count: &tool_failure_count,
tool_transient_failure_count: &tool_transient_failure_count,
tool_cooldown_until_iteration: &mut tool_cooldown_until_iteration,
tool_call_count: &tool_call_count,
unknown_tools: &unknown_tools,
},
)
.await?
{
ToolBlockKind::NotBlocked => {}
ToolBlockKind::Cooldown => {
continue;
}
ToolBlockKind::HardBlock => {
hard_block_streak += 1;
if hard_block_streak >= 3 {
force_text_response = true;
pending_system_messages.push(SystemDirective::HardToolLimitReached);
} else {
pending_system_messages.push(SystemDirective::SpecificToolBlocked {
tool_name: tc.name.clone(),
});
}
continue;
}
}
if tc.name == "read_file" {
if let Some(request) = ReadRequest::from_arguments(&effective_arguments).await {
let decision = read_file_tracker.decide(&request);
match &decision {
ReadDecision::Replay {
covered_intervals, ..
} => {
agent
.emit_warning_decision_point(
emitter,
task_id,
iteration,
DecisionType::SemanticReadDecision,
"Replayed complete task-local file artifact".to_string(),
json!({
"condition": "semantic_read_replay",
"path": &request.canonical_path,
"covered_intervals": covered_intervals,
"uncovered_intervals": [],
}),
)
.await;
}
ReadDecision::PartialOverlap {
covered_intervals,
uncovered_intervals,
} => {
agent
.emit_warning_decision_point(
emitter,
task_id,
iteration,
DecisionType::SemanticReadDecision,
"Blocked overlapping file range read".to_string(),
json!({
"condition": "semantic_read_partial_overlap",
"path": &request.canonical_path,
"covered_intervals": covered_intervals,
"uncovered_intervals": uncovered_intervals,
}),
)
.await;
}
ReadDecision::Execute | ReadDecision::Unknown => {}
}
let synthetic = match &decision {
ReadDecision::Replay { metadata, .. } => {
let rendered_output = if agent.context_window_config.enabled {
crate::tools::render_read_file_output_within(
metadata,
agent.context_window_config.tool_result_chars_for(model),
)
} else {
crate::tools::render_read_file_output(metadata)
};
Some(ToolResultNotice::SemanticReadReplay { rendered_output }.render())
}
ReadDecision::PartialOverlap {
covered_intervals,
uncovered_intervals,
} => Some(
ToolResultNotice::SemanticReadPartialOverlap {
covered_intervals: format_line_intervals(covered_intervals),
uncovered_intervals: format_line_intervals(uncovered_intervals),
}
.render(),
),
ReadDecision::Execute | ReadDecision::Unknown => None,
};
if let Some(result_text) = synthetic {
let tool_msg = Message {
id: Uuid::new_v4().to_string(),
session_id: session_id.to_string(),
role: "tool".to_string(),
content: Some(result_text),
tool_call_id: Some(tc.id.clone()),
tool_name: Some(tc.name.clone()),
tool_calls_json: None,
created_at: Utc::now(),
importance: 0.3,
..Message::runtime_defaults()
};
agent
.append_tool_message_with_result_event(
emitter,
&tool_msg,
true,
0,
None,
Some(task_id),
)
.await?;
execution_state.record_tool_call();
execution_state.complete_current_step(StepExecutionOutcome::Progress);
execution_state.mark_persisted_now();
continue;
}
}
}
if let Some(guard_outcome) = super::guards::maybe_handle_loop_pattern_guards(
agent,
tc,
emitter,
task_id,
session_id,
iteration,
task_start,
task_tokens_used,
learning_ctx,
&mut recent_tool_calls,
&mut recent_tool_names,
&mut consecutive_same_tool,
&mut consecutive_same_tool_arg_hashes,
&tool_result_cache,
)
.await?
{
match guard_outcome {
LoopPatternGuardOutcome::ContinueLoop => {
continue;
}
LoopPatternGuardOutcome::Return(outcome) => {
if let Some(ref mut plan) = execution_state.active_linear_intent_plan {
if !plan.all_steps_complete() {
plan.complete_current_step_with_evidence(
"Force-advanced: stall detected".to_string(),
);
tracing::info!(
session_id,
advanced_step = plan.current_step_cursor - 1,
"Plan step force-advanced due to stall detection — \
returning to main loop for next iteration"
);
commit_state!();
return Ok(ToolExecutionOutcome::NextIteration);
}
}
commit_state!();
return Ok(outcome);
}
}
}
if super::budget_blocking::maybe_handle_duplicate_send_file_noop(
agent,
tc,
&mut DuplicateSendFileNoopCtx {
send_file_key: send_file_key.as_ref(),
successful_send_file_keys: &successful_send_file_keys,
session_id,
iteration,
effective_arguments: &effective_arguments,
force_text_response: &mut force_text_response,
pending_system_messages: &mut pending_system_messages,
successful_tool_calls: &mut successful_tool_calls,
total_successful_tool_calls: &mut total_successful_tool_calls,
tool_call_count: &mut tool_call_count,
learning_ctx,
emitter,
task_id,
policy_bundle,
},
)
.await?
{
continue;
}
let path_specific_mutation = matches!(tc.name.as_str(), "write_file" | "edit_file");
let unknown_may_mutate =
call_semantics.is_empty() && (!tool_caps.read_only || tool_caps.external_side_effect);
if !path_specific_mutation && (call_semantics.mutates_state() || unknown_may_mutate) {
read_file_tracker.clear();
evidence_state.clear_file_read_evidence();
}
let prefetched = match prefetched_io.remove(&tc.id) {
Some(entry) if entry.arguments == effective_arguments => Some(entry.io),
Some(_) => {
warn!(
session_id,
tool = %tc.name,
"Discarding prefetched result: effective arguments diverged"
);
None
}
None => None,
};
let io = match prefetched {
Some(io) => {
info!(
session_id,
tool = %tc.name,
duration_ms = io.tool_duration_ms,
"Using concurrently prefetched tool result"
);
io
}
None => {
super::execution_io::execute_tool_call_io(
agent,
tc,
&ToolExecutionIoCtx {
effective_arguments: &effective_arguments,
model,
idempotency_key: execution_state
.current_step
.as_ref()
.and_then(|step| step.idempotency_key.as_deref()),
injected_project_dir: injected_project_dir.as_deref(),
project_scope: allowed_project_scope,
session_id,
task_id,
status_tx: &status_tx,
channel_ctx,
user_role,
heartbeat,
emitter,
policy_bundle,
},
)
.await
}
};
execution_state.record_tool_call();
execution_state.mark_persisted_now();
let mut result_text = io.result_text;
let mut tool_duration_ms = io.tool_duration_ms;
let mut result_metadata = io.result_metadata;
if tc.name == "run_command" && run_command_policy_block_requires_terminal(&result_text) {
if let Some(terminal_args) =
build_terminal_fallback_arguments_from_run_command(&effective_arguments)
{
let fallback_started = Instant::now();
let terminal_result = agent
.execute_tool_with_watchdog_outcome(
"terminal",
&terminal_args,
&tool_exec::ToolExecCtx {
session_id,
task_id: Some(task_id),
status_tx: status_tx.clone(),
channel_visibility: channel_ctx.visibility,
channel_id: channel_ctx.channel_id.as_deref(),
project_scope: allowed_project_scope,
trusted: channel_ctx.trusted,
user_role,
},
)
.await;
let fallback_duration =
fallback_started.elapsed().as_millis().min(u64::MAX as u128) as u64;
tool_duration_ms = tool_duration_ms.saturating_add(fallback_duration);
let fallback_note = ToolResultNotice::RunCommandPolicyAutoRoutedToTerminal;
result_text = match terminal_result {
Ok(outcome) => {
result_metadata = outcome.metadata;
format!("{}\n\n{}", outcome.output, fallback_note.render())
}
Err(e) => {
result_metadata.transport_error = Some(e.to_string());
format!("Error: {}\n\n{}", e, fallback_note.render())
}
};
if agent.context_window_config.enabled {
result_text = crate::memory::context_window::compress_tool_result(
"terminal",
&result_text,
agent.context_window_config.tool_result_chars_for(model),
);
}
}
}
let background_detached =
tool_result_indicates_background_detach(&tc.name, &result_text, &result_metadata);
if background_detached {
pending_background_ack = Some(build_background_detach_ack(
&tc.name,
&result_text,
&result_metadata,
));
force_text_response = true;
let notifications_active = result_metadata.completion_notifications_enabled;
let system_msg = SystemDirective::BackgroundHandoff {
notifications_active,
};
pending_system_messages.push(system_msg.clone());
result_text = format!("{}\n\n{}", result_text, system_msg.render());
}
*tool_call_count.entry(tc.name.clone()).or_insert(0) += 1;
if result_text.contains("Command denied by user.") {
agent
.with_harness_eval(|eval| eval.record_approval_denied())
.await;
}
if tc.name == "search_files" {
let cache_hash = hash_tool_call(&tc.name, &tc.arguments);
let max_cache_chars = 8000;
let primary_result_text =
crate::traits::extract_primary_message_content(&result_text, &[]);
if primary_result_text.len() <= max_cache_chars
&& !result_text.starts_with("Error")
&& !crate::traits::message_content_is_structural_only(&result_text, &[])
{
tool_result_cache.insert(cache_hash, primary_result_text.into_owned());
} else if primary_result_text.len() > max_cache_chars {
let mut boundary = max_cache_chars;
while boundary > 0 && !primary_result_text.is_char_boundary(boundary) {
boundary -= 1;
}
tool_result_cache.insert(
cache_hash,
format!(
"{}…\n[truncated — {} total chars]",
&primary_result_text[..boundary],
primary_result_text.len()
),
);
}
const MAX_CACHE_ENTRIES: usize = 20;
if tool_result_cache.len() > MAX_CACHE_ENTRIES {
if let Some(key) = tool_result_cache.keys().next().copied() {
tool_result_cache.remove(&key);
}
}
}
let failure_class = classify_tool_result_failure_with_context(
&tc.name,
&result_text,
Some(&effective_arguments),
Some(&result_metadata),
);
let execution_failure_kind = classify_execution_failure_kind(
&tc.name,
&result_text,
Some(&effective_arguments),
Some(&result_metadata),
false,
);
let is_error = failure_class.is_some();
if path_specific_mutation && !is_error {
if let Some(path) = canonical_path_from_arguments(&effective_arguments).await {
read_file_tracker.invalidate_path(&path);
evidence_state.invalidate_file_read_path(&path);
}
}
if tc.name == "read_file" && !is_error {
if let Some(read_metadata) = result_metadata.read_file.clone() {
read_file_tracker.insert(read_metadata);
}
}
let tool_summary = format!(
"{}({})",
tc.name,
summarize_tool_args(&tc.name, &effective_arguments)
);
if is_error {
learning_ctx
.tool_calls
.push(format!("{} [FAILED]", tool_summary));
} else {
learning_ctx.tool_calls.push(tool_summary.clone());
}
execution_state.complete_current_step(classify_step_execution_outcome(
is_error,
background_detached,
));
execution_state.mark_persisted_now();
match execution_failure_kind {
Some(ExecutionFailureKind::ToolContractFailure)
| Some(ExecutionFailureKind::ToolInvocationFailure) => {
validation_state.note_retry(LoopRepetitionReason::RetryStep);
learning_ctx.record_replay_note(
ReplayNoteCategory::RetryReason,
"retry_step",
format!(
"Retried {} locally after {:?}.",
tc.name, execution_failure_kind
),
true,
);
}
Some(ExecutionFailureKind::EnvironmentFailure)
| Some(ExecutionFailureKind::LogicFailure) => {
validation_state.note_replan();
learning_ctx.record_replay_note(
ReplayNoteCategory::RetryReason,
"replan_required",
format!(
"Replanned after {:?} on {}.",
execution_failure_kind, tc.name
),
true,
);
}
None => {
validation_state.clear_loop_repetition_reason();
}
}
{
let caps = available_capabilities
.get(&tc.name)
.copied()
.unwrap_or_default();
let is_external_mutation =
caps.external_side_effect && result_metadata.semantics.mutates_state();
let error_summary = if is_error {
extract_error_summary_line(&result_text)
} else {
None
};
let planned_step = if is_external_mutation {
execution_state
.current_linear_intent_step()
.filter(|step| {
linear_intent_step_matches_tool_call(step, &tc.name, &effective_arguments)
})
.cloned()
} else {
None
};
let expected_step_count = execution_state
.active_linear_intent_plan
.as_ref()
.map(|plan| plan.steps.len());
let plan_version = execution_state
.active_linear_intent_plan
.as_ref()
.map(|plan| plan.plan_version);
execution_state.record_outcome(OutcomeEntry {
tool_name: tc.name.clone(),
success: !is_error,
http_status: result_metadata.http_status,
is_external_mutation,
error_summary,
iteration,
plan_version,
planned_step_id: planned_step.as_ref().map(|s| s.step_id.clone()),
planned_step_index: planned_step.as_ref().map(|s| s.step_index),
planned_step_description: planned_step.as_ref().map(|s| s.description.clone()),
expected_step_count,
});
if !is_error && planned_step.is_some() {
execution_state.advance_linear_intent_step_after_external_success();
}
execution_state.record_tool_output_evidence(&result_text);
execution_state.record_web_source(
&tc.name,
&effective_arguments,
&result_text,
is_error,
);
}
agent
.emit_decision_point(
emitter,
task_id,
iteration,
DecisionType::ExecutionStateSnapshot,
format!("Recorded step outcome for {}", tc.name),
json!({
"condition": "step_completed",
"tool": tc.name,
"outcome": execution_state.last_outcome,
"execution_state": execution_state.clone(),
"background_detached": background_detached,
"is_error": is_error,
}),
)
.await;
info!(
session_id,
iteration,
tool = %tc.name,
is_error,
execution_failure_kind = ?execution_failure_kind,
result_len = result_text.len(),
result_preview = &result_text.chars().take(80).collect::<String>() as &str,
"Tool execution completed"
);
if let Some(execution_failure_kind) = execution_failure_kind {
agent.emit_warning_decision_point(
emitter,
task_id,
iteration,
DecisionType::ExecutionFailureClassification,
format!("Classified execution failure for {}", tc.name),
json!({
"condition": match execution_failure_kind {
ExecutionFailureKind::ToolContractFailure => "tool_contract_failure",
ExecutionFailureKind::ToolInvocationFailure => "tool_invocation_failure",
ExecutionFailureKind::EnvironmentFailure => "environment_failure",
ExecutionFailureKind::LogicFailure => "logic_failure",
},
"tool": tc.name,
"execution_failure_kind": execution_failure_kind,
"failure_class": failure_class.map(|class| match class {
ToolFailureClass::Semantic => "semantic",
ToolFailureClass::Transient => "transient",
}),
"key_error_line": extract_key_error_line(&result_text),
"loop_repetition_reason": validation_state.loop_repetition_reason,
}),
)
.await;
}
let learning_env = ResultLearningEnv {
attempted_required_file_recheck,
send_file_key,
restrict_to_personal_memory_tools,
is_reaffirmation_challenge_turn,
session_id,
task_id,
emitter,
task_start,
iteration,
tool_arguments: &effective_arguments,
tool_summary: &tool_summary,
};
let mut learning_state = ResultLearningState {
learning_ctx,
no_evidence_result_streak: &mut no_evidence_result_streak,
iteration_had_tool_failures: &mut iteration_had_tool_failures,
no_evidence_tools_seen: &mut no_evidence_tools_seen,
evidence_gain_count: &mut evidence_gain_count,
unknown_tools: &mut unknown_tools,
tool_failure_count: &mut tool_failure_count,
tool_failure_signatures: &mut tool_failure_signatures,
tool_transient_failure_count: &mut tool_transient_failure_count,
tool_cooldown_until_iteration: &mut tool_cooldown_until_iteration,
pending_error_solution_ids: &mut pending_error_solution_ids,
tool_error_history: &mut tool_error_history,
pending_reflection_recoveries: &mut pending_reflection_recoveries,
tool_failure_patterns: &mut tool_failure_patterns,
last_tool_failure: &mut last_tool_failure,
in_session_learned: &mut in_session_learned,
force_text_response: &mut force_text_response,
pending_system_messages: &mut pending_system_messages,
successful_tool_calls: &mut successful_tool_calls,
total_successful_tool_calls: &mut total_successful_tool_calls,
successful_send_file_keys: &mut successful_send_file_keys,
cli_agent_boundary_injected: &mut cli_agent_boundary_injected,
recent_tool_calls: &mut recent_tool_calls,
consecutive_same_tool: &mut consecutive_same_tool,
consecutive_same_tool_arg_hashes: &mut consecutive_same_tool_arg_hashes,
recent_tool_names: &mut recent_tool_names,
require_file_recheck_before_answer: &mut require_file_recheck_before_answer,
known_project_dir: &mut known_project_dir,
dirs_with_project_inspect_file_evidence: &mut dirs_with_project_inspect_file_evidence,
dirs_with_search_no_matches: &mut dirs_with_search_no_matches,
};
let learning_outcome = super::result_learning::apply_result_learning(
agent,
tc,
&mut result_text,
is_error,
failure_class,
execution_failure_kind,
&learning_env,
&mut learning_state,
)
.await?;
if let Some(outcome) = learning_outcome.control_flow {
commit_state!();
return Ok(outcome);
}
if let Some(failure) = learning_outcome.semantic_failure.as_ref() {
if let Some(diagnosis) = super::reflection::maybe_trigger_reflection(
agent,
&tc.name,
&effective_arguments,
failure,
_user_text,
active_skill_names,
&tool_error_history,
&mut reflection_completed,
session_id,
)
.await
{
let failure_key = (tc.name.clone(), failure.signature.clone());
pending_system_messages.push(SystemDirective::ReflectionDiagnosis {
tool_name: tc.name.clone(),
root_cause: diagnosis.root_cause.clone(),
recommended_action: diagnosis.recommended_action.clone(),
});
if let Some(draft) = diagnosis.learning {
if let Some(solution_id) =
super::reflection::store_reflection_learning(&agent.state, draft).await
{
pending_reflection_recoveries.insert(
tc.name.clone(),
super::reflection::PendingReflectionRecovery {
signature: failure_key.1,
solution_ids: vec![solution_id],
verify_on_iteration: iteration.saturating_add(1),
},
);
}
}
}
}
if !is_error {
execution_state.extend_budget_on_progress();
let (complete_label, complete_summary) =
crate::tools::sanitize::user_facing_tool_activity(
&tc.name,
&summarize_completed_tool_result(&result_text),
channel_ctx.visibility,
);
send_status(
&status_tx,
StatusUpdate::ToolComplete {
name: complete_label,
summary: complete_summary,
},
);
let caps = available_capabilities
.get(&tc.name)
.copied()
.unwrap_or_default();
let semantics = &result_metadata.semantics;
let evidence_count_before = evidence_state.records.len();
record_successful_tool_evidence(
evidence_state,
&tc.name,
&effective_arguments,
semantics,
);
let action_target = step_plan
.expected_targets
.first()
.or_else(|| step_plan.target_scope.allowed_targets.first())
.map(|target| target.value.clone());
validation_state.record_action(
Some(&tc.name),
action_target,
evidence_state.records.len() > evidence_count_before,
);
validation_state.clear_loop_repetition_reason();
if semantics.mutates_state() {
completion_progress.mark_mutation(&turn_context.completion_contract);
if caps.external_side_effect {
completion_progress.mark_successful_external_mutation();
}
}
if semantics.observes_state() {
let can_verify = tool_result_contains_verifiable_evidence(semantics, &result_text);
let matched_contract = observation_matches_completion_contract(
&turn_context.completion_contract,
semantics,
&effective_arguments,
&result_text,
);
completion_progress.mark_observation(
&turn_context.completion_contract,
can_verify && matched_contract,
);
}
if tc.name == "send_file" && completion_progress.verification_pending {
completion_progress.verification_pending = false;
completion_progress.verification_count =
completion_progress.verification_count.saturating_add(1);
info!(
session_id,
iteration, "send_file success cleared verification_pending"
);
}
if completion_progress.verification_pending {
pending_external_action_ack = None;
} else if !background_detached
&& semantics.mutates_state()
&& caps.external_side_effect
&& should_build_external_action_ack(&result_text)
{
pending_external_action_ack =
Some(build_external_action_completion_ack(&result_text));
}
} else {
pending_external_action_ack = None;
let caps = available_capabilities
.get(&tc.name)
.copied()
.unwrap_or_default();
if caps.external_side_effect && result_metadata.semantics.mutates_state() {
completion_progress.mark_failed_external_mutation();
let error_hint = extract_error_summary_line(&result_text)
.unwrap_or_else(|| "request failed".to_string());
pending_system_messages.push(SystemDirective::ExternalMutationFailed {
tool_name: tc.name.clone(),
status_code: result_metadata.http_status,
error_hint,
});
}
if matches!(
execution_state.last_outcome,
Some(StepExecutionOutcome::NonrecoverableFailure)
) {
validation_state.record_failure(ValidationFailure::NonrecoverableFailure);
}
}
if !is_error {
if let Ok(events) = agent
.event_store
.query_task_events_for_session(session_id, task_id)
.await
{
let duplicate_count = duplicate_successful_tool_result_count(
&events,
&tc.name,
&effective_arguments,
&result_text,
);
if duplicate_count > 0 {
agent
.emit_warning_decision_point(
emitter,
task_id,
iteration,
DecisionType::RepetitiveCallDetection,
format!(
"Repeated successful tool call for {} with the same arguments and result",
tc.name
),
json!({
"condition": "duplicate_successful_tool_call",
"tool": tc.name,
"duplicate_count": duplicate_count,
"arguments_hash": canonical_tool_arguments_hash(&effective_arguments),
"result_len": result_text.len(),
}),
)
.await;
}
}
}
let tool_msg = Message {
content: Some(result_text.clone()),
tool_call_id: Some(tc.id.clone()),
tool_name: Some(tc.name.clone()),
attachments: result_metadata.attachments.clone(),
importance: 0.3, ..Message::new_runtime(Uuid::new_v4().to_string(), session_id, "tool")
};
agent
.append_tool_message_with_result_event(
emitter,
&tool_msg,
!is_error,
tool_duration_ms,
if is_error {
Some(result_text.clone())
} else {
None
},
Some(task_id),
)
.await?;
let direct_response = if !is_error
&& agent.depth == 0
&& resp.tool_calls.len() == 1
&& !background_detached
{
result_metadata.direct_response.clone()
} else {
None
};
if let Some(reply) = direct_response {
let assistant_msg = Message {
id: Uuid::new_v4().to_string(),
session_id: session_id.to_string(),
role: "assistant".to_string(),
content: Some(reply.clone()),
tool_call_id: None,
tool_name: None,
tool_calls_json: None,
created_at: Utc::now(),
importance: 0.5,
..Message::runtime_defaults()
};
agent
.append_assistant_message_with_event(emitter, &assistant_msg, "system", None, None)
.await?;
agent
.emit_task_end(
emitter,
task_id,
TaskStatus::Completed,
TaskOutcome::Succeeded,
task_start,
iteration,
learning_ctx.tool_calls.len(),
None,
Some(reply.chars().take(200).collect()),
)
.await;
learning_ctx.completed_naturally = true;
learning_ctx.task_outcome = Some(crate::events::TaskOutcome::Succeeded);
let learning_ctx_for_task = learning_ctx.clone();
let state = agent.state.clone();
tokio::spawn(async move {
if let Err(e) = post_task::process_learning(&state, learning_ctx_for_task).await {
warn!("Learning failed: {}", e);
}
});
commit_state!();
return Ok(ToolExecutionOutcome::Return(Ok(reply)));
}
if is_error {
let _ = emitter
.emit(
EventType::Error,
ErrorData::tool_error(
tc.name.clone(),
result_text.clone(),
Some(task_id.to_string()),
),
)
.await;
}
if let Some(ref tid) = agent.task_id {
let activity = TaskActivity {
id: 0,
task_id: tid.clone(),
activity_type: "tool_call".to_string(),
tool_name: Some(tc.name.clone()),
tool_args: Some(effective_arguments.chars().take(1000).collect()),
result: Some(result_text.chars().take(2000).collect()),
success: Some(!is_error),
tokens_used: None,
created_at: chrono::Utc::now().to_rfc3339(),
};
if let Err(e) = agent.state.log_task_activity(&activity).await {
warn!(task_id = %tid, error = %e, "Failed to log task activity");
}
}
if background_detached {
info!(
session_id,
iteration,
tool = %tc.name,
"Background task detached; ending tool execution phase early and forcing text response"
);
break;
}
}
info!(
session_id,
iteration,
successful_tool_calls,
iteration_had_tool_failures,
total_successful_tool_calls,
stall_count,
"Tool execution phase completed, entering post-loop"
);
super::post_loop::apply_post_tool_iteration_controls(
agent,
super::post_loop::PostToolIterationInputs {
session_id,
iteration,
task_tokens_used,
successful_tool_calls,
iteration_had_tool_failures,
restrict_to_personal_memory_tools,
base_tool_defs,
available_capabilities,
policy_bundle,
total_tool_calls_attempted,
has_active_goal: resolved_goal_id.is_some(),
completed_tool_calls: &learning_ctx.tool_calls,
recent_tool_names: &recent_tool_names,
user_text: _user_text,
},
super::post_loop::PostToolIterationState {
total_successful_tool_calls: &mut total_successful_tool_calls,
force_text_response: &mut force_text_response,
pending_system_messages: &mut pending_system_messages,
tool_defs: &mut tool_defs,
stall_count: &mut stall_count,
deferred_no_tool_streak: &mut deferred_no_tool_streak,
consecutive_clean_iterations: &mut consecutive_clean_iterations,
fallback_expanded_once: &mut fallback_expanded_once,
},
);
commit_state!();
Ok(ToolExecutionOutcome::NextIteration)
}