diff --git a/src/adapters/google.ts b/src/adapters/google.ts index f152d91741..d3c1995f34 100644 --- a/src/adapters/google.ts +++ b/src/adapters/google.ts @@ -698,9 +698,10 @@ export function createGoogleAdapter(provider: OcxProviderConfig): ProviderAdapte // Antigravity use separate model namespaces so opaque provider state cannot cross routes. const replayModel = provider.googleMode === "cloud-code-assist" ? antigravityModel : vertexReplayModel; const replaySession = provider.googleMode === "cloud-code-assist" ? antigravitySession : vertexReplaySession; + let observedStreamThoughtSig = pendingStreamThoughtSig; if ((provider.googleMode === "cloud-code-assist" || provider.googleMode === "vertex") && parts && replayModel && replaySession) { - pendingStreamThoughtSig = observeAntigravityReplay( + observedStreamThoughtSig = observeAntigravityReplay( replayModel, replaySession, parts as unknown[], @@ -747,6 +748,10 @@ export function createGoogleAdapter(provider: OcxProviderConfig): ProviderAdapte yield { type: "tool_call_end" }; } } + // The observer scans the complete frame to populate replay state. Publish its carried + // signature only after emitting parts in source order, so a later thought part cannot + // supply fallback metadata to an earlier function call in the same frame. + pendingStreamThoughtSig = observedStreamThoughtSig; } return emittedContentEvent ? "content" : "continue"; }; diff --git a/tests/google-signature-history-roundtrip.test.ts b/tests/google-signature-history-roundtrip.test.ts index b40c21f074..67c72811c6 100644 --- a/tests/google-signature-history-roundtrip.test.ts +++ b/tests/google-signature-history-roundtrip.test.ts @@ -185,6 +185,24 @@ describe("#1735 thought signature survives history replay", () => { .toBe(SIGNATURE); }); + test("streaming signatures only attach to function calls that follow them", async () => { + const adapter = createGoogleAdapter(provider); + await adapter.buildRequest(firstTurn()); + const frame = `data: ${JSON.stringify(googleBody([ + { functionCall: { name: "shell_command", args: { command: "pwd" } } }, + { text: "thinking...", thought: true, thought_signature: SIGNATURE }, + { functionCall: { name: "shell_command", args: { command: "ls" } } }, + ]))}\n\n`; + const events: AdapterEvent[] = []; + for await (const event of adapter.parseStream(new Response(frame))) events.push(event); + + const starts = events.filter((event): event is Extract => + event.type === "tool_call_start"); + expect(starts).toHaveLength(2); + expect(starts[0].providerMetadata?.google?.thoughtSignature).toBeUndefined(); + expect(starts[1].providerMetadata?.google?.thoughtSignature).toBe(SIGNATURE); + }); + test("a signature replayed through Responses history reaches the rebuilt Google part", async () => { // No cache is warmed here: this is a cold process replaying client-supplied history. const parsed = parseRequestScoped({