fix: tool call race conditions and missing completions tool result extraction
- store.rs: record_function_call now falls back to active_cascade_id (matching record_usage behavior) instead of blind _latest fallback - store.rs: add cascade-aware take_function_calls(cascade_id) method with priority: exact match → active cascade → _latest → any key - completions.rs: extract tool_calls from assistant messages and tool results from tool messages, storing them for MITM injection. This was the ROOT CAUSE — the completions handler stored tool definitions but never extracted tool results, so modify_request couldn't rewrite the LS conversation history with proper functionCall/functionResponse - responses.rs: use cascade-aware take_function_calls for consistency
This commit is contained in:
@@ -345,10 +345,18 @@ impl MitmStore {
|
||||
}
|
||||
|
||||
/// Record a captured function call from Google's response.
|
||||
///
|
||||
/// Falls back to `active_cascade_id` (set by the API handler) when no
|
||||
/// cascade hint is available from the request body, matching
|
||||
/// `record_usage`'s fallback behavior for consistent correlation.
|
||||
pub async fn record_function_call(&self, cascade_id: Option<&str>, fc: CapturedFunctionCall) {
|
||||
let key = cascade_id
|
||||
.map(|s| s.to_string())
|
||||
.unwrap_or_else(|| "_latest".to_string());
|
||||
let key = if let Some(cid) = cascade_id {
|
||||
cid.to_string()
|
||||
} else if let Some(active) = self.active_cascade_id.read().await.as_ref() {
|
||||
active.clone()
|
||||
} else {
|
||||
"_latest".to_string()
|
||||
};
|
||||
info!(
|
||||
cascade = %key,
|
||||
tool = %fc.name,
|
||||
@@ -383,7 +391,50 @@ impl MitmStore {
|
||||
self.awaiting_tool_result.store(false, Ordering::SeqCst);
|
||||
}
|
||||
|
||||
/// Take pending function calls for a specific cascade.
|
||||
///
|
||||
/// Priority: exact cascade_id → active_cascade_id → `_latest` → any key.
|
||||
/// This prevents cross-cascade contamination when multiple requests are
|
||||
/// in-flight simultaneously.
|
||||
pub async fn take_function_calls(&self, cascade_id: &str) -> Option<Vec<CapturedFunctionCall>> {
|
||||
let mut pending = self.pending_function_calls.write().await;
|
||||
|
||||
// 1. Exact cascade match
|
||||
if let Some(result) = pending.remove(cascade_id) {
|
||||
self.has_active_function_call.store(false, Ordering::SeqCst);
|
||||
return Some(result);
|
||||
}
|
||||
|
||||
// 2. Active cascade (set by API handler)
|
||||
if let Some(active) = self.active_cascade_id.read().await.as_ref() {
|
||||
if active != cascade_id {
|
||||
if let Some(result) = pending.remove(active.as_str()) {
|
||||
self.has_active_function_call.store(false, Ordering::SeqCst);
|
||||
return Some(result);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// 3. Fallback to _latest
|
||||
if let Some(result) = pending.remove("_latest") {
|
||||
self.has_active_function_call.store(false, Ordering::SeqCst);
|
||||
return Some(result);
|
||||
}
|
||||
|
||||
// 4. Last resort: any key
|
||||
if let Some(key) = pending.keys().next().cloned() {
|
||||
let result = pending.remove(&key);
|
||||
if result.is_some() {
|
||||
self.has_active_function_call.store(false, Ordering::SeqCst);
|
||||
}
|
||||
return result;
|
||||
}
|
||||
|
||||
None
|
||||
}
|
||||
|
||||
/// Take any pending function calls (ignoring cascade ID).
|
||||
/// Legacy method — prefer `take_function_calls(cascade_id)` for proper correlation.
|
||||
pub async fn take_any_function_calls(&self) -> Option<Vec<CapturedFunctionCall>> {
|
||||
let mut pending = self.pending_function_calls.write().await;
|
||||
let result = pending.remove("_latest");
|
||||
|
||||
Reference in New Issue
Block a user