Skip to content

fix(agent): replay provider metadata across tool steps - #3820

Merged
kojiwakayama merged 1 commit into
mainfrom
fix/issue-549-gemini-tool-replay
Aug 17, 2026
Merged

fix(agent): replay provider metadata across tool steps#3820
kojiwakayama merged 1 commit into
mainfrom
fix/issue-549-gemini-tool-replay

Conversation

@kojiwakayama

@kojiwakayama kojiwakayama commented Aug 17, 2026

Copy link
Copy Markdown
Contributor

Summary

  • retain provider replay metadata across direct generate, stream-backed generate, legacy stream, and active stream-lifecycle tool steps
  • attach the metadata only to the next internal model request
  • keep thought signatures out of public messages, SSE, public schemas, memory serialization, and durable model-call events
  • verify the exact signed Gemini model part precedes its matching function response on the second wire request
  • regenerate affected API reference source links

Root cause

The Google adapter already captured exact Gemini assistant parts, including thoughtSignature, in providerMetadata. The central runtime bridge discarded that metadata while normalizing generate and stream results, and the agent history builders therefore could not return it on the tool-result leg. The active stream lifecycle also discarded finish metadata while projecting its terminal snapshot. Gemini 2.5 tolerated the omission, while Gemini 3 rejects the continuation with HTTP 400 because function-call thought signatures are mandatory.

Google protocol reference: https://ai.google.dev/gemini-api/docs/generate-content/thought-signatures

RED-GREEN TDD

RED:

deno test --preload=src/testing/preload.ts --no-check --allow-all src/agent/runtime/provider-metadata-continuation.test.ts

Generate and legacy stream initially made the second provider request with providerMetadata: undefined. After fixing those paths, an explicit active-lifecycle regression still failed with providerMetadata: undefined.

GREEN:

  • generate, legacy stream, and active stream replay the exact metadata on the tool-result request
  • a credential-free Google wire regression verifies the exact signed assistant part is replayed before the function response
  • the signature is absent from returned messages, streamed SSE, and durable model-call context
  • stream-backed generate preserves finish metadata

Verification

  • focused exact-head suite: 5 files, 67 steps passed
  • deno task --quiet verify:quick
  • deno task --quiet typecheck
  • deno task --quiet lint
  • deno task docs:api-reference:check
  • earlier full unit suite before the active-lifecycle extension: 3,922 tests and 29,969 steps passed, plus cwd suites
  • git diff --check

Fixes veryfront/veryfront-issue-inbox#549

Summary by CodeRabbit

  • New Features

    • Preserved assistant provider metadata across generated responses, streamed responses, tool-result continuations, and lifecycle events.
    • Maintained provider-specific information needed for reliable multi-step and streamed interactions.
  • Bug Fixes

    • Corrected API reference links for the agent runtime and embedding similarity function.
  • Tests

    • Added coverage for provider metadata continuity across generation and streaming workflows.

@kojiwakayama

Copy link
Copy Markdown
Contributor Author

@codex review exact head 609cb88

@github-actions

Copy link
Copy Markdown

📦 Client bundle boundary

Entrypoint Modules Source size Server leaks
src/index.client.ts 325 1937 KiB ✅ 0

A server module in a client graph aborts hydration in the browser. New leaks fail CI; known leaks are tracked in scripts/lint/client-bundle-baseline.json to burn down.

@coderabbitai

coderabbitai Bot commented Aug 17, 2026

Copy link
Copy Markdown

Review Change Stack

Caution

Review failed

The pull request is closed.

ℹ️ Recent review info
⚙️ Run configuration

Configuration used: Repository UI

Review profile: CHILL

Plan: Pro Plus

Run ID: 741ef4c9-254e-47ac-b35c-842f0570678f

📥 Commits

Reviewing files that changed from the base of the PR and between 5887da1 and 57a9313.

📒 Files selected for processing (15)
  • docs/api-reference/veryfront/agent.md
  • docs/api-reference/veryfront/embedding.md
  • src/agent/runtime/chat-stream-handler.ts
  • src/agent/runtime/index.ts
  • src/agent/runtime/provider-metadata-continuation.test.ts
  • src/agent/runtime/provider-metadata.ts
  • src/agent/runtime/runtime-tool-types.ts
  • src/agent/runtime/text-generation-runtime-message-converter.ts
  • src/agent/runtime/text-generation-runtime-message-types.ts
  • src/agent/streaming/lifecycle/live-adapter.ts
  • src/agent/streaming/lifecycle/reducer.ts
  • src/agent/streaming/lifecycle/runtime-provider-adapter.ts
  • src/agent/streaming/lifecycle/types.ts
  • src/runtime/runtime-bridge.test.ts
  • src/runtime/runtime-bridge.ts

