mod hook_records;
mod subdir_context;
pub(super) use hook_records::emit_hook_context_injection_records;
pub(super) use hook_records::emit_hook_lifecycle_records;
pub(super) use hook_records::emit_provider_context_injection_display;
pub(super) use hook_records::emit_hook_diagnostics;
pub(super) use hook_records::run_message_phase_hooks;
pub(super) use subdir_context::record_provider_context_item;
use super::{
AgentOutputSink, AgentRunOutput,
session_persistence::SessionPersistence,
subdir_instructions,
tool_continuation::{activity_id_for_call, tool_activity_metadata},
tool_result_diagnostics::failed_search_diagnostic_output,
turn_state::AgentTurnState,
};
use crate::{
cancellation::{AgentCancellation, AgentRunCanceled},
config::{InjectedContentSettings, InjectedContentStyle},
herdr::HerdrReporter,
hooks::{
HookAction, HookContextInjectionRecord, HookContextInjectionStatus, HookDiagnostic,
HookLifecycleRecord, HookPhase, HookPolicyError, HookRuntime,
},
instructions::subdir::discover_subdir_instructions,
lsp::EditDiagnosticsRequest,
output::{
ActivityEvent, ActivityId, ActivityKind, ActivityMetadata, ActivitySender, ActivityStatus,
ContextWindowUsage, HookContextMetadata, OutputEvent, ProviderContextInjectionDisplay,
ToolDispatchContext, pending_activity_status, tool_display_summary,
},
providers::{ChatMessage, ProviderConversationItem, ProviderToolResult, ToolCall},
sessions::SessionEventKind,
tools::{
ToolCapability, ToolResult, ToolResultDisplay, ToolRuntime,
contract::tool_name,
dispatch::{ToolDispatchOutcome, TouchedPath, TouchedPathKind},
skill_provenance::validated_skill_reads,
},
};
use serde_json::json;
use std::{
hash::{Hash, Hasher},
time::Instant,
};
use subdir_context::discover_and_record_subdir_context_items;
pub(crate) struct StartedToolActivity {
pub(crate) activity_id: ActivityId,
pub(crate) activity_sender: Option<ActivitySender>,
}
pub(crate) fn emit_tool_started(
output_sink: &mut Option<&mut dyn AgentOutputSink>,
iteration: usize,
tool_index: usize,
call: &ToolCall,
) -> anyhow::Result<StartedToolActivity> {
let activity_id = activity_id_for_call(iteration, tool_index, call);
let mut display_call = call.clone();
if display_call.id.trim().is_empty() {
display_call.id = activity_id.as_str().to_string();
}
let parent_activity_id = output_sink
.as_deref()
.and_then(AgentOutputSink::current_parent_activity_id);
let activity_sender = output_sink
.as_deref()
.and_then(AgentOutputSink::activity_sender);
let metadata = tool_activity_metadata(call, None);
if let Some(sink) = output_sink.as_deref_mut() {
sink.activity_event(ActivityEvent::Started {
id: activity_id.clone(),
parent_id: parent_activity_id,
kind: ActivityKind::Tool,
status: pending_activity_status(call),
metadata,
})?;
sink.output_event(OutputEvent::ToolStarted {
call: Box::new(display_call.clone()),
label: crate::output::tool_display_label(call),
})?;
}
Ok(StartedToolActivity {
activity_id,
activity_sender,
})
}
pub(crate) fn emit_tool_finished(
output_sink: &mut Option<&mut dyn AgentOutputSink>,
activity_id: ActivityId,
call: &ToolCall,
result: &ToolResult,
) -> anyhow::Result<()> {
if let Some(sink) = output_sink.as_deref_mut() {
sink.activity_event(ActivityEvent::Finished {
id: activity_id,
status: if result.success {
ActivityStatus::Success
} else {
ActivityStatus::Failed
},
metadata: Some(tool_activity_metadata(call, Some(result))),
})?;
}
Ok(())
}
pub(crate) fn emit_tool_canceled(
output_sink: &mut Option<&mut dyn AgentOutputSink>,
activity_id: ActivityId,
) -> anyhow::Result<()> {
if let Some(sink) = output_sink.as_deref_mut() {
sink.activity_event(ActivityEvent::Finished {
id: activity_id,
status: ActivityStatus::Canceled,
metadata: None,
})?;
}
Ok(())
}
pub(crate) enum ToolExecutionOutcome {
Dispatched {
provider_result: ProviderToolResult,
tool_result: Box<ToolResult>,
after_hook_failure: Option<HookDiagnostic>,
subdir_context_items: Vec<ProviderConversationItem>,
after_hook_context_items: Vec<ProviderConversationItem>,
},
BeforeHookFailed {
diagnostic: HookDiagnostic,
tool_result: Box<ToolResult>,
},
}
fn display_tool_result_call(call: &ToolCall, context: &ToolDispatchContext) -> ToolCall {
let mut display_call = call.clone();
if display_call.id.trim().is_empty()
&& let Some(activity_id) = &context.parent_activity_id
{
display_call.id = activity_id.as_str().to_string();
}
display_call
}
pub(crate) fn before_hook_failure_activity_result(
call: &ToolCall,
diagnostic: &HookDiagnostic,
) -> ToolResult {
ToolResult {
tool_name: call.name.clone(),
success: false,
content: format!(
"tool call failed by local before_tool hook policy: tool={} hook=<redacted> category={}",
call.name,
diagnostic.category.as_str()
),
metadata: json!({"call_id": call.id.clone(), "hook_failed": true}),
display: ToolResultDisplay::default(),
}
}
fn emit_tool_metadata(
output_sink: &mut Option<&mut dyn AgentOutputSink>,
call: &ToolCall,
key: &str,
value: serde_json::Value,
) -> anyhow::Result<()> {
if let Some(sink) = output_sink.as_deref_mut() {
sink.activity_event(ActivityEvent::ToolMetadata {
id: ActivityId::new(call.id.clone()),
metadata: json!({key: value}),
})?;
}
Ok(())
}
fn request_bash_approval_if_needed(
tools: Option<&ToolRuntime>,
output_sink: &mut Option<&mut dyn AgentOutputSink>,
call: &ToolCall,
cancellation: &AgentCancellation,
combined_assessment: Option<crate::protection::bash::BashRiskAssessment>,
) -> anyhow::Result<(bool, Option<crate::protection::bash::BashProtectionDisplay>)> {
if call.name != tool_name::BASH {
return Ok((true, None));
}
let Some(command) = call
.arguments
.get("command")
.or_else(|| call.arguments.get("cmd"))
.and_then(serde_json::Value::as_str)
else {
return Ok((true, None));
};
let Some(tools) = tools else {
return Ok((true, None));
};
let Some(protection_level) = tools.bash_protection_level() else {
return Ok((true, None));
};
let assessment = match combined_assessment.map_or_else(
|| tools.assess_bash_command(command),
|assessment| Ok(Some(assessment)),
) {
Ok(Some(assessment)) => assessment,
Ok(None) => return Ok((true, None)),
Err(error) => {
if let Some(sink) = output_sink.as_deref_mut() {
sink.output_event(OutputEvent::Diagnostic {
level: "warning".to_string(),
message: format!(
"bash protection check failed; applying configured failure policy: {}",
crate::output::redact_sensitive_text(&error.to_string())
),
})?;
}
let failure_policy = tools.bash_protection_failure_policy().unwrap_or_default();
let allowed = failure_policy.allows_execution();
return Ok((
allowed,
Some(crate::protection::bash::BashProtectionDisplay {
protection_level,
score: None,
confidence: None,
probabilities: Default::default(),
decision: if allowed {
"allowed by API failure policy"
} else {
"denied by API failure policy"
},
failure_policy: Some(failure_policy),
cached: false,
dimensions: Default::default(),
model: None,
}),
));
}
};
let mut display = crate::protection::bash::BashProtectionDisplay {
protection_level,
score: Some(assessment.answer.score),
confidence: Some(assessment.answer.confidence),
probabilities: assessment.answer.probabilities.clone(),
decision: "allowed automatically",
failure_policy: None,
cached: assessment.cached,
dimensions: assessment.dimensions.clone(),
model: Some(assessment.model.clone()),
};
if assessment.requires_approval {
display.decision = "approval required";
}
emit_tool_metadata(output_sink, call, "bash_protection", display.metadata())?;
if !assessment.requires_approval {
return Ok((true, Some(display)));
}
let Some(sink) = output_sink.as_deref_mut() else {
display.decision = "denied; approval unavailable";
return Ok((false, Some(display)));
};
let allowed = sink.request_bash_approval(
crate::protection::bash::BashApprovalRequest::from_assessment(
command.to_string(),
assessment,
),
cancellation,
)?;
cancellation.check()?;
display.decision = if allowed {
"allowed by user"
} else {
"denied by user"
};
Ok((allowed, Some(display)))
}
pub(super) struct AgentDispatchOutcome {
pub(super) dispatch: ToolDispatchOutcome,
pub(super) context_payloads: Vec<serde_json::Value>,
pub(super) fatal: Option<anyhow::Error>,
}
impl From<ToolDispatchOutcome> for AgentDispatchOutcome {
fn from(dispatch: ToolDispatchOutcome) -> Self {
Self {
dispatch,
context_payloads: Vec::new(),
fatal: None,
}
}
}
fn dispatch_agent_tool(
tools: Option<&ToolRuntime>,
hooks: Option<&HookRuntime>,
session_persistence: &SessionPersistence<'_>,
output_sink: &mut Option<&mut dyn AgentOutputSink>,
subdir_instruction_state: Option<&mut super::subdir_instructions::SubdirInstructionState>,
call: &ToolCall,
context: &ToolDispatchContext,
) -> AgentDispatchOutcome {
let Some(tools) = tools else {
return ToolDispatchOutcome {
result: ToolResult {
tool_name: call.name.clone(),
success: false,
content: "tool runtime is not configured".to_string(),
metadata: json!({"call_id": call.id.clone()}),
display: ToolResultDisplay::default(),
},
touched_paths: Vec::new(),
changed_paths: Vec::new(),
}
.into();
};
if call.name == crate::code_mode::TOOL_NAME {
return super::code_mode::execute(
tools,
hooks,
session_persistence,
output_sink,
subdir_instruction_state,
call,
context,
);
}
tools
.dispatch_with_context_outcome(&call.name, call.arguments.clone(), context.clone())
.into()
}
pub(crate) fn execute_tool_call(
tools: Option<&ToolRuntime>,
hooks: Option<&HookRuntime>,
session_persistence: &mut SessionPersistence<'_>,
output_sink: &mut Option<&mut dyn AgentOutputSink>,
mut subdir_instruction_state: Option<&mut super::subdir_instructions::SubdirInstructionState>,
call: ToolCall,
mut context: ToolDispatchContext,
) -> anyhow::Result<ToolExecutionOutcome> {
context.cancellation.check()?;
context.provider_call_id = Some(call.id.clone());
if let Some(hooks) = hooks {
let outcome = hooks.run_before_with_activity(&call, &context);
emit_hook_lifecycle_records(session_persistence, output_sink, &outcome.lifecycle_records)?;
let mut phase_diagnostics = outcome.diagnostics;
if outcome.canceled {
emit_hook_diagnostics(session_persistence, output_sink, &phase_diagnostics)?;
if let Some(activity_id) = context.parent_activity_id.clone() {
emit_tool_canceled(output_sink, activity_id)?;
}
return Err(AgentRunCanceled.into());
}
match outcome.action {
HookAction::Continue => {
emit_hook_diagnostics(session_persistence, output_sink, &phase_diagnostics)?;
}
HookAction::Block(diagnostic) => {
phase_diagnostics.push(diagnostic.clone());
emit_hook_diagnostics(session_persistence, output_sink, &phase_diagnostics)?;
let result = ToolResult {
tool_name: call.name.clone(),
success: false,
content: format!(
"tool call blocked by local before_tool hook policy: tool={} hook=<redacted> category={}",
call.name,
diagnostic.category.as_str()
),
metadata: json!({"call_id": call.id.clone(), "hook_blocked": true}),
display: ToolResultDisplay::default(),
};
session_persistence.record_required(
SessionEventKind::ToolResult,
json!({"call_id": call.id.clone(), "result": result.clone()}),
)?;
if let Some(sink) = output_sink.as_deref_mut() {
sink.output_event(OutputEvent::ToolResult {
summary: Box::new(tool_display_summary(&call, &result)),
call: Box::new(display_tool_result_call(&call, &context)),
result: Box::new(result.clone()),
changed_paths: Vec::new(),
})?;
}
let provider_result = ProviderToolResult {
call_id: call.id,
tool_name: result.tool_name.clone(),
success: false,
output: result.content.clone(),
skill_reads: Vec::new(),
};
return Ok(ToolExecutionOutcome::Dispatched {
provider_result,
tool_result: Box::new(result),
after_hook_failure: None,
subdir_context_items: Vec::new(),
after_hook_context_items: Vec::new(),
});
}
HookAction::Fail(diagnostic) => {
phase_diagnostics.push(diagnostic.clone());
emit_hook_diagnostics(session_persistence, output_sink, &phase_diagnostics)?;
let result = before_hook_failure_activity_result(&call, &diagnostic);
session_persistence.record_required(
SessionEventKind::ToolDisplayResult,
json!({"call_id": call.id.clone(), "result": result.clone()}),
)?;
if let Some(sink) = output_sink.as_deref_mut() {
sink.output_event(OutputEvent::ToolResult {
summary: Box::new(tool_display_summary(&call, &result)),
call: Box::new(display_tool_result_call(&call, &context)),
result: Box::new(result.clone()),
changed_paths: Vec::new(),
})?;
}
return Ok(ToolExecutionOutcome::BeforeHookFailed {
diagnostic,
tool_result: Box::new(result),
});
}
}
}
let (bash_allowed, bash_protection_display) =
request_bash_approval_if_needed(tools, output_sink, &call, &context.cancellation, None)?;
if !bash_allowed {
let result = ToolResult {
tool_name: call.name.clone(),
success: false,
content: "bash command denied by bash protection".to_string(),
metadata: json!({
"call_id": call.id.clone(),
"bash_protection_denied": true,
"bash_protection": bash_protection_display.map(|display| display.metadata()),
}),
display: ToolResultDisplay::default(),
};
session_persistence.record_required(
SessionEventKind::ToolResult,
json!({"call_id": call.id.clone(), "result": result.clone()}),
)?;
if let Some(sink) = output_sink.as_deref_mut() {
sink.output_event(OutputEvent::ToolResult {
summary: Box::new(tool_display_summary(&call, &result)),
call: Box::new(display_tool_result_call(&call, &context)),
result: Box::new(result.clone()),
changed_paths: Vec::new(),
})?;
}
if let Some(activity_id) = context.parent_activity_id.clone() {
emit_tool_finished(output_sink, activity_id, &call, &result)?;
}
return Err(crate::protection::bash::BashProtectionDenied.into());
}
let AgentDispatchOutcome {
dispatch: dispatch_outcome,
context_payloads,
fatal,
} = dispatch_agent_tool(
tools,
hooks,
session_persistence,
output_sink,
subdir_instruction_state.as_deref_mut(),
&call,
&context,
);
let mut result = dispatch_outcome.result;
if let Some(display) = bash_protection_display {
if !result.metadata.is_object() {
result.metadata = json!({});
}
result.metadata["bash_protection"] = display.metadata();
}
let touched_paths = dispatch_outcome.touched_paths;
maybe_append_lsp_diagnostics(
tools,
&call.name,
&mut result,
&touched_paths,
&context.cancellation,
);
let enforcement = tools.and_then(|tools| tools.prompt_injection_enforcement(&result.tool_name));
let result_id = enforcement.map(|_| uuid::Uuid::new_v4().to_string());
if !result.metadata.is_object() {
result.metadata = json!({});
}
result.metadata["prompt_injection_protection"] = json!({
"action": if enforcement.is_some() { "pending" } else { "not_assessed" },
"enforced": enforcement.unwrap_or(false),
"result_id": result_id,
});
session_persistence.record_required(
SessionEventKind::ToolResult,
json!({"call_id": call.id.clone(), "result": result.clone(),
"changed_paths": dispatch_outcome.changed_paths,
"execution_effects": crate::sessions::effects::mutation_evidence(&dispatch_outcome.changed_paths)}),
)?;
let mut dispatch_context_items = Vec::new();
for payload in context_payloads {
session_persistence
.record_required(SessionEventKind::ProviderContextItem, payload.clone())?;
if let Some(item) = super::code_mode::context_item(&payload) {
dispatch_context_items.push(item);
}
}
let assessment_content = tools
.and_then(|_| failed_search_diagnostic_output(&call, &result))
.unwrap_or_else(|| result.content.clone());
let mut provider_output = crate::protection::prompt_injection::replay_projection(
&assessment_content,
&result.metadata["prompt_injection_protection"],
)
.unwrap_or_else(|| assessment_content.clone());
if let Some((assessment, enforcement_enabled)) = tools
.filter(|_| {
!context.cancellation.is_canceled()
&& !super::code_mode::assesses_structured_content(session_persistence, &result)
})
.and_then(|tools| {
tools.assess_tool_result_for_prompt_injection(
&result.tool_name,
&assessment_content,
&json!({
"user_context": context.request_context,
"tool_arguments": crate::typesafe::evidence::excerpt(&call.arguments.to_string(), 4096),
}),
)
})
{
provider_output = assessment.provider_projection(&assessment_content, enforcement_enabled);
result.metadata["prompt_injection_protection"] = assessment.metadata(enforcement_enabled);
result.metadata["prompt_injection_protection"]["result_id"] = json!(result_id);
let mut persisted_assessment =
assessment.metadata_for_persistence(&assessment_content, enforcement_enabled);
persisted_assessment["result_id"] = json!(result_id);
session_persistence.record_required(
SessionEventKind::ToolResultAssessment,
json!({"call_id": call.id, "prompt_injection_protection": persisted_assessment}),
)?;
}
if let Some(metadata) = result.metadata.get("humanize_protection") {
emit_tool_metadata(output_sink, &call, "humanize_protection", metadata.clone())?;
}
if let Some(metadata) = result
.metadata
.get("prompt_injection_protection")
.filter(|_| enforcement.is_some())
{
emit_tool_metadata(
output_sink,
&call,
"prompt_injection_protection",
metadata.clone(),
)?;
}
crate::code_mode::preserve_execution_evidence(&result, &mut provider_output);
if let Some(sink) = output_sink.as_deref_mut() {
sink.output_event(OutputEvent::ToolResult {
summary: Box::new(tool_display_summary(&call, &result)),
call: Box::new(display_tool_result_call(&call, &context)),
result: Box::new(result.clone()),
changed_paths: dispatch_outcome.changed_paths,
})?;
}
let skill_reads = validated_skill_reads(
&result.tool_name,
result.success,
&provider_output,
&result.metadata,
);
let provider_result = ProviderToolResult {
call_id: call.id.clone(),
tool_name: result.tool_name.clone(),
success: result.success,
output: provider_output,
skill_reads,
};
let subdir_context_items = discover_and_record_subdir_context_items(
tools,
session_persistence,
output_sink,
subdir_instruction_state,
&touched_paths,
context.activity_sender.as_ref(),
context.parent_activity_id.as_ref().map(ActivityId::as_str),
&call.id,
)?;
let mut after_hook_failure = None;
let mut after_hook_context_items = Vec::new();
if let Some(hooks) = hooks {
let outcome = hooks.run_after_with_activity(&call, &result, &context);
emit_hook_lifecycle_records(session_persistence, output_sink, &outcome.lifecycle_records)?;
emit_hook_context_injection_records(
session_persistence,
output_sink,
&outcome.context_injection_records,
)?;
emit_provider_context_injection_display(
output_sink,
&outcome.context_injection_records,
&outcome.context_items,
&hooks.injected_content_settings(),
context.parent_activity_id.as_ref(),
context.activity_sender.as_ref(),
)?;
let mut phase_diagnostics = outcome.diagnostics;
after_hook_context_items = outcome.context_items;
if session_persistence.code_mode_parent.is_some() {
for item in after_hook_context_items.drain(..) {
record_provider_context_item(session_persistence, output_sink, &item)?;
}
}
match outcome.action {
HookAction::Continue => {}
HookAction::Block(diagnostic) | HookAction::Fail(diagnostic) => {
phase_diagnostics.push(diagnostic.clone());
after_hook_failure = Some(diagnostic);
}
}
emit_hook_diagnostics(session_persistence, output_sink, &phase_diagnostics)?;
if outcome.canceled {
if let Some(activity_id) = context.parent_activity_id.clone() {
emit_tool_finished(output_sink, activity_id, &call, &result)?;
}
return Err(AgentRunCanceled.into());
}
}
if let Some(error) = fatal {
if let Some(activity_id) = context.parent_activity_id.clone() {
emit_tool_finished(output_sink, activity_id, &call, &result)?;
}
return Err(error);
}
dispatch_context_items.extend(subdir_context_items);
let subdir_context_items = dispatch_context_items;
Ok(ToolExecutionOutcome::Dispatched {
provider_result,
tool_result: Box::new(result),
after_hook_failure,
subdir_context_items,
after_hook_context_items,
})
}
fn maybe_append_lsp_diagnostics(
tools: Option<&ToolRuntime>,
tool_name: &str,
result: &mut ToolResult,
touched_paths: &[TouchedPath],
cancellation: &AgentCancellation,
) {
if !result.success {
return;
}
if tool_name != tool_name::WRITE && tool_name != tool_name::HASH_EDIT {
return;
}
let Some(manager) = tools.and_then(ToolRuntime::lsp_manager) else {
return;
};
if !manager.is_enabled() || !manager.inject_diagnostics_on_edit() {
return;
}
let deadline = Instant::now() + manager.edit_budget();
for touched in touched_paths.iter().filter(|path| {
matches!(
path.kind,
TouchedPathKind::Write | TouchedPathKind::HashEdit
) && path.success
&& path.inside_root
&& !path.is_dir
}) {
let path = &touched.canonical;
let Ok(content) = std::fs::read_to_string(path) else {
continue;
};
if let Some(block) = manager.sync_and_wait_diagnostics(
EditDiagnosticsRequest { path, content },
deadline,
cancellation,
) && !block.trim().is_empty()
{
result.content.push_str("\n\n");
result.content.push_str(&block);
}
}
}
pub(super) struct ToolLifecycleRun<'a, 'sink, 'session, 'run> {
pub(super) tool_calls: Vec<ToolCall>,
pub(super) tools: Option<&'run ToolRuntime>,
pub(super) hooks: Option<&'run HookRuntime>,
pub(super) herdr_reporter: Option<&'run HerdrReporter>,
pub(super) output_sink: &'a mut Option<&'sink mut dyn AgentOutputSink>,
pub(super) cancellation: &'a AgentCancellation,
pub(super) turn_state: &'a mut AgentTurnState,
pub(super) output: &'a mut AgentRunOutput,
pub(super) session_persistence: &'a mut SessionPersistence<'session>,
pub(super) hook_context: HookContextMetadata,
pub(super) context_window_usage: Option<ContextWindowUsage>,
pub(super) request_context: serde_json::Value,
pub(super) subdir_instruction_state:
Option<&'a mut super::subdir_instructions::SubdirInstructionState>,
}
const HERDR_UNKNOWN_TOOL_NAME: &str = "tool";
fn herdr_tool_name(provider_name: &str) -> &'static str {
ToolCapability::from_dispatch_name(provider_name)
.map(ToolCapability::canonical_name)
.unwrap_or(HERDR_UNKNOWN_TOOL_NAME)
}
pub(super) fn run_tool_lifecycle(mut run: ToolLifecycleRun<'_, '_, '_, '_>) -> anyhow::Result<()> {
let reject_control_batch = run.tool_calls.len() > 1
&& run
.tool_calls
.iter()
.any(|call| call.name == tool_name::MAGI_CONTROL);
for (tool_index, call) in run.tool_calls.into_iter().enumerate() {
run.cancellation.check()?;
run.turn_state.register_tool_call(&call)?;
let call_payload = json!({"id":call.id,"name":call.name,"arguments":call.arguments});
run.session_persistence
.record_required(SessionEventKind::ToolCall, call_payload)
.map_err(|error| {
anyhow::anyhow!("failed to persist tool call before dispatch: {error}")
})?;
run.turn_state.append_function_call_if_missing(&call);
let activity = emit_tool_started(
run.output_sink,
run.turn_state.iteration(),
tool_index,
&call,
)?;
if let Some(reporter) = run.herdr_reporter {
reporter.report_tool(herdr_tool_name(&call.name));
}
let turn_id = format!("turn-{}", run.turn_state.iteration());
let message_id = format!("tool-{}", activity.activity_id.as_str());
let mut dispatch_context = ToolDispatchContext::new_with_hook_context_and_cancellation(
Some(activity.activity_id.clone()),
activity.activity_sender.clone(),
run.hook_context.for_tool_call(turn_id, message_id),
run.cancellation.clone(),
);
dispatch_context.request_context = run.request_context.clone();
if let Some(usage) = run.context_window_usage {
dispatch_context = dispatch_context.with_context_window_usage(usage);
}
let outcome = if reject_control_batch {
let result = ToolResult {
tool_name: call.name.clone(),
success: false,
content: "magi_control must be called alone; no tools in this batch were executed"
.to_string(),
metadata: json!({"call_id": call.id.clone(), "batch_rejected": true}),
display: ToolResultDisplay::default(),
};
run.session_persistence.record_required(
SessionEventKind::ToolResult,
json!({"call_id": call.id.clone(), "result": result.clone()}),
)?;
if let Some(sink) = run.output_sink.as_deref_mut() {
sink.output_event(OutputEvent::ToolResult {
summary: Box::new(tool_display_summary(&call, &result)),
call: Box::new(display_tool_result_call(&call, &dispatch_context)),
result: Box::new(result.clone()),
changed_paths: Vec::new(),
})?;
}
ToolExecutionOutcome::Dispatched {
provider_result: ProviderToolResult {
call_id: call.id.clone(),
tool_name: call.name.clone(),
success: false,
output: result.content.clone(),
skill_reads: Vec::new(),
},
tool_result: Box::new(result),
after_hook_failure: None,
subdir_context_items: Vec::new(),
after_hook_context_items: Vec::new(),
}
} else {
execute_tool_call(
run.tools,
run.hooks,
run.session_persistence,
run.output_sink,
run.subdir_instruction_state.as_deref_mut(),
call.clone(),
dispatch_context,
)?
};
match outcome {
ToolExecutionOutcome::Dispatched {
provider_result,
tool_result,
after_hook_failure,
subdir_context_items,
mut after_hook_context_items,
} => {
emit_tool_finished(
run.output_sink,
activity.activity_id.clone(),
&call,
&tool_result,
)?;
let nudge = run.tools.and_then(|tools| {
run.turn_state
.direct_tool_usage
.observe(&tools.code_mode, &provider_result)
});
run.turn_state.append_tool_result(provider_result);
run.turn_state
.append_provider_context_items(subdir_context_items);
after_hook_context_items.extend(nudge.map(|hint| {
ProviderConversationItem::Message(crate::providers::ChatMessage::user(hint))
}));
for item in &after_hook_context_items {
record_provider_context_item(run.session_persistence, run.output_sink, item)?;
}
run.turn_state
.append_provider_context_items(after_hook_context_items);
run.output.tool_results.push((*tool_result).clone());
if let Some(diagnostic) = after_hook_failure {
return Err(HookPolicyError::new(diagnostic).into());
}
}
ToolExecutionOutcome::BeforeHookFailed {
diagnostic,
tool_result,
} => {
emit_tool_finished(run.output_sink, activity.activity_id, &call, &tool_result)?;
return Err(HookPolicyError::new(diagnostic).into());
}
}
}
Ok(())
}