Skip to content

Commit 462c076

Browse files
authored
fix(anthropic): merge consecutive assistant replay turns (#87346)
Merge adjacent Anthropic assistant turns before dangling tool-use validation so signed tool calls remain immediately paired with their tool results. Preserve contributor credit. Fixes #87329.
1 parent d68de3f commit 462c076

2 files changed

Lines changed: 64 additions & 4 deletions

File tree

src/agents/embedded-agent-helpers.validate-turns.test.ts

Lines changed: 55 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -323,7 +323,7 @@ describe("validateAnthropicTurns", () => {
323323
]);
324324
});
325325

326-
it("should not merge consecutive assistant messages", () => {
326+
it("merges consecutive assistant messages", () => {
327327
const msgs = asMessages([
328328
{ role: "user", content: [{ type: "text", text: "Question" }] },
329329
{
@@ -338,7 +338,60 @@ describe("validateAnthropicTurns", () => {
338338

339339
const result = validateAnthropicTurns(msgs);
340340

341-
expect(result).toEqual(msgs);
341+
expect(result).toEqual([
342+
{ role: "user", content: [{ type: "text", text: "Question" }] },
343+
{
344+
role: "assistant",
345+
content: [
346+
{ type: "text", text: "Answer 1" },
347+
{ type: "text", text: "Answer 2" },
348+
],
349+
},
350+
]);
351+
});
352+
353+
it("merges an injected assistant turn before validating signed tool-result pairing", () => {
354+
const msgs = asMessages([
355+
{ role: "user", content: [{ type: "text", text: "Use the gateway" }] },
356+
{
357+
role: "assistant",
358+
content: makeSignedThinkingGatewayToolCall("tool-1"),
359+
stopReason: "toolUse",
360+
},
361+
{
362+
role: "assistant",
363+
content: [{ type: "text", text: "Subagent completion delivered." }],
364+
stopReason: "stop",
365+
},
366+
{
367+
role: "toolResult",
368+
toolUseId: "tool-1",
369+
toolName: "gateway",
370+
content: [{ type: "text", text: "done" }],
371+
isError: false,
372+
},
373+
]);
374+
375+
const result = validateAnthropicTurns(msgs);
376+
377+
expect(result).toEqual([
378+
{ role: "user", content: [{ type: "text", text: "Use the gateway" }] },
379+
{
380+
role: "assistant",
381+
content: [
382+
...makeSignedThinkingGatewayToolCall("tool-1"),
383+
{ type: "text", text: "Subagent completion delivered." },
384+
],
385+
stopReason: "stop",
386+
},
387+
{
388+
role: "toolResult",
389+
toolUseId: "tool-1",
390+
toolName: "gateway",
391+
content: [{ type: "text", text: "done" }],
392+
isError: false,
393+
},
394+
]);
342395
});
343396

344397
it("should handle mixed scenario with steering messages", () => {

src/agents/embedded-agent-helpers/turns.ts

Lines changed: 9 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -386,8 +386,15 @@ function normalizeUserContentForMerge(content: unknown): UserContentBlock[] {
386386
* Also strips dangling tool_use blocks that lack corresponding tool_result blocks.
387387
*/
388388
export function validateAnthropicTurns(messages: AgentMessage[]): AgentMessage[] {
389-
// First, strip dangling tool-call blocks from assistant messages.
390-
const stripped = stripDanglingAnthropicToolUses(messages);
389+
// Merge first so an injected assistant turn cannot hide the tool result that
390+
// resolves the preceding signed tool call. Stripping first would destroy the
391+
// active Anthropic tool-use turn before the adjacent turns can be repaired.
392+
const mergedAssistant = validateTurnsWithConsecutiveMerge({
393+
messages,
394+
role: "assistant",
395+
merge: mergeConsecutiveAssistantTurns,
396+
});
397+
const stripped = stripDanglingAnthropicToolUses(mergedAssistant);
391398

392399
return validateTurnsWithConsecutiveMerge({
393400
messages: stripped,

0 commit comments

Comments
 (0)