Skip to content

Commit a603dc9

Browse files
authored
Merge pull request #1011 from nldhuyen0047/fix/gemini-function-call-turn-ordering
fix: ensure message history starts with a user turn for Gemini compatibility
2 parents 8a21971 + 3c221dc commit a603dc9

File tree

3 files changed

+47
-2
lines changed

3 files changed

+47
-2
lines changed

crates/openfang-runtime/src/agent_loop.rs

Lines changed: 12 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -349,6 +349,10 @@ pub async fn run_agent_loop(
349349
// pair across the cut boundary, leaving orphaned blocks that cause the LLM
350350
// to return empty responses (input_tokens=0).
351351
messages = crate::session_repair::validate_and_repair(&messages);
352+
// Ensure history starts with a user turn: trimming may have left an
353+
// assistant turn at position 0, which strict providers (e.g. Gemini)
354+
// reject with INVALID_ARGUMENT on function-call turns.
355+
messages = crate::session_repair::ensure_starts_with_user(messages);
352356
}
353357

354358
// Use autonomous config max_iterations if set, else default
@@ -388,6 +392,8 @@ pub async fn run_agent_loop(
388392
// which may have broken assistant→tool ordering invariants.
389393
if recovery != RecoveryStage::None {
390394
messages = crate::session_repair::validate_and_repair(&messages);
395+
// Ensure history starts with a user turn after overflow recovery.
396+
messages = crate::session_repair::ensure_starts_with_user(messages);
391397
}
392398

393399
// Context guard: compact oversized tool results before LLM call
@@ -1512,6 +1518,10 @@ pub async fn run_agent_loop_streaming(
15121518
// pair across the cut boundary, leaving orphaned blocks that cause the LLM
15131519
// to return empty responses (input_tokens=0).
15141520
messages = crate::session_repair::validate_and_repair(&messages);
1521+
// Ensure history starts with a user turn: trimming may have left an
1522+
// assistant turn at position 0, which strict providers (e.g. Gemini)
1523+
// reject with INVALID_ARGUMENT on function-call turns.
1524+
messages = crate::session_repair::ensure_starts_with_user(messages);
15151525
}
15161526

15171527
// Use autonomous config max_iterations if set, else default
@@ -1569,6 +1579,8 @@ pub async fn run_agent_loop_streaming(
15691579
// be followed by tool messages" errors after context overflow recovery.)
15701580
if recovery != RecoveryStage::None {
15711581
messages = crate::session_repair::validate_and_repair(&messages);
1582+
// Ensure history starts with a user turn after overflow recovery.
1583+
messages = crate::session_repair::ensure_starts_with_user(messages);
15721584
}
15731585

15741586
// Context guard: compact oversized tool results before LLM call

crates/openfang-runtime/src/drivers/gemini.rs

Lines changed: 8 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -370,7 +370,11 @@ fn sanitize_gemini_turns(contents: Vec<GeminiContent>) -> Vec<GeminiContent> {
370370
}
371371

372372
// Step 2: Drop orphaned functionCall parts from model turns.
373-
// A model turn with functionCall must be followed by a user turn with functionResponse.
373+
// A model turn with functionCall must be:
374+
// (a) followed by a user turn with functionResponse, AND
375+
// (b) preceded by a user turn (i.e. not at position 0).
376+
// Gemini rejects with INVALID_ARGUMENT if a functionCall turn is at
377+
// position 0 with no preceding user turn, even when (a) is satisfied.
374378
let len = merged.len();
375379
for i in 0..len {
376380
let is_model = merged[i].role.as_deref() == Some("model");
@@ -394,7 +398,9 @@ fn sanitize_gemini_turns(contents: Vec<GeminiContent>) -> Vec<GeminiContent> {
394398
.iter()
395399
.any(|p| matches!(p, GeminiPart::FunctionResponse { .. }));
396400

397-
if !next_has_response {
401+
// After Step 1 merge, i > 0 guarantees a user turn precedes this model
402+
// turn (alternating roles). i == 0 means no preceding user turn.
403+
if i == 0 || !next_has_response {
398404
// Drop the functionCall parts from this model turn (keep text parts)
399405
merged[i]
400406
.parts

crates/openfang-runtime/src/session_repair.rs

Lines changed: 27 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -196,6 +196,33 @@ pub fn validate_and_repair_with_stats(messages: &[Message]) -> (Vec<Message>, Re
196196
(merged, stats)
197197
}
198198

199+
/// Ensure the message history starts with a user turn.
200+
///
201+
/// After context trimming the drain boundary may land on an assistant turn,
202+
/// leaving it at position 0. Providers (especially Gemini) require the first
203+
/// message to be from the user. This function drops leading assistant messages
204+
/// and re-validates to clean up newly-orphaned ToolResults.
205+
///
206+
/// The loop handles the edge case where the first user turn consisted entirely
207+
/// of ToolResult blocks that became orphaned (dropped by `validate_and_repair`),
208+
/// which would re-expose another leading assistant turn.
209+
pub fn ensure_starts_with_user(mut messages: Vec<Message>) -> Vec<Message> {
210+
loop {
211+
match messages.iter().position(|m| m.role == Role::User) {
212+
Some(0) | None => break,
213+
Some(i) => {
214+
warn!(
215+
dropped = i,
216+
"Dropping leading assistant turn(s) to ensure history starts with user"
217+
);
218+
messages.drain(..i);
219+
messages = validate_and_repair(&messages);
220+
}
221+
}
222+
}
223+
messages
224+
}
225+
199226
/// Phase 2b: Reorder misplaced ToolResults -- ensure each result follows its use.
200227
///
201228
/// Builds a map of tool_use_id to the index of the assistant message containing it.

0 commit comments

Comments
 (0)