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:
Nikketryhard
2026-02-16 18:43:16 -06:00
parent 38b4130c55
commit 6bda2ecafa
3 changed files with 153 additions and 7 deletions

View File

@@ -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");