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:
ContentBlock::ToolCall gains signature / signature_model, both #[serde(default, skip_serializing_if = "Option::is_none")] so saved sessions still deserialize.
- Capture reads
ToolCall.signature instead of discarding it; the factory stamps the minting model, as it does for thinking blocks.
- 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.
Summary
Same defect as #821, one field over. Gemini requires a
thought_signatureonfunctionCallparts; dirge discards it, so any tool-using turn on Gemini fails.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 firstgrepfails on the replay.Cause
src/agent/agent_loop/rig_stream_factory.rs, the"toolCall"replay arm:And the storage type has nowhere to keep one —
src/agent/agent_loop/message.rs:rig already provides the value, so this is entirely dirge-side.
rig-core-0.41.0/src/providers/gemini/completion.rs:481:rig::completion::message::ToolCallcarriespub signature: Option<String>. dirge drops it at capture and hardcodesNoneat replay — exactly the shape #821 had forReasoning.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:ContentBlock::ToolCallgainssignature/signature_model, both#[serde(default, skip_serializing_if = "Option::is_none")]so saved sessions still deserialize.ToolCall.signatureinstead of discarding it; the factory stamps the minting model, as it does for thinking blocks.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_resultwould 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:tool_calland its paired result together, losing that step of history;I have no strong view and it is your call. It also means the
signature_modelmachinery 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-flashis 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.