v3.11.10: fix Antigravity — interleave function_call/output pairs, trim Gemini turns (PR #11)

This commit is contained in:
Roman | RyzenAdvanced
2026-05-26 21:47:32 +04:00
Unverified
parent f6827f6c84
commit aa7b9e8280
6 changed files with 51 additions and 6 deletions

View File

@@ -27,6 +27,10 @@ model_catalog_json = ""
"""
CHANGELOG = [
("3.11.10", "2026-05-26", [
"Fix Antigravity: interleave function_call/output pairs (PR #11)",
"Gemini sanitizer: trim non-user turns for Google API compliance",
]),
("3.11.9", "2026-05-26", [
"Fix Antigravity: preserve functionCall/functionResponse (PR #10)",
"Prevents tool responses from being dropped in multi-turn sessions",

View File

@@ -83,6 +83,11 @@ model_catalog_json = ""
"""
CHANGELOG = [
("3.11.10", "2026-05-26", [
"Fix Antigravity: interleave function_call/output pairs in correct sequence (PR #11)",
"Fix Gemini sanitizer: trim leading/trailing non-user turns for Google API compliance",
"Stricter function call/response isolation — no merging across role boundaries",
]),
("3.11.9", "2026-05-26", [
"Fix Antigravity: preserve functionCall/functionResponse in Gemini sanitizer (PR #10)",
"Prevents tool responses from being merged/dropped in multi-turn Antigravity sessions",

View File

@@ -4958,6 +4958,7 @@ def _antigravity_normalize_context(input_data, model=""):
if cid in tool_call_ids:
paired_calls.append((idx, item))
# Build result maintaining proper function_call -> function_call_output pairing
result = list(dev_messages)
compaction_summaries = []
@@ -4973,11 +4974,30 @@ def _antigravity_normalize_context(input_data, model=""):
summary_text = f"[Tool history summary: {n_summarized} older tool outputs omitted. {n_tool_calls} prior function calls were made for file inspection/editing.]"
result.append({"type": "message", "role": "user", "content": [{"type": "input_text", "text": summary_text}]})
for _, call_item in paired_calls:
result.append(call_item)
# CRITICAL FIX: Interleave function_calls with their corresponding function_call_outputs
# to maintain the required sequence: function_call -> function_call_output -> function_call -> ...
# Build a lookup map: call_id -> function_call_output item
tool_output_map = {}
for _, tool_item in kept_tools:
result.append(tool_item)
cid = tool_item.get("call_id", tool_item.get("id", ""))
if cid:
tool_output_map[cid] = tool_item
# First, add all paired function_calls followed immediately by their responses
added_call_ids = set()
for _, call_item in paired_calls:
cid = call_item.get("call_id", call_item.get("id", ""))
result.append(call_item)
added_call_ids.add(cid)
# Immediately append the corresponding function_call_output if available
if cid in tool_output_map:
result.append(tool_output_map[cid])
# Add any remaining tool outputs that weren't paired (orphans)
for _, tool_item in kept_tools:
cid = tool_item.get("call_id", tool_item.get("id", ""))
if cid not in added_call_ids:
result.append(tool_item)
for cs_item in compaction_summaries:
result.append(cs_item)
@@ -5569,17 +5589,21 @@ class Handler(http.server.BaseHTTPRequestHandler):
# Skip duplicate user text messages, but NEVER skip function responses
if role == "user" and text_key and text_key == last_user_text and not has_function_response:
continue
# Only merge same-role messages if they don't contain function calls/responses
# Function calls and responses must remain as separate turns
# CRITICAL FIX: Function calls (model role) and function responses (user role) MUST NOT be merged
# Google's API requires strict alternation: functionCall (model) -> functionResponse (user)
# Never merge across role boundaries when function calls/responses are involved
if role == last_role and role in ("user", "model") and sanitized and not has_function_call and not has_function_response:
# Only merge same-role consecutive text messages without tool content
sanitized[-1].setdefault("parts", []).extend(parts)
else:
sanitized.append({"role": role, "parts": parts})
if role == "user" and text_key:
last_user_text = text_key
last_role = role
# Trim leading non-user messages (Google expects conversation to start with user)
while sanitized and sanitized[0].get("role") != "user":
sanitized.pop(0)
# Trim trailing non-user messages (must end with user turn for continuation)
while sanitized and sanitized[-1].get("role") != "user":
sanitized.pop()
contents = sanitized