Skip to content

fix: preserve thought_signature in functionCall roundtrip to fix multi-turn tool use - #27

Open
Ken Chau (kenotron-ms) wants to merge 2 commits into
mainfrom
fix/thought-signature-roundtrip
Open

fix: preserve thought_signature in functionCall roundtrip to fix multi-turn tool use#27
Ken Chau (kenotron-ms) wants to merge 2 commits into
mainfrom
fix/thought-signature-roundtrip

Conversation

@kenotron-ms

Copy link
Copy Markdown
Contributor

Summary

Closes / related to: microsoft-amplifier/amplifier-shared#41

Gemini's API now returns a thought_signature field on functionCall parts in responses. When those parts are included as conversation history in subsequent request turns, the thought_signature must be present — otherwise the API returns 400 INVALID_ARGUMENT. The provider was discarding this field during parsing and never re-attaching it during serialization, breaking all multi-turn tool use for any Gemini-routed agent (browser-tester, any vision-role agent) after the first tool call.

Root Cause

Two locations in amplifier_module_provider_gemini/__init__.py:

Parse (drop site)_convert_to_chat_response() (~line 964): ToolCallBlock and ToolCall were built without preserving fc.thought_signature.

Serialize (re-submit site)_convert_messages() (~line 1121): function_call dicts were built with only name and args, never re-attaching any signature field.

Changes

amplifier_module_provider_gemini/__init__.py

  • _convert_to_chat_response(): When a functionCall part contains thought_signature, store it as an extra field on both ToolCallBlock and ToolCall. Both already carry model_config = ConfigDict(extra="allow"), so no model changes needed.
  • _convert_messages(): When building a function_call dict for a subsequent request turn, re-attach thought_signature if the stored ToolCall has the field.
  • Absence handled gracefully: responses without thought_signature pass through unchanged (backward-compat).

New: tests/test_thought_signature.py — 8 tests

Test Covers
test_parse_preserves_thought_signature_in_tool_call Parse: field stored on ToolCall
test_parse_preserves_thought_signature_in_tool_call_block Parse: field stored on ToolCallBlock
test_serialize_reattaches_thought_signature Serialize: field re-attached in function_call dict
test_serialize_without_thought_signature_still_works Backward-compat: no field when absent
test_thought_signature_full_roundtrip Full parse → model_dump() → serialize roundtrip
test_multiple_tool_calls_each_thought_signature_preserved Each tool call keeps its own signature
test_mixed_tool_calls_only_signed_ones_get_signature Mixed: only signed calls re-attach
test_second_tool_call_turn_does_not_raise_with_thought_signature Integration: second complete() with prior history

Test evidence

  • 104 tests pass (2 pre-existing failures in test_pricing_and_vision.py require live GOOGLE_API_KEY — failing on main before this branch, unrelated)
  • DTU validated (instance issue-41-gemini):
    • Provider module loads correctly ✅
    • 8 new tests all pass ✅
    • Live round-trip JSON confirms thought_signature survives parse → serialize:
      {"function_call": {"name": "search", "args": {"q": "amplifier"}, "thought_signature": "abc123-secret-signature-XYZ"}}
    • Backward-compat (absent signature → no field on output) ✅

Affected agents (all fixed by this PR)

  • browser-tester:browser-operator (confirmed reproducing on chore: add CI workflow #41)
  • browser-tester:browser-researcher (same routing)
  • browser-tester:visual-documenter (same routing)
  • Any other agent using model_role=vision or direct Gemini config with tool use

Reference

Amplifier and others added 2 commits May 5, 2026 10:44
Root cause: Gemini API now returns thought_signature on functionCall parts
in responses. The provider was discarding it during parsing and not
re-attaching it during serialization of conversation history.

Fixes:
- In _convert_to_chat_response() (~line 964): preserve thought_signature
  as an extra field on ToolCallBlock and ToolCall (both support extra fields)
- In _convert_messages() (~line 1121): re-attach thought_signature to each
  functionCall dict if the field is present during conversation history
  serialization

Backward compatible: responses without thought_signature pass through
unchanged.

Testing:
- New test file: tests/test_thought_signature.py with 8 comprehensive tests
  covering parse, serialize, full roundtrip, multiple tool calls, and mixed
  presence scenarios
- 104 tests pass; 2 pre-existing unrelated failures in test_pricing_and_vision.py

Generated with [Amplifier](https://github.com/microsoft/amplifier)

Co-Authored-By: Amplifier <240397093+microsoft-amplifier@users.noreply.github.com>

Fixes microsoft-amplifier/amplifier-shared#41
…m ToolCallBlock

- Changed  →  at both the
  parse site (tc_extra guard) and serialize site (_convert_messages) to preserve
  empty-string signatures that were previously silently dropped
- Removed thought_signature/block_extra storage from ToolCallBlock entirely — it was
  dead code since _convert_messages only reads from msg["tool_calls"] (ToolCall),
  never from content_blocks (ToolCallBlock)
- Replaced test_parse_preserves_thought_signature_in_tool_call_block with
  test_toolcallblock_does_not_store_thought_signature documenting the intentional
  design and why ToolCallBlock never had the field
- Added two new edge-case tests:
  - test_empty_string_thought_signature_is_preserved: validates the `is not None`
    check prevents empty-string drop at parse time
  - test_empty_string_thought_signature_serialized: validates empty-string round-trip
    through _convert_messages back into Gemini API request

Fixes microsoft-amplifier/amplifier-shared#41

Generated with Amplifier

Co-Authored-By: Amplifier <240397093+microsoft-amplifier@users.noreply.github.com>
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