Merge pull request #1011 from nldhuyen0047/fix/gemini-function-call-turn-ordering

fix: ensure message history starts with a user turn for Gemini compatibility
This commit is contained in:
Jaber Jaber
2026-04-10 19:12:47 +03:00
committed by GitHub
3 changed files with 47 additions and 2 deletions

View File

@@ -349,6 +349,10 @@ pub async fn run_agent_loop(
// pair across the cut boundary, leaving orphaned blocks that cause the LLM
// to return empty responses (input_tokens=0).
messages = crate::session_repair::validate_and_repair(&messages);
// Ensure history starts with a user turn: trimming may have left an
// assistant turn at position 0, which strict providers (e.g. Gemini)
// reject with INVALID_ARGUMENT on function-call turns.
messages = crate::session_repair::ensure_starts_with_user(messages);
}
// Use autonomous config max_iterations if set, else default
@@ -388,6 +392,8 @@ pub async fn run_agent_loop(
// which may have broken assistant→tool ordering invariants.
if recovery != RecoveryStage::None {
messages = crate::session_repair::validate_and_repair(&messages);
// Ensure history starts with a user turn after overflow recovery.
messages = crate::session_repair::ensure_starts_with_user(messages);
}
// Context guard: compact oversized tool results before LLM call
@@ -1512,6 +1518,10 @@ pub async fn run_agent_loop_streaming(
// pair across the cut boundary, leaving orphaned blocks that cause the LLM
// to return empty responses (input_tokens=0).
messages = crate::session_repair::validate_and_repair(&messages);
// Ensure history starts with a user turn: trimming may have left an
// assistant turn at position 0, which strict providers (e.g. Gemini)
// reject with INVALID_ARGUMENT on function-call turns.
messages = crate::session_repair::ensure_starts_with_user(messages);
}
// Use autonomous config max_iterations if set, else default
@@ -1569,6 +1579,8 @@ pub async fn run_agent_loop_streaming(
// be followed by tool messages" errors after context overflow recovery.)
if recovery != RecoveryStage::None {
messages = crate::session_repair::validate_and_repair(&messages);
// Ensure history starts with a user turn after overflow recovery.
messages = crate::session_repair::ensure_starts_with_user(messages);
}
// Context guard: compact oversized tool results before LLM call

View File

@@ -370,7 +370,11 @@ fn sanitize_gemini_turns(contents: Vec<GeminiContent>) -> Vec<GeminiContent> {
}
// Step 2: Drop orphaned functionCall parts from model turns.
// A model turn with functionCall must be followed by a user turn with functionResponse.
// A model turn with functionCall must be:
// (a) followed by a user turn with functionResponse, AND
// (b) preceded by a user turn (i.e. not at position 0).
// Gemini rejects with INVALID_ARGUMENT if a functionCall turn is at
// position 0 with no preceding user turn, even when (a) is satisfied.
let len = merged.len();
for i in 0..len {
let is_model = merged[i].role.as_deref() == Some("model");
@@ -394,7 +398,9 @@ fn sanitize_gemini_turns(contents: Vec<GeminiContent>) -> Vec<GeminiContent> {
.iter()
.any(|p| matches!(p, GeminiPart::FunctionResponse { .. }));
if !next_has_response {
// After Step 1 merge, i > 0 guarantees a user turn precedes this model
// turn (alternating roles). i == 0 means no preceding user turn.
if i == 0 || !next_has_response {
// Drop the functionCall parts from this model turn (keep text parts)
merged[i]
.parts

View File

@@ -196,6 +196,33 @@ pub fn validate_and_repair_with_stats(messages: &[Message]) -> (Vec<Message>, Re
(merged, stats)
}
/// Ensure the message history starts with a user turn.
///
/// After context trimming the drain boundary may land on an assistant turn,
/// leaving it at position 0. Providers (especially Gemini) require the first
/// message to be from the user. This function drops leading assistant messages
/// and re-validates to clean up newly-orphaned ToolResults.
///
/// The loop handles the edge case where the first user turn consisted entirely
/// of ToolResult blocks that became orphaned (dropped by `validate_and_repair`),
/// which would re-expose another leading assistant turn.
pub fn ensure_starts_with_user(mut messages: Vec<Message>) -> Vec<Message> {
loop {
match messages.iter().position(|m| m.role == Role::User) {
Some(0) | None => break,
Some(i) => {
warn!(
dropped = i,
"Dropping leading assistant turn(s) to ensure history starts with user"
);
messages.drain(..i);
messages = validate_and_repair(&messages);
}
}
}
messages
}
/// Phase 2b: Reorder misplaced ToolResults -- ensure each result follows its use.
///
/// Builds a map of tool_use_id to the index of the assistant message containing it.