Skip to content

fix(agent): keep tool-call metadata after skills-like re-query - #10620

Open
ArcueidMP wants to merge 2 commits into
AstrBotDevs:masterfrom
ArcueidMP:fix/9217-skills-like-tool-metadata
Open

ArcueidMP wants to merge 2 commits into
AstrBotDevs:masterfrom
ArcueidMP:fix/9217-skills-like-tool-metadata

Conversation

@ArcueidMP

@ArcueidMP ArcueidMP commented Oct 18, 2026 •

Copy link
Copy Markdown

Fixes #9217. Supersedes #9220, which was closed as stale.

In skills_like tool schema mode, ToolLoopAgentRunner.step() first asks the model to select tools with the light schema, then _resolve_tool_exec() re-queries with the param-only schema. The runner adopts the re-query's tools_call_name / tools_call_args / tools_call_ids but keeps the first response's tools_call_extra_content. LLMResponse.to_openai_tool_calls_model() looks that metadata up by the final tool-call ID, so the assistant turn written to history either has no signature (the re-query returned new IDs) or carries the signature of the selection call (same IDs; Gemini falls back to the tool name as the ID, so this is the common case). On the next request ProviderGoogleGenAI._prepare_conversation() replays the function call without the signature of the actual call, Gemini rejects it with Function call is missing a thought_signature, and the provider fallback kicks in.

Modifications / 改动点

  • astrbot/core/agent/runners/tool_loop_agent_runner.py: when step() adopts the re-query result, also adopt tools_call_extra_content. One assignment next to the existing three; no helper. full mode and providers without per-call metadata are unaffected, and the first response's user-visible narration is still kept.
  • tests/test_tool_loop_agent_runner.py: a two-step provider and three regression tests: a single tool with different and with identical IDs, two tools each keeping their own metadata (nothing is broadcast), and a round trip that feeds the recorded history into ProviderGoogleGenAI._prepare_conversation() and asserts the function-call part carries the re-query's thought_signature.

Out of scope: the tool_schema_mode=full path is unchanged. If the error still reproduces there, that is a separate path, as noted in the issue analysis.

  • This is NOT a breaking change. / 这不是一个破坏性变更。

Screenshots or Test Results / 运行截图或测试结果

TESTING=true uv run pytest tests/test_tool_loop_agent_runner.py tests/test_gemini_source.py -q
78 passed in 4.66s

With the runner change stashed, the new tests fail in the two expected ways: extra_content is None when the re-query returns new IDs, and equals the selection signature when the IDs are identical. uv run ruff format and uv run ruff check are clean on the changed files. No live Gemini request was made; the round trip runs against the real runner and the real Gemini history converter.


Checklist / 检查清单

  • 😊 Bug fix, no new feature; the approach follows the discussion in [Bug] Gemini API 在工具调用时遇到丢失思考签名错误 #9217.
  • 👀 My changes have been well-tested, and "Verification Steps" and test results are provided above.
  • 📚 No WebUI navigation, page structure, or labels changed, so no docs/zh / docs/en update is needed.
  • 🤓 No new dependencies are introduced.
  • 😮 My changes do not introduce malicious code.

Summary by Sourcery

Preserve executed tool-call metadata across skills-like re-queries to prevent missing or stale signatures during subsequent provider requests.

Bug Fixes:

  • Preserve tool-call metadata and reasoning signatures from skills-like schema re-queries so subsequent provider history replay uses the metadata for the executed calls.

Enhancements:

  • Keep re-query tool-call IDs, per-call metadata, and reasoning state synchronized while retaining the initial response's user-facing narration.

Tests:

  • Add regression coverage for differing and identical tool-call IDs, per-call metadata across multiple tools, and Gemini signature round trips.

In skills_like mode, step() adopts the re-query's tool names, args and IDs
but kept the first response's tools_call_extra_content. Provider metadata
such as Gemini thought signatures is keyed by tool-call ID, so the recorded
assistant turn either lost the signature (new IDs) or replayed the stale
selection signature (same IDs), and Gemini rejected the next request.

Fixes AstrBotDevs#9217

@sourcery-ai sourcery-ai Bot left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Hey - I've found 2 issues

Prompt for AI Agents
Please address the comments from this code review:

## Individual Comments

### Comment 1
<location path="astrbot/core/agent/runners/tool_loop_agent_runner.py" line_range="1014-1015" />
<code_context>
+                    # the final tool-call IDs, so it must move with them. Keeping the
+                    # first response's metadata here replays a missing or stale
+                    # signature to the provider on the next turn.
+                    llm_resp.tools_call_extra_content = (
+                        requery_resp.tools_call_extra_content
+                    )

</code_context>
<issue_to_address>
**Stale signature is replayed**

When the selection response has a Gemini thought signature and the re-query returns a different one, `llm_resp.reasoning_signature` remains from the selection response while the tool-call metadata comes from the re-query, so the runner stores the selection signature alongside the re-query’s call and `ProviderGoogleGenAI._prepare_conversation()` replays mismatched signature state to Gemini.

Adopt the re-query’s reasoning signature when adopting its tool-call metadata, while preserving the selection response’s user-visible narration.
</issue_to_address>

### Comment 2
<location path="astrbot/core/agent/runners/tool_loop_agent_runner.py" line_range="1014-1016" />
<code_context>
+                    # the final tool-call IDs, so it must move with them. Keeping the
+                    # first response's metadata here replays a missing or stale
+                    # signature to the provider on the next turn.
+                    llm_resp.tools_call_extra_content = (
+                        requery_resp.tools_call_extra_content
+                    )

             tool_call_result_blocks = []
</code_context>
<issue_to_address>
**Repeated calls share signatures**

When a re-query returns multiple calls to the same Gemini tool and those calls have no function-call IDs, `_process_content_parts()` uses the tool name as the ID when a Gemini function call has no ID, and stores signatures in a dict keyed by that ID. For repeated calls to the same tool, only the last signature survives; `to_openai_tool_calls_model()` then replays it for both calls, so Gemini rejects the mismatched call.

Preserve per-call signatures when IDs are absent or duplicated, for example by assigning unique IDs and retaining a per-call metadata mapping.
</issue_to_address>

Sourcery assessment

Approval pending. 2 findings to address first.

Blocking findings: astrbot/core/agent/runners/tool_loop_agent_runner.py:1015, astrbot/core/agent/runners/tool_loop_agent_runner.py:1016


Sourcery is free for open source - if you like our reviews please consider sharing them ✨

Comment thread astrbot/core/agent/runners/tool_loop_agent_runner.py
Comment thread astrbot/core/agent/runners/tool_loop_agent_runner.py
The re-query's reasoning_content and reasoning_signature now move with
its tool-call bundle. Anthropic rebuilds the thinking block from both
and validates the signature against the text, so they cannot be split,
and Gemini otherwise replays two signatures from different turns inside
one model content. Only the first response's narration is kept, as in
the existing no-tool-call fallback.

Refs AstrBotDevs#9217

This branch has not been deployed

No deployments
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.

[Bug] Gemini API 在工具调用时遇到丢失思考签名错误

1 participant