Skip to content

Gemini tool calls are replayed without their thought_signature — any tool-using turn 400s (cf. #821) #833

Description

@chrismurrph

Summary

Same defect as #821, one field over. Gemini requires a thought_signature on functionCall parts; dirge discards it, so any tool-using turn on Gemini fails.

error: HttpError: Invalid status code 400 Bad Request with message: {
  "error": {
    "code": 400,
    "message": "Function call is missing a thought_signature in functionCall parts. This is
      required for tools to work correctly, and missing thought_signature may lead to degraded
      model performance. Additional data, function call `default_api:grep`, position 5.",
    "status": "INVALID_ARGUMENT"
  }
}

Reproduction: point the active model at a current Gemini (gemini-3.6-flash) and ask something that makes the model use a tool. Turns without a tool call succeed; the first grep fails on the replay.

Cause

src/agent/agent_loop/rig_stream_factory.rs, the "toolCall" replay arm:

"toolCall" => {
    Some(AssistantContent::ToolCall(ToolCall {
        call_id:, id,
        function: ToolFunction { name, arguments },
        signature: None,            // ← hardcoded

And the storage type has nowhere to keep one — src/agent/agent_loop/message.rs:

ToolCall { id: String, name: String, arguments: Value }

rig already provides the value, so this is entirely dirge-side. rig-core-0.41.0/src/providers/gemini/completion.rs:481:

PartKind::FunctionCall(function_call) => {
    let tool_call = message::ToolCall::new()
        .with_signature(thought_signature.clone());

rig::completion::message::ToolCall carries pub signature: Option<String>. dirge drops it at capture and hardcodes None at replay — exactly the shape #821 had for Reasoning.

Relation to #823

#823 fixed this for Anthropic thinking blocks and its body disclosed the limit: "Gemini sessions now store rig-provided thought signatures. No wire change: attach is gated to Anthropic." That scope was deliberate — there was no Gemini reproduction at the time. This is that disclosed gap arriving.

The mechanism is therefore precedented: capture the signature, record the model that minted it (signature_model), attach only when the replay target matches. Three edits, mirroring #823:

  1. ContentBlock::ToolCall gains signature / signature_model, both #[serde(default, skip_serializing_if = "Option::is_none")] so saved sessions still deserialize.
  2. Capture reads ToolCall.signature instead of discarding it; the factory stamps the minting model, as it does for thinking blocks.
  3. The replay arm attaches when the model matches, and the attach gate widens beyond Anthropic.

The open question — dropping is not available here

For thinking blocks, attach-or-drop were the only options and drop was safe: an unsigned block is rejected, and dropping cost only reasoning context.

A tool call cannot be dropped. Its paired tool_result would be orphaned and the conversation malformed. So when no signature is available — a session saved before this change, or a cross-model switch — the choices are:

  • send it unsigned and take the 400;
  • drop the tool_call and its paired result together, losing that step of history;
  • refuse the turn with an explanatory message.

I have no strong view and it is your call. It also means the signature_model machinery added in #823 stops being a single-provider guard and becomes load-bearing across providers, which is what it was built for but worth naming.

Happy to write the PR once you have said which behaviour you want for the missing-signature case.

Note for reproducing

gemini-2.5-flash is now refused by Google for new users (404 … no longer available to new users … use models/gemini-3.6-flash), so a repro needs a current model pinned.

Activity

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Metadata

Metadata

Assignees

No one assigned

    Labels

    No labels
    No labels

    Type

    No type

    Projects

    No projects

      Milestone

      No milestone

      Relationships

      None yet

      Development

      No branches or pull requests

      Issue actions