fix(devin-connect): emit one ChatMessage per assistant turn with parallel tool_calls - #267
fix(devin-connect): emit one ChatMessage per assistant turn with parallel tool_calls#267lovedxc wants to merge 1 commit into
Conversation
…llel tool_calls When a history assistant turn carries multiple parallel tool_calls, the encoder pushed a separate ChatMessage (dwgx#6) per call, producing consecutive ASSISTANT-source wire messages. The upstream request validator rejects back-to-back same-source messages with invalid_argument ("an internal error occurred") — the request decodes and begins processing, then the trailer fails. Two changes in buildGetChatMessageRequest: - Merge runs of consecutive same-role text-only messages before wire encoding (a client history may split one turn into several entries). Entries carrying tool_calls / tool_call_id / reasoning / images are never merged; system turns are skipped for adjacency since they hoist to field dwgx#2. - Encode an assistant turn's tool_calls as repeated dwgx#6 entries on a single ChatMessage, with leading turn text in dwgx#3 of the same message — symmetric with response frames that carry content + multiple delta_tool_calls together. Verified by bisected replay of a captured failing request: the original body reproduced invalid_argument deterministically; after the fix the unmodified body is accepted and generation proceeds. Histories with single-call turns and text-only runs regress clean.
|
补充说明:本 PR 与 #261 修复的是同一个根因——历史中的并行 tool_calls 在 wire 上产生连续 ASSISTANT-source 消息,触发上游 invalid_argument。两个 PR 修改的文件不重叠,思路差异如下供参考: 位置不同:#261 在消息层(tool-emulation/chat handler)把并行调用重排成交替的单调用序列;本 PR 在 wire 编码层(buildGetChatMessageRequest)把一个回合的所有调用编码进同一条 ChatMessage(repeated #6),回合文本放同条 #3。 覆盖面不同:#261 只作用于 nativeStructured 和直接连接链路;本 PR 在编码最后一环生效,所有进入该函数的路径都受益。另外本 PR 同时处理了"连续同 role 纯文本消息"(客户端把一个回合拆成多条文本记录时产生)——这类连续 ASSISTANT 文本同样会触发连续同源校验,交错重排不覆盖这种情况。 语义不同:本 PR 保留回合的并行结构(repeated #6,与 response 帧同时携带 content + 多个 delta_tool_calls 的形态对称);#261 会把历史改写为串行回合,模型看到的语义略有差异。 已验证:对真实失败请求做变体重放,修复前 100% 复现 invalid_argument,修复后原样请求被上游接受并正常生成(含并行调用的回合被整体接受——若 #6 为单数字段,被丢弃的调用会使对应 tool_result 成为孤儿而再次被拒,实测通过反证 repeated #6 合法)。devin-connect.test.js 181/181 通过。 |
|
评审: 先说时序:#261 今天已经进 master( head
四路 wire 实测(这是全部裁决依据)
从这张表里我要说两件事,第二件对你是好消息。 第一:两份修复都把"连续 3 条同源"消掉了,2-call 场景下都只降到 2,而两份都被各自作者报为 live 通过 ⇒ 与"上游允许连续 2、拒绝 3"一致。缺陷是 run ≥ 3,不是任何连续同源。 你正文里"上游拒绝连续同源"这个表述比实际机制更强,机制上是 run ≥ 3;这不影响你的修复有效,但写进代码注释会误导下一个人。 第二:合体后 wire 与单独 #261 逐字节相同——因为归一化之后,编码器已经看不到"一条消息带多个 call"的输入了。所以你那半一旦 #261 在树上,你在 2-call 场景下拿不到任何行为差异。 M1(回绝的理由)— repeated-
|
Summary / 摘要
EN: Fix a deterministic upstream
invalid_argument("an internal error occurred") rejection when replaying history where one assistant turn carries multiple paralleltool_calls.中文: 修复回放"单个 assistant 回合携带多个并行
tool_calls"的历史时,上游确定性返回invalid_argument("an internal error occurred")的问题。Root cause / 根因
EN:
buildGetChatMessageRequestpushed a separate ChatMessage per tool_call, producing consecutive ASSISTANT-source wire messages. The upstream request validator rejects back-to-back same-source messages — the request decodes and begins processing, then the trailer fails.中文:
buildGetChatMessageRequest此前为每个 tool_call 单独 push 一条 ChatMessage(#6),wire 序列出现连续多条 ASSISTANT-source 消息。上游校验器拒绝连续同源消息——请求先被解码并开始处理,随后 trailer 报错。EN: The same violation arises when a client history splits one turn into several consecutive same-role text entries.
中文: 当客户端历史把一个回合拆成多条连续同 role 的纯文本消息时,会构成同类违规。
Changes / 修改
EN:
tool_calls/tool_call_id/ reasoning / images are never merged; system turns are skipped for adjacency (they hoist to field 大佬,这个非常厉害,calude code 不支持好像 #2); merge produces fresh objects, never mutating the caller array (safe under retry/failover).tool_callsas repeated <(0o0)> #6 entries on a single ChatMessage, with leading turn text in Firebase 登入失敗: 信箱或密碼錯誤 #3 — symmetric with response frames that carry content + multipledelta_tool_callstogether.中文:
tool_calls/tool_call_id/ reasoning / 图片的消息不参与合并;system 不参与相邻判断(会提升到 大佬,这个非常厉害,calude code 不支持好像 #2 字段);合并生成新对象,不改原数组(重试/故障转移安全)。tool_calls编码为同一条 ChatMessage 上的 repeated <(0o0)> #6,回合文本放同条 Firebase 登入失敗: 信箱或密碼錯誤 #3 —— 与 response 帧同时携带 content + 多个delta_tool_calls的形态对称。Verification / 验证
EN: Bisected replay of a captured failing request: the unmodified body reproduced
invalid_argumentdeterministically; after the fix the identical body is accepted and generation proceeds. Single-call turns and text-only histories regress clean.devin-connect.test.js: 181/181 pass.中文: 对捕获的失败请求做变体重放:原请求 100% 复现
invalid_argument;修复后原样请求被上游接受并正常生成。单调用回合、纯文本历史回归正常。devin-connect.test.js181/181 通过。