📝 Walkthrough

Walkthrough

The change preserves assistant provider metadata through direct generation, streaming, lifecycle snapshots, agent messages, and tool-result continuations. It also updates two API reference source links and adds focused Google continuation tests.

Changes

Provider metadata propagation

Layer / File(s) Summary
Runtime bridge metadata transport
src/runtime/runtime-bridge.ts, src/agent/runtime/runtime-tool-types.ts
Runtime prompts and generation results preserve provider metadata on assistant messages. Persisted model-call context excludes the metadata.
Streaming lifecycle propagation
src/agent/streaming/lifecycle/*, src/agent/runtime/chat-stream-handler.ts
Finish events and stream snapshots carry provider metadata into ChatStreamState.
Agent message continuation
src/agent/runtime/provider-metadata.ts, src/agent/runtime/index.ts, src/agent/runtime/text-generation-runtime-message-*, src/agent/runtime/provider-metadata-continuation.test.ts
Assistant messages store metadata in weak storage. Conversion and tool-result continuation flows restore it. Tests cover Google generation, streaming, lifecycle restoration, and signature handling.

API reference links

Layer / File(s) Summary
Updated source references
docs/api-reference/veryfront/agent.md, docs/api-reference/veryfront/embedding.md
The AgentRuntime and similarity source links use updated line numbers.

Estimated code review effort: 3 (Moderate) | ~25 minutes

✨ Finishing Touches
📝 Generate docstrings
  • Create stacked PR
  • Commit on current branch
🧪 Generate unit tests (beta)
  • Create PR with unit tests
  • Commit unit tests in branch fix/issue-549-gemini-tool-replay

Thanks for using CodeRabbit! It's free for OSS, and your support helps us grow. If you like it, consider giving us a shout-out.

❤️ Share

Comment @coderabbitai help to get the list of available commands.

@kojiwakayama
kojiwakayama force-pushed the fix/issue-549-gemini-tool-replay branch from 609cb88 to 0a86157 Compare August 17, 2026 12:45
@kojiwakayama

Copy link
Copy Markdown
Contributor Author

Please run an exact-head Codex review on 0a86157541d1f3d9f6ef143e1b2169c293ff27e5. The amended commit only adds a regression assertion that stream-backed generate preserves provider metadata; focused tests pass (42 steps).

@chatgpt-codex-connector

Copy link
Copy Markdown

Codex Review: Didn't find any major issues. Breezy!

Reviewed commit: 609cb88030

ℹ️ About Codex in GitHub

Your team has set up Codex to review pull requests in this repo. Reviews are triggered when you

  • Open a pull request for review
  • Mark a draft as ready
  • Comment "@codex review".

If Codex has suggestions, it will comment; otherwise it will react with 👍.

Codex can also answer questions or update the PR. Try commenting "@codex address that feedback".

@kojiwakayama
kojiwakayama force-pushed the fix/issue-549-gemini-tool-replay branch from 0a86157 to 57a9313 Compare August 17, 2026 12:50
@kojiwakayama

Copy link
Copy Markdown
Contributor Author

@codex review exact head 57a9313307131b35c5d43420b2a0ec4d1131b7f0. This head adds RED-GREEN active-lifecycle coverage and a credential-free exact Gemini wire replay regression while retaining the internal-only metadata design.

@chatgpt-codex-connector

Copy link
Copy Markdown

Codex Review: Didn't find any major issues. Bravo.

Reviewed commit: 57a9313307

ℹ️ About Codex in GitHub

Your team has set up Codex to review pull requests in this repo. Reviews are triggered when you

  • Open a pull request for review
  • Mark a draft as ready
  • Comment "@codex review".

If Codex has suggestions, it will comment; otherwise it will react with 👍.

Codex can also answer questions or update the PR. Try commenting "@codex address that feedback".

@kojiwakayama
kojiwakayama enabled auto-merge August 17, 2026 12:56
@kojiwakayama
kojiwakayama added this pull request to the merge queue Aug 17, 2026
@kojiwakayama

Copy link
Copy Markdown
Contributor Author

@coderabbitai review exact head 57a9313307131b35c5d43420b2a0ec4d1131b7f0

@coderabbitai

coderabbitai Bot commented Aug 17, 2026

Copy link
Copy Markdown
🧩 Analysis chain

🏁 Script executed:

#!/bin/bash
set -euo pipefail

target='57a9313307131b35c5d43420b2a0ec4d1131b7f0'

printf '%s\n' '== Exact commit =='
git show -s --format='commit=%H%nparents=%P%nsubject=%s' "$target"
printf '%s\n' '== Checked-out HEAD =='
git rev-parse HEAD

printf '%s\n' '== Changed files against first parent =='
git diff-tree --no-commit-id --name-status -r "$target"
printf '%s\n' '== Diff statistics =='
git diff-tree --no-commit-id --stat -r "$target"

printf '%s\n' '== TypeScript file outlines =='
git diff-tree --no-commit-id --name-only -r "$target" \
  | while IFS= read -r file; do
      case "$file" in
        *.ts|*.tsx)
          printf '\n-- %s --\n' "$file"
          ast-grep outline "$file" --items all --view expanded || true
          ;;
      esac
    done

Length of output: 35296


🏁 Script executed:

#!/bin/bash
set -euo pipefail

target='57a9313307131b35c5d43420b2a0ec4d1131b7f0'
parent="$(git rev-parse "${target}^")"

printf '%s\n' '== Exact diff =='
git diff --find-renames --find-copies "$parent" "$target" -- \
  src/agent/runtime/chat-stream-handler.ts \
  src/agent/runtime/index.ts \
  src/agent/runtime/provider-metadata.ts \
  src/agent/runtime/runtime-tool-types.ts \
  src/agent/runtime/text-generation-runtime-message-converter.ts \
  src/agent/runtime/text-generation-runtime-message-types.ts \
  src/agent/streaming/lifecycle/live-adapter.ts \
  src/agent/streaming/lifecycle/reducer.ts \
  src/agent/streaming/lifecycle/runtime-provider-adapter.ts \
  src/agent/streaming/lifecycle/types.ts \
  src/runtime/runtime-bridge.ts \
  src/runtime/runtime-bridge.test.ts \
  src/agent/runtime/provider-metadata-continuation.test.ts

printf '%s\n' '== Metadata attachment and replay uses =='
rg -n -C 5 '\b(attachProviderMetadata|readAttachedProviderMetadata|providerMetadata)\b' \
  src/agent/runtime \
  src/agent/streaming/lifecycle \
  src/runtime/runtime-bridge.ts

printf '%s\n' '== Targeted function slices =='
sed -n '850,1045p' src/runtime/runtime-bridge.ts
sed -n '1180,1325p' src/agent/runtime/index.ts
sed -n '1430,1625p' src/agent/runtime/index.ts
sed -n '2080,2305p' src/agent/runtime/index.ts
sed -n '250,565p' src/agent/runtime/text-generation-runtime-message-converter.ts
sed -n '360,430p' src/agent/runtime/chat-stream-handler.ts
sed -n '720,775p' src/agent/streaming/lifecycle/reducer.ts
sed -n '215,285p' src/agent/streaming/lifecycle/live-adapter.ts
sed -n '430,510p' src/agent/streaming/lifecycle/runtime-provider-adapter.ts

Length of output: 105374


🏁 Script executed:

#!/bin/bash
set -euo pipefail

printf '%s\n' '== Agent loop response and history updates =='
sed -n '1560,1880p' src/agent/runtime/index.ts
sed -n '2440,2740p' src/agent/runtime/index.ts

printf '%s\n' '== Streaming tool-result history updates =='
sed -n '2440,2740p' src/agent/runtime/index.ts
sed -n '2740,3030p' src/agent/runtime/index.ts

printf '%s\n' '== All request conversion call sites =='
rg -n -C 5 'convertToTextGenerationRuntimeRequestMessages\s*\(' src

printf '%s\n' '== All provider metadata consumers and model prompt adapters =='
rg -n -C 6 '\.providerMetadata\b|providerMetadata\b' \
  extensions \
  src \
  -g '*.ts' \
  -g '!src/agent/runtime/provider-metadata-continuation.test.ts'

printf '%s\n' '== Message mutation and memory append helpers =='
rg -n -C 4 '\b(currentMessages\.push|messages\.push|addMessage|appendMessage|saveMessage|assistantMessage)\b' \
  src/agent/runtime/index.ts \
  src/agent/memory \
  -g '*.ts'

Length of output: 201480


@kojiwakayama I reviewed exact head 57a9313307131b35c5d43420b2a0ec4d1131b7f0.

Found one issue.

  • Metadata persists past the next model request. attachProviderMetadata stores metadata in providerMetadataByMessage, but no code consumes or deletes it. The agent retains the same assistantMessage in currentMessages and memory. Each later call to convertToTextGenerationRuntimeRequestMessages() reads the metadata again and sends it again. This affects a three-step tool loop and future turns with an in-memory memory backend. It conflicts with the requirement that replay metadata attach only to the next internal model request. Add consume-once behavior, and add a regression with at least three model requests that verifies the first response metadata is absent from request three.

    Affected code: src/agent/runtime/provider-metadata.ts, Line 10; src/agent/runtime/text-generation-runtime-message-converter.ts, Line 322; src/agent/runtime/text-generation-runtime-message-converter.ts, Line 474.

⚠️ Action not completed

Pull request is closed.

Note: CodeRabbit is an incremental review system and does not re-review already reviewed commits. This command is applicable only when automatic reviews are paused.

Merged via the queue into main with commit e297edb Aug 17, 2026
33 of 34 checks passed
@kojiwakayama
kojiwakayama deleted the fix/issue-549-gemini-tool-replay branch August 17, 2026 13:09
@kojiwakayama

Copy link
Copy Markdown
Contributor Author

Addressed after merge in follow-up #3821. The suggested consume-once behavior would violate Gemini 3 sequential function calling: Google explicitly defines request 3 as retaining the first signed function call and says to include signatures for all function calls in the loop. See https://ai.google.dev/gemini-api/docs/generate-content/thought-signatures#sequential_function_calling_example. The existing WeakMap replay behavior is therefore correct for every request that replays that assistant step within the current turn. #3821 adds a three-request regression requiring signature A on requests 2 and 3, plus signature B on request 3, while confirming neither leaks into public messages. PR #3820 was merged externally before this response and test-only follow-up could be pushed.

@kojiwakayama

Copy link
Copy Markdown
Contributor Author

Addressed in follow-up PR #3824. The RED reproduced request 3 replaying both the prior and current thought signatures; the GREEN consumes replay metadata only when its assistant runtime message is retained in the actual outbound provider request, preserving metadata for trimmed messages until they are later sent. Full pre-push suite is green.

@kojiwakayama

Copy link
Copy Markdown
Contributor Author

Correction to my earlier follow-up note: the consume-once suggestion is rejected after validating the provider contract. Google requires all accumulated thoughtSignature values for every sequential function-call step in the current turn; request 3 is defined as request 2 plus function call 2/signature 2 plus function response 2, so signature 1 remains required. Removing it would reintroduce the Gemini 3 400. Test-only PR #3821 is merged and locks [signature1, signature2] on request 3. Invalid follow-up #3824 is closed.

Primary source: https://ai.google.dev/gemini-api/docs/generate-content/thought-signatures#sequential_function_calling_example

kojiwakayama added a commit that referenced this pull request Aug 17, 2026
PR #3820 attaches the provider's raw replay metadata to every streamed
assistant message, but when the runtime suppresses an unavailable tool
call the persisted parts no longer mirror the raw model turn. The Google
request builder then fails its exact-history validation
(validateGoogleToolReplay throws TypeError) before any request is sent,
killing the continuation client-side.

Skip the metadata attach when state.suppressedToolCalls is non-empty so
the continuation falls back to synthesized parts — the pre-#3820
behavior for that turn — while the extension's fail-fast validation for
genuinely corrupted history stays intact.

Refs VERYFRONT-AGENT-9
kojiwakayama added a commit that referenced this pull request Aug 17, 2026
PR #3820 attaches the provider's raw replay metadata to every streamed
assistant message, but when the runtime suppresses an unavailable tool
call the persisted parts no longer mirror the raw model turn. The Google
request builder then fails its exact-history validation
(validateGoogleToolReplay throws TypeError) before any request is sent,
killing the continuation client-side.

Skip the metadata attach when state.suppressedToolCalls is non-empty so
the continuation falls back to synthesized parts — the pre-#3820
behavior for that turn — while the extension's fail-fast validation for
genuinely corrupted history stays intact.

Refs VERYFRONT-AGENT-9
kwakayama added a commit that referenced this pull request Aug 17, 2026
…hape

Gemini 3.x rejects the tool-result leg with HTTP 400 "Function call is
missing a thought_signature in functionCall parts" unless the signed
model turn is replayed verbatim. 2.5 accepts an unsigned replay, so the
regression is invisible on that generation.

The existing continuation tests use a synthetic single-part stream. Add a
case modelled on a gemini-3.1-pro-preview streamGenerateContent response,
where the signature rides the functionCall part in the first chunk and a
trailing empty-text part arrives in a separate chunk with the finish
reason. That two-chunk shape is what the live API accepts, and it is the
shape the retained raw parts must reproduce. The signature value is
opaque to the replay path, so the fixture carries a fabricated
placeholder instead of a provider-generated value.

Verified red against 5887da1 (the commit before #3820) and green on
main, plus a live round trip through the staging AI gateway on
gemini-3.1-pro-preview, gemini-3.5-flash, gemini-2.5-pro and
gemini-2.5-flash.

Refs veryfront-issue-inbox#549

Claude-Session: https://claude.ai/code/session_01J2e7P4tmqYjYZQ1paDomg5
@kwakayama kwakayama mentioned this pull request Aug 17, 2026
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant