gitdataai/libs/agent/tool/executor.rs
ZhenYi 09645d8641 fix: resolve multiple bugs across backend and frontend
Security fixes:
- Remove WS token from plaintext log output (ws_universal.rs)
- Replace weak LCG PRNG with rand::thread_rng() for access key generation
- Add project membership check to issue triage endpoint (prevent unauthorized AI usage)
- Validate deepLinkUrl to prevent javascript: navigation (XSS defense-in-depth)

Data integrity fixes:
- Fix UUID truncation in AI model sync (as_u128() as i64 -> timestamp_millis)
- Wrap PR cascade delete in database transaction
- Add missing cascade deletes for room_message_reaction, room_message_edit_history, room_notifications
- Fix N+1 query for last_commit_times (single grouped query instead of per-repo)

Panic prevention:
- Replace unwrap() with safe fallbacks in health/metrics endpoints (email, git-hook apps)
- Replace unwrap() in access key scopes serialization
- Replace expect() in tool executor result map with synthetic error
- Replace expect() in log level parsing with default fallback

Logic bugs:
- Fix users_online metric double-decrement (decrement only when count reaches 0)
- Fix Map iteration + deletion bug in universal-ws.ts onclose handler
- Fix stale audioStream reference in catch block (use local stream variable)
- Add missing reInit event cleanup in carousel.tsx
- Fix email retry backoff integer overflow ((1 << i) as u64 -> 1u64 << i)

React fixes:
- Use message.id instead of index as key in message-list
- Add audio stream cleanup on unmount in use-audio-recording
2026-04-27 13:54:21 +08:00

137 lines
4.3 KiB
Rust

//! Executes tool calls and converts results to OpenAI `tool` messages.
use futures::stream;
use futures::StreamExt;
use crate::client::ChatRequestMessage;
use super::call::{ToolCall, ToolCallResult, ToolError, ToolResult};
use super::context::ToolContext;
pub struct ToolExecutor {
max_tool_calls: usize,
max_depth: u32,
max_concurrency: usize,
}
impl Default for ToolExecutor {
fn default() -> Self {
Self {
max_tool_calls: 128,
max_depth: 5,
max_concurrency: 8,
}
}
}
impl ToolExecutor {
pub fn new() -> Self {
Self::default()
}
pub fn with_max_tool_calls(mut self, max: usize) -> Self {
self.max_tool_calls = max;
self
}
pub fn with_max_depth(mut self, depth: u32) -> Self {
self.max_depth = depth;
self
}
/// Set the maximum number of tool calls executed concurrently.
/// Defaults to 8. Set to 1 for strictly sequential execution.
pub fn with_max_concurrency(mut self, n: usize) -> Self {
self.max_concurrency = n;
self
}
/// # Errors
///
/// Returns `ToolError::MaxToolCallsExceeded` if the total number of tool calls
/// exceeds `max_tool_calls`.
pub async fn execute_batch(
&self,
calls: Vec<ToolCall>,
ctx: &mut ToolContext,
) -> Result<Vec<ToolCallResult>, ToolError> {
if ctx.tool_calls_exceeded() {
return Err(ToolError::MaxToolCallsExceeded(ctx.tool_call_count()));
}
if ctx.recursion_exceeded() {
return Err(ToolError::RecursionLimitExceeded {
max_depth: ctx.depth(),
});
}
ctx.increment_tool_calls();
let concurrency = self.max_concurrency;
let calls_clone: Vec<ToolCall> = calls.clone();
// Execute tool calls concurrently but preserve input order for ID matching.
// buffer_unordered returns results in *completion* order, which mispairs IDs
// on concurrent errors. Instead, track each result with its original index.
let indexed_results: Vec<(usize, Result<ToolCallResult, ToolError>)> = stream::iter(
calls.into_iter().enumerate().map(|(i, call)| {
let child_ctx = ctx.child_context();
async move { (i, self.execute_one(call, child_ctx).await) }
})
)
.buffer_unordered(concurrency)
.collect()
.await;
// Re-sort by original index to restore input order, then pair with original calls.
let mut result_map: std::collections::HashMap<usize, Result<ToolCallResult, ToolError>> =
indexed_results.into_iter().collect();
let results: Vec<ToolCallResult> = calls_clone.into_iter().enumerate().map(|(i, call)| {
let r = result_map.remove(&i).unwrap_or_else(|| Err(ToolError::ExecutionError("missing result for tool call".into())));
r.unwrap_or_else(|e: ToolError| {
ToolCallResult::error(call, e.to_string())
})
}).collect();
Ok(results)
}
async fn execute_one(
&self,
call: ToolCall,
ctx: ToolContext,
) -> Result<ToolCallResult, ToolError> {
let handler = ctx
.registry()
.get(&call.name)
.ok_or_else(|| ToolError::NotFound(call.name.clone()))?
.clone();
let args = call.arguments_json()?;
match handler.execute(ctx, args).await {
Ok(value) => Ok(ToolCallResult::ok(call, value)),
Err(e) => Ok(ToolCallResult::error(call, e.to_string())),
}
}
pub fn to_tool_messages(results: &[ToolCallResult]) -> Vec<ChatRequestMessage> {
results
.iter()
.map(|r| {
let content = match &r.result {
ToolResult::Ok(v) => {
serde_json::to_string(v).unwrap_or_else(|_| "null".to_string())
}
ToolResult::Error(msg) => serde_json::to_string(&serde_json::json!({
"error": msg
}))
.unwrap_or_else(|_| r#"{"error":"unknown error"}"#.to_string()),
};
ChatRequestMessage::tool(&r.call.id, &content)
})
.collect()
}
}