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
137 lines
4.3 KiB
Rust
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()
|
|
}
|
|
}
|