mod hooks;
mod lifecycle;
mod results;
mod state;
#[cfg(test)]
mod tests;
mod trace;
use agent_contracts::runtime::RuntimeView;
use agent_contracts::tool::{ToolCall, ToolExecutor, ToolSpecView};
use agent_contracts::trace::TraceOutcome;
use agent_types::events::ToolLifecycleEvent;
use agent_types::tool::{FinalToolCall, PreHookResult, ToolExecutionError, ToolExecutionResult};
use async_trait::async_trait;
use std::sync::Arc;
use self::results::{denied_reason, format_tool_args_preview, result_error_message};
use self::state::{ExecutorPhaseResult, ToolExecutionState};
pub struct ToolCallImpl {
final_call: FinalToolCall,
spec: Arc<dyn ToolSpecView>,
executor: Arc<dyn ToolExecutor>,
}
impl ToolCallImpl {
pub fn new(
final_call: FinalToolCall,
spec: Arc<dyn ToolSpecView>,
executor: Arc<dyn ToolExecutor>,
) -> Self {
Self {
final_call,
spec,
executor,
}
}
fn initialize_execution_state(&self) -> ToolExecutionState {
ToolExecutionState {
final_call: self.final_call.clone(),
trace_span: None,
lifecycle_record: None,
pre_hook_results: Vec::new(),
post_hook_results: Vec::new(),
error_hook_results: Vec::new(),
raw_outcome: None,
execution_error: None,
}
}
}
#[async_trait]
impl ToolCall for ToolCallImpl {
fn final_call(&self) -> &FinalToolCall {
&self.final_call
}
async fn execute(
&self,
runtime: &dyn RuntimeView,
) -> Result<ToolExecutionResult, ToolExecutionError> {
let mut state = self.initialize_execution_state();
self.begin_trace_span(&mut state, runtime).await;
match self.run_pre_hook_sequence(&mut state, runtime).await {
Ok(pre_hook_results) => {
state.pre_hook_results = pre_hook_results;
self.update_trace_span(&state, runtime, "pre_hooks_done")
.await;
}
Err(error) => {
tracing::warn!(
call_id = %state.final_call.call_id,
tool = %state.final_call.tool_name,
error = %error,
"pre-hook phase failed"
);
}
}
if state
.pre_hook_results
.iter()
.any(|result| matches!(result, PreHookResult::Deny { .. }))
{
let result = self.build_denied_result(&state);
self.emit_tool_event(
runtime,
ToolLifecycleEvent::Denied {
call_id: state.final_call.call_id.clone(),
tool_name: state.final_call.tool_name.clone(),
reason: denied_reason(&state),
args_preview: format_tool_args_preview(&state.final_call),
},
);
self.end_trace_span(
&mut state,
runtime,
TraceOutcome::Denied,
&result,
"pre_hook_deny",
)
.await;
return Ok(result);
}
match self.load_execution_context(&state.final_call, runtime) {
Ok(lifecycle_record) => {
state.lifecycle_record = Some(lifecycle_record);
self.mark_lifecycle_running(&mut state, runtime);
self.emit_tool_event(
runtime,
ToolLifecycleEvent::Running {
call_id: state.final_call.call_id.clone(),
tool_name: state.final_call.tool_name.clone(),
args_preview: format_tool_args_preview(&state.final_call),
},
);
self.update_trace_span(&state, runtime, "running").await;
}
Err(error) => {
state.execution_error = Some(error.clone());
match self.run_error_hook_sequence(&state, &error, runtime).await {
Ok(error_hook_results) => {
state.error_hook_results = error_hook_results;
}
Err(error_hook_failure) => {
tracing::warn!(
call_id = %state.final_call.call_id,
tool = %state.final_call.tool_name,
error = %error_hook_failure,
"error-hook phase failed after lifecycle begin failure"
);
}
}
let result = self.build_failed_result(&state, error);
self.emit_tool_event(
runtime,
ToolLifecycleEvent::Failed {
call_id: state.final_call.call_id.clone(),
tool_name: state.final_call.tool_name.clone(),
error: result_error_message(&result),
args_preview: format_tool_args_preview(&state.final_call),
},
);
self.end_trace_span(
&mut state,
runtime,
TraceOutcome::Error,
&result,
"lifecycle_begin_failed",
)
.await;
return Ok(result);
}
}
match self
.invoke_resolved_executor(&state.final_call, runtime)
.await
{
Ok(ExecutorPhaseResult::Completed(raw_outcome)) => {
state.raw_outcome = Some(raw_outcome);
self.update_trace_span(&state, runtime, "executor_completed")
.await;
}
Ok(ExecutorPhaseResult::Suspended(suspend_token)) => {
let result = self.build_suspended_result(&state, suspend_token);
self.persist_terminal_lifecycle(&mut state, &result, runtime);
self.end_trace_span(&mut state, runtime, TraceOutcome::Ok, &result, "suspended")
.await;
return Ok(result);
}
Ok(ExecutorPhaseResult::Failed(execution_error)) => {
state.execution_error = Some(execution_error);
self.update_trace_span(&state, runtime, "executor_failed")
.await;
}
Err(error) => {
state.execution_error = Some(error.clone());
self.collect_error_hook_results_after_begin(&mut state, &error, runtime)
.await;
self.update_trace_span(&state, runtime, "executor_invoke_error")
.await;
let result = self.build_failed_result(&state, error);
self.persist_terminal_lifecycle(&mut state, &result, runtime);
self.emit_tool_event(
runtime,
ToolLifecycleEvent::Failed {
call_id: state.final_call.call_id.clone(),
tool_name: state.final_call.tool_name.clone(),
error: result_error_message(&result),
args_preview: format_tool_args_preview(&state.final_call),
},
);
self.end_trace_span(
&mut state,
runtime,
TraceOutcome::Error,
&result,
"executor_invoke_error",
)
.await;
return Ok(result);
}
}
if let Some(execution_error) = state.execution_error.as_ref() {
let execution_error = execution_error.clone();
self.collect_error_hook_results_after_begin(&mut state, &execution_error, runtime)
.await;
self.update_trace_span(&state, runtime, "error_hooks_done")
.await;
let result = self.build_failed_result(&state, execution_error);
self.persist_terminal_lifecycle(&mut state, &result, runtime);
self.emit_tool_event(
runtime,
ToolLifecycleEvent::Failed {
call_id: state.final_call.call_id.clone(),
tool_name: state.final_call.tool_name.clone(),
error: result_error_message(&result),
args_preview: format_tool_args_preview(&state.final_call),
},
);
self.end_trace_span(
&mut state,
runtime,
TraceOutcome::Error,
&result,
"executor_error",
)
.await;
return Ok(result);
}
if state.raw_outcome.is_some() {
match self.run_post_hook_sequence(&mut state, runtime).await {
Ok(post_hook_results) => {
state.post_hook_results = post_hook_results;
self.update_trace_span(&state, runtime, "post_hooks_done")
.await;
}
Err(error) => {
state.execution_error = Some(error.clone());
self.collect_error_hook_results_after_begin(&mut state, &error, runtime)
.await;
self.update_trace_span(&state, runtime, "post_hook_error")
.await;
let result = self.build_completed_result(
&state,
state
.raw_outcome
.clone()
.expect("post-hook error should preserve completed raw_outcome"),
);
self.persist_terminal_lifecycle(&mut state, &result, runtime);
self.emit_tool_event(
runtime,
ToolLifecycleEvent::Completed {
call_id: state.final_call.call_id.clone(),
tool_name: state.final_call.tool_name.clone(),
args_preview: format_tool_args_preview(&state.final_call),
},
);
self.end_trace_span(
&mut state,
runtime,
TraceOutcome::Error,
&result,
"post_hook_error",
)
.await;
return Ok(result);
}
}
}
if let Some(execution_error) = state.execution_error.as_ref() {
let execution_error = execution_error.clone();
self.collect_error_hook_results_after_begin(&mut state, &execution_error, runtime)
.await;
self.update_trace_span(&state, runtime, "terminal_error_hooks_done")
.await;
let result = self.build_failed_result(&state, execution_error);
self.persist_terminal_lifecycle(&mut state, &result, runtime);
self.emit_tool_event(
runtime,
ToolLifecycleEvent::Failed {
call_id: state.final_call.call_id.clone(),
tool_name: state.final_call.tool_name.clone(),
error: result_error_message(&result),
args_preview: format_tool_args_preview(&state.final_call),
},
);
self.end_trace_span(
&mut state,
runtime,
TraceOutcome::Error,
&result,
"terminal_error",
)
.await;
return Ok(result);
}
let result = self.build_completed_result(
&state,
state
.raw_outcome
.clone()
.expect("completed tool call should have raw_outcome"),
);
self.persist_terminal_lifecycle(&mut state, &result, runtime);
self.emit_tool_event(
runtime,
ToolLifecycleEvent::Completed {
call_id: state.final_call.call_id.clone(),
tool_name: state.final_call.tool_name.clone(),
args_preview: format_tool_args_preview(&state.final_call),
},
);
self.end_trace_span(&mut state, runtime, TraceOutcome::Ok, &result, "completed")
.await;
Ok(result)
}
}