Skip to content

BUG: Gemini tool-calling sessions fail on follow-up requests (missing thought_signature / "Requests ending with a model turn") #2913

Description

@SeleneXX

Summary / 概述

With a google-genai provider (gemini-3.7-flash, a thinking model), a session that calls tools fails on the follow-up request (the step right after a tool call) with one of two upstream 400 errors:

使用 google-genai 协议(gemini-3.7-flash 思考模型)时,调用过工具的会话在后续请求(工具调用后的下一步)会命中以下两个上游 400 错误之一:

  1. Function call is missing a thought_signature in functionCall parts. ... function call default_api:<tool>, position N
  2. Requests ending with a model turn are not supported.

Reproducible on v0.36.0 with the agent-core-v2 engine (default). Error #1 masks #2: after #1 is fixed, long tool-calling sessions then hit #2.

在 v0.36.0(默认的 agent-core-v2 引擎)上可稳定复现。错误 #1 会掩盖 #2:修复 #1 后,长工具调用会话会继续命中 #2


Root causes (both client-side) / 根因(均在客户端)

1. tool.call loop event drops ToolCall.extras / tool.call 循环事件丢失 ToolCall.extras

packages/agent-core-v2/src/agent/loop/loopService.ts: executeStepTools records the event with only {toolCallId, name, args}, dropping extras (which carries thought_signature_b64). Context history is rebuilt from loop events via the fold (contextMemory/loopEventFold.ts), so the reconstructed assistant message loses the signature; the next request can't echo thoughtSignature on the functionCall part. The v1 engine (packages/agent-core/src/loop/tool-call.ts) passes extras correctly at both emit sites; the v2 port missed it. LoopRecordedEvent already declares the field.

packages/agent-core-v2/src/agent/loop/loopService.tsexecuteStepTools 记录 tool.call 事件时只传了 {toolCallId, name, args},丢掉了 extras(其中携带 thought_signature_b64)。上下文历史通过 fold(contextMemory/loopEventFold.ts)从事件重建,恢复出的 assistant 消息因此丢失签名,后续请求无法在 functionCall part 上回传 thoughtSignature,被上游拒绝。v1 引擎(packages/agent-core/src/loop/tool-call.ts)的两处事件都在正确传递 extras,v2 移植时遗漏。LoopRecordedEvent 类型本就声明了该字段。

2. User-message merge puts text after functionResponse / 用户消息合并把文本放在 functionResponse 之后

packages/kosong/src/providers/google-genai.ts (and the agent-core-v2 copy): mergeConsecutiveUserMessages folds a trailing plain user message (e.g. an injected system reminder) into the preceding tool-result Content as [functionResponse, text]. Gemini 3.x rejects a request whose last Content starts with functionResponse followed by text. Verified empirically against the upstream: [functionResponse], [text, functionResponse], and separate messages all pass (200); only trailing [functionResponse, text] fails with error #2 verbatim.

packages/kosong/src/providers/google-genai.ts(以及 agent-core-v2 中的对应实现):mergeConsecutiveUserMessages 会把紧随 tool-result 消息之后的普通用户消息(例如注入的 system reminder)合并成 [functionResponse, text]。Gemini 3.x 拒绝"最后一条 Content 以 functionResponse 开头、其后跟文本"的请求。已对上游实测:[functionResponse][text, functionResponse]、以及分开的两条消息均通过(200);只有结尾为 [functionResponse, text] 时逐字复现错误 #2


Fix / 修复

  • loopService.ts: pass extras (looked up from response.message.toolCalls) into the tool.call event.
    loopService.ts:从 response.message.toolCalls 反查 extras 并写入 tool.call 事件。

  • google-genai merge callback (v1 + v2): when merging a tool-result Content with a following text-only user Content, keep the text first ([text, functionResponse]).
    google-genai 合并回调(v1 + v2):当 tool-result Content 与后续纯文本用户消息合并时,保持文本在前([text, functionResponse])。


Validation / 验证

  • New regression test in packages/agent-core-v2/test/agent/loop/loop.test.ts (extras survive tool.call → fold → context); reverting the fix makes it fail with expected undefined to deeply equal {thought_signature_b64}.
    packages/agent-core-v2/test/agent/loop/loop.test.ts 新增回归测试(extras 经 tool.call → fold → context 保留);回退修复后该测试精确失败(expected undefined to deeply equal {thought_signature_b64})。

  • Order assertion added to packages/kosong/test/google-genai.test.ts.
    packages/kosong/test/google-genai.test.ts 补充了顺序断言。

  • Replayed a real failing session's wire log through fold → projector → content assembly: the trailing Content is now [text, functionResponse, text], and the same structure is accepted by the upstream (200), while the pre-fix [functionResponse, text] reproduced the exact 400.
    用真实失败会话的 wire 日志重放 fold → projector → 内容组装:结尾 Content 现为 [text, functionResponse, text],该结构被上游接受(200);修复前的 [functionResponse, text] 则逐字复现 400。

  • kosong 85 tests + agent-core-v2 loop 49 tests pass; both packages typecheck.
    kosong 85 个测试 + agent-core-v2 loop 49 个测试全部通过;两个包类型检查通过。


Environment / 环境

  • kimi-code CLI 0.36.0, google-genai protocol, gemini-3.7-flash (thinking model) via a Gemini-compatible gateway.
    kimi-code CLI 0.36.0,google-genai 协议,gemini-3.7-flash(思考模型,经 Gemini 兼容网关)。

Branch ready / 分支已就绪

SeleneXX/kimi-codefix/gemini-tool-calls (two commits; happy to open a PR once the issue is up).

SeleneXX/kimi-codefix/gemini-tool-calls(两个 commit;issue 创建后可开 PR)。

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