fix: extend multi-round tool history to responses and gemini endpoints

- proxy.rs: push_tool_round_calls alongside set_last_function_calls
  when Google responds with functionCall — accumulates rounds
- responses.rs: attach_tool_round_results to pair tool results with
  the correct round instead of flat add_tool_result
- gemini.rs: same attach_tool_round_results integration
- store.rs: add push_tool_round_calls and attach_tool_round_results
  methods for cross-request round accumulation
- Legacy add_tool_result kept for backward compat alongside new path
This commit is contained in:
Nikketryhard
2026-02-16 19:11:38 -06:00
parent 39381a4dfe
commit 32f02d6456
4 changed files with 60 additions and 6 deletions

View File

@@ -209,22 +209,34 @@ pub(crate) async fn handle_gemini(
// Handle tool results (Gemini format: functionResponse) // Handle tool results (Gemini format: functionResponse)
if let Some(ref results) = body.tool_results { if let Some(ref results) = body.tool_results {
let mut pending: Vec<PendingToolResult> = Vec::new();
for r in results { for r in results {
if let Some(fr) = r.get("functionResponse") { if let Some(fr) = r.get("functionResponse") {
let name = fr["name"].as_str().unwrap_or("unknown").to_string(); let name = fr["name"].as_str().unwrap_or("unknown").to_string();
let response = fr.get("response").cloned().unwrap_or(serde_json::json!({})); let response = fr.get("response").cloned().unwrap_or(serde_json::json!({}));
// Legacy compat
state state
.mitm_store .mitm_store
.add_tool_result(PendingToolResult { .add_tool_result(PendingToolResult {
name, name: name.clone(),
result: response, result: response.clone(),
}) })
.await; .await;
pending.push(PendingToolResult {
name,
result: response,
});
} }
} }
if !pending.is_empty() {
state
.mitm_store
.attach_tool_round_results(pending)
.await;
}
info!( info!(
count = results.len(), count = results.len(),
"Stored Gemini-native tool results for MITM injection" "Stored Gemini-native tool results for MITM injection (attached to tool round)"
); );
} }

View File

@@ -242,6 +242,7 @@ pub(crate) async fn handle_responses(
// Handle tool result submission (function_call_output in input) // Handle tool result submission (function_call_output in input)
let is_tool_result_turn = !tool_results.is_empty(); let is_tool_result_turn = !tool_results.is_empty();
if is_tool_result_turn { if is_tool_result_turn {
let mut pending: Vec<PendingToolResult> = Vec::new();
for tr in &tool_results { for tr in &tool_results {
// Look up function name from call_id // Look up function name from call_id
let name = state let name = state
@@ -254,17 +255,28 @@ pub(crate) async fn handle_responses(
let result_value = serde_json::from_str::<serde_json::Value>(&tr.output) let result_value = serde_json::from_str::<serde_json::Value>(&tr.output)
.unwrap_or_else(|_| serde_json::json!({"result": tr.output})); .unwrap_or_else(|_| serde_json::json!({"result": tr.output}));
// Also store as pending (legacy compat)
state state
.mitm_store .mitm_store
.add_tool_result(PendingToolResult { .add_tool_result(PendingToolResult {
name, name: name.clone(),
result: result_value, result: result_value.clone(),
}) })
.await; .await;
pending.push(PendingToolResult {
name,
result: result_value,
});
} }
// Attach results to the latest open ToolRound (pushed by proxy.rs)
state
.mitm_store
.attach_tool_round_results(pending)
.await;
info!( info!(
count = tool_results.len(), count = tool_results.len(),
"Stored tool results for MITM injection" "Stored tool results for MITM injection (attached to tool round)"
); );
} }

View File

@@ -826,6 +826,7 @@ async fn handle_http_over_tls(
.await; .await;
} }
store.set_last_function_calls(calls.clone()).await; store.set_last_function_calls(calls.clone()).await;
store.push_tool_round_calls(calls.clone()).await;
info!( info!(
"MITM: stored {} function call(s) from initial body", "MITM: stored {} function call(s) from initial body",
calls.len() calls.len()
@@ -902,6 +903,7 @@ async fn handle_http_over_tls(
.await; .await;
} }
store.set_last_function_calls(calls.clone()).await; store.set_last_function_calls(calls.clone()).await;
store.push_tool_round_calls(calls.clone()).await;
info!( info!(
"MITM: stored {} function call(s) from body chunk", "MITM: stored {} function call(s) from body chunk",
calls.len() calls.len()

View File

@@ -537,6 +537,34 @@ impl MitmStore {
std::mem::take(&mut *self.tool_rounds.write().await) std::mem::take(&mut *self.tool_rounds.write().await)
} }
/// Push a new tool round from Google's response (calls only, results empty).
/// Called by proxy.rs when the MITM intercepts functionCall parts.
pub async fn push_tool_round_calls(&self, calls: Vec<CapturedFunctionCall>) {
if !calls.is_empty() {
self.tool_rounds.write().await.push(ToolRound {
calls,
results: Vec::new(),
});
}
}
/// Attach tool results to the latest incomplete tool round (one with empty results).
/// Called by responses.rs/gemini.rs when the client sends tool results.
/// If there's no open round, creates a legacy round with no calls.
pub async fn attach_tool_round_results(&self, results: Vec<PendingToolResult>) {
let mut rounds = self.tool_rounds.write().await;
// Find the last round that has no results yet
if let Some(round) = rounds.iter_mut().rev().find(|r| r.results.is_empty()) {
round.results = results;
} else {
// No open round — probably a race or legacy path, create standalone
rounds.push(ToolRound {
calls: Vec::new(),
results,
});
}
}
// ── Direct response capture (bypass LS) ────────────────────────────── // ── Direct response capture (bypass LS) ──────────────────────────────
/// Set (replace) the captured response text. /// Set (replace) the captured response text.