use std::sync::Arc;
use serde_json::Value;
use tokio::sync::{RwLock, broadcast, mpsc};
use crate::engine::approval::ApprovalHandler;
use crate::engine::pipeline::{DefaultPipeline, ToolExecutionPipeline};
use crate::engine::recovery::ToolErrorRecovery;
use crate::engine::runtime::event_bus::EventBus;
use crate::engine::runtime::session_manager::SessionManager;
use crate::tool::{ToolContext, ToolControlFlow, ToolOutput, ToolPolicy, ToolRegistry};
use crate::types::{AgentError, AgentResult, Language, RuntimeEvent, SessionId, UserEvent};
pub(crate) struct ToolEngine {
tools: Arc<RwLock<ToolRegistry>>,
approval_handler: Option<Arc<dyn ApprovalHandler>>,
tool_policy: Option<Arc<dyn ToolPolicy>>,
error_recovery: Arc<dyn ToolErrorRecovery>,
event_bus: EventBus,
pipeline: DefaultPipeline,
}
impl ToolEngine {
pub fn new(
tools: ToolRegistry,
approval_handler: Option<Arc<dyn ApprovalHandler>>,
tool_policy: Option<Arc<dyn ToolPolicy>>,
error_recovery: Arc<dyn ToolErrorRecovery>,
event_bus: EventBus,
) -> Self {
let pipeline = DefaultPipeline::new(tool_policy.clone(), None, None);
Self {
tools: Arc::new(RwLock::new(tools)),
approval_handler,
tool_policy,
error_recovery,
event_bus,
pipeline,
}
}
pub async fn definitions(&self) -> Vec<Value> {
self.tools.read().await.definitions()
}
pub fn inject_event_bus_into(&self, tools: &crate::tool::ToolRegistry) {
tools.inject_event_bus(&self.event_bus);
}
pub fn execution_pipeline(&self) -> DefaultPipeline {
self.pipeline.clone()
}
pub async fn execute_tool<F>(
&self,
session_id: &SessionId,
id: &str,
name: &str,
args: &Value,
tool_args_json: &str,
ctx: &ExecutionContext,
event_rx: &mut broadcast::Receiver<RuntimeEvent>,
on_event: &mut F,
) -> AgentResult<ToolExecutionResult>
where
F: FnMut(RuntimeEvent) -> AgentResult<()> + Send,
{
tracing::debug!(
session_id = session_id.id,
tool = name,
args_len = tool_args_json.len(),
"execute tool start"
);
self.event_bus.emit(RuntimeEvent::ToolCallStarted {
session_id: session_id.clone(),
tool_name: name.to_string(),
args_json: tool_args_json.to_string(),
});
EventBus::drain_async_events(event_rx, on_event)?;
let (user_event_tx, mut user_event_rx) = mpsc::unbounded_channel::<UserEvent>();
let tool_context = ToolContext {
session_id: session_id.clone(),
user_event_tx,
llm_client: ctx.llm_client.clone(),
session_store: Some(ctx.session_manager.session_store().clone()),
language: ctx.language.clone(),
cancel_token: ctx.cancel_token.clone(),
};
tracing::debug!(
session_id = session_id.id,
tool = name,
"looking up tool in registry"
);
let tools_guard = self.tools.read().await;
let tool_result = match tools_guard.get(name) {
Some(tool) => {
tracing::debug!(
session_id = session_id.id,
tool = name,
"tool found, executing via pipeline"
);
let pipeline = DefaultPipeline::new(
self.pipeline.policy(),
ctx.tool_timeout_ms,
ctx.max_output_chars,
);
let future = pipeline.execute(tool.as_ref(), args, &tool_context);
tokio::pin!(future);
let output = loop {
tokio::select! {
result = &mut future => break result,
Some(user_event) = user_event_rx.recv() => {
on_event(RuntimeEvent::UserEvent {
session_id: session_id.clone(),
event: user_event,
})?;
}
_ = ctx.cancel_token.cancelled() => {
tracing::info!(session_id = session_id.id, tool = name, "tool execution cancelled");
return Err(crate::types::AgentError::Cancelled);
}
}
};
while let Ok(user_event) = user_event_rx.try_recv() {
on_event(RuntimeEvent::UserEvent {
session_id: session_id.clone(),
event: user_event,
})?;
}
match output {
Ok(output) => output,
Err(e) => {
tracing::error!(session_id = session_id.id, tool_name = name, error = %e, "Tool execution failed");
let error_summary = if ctx.language == Language::Zh {
format!("❌ 执行失败: {}", e)
} else {
format!("❌ Tool execution failed: {}", e)
};
self.event_bus.emit(RuntimeEvent::ToolCallFinished {
session_id: session_id.clone(),
tool_name: name.to_string(),
summary: error_summary,
});
let _ = EventBus::drain_async_events(event_rx, on_event);
return Err(AgentError::ToolExecution {
name: name.to_string(),
source: Box::new(e),
});
}
}
}
None => {
tracing::warn!(
session_id = session_id.id,
tool = name,
"tool not found in registry"
);
ToolOutput {
summary: if ctx.language == Language::Zh {
format!("工具 {} 未找到", name)
} else {
format!("Tool {} not found", name)
},
raw: None,
control_flow: ToolControlFlow::Break,
truncation: None,
}
}
};
self.event_bus.emit(RuntimeEvent::ToolCallFinished {
session_id: session_id.clone(),
tool_name: name.to_string(),
summary: tool_result.summary.clone(),
});
EventBus::drain_async_events(event_rx, on_event)?;
Ok(ToolExecutionResult {
id: id.to_string(),
name: name.to_string(),
output: tool_result,
})
}
pub async fn process_approval<F>(
&self,
session_id: &SessionId,
tool_name: &str,
args: &Value,
_tool_args_json: &str,
ctx: &ExecutionContext,
event_rx: &mut broadcast::Receiver<RuntimeEvent>,
on_event: &mut F,
) -> AgentResult<()>
where
F: FnMut(RuntimeEvent) -> AgentResult<()> + Send,
{
let approval_request = match self.tool_policy.as_ref() {
Some(policy) => policy.evaluate_approval(tool_name, args).await,
None => None,
};
let Some(request) = approval_request else {
return Ok(());
};
let approved = if let Some(key) = request.action_key.as_deref() {
ctx.session_manager.cached_approval(session_id, key).await
} else {
false
};
if approved {
tracing::debug!(
session_id = session_id.id,
tool = tool_name,
"approval cached, skipping"
);
return Ok(());
}
tracing::debug!(session_id = session_id.id, tool = tool_name, risk = ?request.risk_level, "requesting approval");
self.event_bus.emit(RuntimeEvent::AwaitingApproval {
session_id: session_id.clone(),
request: request.clone(),
});
EventBus::drain_async_events(event_rx, on_event)?;
let decision = match self.approval_handler.as_ref() {
Some(handler) => {
let timeout = std::time::Duration::from_secs(
std::env::var("APPROVAL_TIMEOUT_SECS")
.ok()
.and_then(|v| v.parse().ok())
.unwrap_or(300),
);
let result = tokio::time::timeout(
timeout,
handler.approve(request.clone(), ctx.cancel_token.clone()),
)
.await;
match result {
Ok(result) => result.map_err(|e| {
AgentError::internal(format!("Approval handler failed: {e}"))
})?,
Err(_) => {
tracing::warn!(
session_id = session_id.id,
?timeout,
"Approval timed out, defaulting to Deny"
);
crate::types::ApprovalDecision::Deny
}
}
}
None => crate::types::ApprovalDecision::Deny,
};
match decision {
crate::types::ApprovalDecision::AllowOnce => {
tracing::info!(
session_id = session_id.id,
tool = tool_name,
decision = "AllowOnce",
"approval granted"
);
}
crate::types::ApprovalDecision::AllowAlways => {
tracing::info!(
session_id = session_id.id,
tool = tool_name,
decision = "AllowAlways",
"approval granted (cached)"
);
if let Some(action_key) = request.action_key.clone() {
ctx.session_manager
.cache_approval(session_id, action_key)
.await;
}
}
crate::types::ApprovalDecision::Deny => {
tracing::warn!(
session_id = session_id.id,
tool = tool_name,
decision = "Deny",
"approval denied"
);
let denial_summary =
format!("[Action Denied]: tool {} rejected by approval", tool_name);
self.event_bus.emit(RuntimeEvent::ToolCallFinished {
session_id: session_id.clone(),
tool_name: tool_name.to_string(),
summary: denial_summary,
});
let _ = EventBus::drain_async_events(event_rx, on_event);
return Err(AgentError::ApprovalDenied {
tool_name: tool_name.to_string(),
});
}
}
Ok(())
}
pub async fn orchestrate<F>(
&self,
session_id: &SessionId,
tool_calls: &[(String, String, String)],
ctx: &ExecutionContext,
event_rx: &mut broadcast::Receiver<RuntimeEvent>,
on_event: &mut F,
) -> AgentResult<Vec<ToolExecutionResult>>
where
F: FnMut(RuntimeEvent) -> AgentResult<()> + Send,
{
let mut results = Vec::with_capacity(tool_calls.len());
for (id, name, args_str) in tool_calls {
let args: Value =
serde_json::from_str(args_str).map_err(|_| AgentError::ToolArgsInvalid {
name: name.clone(),
raw: args_str.clone(),
})?;
self.process_approval(session_id, name, &args, args_str, ctx, event_rx, on_event)
.await?;
let result = self
.execute_tool(
session_id, id, name, &args, args_str, ctx, event_rx, on_event,
)
.await?;
results.push(result);
}
Ok(results)
}
pub fn error_recovery(&self) -> &Arc<dyn ToolErrorRecovery> {
&self.error_recovery
}
pub fn approval_handler(&self) -> Option<&Arc<dyn ApprovalHandler>> {
self.approval_handler.as_ref()
}
pub fn tools_arc(&self) -> Arc<RwLock<ToolRegistry>> {
self.tools.clone()
}
}
pub struct ToolExecutionResult {
pub id: String,
pub name: String,
pub output: ToolOutput,
}
pub(crate) struct ExecutionContext {
pub session_manager: SessionManager,
pub llm_client: Option<Arc<dyn crate::llm::LlmClient>>,
pub language: Language,
pub tool_timeout_ms: Option<u64>,
pub max_output_chars: Option<usize>,
pub cancel_token: tokio_util::sync::CancellationToken,
}