-
Notifications
You must be signed in to change notification settings - Fork 1.1k
fix(google-antigravity): fallback to skip_thought_signature_validator for Gemini 3 function calls #2693
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
fix(google-antigravity): fallback to skip_thought_signature_validator for Gemini 3 function calls #2693
Changes from all commits
File filter
Filter by extension
Conversations
Jump to
Diff view
Diff view
There are no files selected for viewing
| Original file line number | Diff line number | Diff line change |
|---|---|---|
|
|
@@ -745,9 +745,6 @@ export function applyAntigravityReplay(model: string, sessionId: string, content | |
| const now = Date.now(); | ||
| deleteExpiredReplaySessionsThrottled(now); | ||
| const entry = replayCache.get(replayKey(model, sessionId)); | ||
| if (!entry) { | ||
| return contents; | ||
| } | ||
|
|
||
| let touched = false; | ||
| // Align from the END of the recorded signature list. History may have been truncated | ||
|
|
@@ -758,6 +755,21 @@ export function applyAntigravityReplay(model: string, sessionId: string, content | |
| for (let ci = (contents as { role?: string; parts?: unknown[] }[]).length - 1; ci >= 0; ci--) { | ||
| const c = (contents as { role?: string; parts?: unknown[] }[])[ci]; | ||
| if (!c || typeof c !== "object" || c.role !== "model" || !Array.isArray(c.parts)) continue; | ||
| let turnHasSignature = false; | ||
| let firstMissingPart: Record<string, unknown> | undefined; | ||
| for (let pi = 0; pi < c.parts.length; pi++) { | ||
| const raw = c.parts[pi]; | ||
| if (!raw || typeof raw !== "object") continue; | ||
| const part = raw as Record<string, unknown>; | ||
| const fc = part.functionCall as { name?: unknown; args?: unknown } | undefined; | ||
| if (!fc) continue; | ||
| if (part.thoughtSignature !== undefined || part.thought_signature !== undefined) { | ||
| turnHasSignature = true; | ||
| } else if (!firstMissingPart) { | ||
| firstMissingPart = part; | ||
| } | ||
| } | ||
| if (entry) { | ||
| for (let pi = c.parts.length - 1; pi >= 0; pi--) { | ||
| const raw = c.parts[pi]; | ||
| if (!raw || typeof raw !== "object") continue; | ||
|
|
@@ -809,15 +821,24 @@ export function applyAntigravityReplay(model: string, sessionId: string, content | |
| entry.byCall.delete(matchedKey); | ||
| entry.byCall.set(matchedKey, { ...call, touchedAtMs: now }); | ||
| touched = true; | ||
| turnHasSignature = true; | ||
| } | ||
| } else if (part.thoughtSignature === undefined && part.thought_signature === undefined && call) { | ||
| part.thoughtSignature = call.signature; | ||
| touched = true; | ||
| turnHasSignature = true; | ||
| } | ||
| } | ||
| } | ||
| // Fallback: Gemini models require thought_signature on the first functionCall part of a turn. | ||
| // If neither the wire metadata nor replay cache had a valid signature for this model turn, | ||
| // inject the official validator bypass token on the first functionCall part. | ||
| if (!turnHasSignature && firstMissingPart) { | ||
| firstMissingPart.thoughtSignature = "skip_thought_signature_validator"; | ||
| } | ||
|
Comment on lines
+836
to
+838
Contributor
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. 🗄️ Data Integrity & Integration | 🟠 Major | 🏗️ Heavy lift 🔎 Supported by static analysis🏁 Script executed: #!/usr/bin/env bash
set -euo pipefail
rg -n -C 6 '\bapplyAntigravityReplay\s*\(' src tests
rg -n -C 6 '\bantigravityUsesReplayCache\s*\(' src tests
rg -n -C 5 'gemini|claude|gpt-oss|model' src/adapters tests/google-antigravity-replay.test.tsRepository: lidge-jun/opencodex Length of output: 50376 🏁 Script executed: #!/usr/bin/env bash
set -euo pipefail
printf '%s\n' '--- repository conventions ---'
for f in /tmp/coderabbit-repo-knowledge/lidge-jun-opencodex-7afea732/*/*.md; do
[ -f "$f" ] || continue
case "$f" in
*learnings*) continue ;;
esac
printf '\n### %s\n' "$f"
head -80 "$f"
done
printf '%s\n' '--- target definitions and call sites ---'
rg -n -C 12 'function (applyAntigravityReplay|antigravityUsesReplayCache)|const (applyAntigravityReplay|antigravityUsesReplayCache)|export (function|const) (applyAntigravityReplay|antigravityUsesReplayCache)|\b(applyAntigravityReplay|antigravityUsesReplayCache)\b' src/adapters/google-antigravity-replay.ts src tests/google-antigravity-replay.test.ts
printf '%s\n' '--- target implementation ---'
sed -n '760,870p' src/adapters/google-antigravity-replay.ts
printf '%s\n' '--- adapter imports and request emission ---'
sed -n '1,120p' src/adapters/google-antigravity-replay.ts
rg -n -C 10 'thoughtSignature|skip_thought_signature_validator|applyAntigravityReplay|fetch|request|model' src/adapters/google-antigravity-replay.tsRepository: lidge-jun/opencodex Length of output: 50376 🏁 Script executed: #!/usr/bin/env bash
set -euo pipefail
printf '%s\n' '--- apply implementation and fallback branch ---'
sed -n '737,850p' src/adapters/google-antigravity-replay.ts
printf '%s\n' '--- production call sites only ---'
rg -l '\bapplyAntigravityReplay\b' src | sort
while IFS= read -r f; do
printf '\n### %s\n' "$f"
rg -n -C 12 '\bapplyAntigravityReplay\b' "$f"
done < <(rg -l '\bapplyAntigravityReplay\b' src | sort)
printf '%s\n' '--- antigravity adapter files and relevant wire fields ---'
fd -i 'antigravity' src
rg -n -C 8 'applyAntigravityReplay|antigravityUsesReplayCache|thoughtSignature|thought_signature|contents|model' src/adapters -g '*google*' -g '*antigravity*'Repository: lidge-jun/opencodex Length of output: 5771 🏁 Script executed: #!/usr/bin/env bash
set -euo pipefail
printf '%s\n' '--- google.ts replay call sites and model routing ---'
rg -n -C 18 '\b(applyAntigravityReplay|observeAntigravityReplay|clearAntigravityReplay|google-antigravity-replay|antigravity)\b' src/adapters/google.ts
printf '%s\n' '--- google.ts request construction and transport selection ---'
rg -n -C 14 'model|provider|baseUrl|fetch|contents|thoughtSignature|thought_signature' src/adapters/google.ts
printf '%s\n' '--- provider/config model declarations ---'
rg -n -C 8 'antigravity|google-antigravity|gemini|claude|gpt-oss' src tests -g '*.ts' -g '*.json' -g '*.toml'Repository: lidge-jun/opencodex Length of output: 50376 🏁 Script executed: #!/usr/bin/env bash
set -euo pipefail
printf '%s\n' '--- exact google.ts references ---'
rg -n '\b(applyAntigravityReplay|observeAntigravityReplay|clearAntigravityReplay|antigravityUsesReplayCache)\b' src/adapters/google.ts
printf '%s\n' '--- google.ts import and each replay call context ---'
sed -n '1,90p' src/adapters/google.ts
while IFS=: read -r line _; do
start=$((line - 20)); [ "$start" -lt 1 ] && start=1
end=$((line + 25))
sed -n "${start},${end}p" src/adapters/google.ts
done < <(rg -n '\b(applyAntigravityReplay|observeAntigravityReplay|clearAntigravityReplay)\b' src/adapters/google.ts | cut -d: -f1)
printf '%s\n' '--- only model/provider declarations bound to google adapter ---'
rg -n 'google|antigravity|gemini|claude|gpt-oss' src/adapters/google.ts src/config.ts src -g '*.ts' --glob '!src/adapters/google.ts' --glob '!src/adapters/cursor/**' | head -250Repository: lidge-jun/opencodex Length of output: 5836 🏁 Script executed: #!/usr/bin/env bash
set -euo pipefail
printf '%s\n' '--- google.ts request path around replay injection ---'
sed -n '780,885p' src/adapters/google.ts
printf '%s\n' '--- google.ts response observation and invalid-signature handling ---'
sed -n '1015,1075p' src/adapters/google.ts
sed -n '1285,1350p' src/adapters/google.ts
printf '%s\n' '--- Antigravity model catalog and adapter contracts ---'
sed -n '1,260p' src/providers/antigravity-models.ts
rg -n -C 12 'adapter:.*google|google.*adapter|antigravity|google-direct|gemini|claude|gpt-oss' src/providers src/config.ts src/types.ts --glob '*.ts' | head -300Repository: lidge-jun/opencodex Length of output: 44939 🌐 Web query:
💡 Result: Google Antigravity is a suite of AI-driven developer tools, including the Antigravity CLI, SDK, and IDE extensions, integrated with the Gemini Enterprise Agent Platform to provide agentic coding and orchestration capabilities within Google Cloud environments [1][2][3]. The technical terms you referenced relate to the management of "thought signatures," which are cryptographic representations of a model’s internal reasoning state used by Gemini models to maintain continuity across multi-turn and multi-step conversations (such as function calling) [4][5][6]. Key details regarding these components include: 1. Thought Signatures: These are essential for Gemini models when performing sequential function calls. When a model returns a thought signature, it must be included in the subsequent API request to ensure the model maintains its reasoning state [4][6]. Omitting a required signature during multi-step function calling typically results in a 400-level error [4][5]. 2. skip_thought_signature_validator: This is a special bypass string ("skip_thought_signature_validator") that can be provided in the thought_signature field as a last resort [7][5]. It is intended for scenarios where you must inject function call history that was not generated by the Google API (e.g., transferring traces from other models or executing deterministic client-side functions) [7][5]. Using this bypass is discouraged because it can negatively impact model performance [5]. Developers have reported challenges using this bypass through some SDKs because the API expects the literal string, while SDK serialization pipelines may automatically base64-encode the input [8]. 3. gpt-oss / thoughtSignature: While you mentioned gpt-oss in the context of thought signatures, it is important to note that gpt-oss generally refers to research or open-model efforts (often associated with OpenAI's approach to raw Chain of Thought/CoT handling) [9][10]. Gemini's thought signature mechanism is specific to the Gemini Enterprise Agent Platform and Google's Generative AI APIs [4][6]. Managing thought signatures manually is generally avoided by using the Interactions API in stateful mode, which handles these signatures automatically [6]. Citations:
Restrict the thought-signature fallback to Gemini wire models.
🤖 Prompt for AI AgentsSource: MCP tools |
||
| } | ||
|
|
||
| if (touched) { | ||
| if (touched && entry) { | ||
| entry.lastActiveAtMs = now; | ||
| refreshReplaySessionCandidate(replayKey(model, sessionId), entry); | ||
| markReplayDirty(); | ||
|
|
||
| Original file line number | Diff line number | Diff line change |
|---|---|---|
|
|
@@ -78,7 +78,7 @@ describe("antigravity reasoning-replay cache", () => { | |
| observeAntigravityReplay(MODEL, SESSION, [fcPart("get_x", {}, "short")]); | ||
| const contents = [{ role: "model", parts: [{ functionCall: { name: "get_x", args: {} } }] }]; | ||
| applyAntigravityReplay(MODEL, SESSION, contents); | ||
| expect((contents[0].parts[0] as { thoughtSignature?: string }).thoughtSignature).toBeUndefined(); | ||
| expect((contents[0].parts[0] as { thoughtSignature?: string }).thoughtSignature).toBe("skip_thought_signature_validator"); | ||
| }); | ||
|
|
||
| test("does not clobber an existing signature on the outgoing part", () => { | ||
|
|
@@ -153,7 +153,7 @@ describe("antigravity reasoning-replay cache", () => { | |
| ], | ||
| }]; | ||
| applyAntigravityReplay(MODEL, SESSION, contents); | ||
| expect((contents[0].parts[0] as { thoughtSignature?: string }).thoughtSignature).toBeUndefined(); | ||
| expect((contents[0].parts[0] as { thoughtSignature?: string }).thoughtSignature).toBe("skip_thought_signature_validator"); | ||
| expect((contents[0].parts[1] as { thoughtSignature?: string }).thoughtSignature).toBeUndefined(); | ||
| }); | ||
|
|
||
|
|
@@ -165,15 +165,15 @@ describe("antigravity reasoning-replay cache", () => { | |
| ]); | ||
| const contents = [{ role: "model", parts: [{ functionCall: { name: "get_x", args: {} } }] }]; | ||
| applyAntigravityReplay(MODEL, SESSION, contents); | ||
| expect((contents[0].parts[0] as { thoughtSignature?: string }).thoughtSignature).toBeUndefined(); | ||
| expect((contents[0].parts[0] as { thoughtSignature?: string }).thoughtSignature).toBe("skip_thought_signature_validator"); | ||
| }); | ||
|
|
||
| test("clear-on-invalid empties the entry", () => { | ||
| observeAntigravityReplay(MODEL, SESSION, [fcPart("get_x", {}, SIG)]); | ||
| clearAntigravityReplay(MODEL, SESSION); | ||
| const contents = [{ role: "model", parts: [{ functionCall: { name: "get_x", args: {} } }] }]; | ||
| applyAntigravityReplay(MODEL, SESSION, contents); | ||
| expect((contents[0].parts[0] as { thoughtSignature?: string }).thoughtSignature).toBeUndefined(); | ||
| expect((contents[0].parts[0] as { thoughtSignature?: string }).thoughtSignature).toBe("skip_thought_signature_validator"); | ||
| }); | ||
|
|
||
| test("retains EVERY signature across a sequential tool loop (regression)", () => { | ||
|
|
@@ -278,7 +278,7 @@ describe("antigravity reasoning-replay cache", () => { | |
| }]; | ||
| try { | ||
| applyAntigravityReplay(MODEL, SESSION, contents); | ||
| expect((contents[0].parts[0] as { thoughtSignature?: string }).thoughtSignature).toBeUndefined(); | ||
| expect((contents[0].parts[0] as { thoughtSignature?: string }).thoughtSignature).toBe("skip_thought_signature_validator"); | ||
| expect(parseCalled).toBe(false); | ||
| expect(encodeCalledOnPayload).toBe(false); | ||
| } finally { | ||
|
|
@@ -313,7 +313,7 @@ describe("antigravity reasoning-replay cache", () => { | |
| observeAntigravityReplay(MODEL, SESSION, [fcPart("three", {}, "sig-three-cccccccccc")]); | ||
| const contents = ["one", "two", "three"].map(name => ({ role: "model", parts: [fcPart(name, {})] })); | ||
| applyAntigravityReplay(MODEL, SESSION, contents); | ||
| expect((contents[0].parts[0] as { thoughtSignature?: string }).thoughtSignature).toBeUndefined(); | ||
| expect((contents[0].parts[0] as { thoughtSignature?: string }).thoughtSignature).toBe("skip_thought_signature_validator"); | ||
| expect((contents[1].parts[0] as { thoughtSignature?: string }).thoughtSignature).toContain("sig-two"); | ||
| expect((contents[2].parts[0] as { thoughtSignature?: string }).thoughtSignature).toContain("sig-three"); | ||
| }); | ||
|
|
@@ -330,7 +330,7 @@ describe("antigravity reasoning-replay cache", () => { | |
| expect(metrics.calls).toBe(2); | ||
| const contents = ["one", "two", "three"].map(name => ({ role: "model", parts: [fcPart(name, {})] })); | ||
| applyAntigravityReplay(MODEL, SESSION, contents); | ||
| expect((contents[0].parts[0] as { thoughtSignature?: string }).thoughtSignature).toBeUndefined(); | ||
| expect((contents[0].parts[0] as { thoughtSignature?: string }).thoughtSignature).toBe("skip_thought_signature_validator"); | ||
| }); | ||
|
|
||
| test("does not cache one oversized signature", () => { | ||
|
|
@@ -354,7 +354,7 @@ describe("antigravity reasoning-replay cache", () => { | |
| now = 1_001 + 60 * 60 * 1000; | ||
| const expired = [{ role: "model", parts: [fcPart("one", {})] }]; | ||
| applyAntigravityReplay(MODEL, SESSION, expired); | ||
| expect((expired[0].parts[0] as { thoughtSignature?: string }).thoughtSignature).toBeUndefined(); | ||
| expect((expired[0].parts[0] as { thoughtSignature?: string }).thoughtSignature).toBe("skip_thought_signature_validator"); | ||
| } finally { | ||
| Date.now = originalNow; | ||
| } | ||
|
|
@@ -382,7 +382,7 @@ describe("antigravity reasoning-replay cache", () => { | |
| expect(antigravityReplayRetainedStoreSnapshot().bytes).toBe(before.bytes - released); | ||
| const old = [{ role: "model", parts: [fcPart("one", {})] }]; | ||
| applyAntigravityReplay(MODEL, "old", old); | ||
| expect((old[0].parts[0] as { thoughtSignature?: string }).thoughtSignature).toBeUndefined(); | ||
| expect((old[0].parts[0] as { thoughtSignature?: string }).thoughtSignature).toBe("skip_thought_signature_validator"); | ||
| } finally { | ||
| Date.now = originalNow; | ||
| } | ||
|
|
@@ -705,7 +705,7 @@ describe("durable antigravity replay snapshot", () => { | |
| expect(keys).toContain(antigravityReplayKeyForTests(MODEL, SESSION)); | ||
| // The surviving session still replays. | ||
| applyAntigravityReplay(MODEL, "-stale", staleContents); | ||
| expect((staleContents[0].parts[0] as { thoughtSignature?: string }).thoughtSignature).toBeUndefined(); | ||
| expect((staleContents[0].parts[0] as { thoughtSignature?: string }).thoughtSignature).toBe("skip_thought_signature_validator"); | ||
| const contents = [{ role: "model", parts: [{ functionCall: { name: "get_x", args: { a: 1 } } }] }]; | ||
| applyAntigravityReplay(MODEL, SESSION, contents); | ||
| expect((contents[0].parts[0] as { thoughtSignature?: string }).thoughtSignature).toBe(SIG); | ||
|
|
@@ -854,11 +854,11 @@ describe("durable antigravity replay snapshot", () => { | |
| writeFileSync(snapshotPath(), "{not valid json"); | ||
| const contents = [{ role: "model", parts: [{ functionCall: { name: "get_x", args: { a: 1 } } }] }]; | ||
| applyAntigravityReplay(MODEL, SESSION, contents); | ||
| expect((contents[0].parts[0] as { thoughtSignature?: string }).thoughtSignature).toBeUndefined(); | ||
| expect((contents[0].parts[0] as { thoughtSignature?: string }).thoughtSignature).toBe("skip_thought_signature_validator"); | ||
| setAntigravityReplayLimitsForTests(); | ||
| writeFileSync(snapshotPath(), JSON.stringify({ version: 99, sessions: [] })); | ||
| applyAntigravityReplay(MODEL, SESSION, contents); | ||
| expect((contents[0].parts[0] as { thoughtSignature?: string }).thoughtSignature).toBeUndefined(); | ||
| expect((contents[0].parts[0] as { thoughtSignature?: string }).thoughtSignature).toBe("skip_thought_signature_validator"); | ||
|
Comment on lines
+857
to
+861
Contributor
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. 🎯 Functional Correctness | 🟡 Minor | ⚡ Quick win Use a fresh unsigned payload for the second snapshot case. The first Create a fresh unsigned 🤖 Prompt for AI Agents |
||
| }); | ||
|
|
||
| test("clear-on-invalid removes the session from the next snapshot", async () => { | ||
|
|
@@ -873,7 +873,7 @@ describe("durable antigravity replay snapshot", () => { | |
| writeFileSync(snapshotPath(), "x".repeat(33 * 1024 * 1024)); | ||
| const contents = [{ role: "model", parts: [{ functionCall: { name: "get_x", args: { a: 1 } } }] }]; | ||
| applyAntigravityReplay(MODEL, SESSION, contents); | ||
| expect((contents[0].parts[0] as { thoughtSignature?: string }).thoughtSignature).toBeUndefined(); | ||
| expect((contents[0].parts[0] as { thoughtSignature?: string }).thoughtSignature).toBe("skip_thought_signature_validator"); | ||
| }); | ||
|
|
||
| test("flush waits out a blocked writer and persists mutations that land during it", async () => { | ||
|
|
@@ -1000,4 +1000,19 @@ describe("durable antigravity replay snapshot", () => { | |
| warn.mockRestore(); | ||
| } | ||
| }); | ||
| test("fallback to skip_thought_signature_validator on the first functionCall when replay cache misses", () => { | ||
| const contents = [ | ||
| { | ||
| role: "model", | ||
| parts: [ | ||
| { functionCall: { name: "exec", args: { input: "ls" } } }, | ||
| { functionCall: { name: "read", args: { path: "/a" } } } | ||
| ] | ||
| } | ||
| ]; | ||
| applyAntigravityReplay(MODEL, "-uncached-session", contents); | ||
| const parts = (contents[0] as { parts: Record<string, unknown>[] }).parts; | ||
| expect(parts[0].thoughtSignature).toBe("skip_thought_signature_validator"); | ||
| expect(parts[1].thoughtSignature).toBeUndefined(); | ||
| }); | ||
| }); | ||
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
🎯 Functional Correctness | 🟠 Major | ⚡ Quick win
🔎 Supported by static analysis
🏁 Script executed:
Repository: lidge-jun/opencodex
Length of output: 50375
🏁 Script executed:
Repository: lidge-jun/opencodex
Length of output: 20165
Base fallback on the first function call’s valid signature. At
src/adapters/google-antigravity-replay.ts:766, useextractSignatureso short signatures and nestedextra_content.google.thought_signaturevalues are validated correctly. Do not setturnHasSignatureat lines 824 and 829 when a later sibling receives a cached signature; otherwise the first function call can remain unsigned and the fallback at lines 836–838 will not run. Track the first function call, apply the cache, then injectskip_thought_signature_validatoronly whenextractSignature(firstFunctionCall)returnsundefined.🤖 Prompt for AI Agents