Skip to content

Commit 8731709

Browse files
authored
fix: add missing tool_result blocks to prevent API errors (#10015)
1 parent 47320dc commit 8731709

File tree

2 files changed

+385
-26
lines changed

2 files changed

+385
-26
lines changed

src/core/task/__tests__/validateToolResultIds.spec.ts

Lines changed: 305 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,10 @@
11
import { Anthropic } from "@anthropic-ai/sdk"
22
import { TelemetryService } from "@roo-code/telemetry"
3-
import { validateAndFixToolResultIds, ToolResultIdMismatchError } from "../validateToolResultIds"
3+
import {
4+
validateAndFixToolResultIds,
5+
ToolResultIdMismatchError,
6+
MissingToolResultError,
7+
} from "../validateToolResultIds"
48

59
// Mock TelemetryService
610
vi.mock("@roo-code/telemetry", () => ({
@@ -394,7 +398,7 @@ describe("validateAndFixToolResultIds", () => {
394398
})
395399

396400
describe("when there are more tool_uses than tool_results", () => {
397-
it("should fix the available tool_results", () => {
401+
it("should fix the available tool_results and add missing ones", () => {
398402
const assistantMessage: Anthropic.MessageParam = {
399403
role: "assistant",
400404
content: [
@@ -426,15 +430,174 @@ describe("validateAndFixToolResultIds", () => {
426430

427431
const result = validateAndFixToolResultIds(userMessage, [assistantMessage])
428432

433+
expect(Array.isArray(result.content)).toBe(true)
434+
const resultContent = result.content as Anthropic.ToolResultBlockParam[]
435+
// Should now have 2 tool_results: one fixed and one added for the missing tool_use
436+
expect(resultContent.length).toBe(2)
437+
// The missing tool_result is prepended
438+
expect(resultContent[0].tool_use_id).toBe("tool-2")
439+
expect(resultContent[0].content).toBe("Tool execution was interrupted before completion.")
440+
// The original is fixed
441+
expect(resultContent[1].tool_use_id).toBe("tool-1")
442+
})
443+
})
444+
445+
describe("when tool_results are completely missing", () => {
446+
it("should add missing tool_result for single tool_use", () => {
447+
const assistantMessage: Anthropic.MessageParam = {
448+
role: "assistant",
449+
content: [
450+
{
451+
type: "tool_use",
452+
id: "tool-123",
453+
name: "read_file",
454+
input: { path: "test.txt" },
455+
},
456+
],
457+
}
458+
459+
const userMessage: Anthropic.MessageParam = {
460+
role: "user",
461+
content: [
462+
{
463+
type: "text",
464+
text: "Some user message without tool results",
465+
},
466+
],
467+
}
468+
469+
const result = validateAndFixToolResultIds(userMessage, [assistantMessage])
470+
471+
expect(Array.isArray(result.content)).toBe(true)
472+
const resultContent = result.content as Array<Anthropic.ToolResultBlockParam | Anthropic.TextBlockParam>
473+
expect(resultContent.length).toBe(2)
474+
// Missing tool_result should be prepended
475+
expect(resultContent[0].type).toBe("tool_result")
476+
expect((resultContent[0] as Anthropic.ToolResultBlockParam).tool_use_id).toBe("tool-123")
477+
expect((resultContent[0] as Anthropic.ToolResultBlockParam).content).toBe(
478+
"Tool execution was interrupted before completion.",
479+
)
480+
// Original text block should be preserved
481+
expect(resultContent[1].type).toBe("text")
482+
})
483+
484+
it("should add missing tool_results for multiple tool_uses", () => {
485+
const assistantMessage: Anthropic.MessageParam = {
486+
role: "assistant",
487+
content: [
488+
{
489+
type: "tool_use",
490+
id: "tool-1",
491+
name: "read_file",
492+
input: { path: "a.txt" },
493+
},
494+
{
495+
type: "tool_use",
496+
id: "tool-2",
497+
name: "write_to_file",
498+
input: { path: "b.txt", content: "test" },
499+
},
500+
],
501+
}
502+
503+
const userMessage: Anthropic.MessageParam = {
504+
role: "user",
505+
content: [
506+
{
507+
type: "text",
508+
text: "User message",
509+
},
510+
],
511+
}
512+
513+
const result = validateAndFixToolResultIds(userMessage, [assistantMessage])
514+
515+
expect(Array.isArray(result.content)).toBe(true)
516+
const resultContent = result.content as Array<Anthropic.ToolResultBlockParam | Anthropic.TextBlockParam>
517+
expect(resultContent.length).toBe(3)
518+
// Both missing tool_results should be prepended
519+
expect(resultContent[0].type).toBe("tool_result")
520+
expect((resultContent[0] as Anthropic.ToolResultBlockParam).tool_use_id).toBe("tool-1")
521+
expect(resultContent[1].type).toBe("tool_result")
522+
expect((resultContent[1] as Anthropic.ToolResultBlockParam).tool_use_id).toBe("tool-2")
523+
// Original text should be preserved
524+
expect(resultContent[2].type).toBe("text")
525+
})
526+
527+
it("should add only the missing tool_results when some exist", () => {
528+
const assistantMessage: Anthropic.MessageParam = {
529+
role: "assistant",
530+
content: [
531+
{
532+
type: "tool_use",
533+
id: "tool-1",
534+
name: "read_file",
535+
input: { path: "a.txt" },
536+
},
537+
{
538+
type: "tool_use",
539+
id: "tool-2",
540+
name: "write_to_file",
541+
input: { path: "b.txt", content: "test" },
542+
},
543+
],
544+
}
545+
546+
const userMessage: Anthropic.MessageParam = {
547+
role: "user",
548+
content: [
549+
{
550+
type: "tool_result",
551+
tool_use_id: "tool-1",
552+
content: "Content for tool 1",
553+
},
554+
],
555+
}
556+
557+
const result = validateAndFixToolResultIds(userMessage, [assistantMessage])
558+
559+
expect(Array.isArray(result.content)).toBe(true)
560+
const resultContent = result.content as Anthropic.ToolResultBlockParam[]
561+
expect(resultContent.length).toBe(2)
562+
// Missing tool_result for tool-2 should be prepended
563+
expect(resultContent[0].tool_use_id).toBe("tool-2")
564+
expect(resultContent[0].content).toBe("Tool execution was interrupted before completion.")
565+
// Existing tool_result should be preserved
566+
expect(resultContent[1].tool_use_id).toBe("tool-1")
567+
expect(resultContent[1].content).toBe("Content for tool 1")
568+
})
569+
570+
it("should handle empty user content array by adding all missing tool_results", () => {
571+
const assistantMessage: Anthropic.MessageParam = {
572+
role: "assistant",
573+
content: [
574+
{
575+
type: "tool_use",
576+
id: "tool-1",
577+
name: "read_file",
578+
input: { path: "test.txt" },
579+
},
580+
],
581+
}
582+
583+
const userMessage: Anthropic.MessageParam = {
584+
role: "user",
585+
content: [],
586+
}
587+
588+
const result = validateAndFixToolResultIds(userMessage, [assistantMessage])
589+
429590
expect(Array.isArray(result.content)).toBe(true)
430591
const resultContent = result.content as Anthropic.ToolResultBlockParam[]
431592
expect(resultContent.length).toBe(1)
593+
expect(resultContent[0].type).toBe("tool_result")
432594
expect(resultContent[0].tool_use_id).toBe("tool-1")
595+
expect(resultContent[0].content).toBe("Tool execution was interrupted before completion.")
433596
})
434597
})
435598

436599
describe("telemetry", () => {
437-
it("should call captureException when there is a mismatch", () => {
600+
it("should call captureException for both missing and mismatch when there is a mismatch", () => {
438601
const assistantMessage: Anthropic.MessageParam = {
439602
role: "assistant",
440603
content: [
@@ -460,7 +623,17 @@ describe("validateAndFixToolResultIds", () => {
460623

461624
validateAndFixToolResultIds(userMessage, [assistantMessage])
462625

463-
expect(TelemetryService.instance.captureException).toHaveBeenCalledTimes(1)
626+
// A mismatch also triggers missing detection since the wrong-id doesn't match any tool_use
627+
expect(TelemetryService.instance.captureException).toHaveBeenCalledTimes(2)
628+
expect(TelemetryService.instance.captureException).toHaveBeenCalledWith(
629+
expect.any(MissingToolResultError),
630+
expect.objectContaining({
631+
missingToolUseIds: ["correct-id"],
632+
existingToolResultIds: ["wrong-id"],
633+
toolUseCount: 1,
634+
toolResultCount: 1,
635+
}),
636+
)
464637
expect(TelemetryService.instance.captureException).toHaveBeenCalledWith(
465638
expect.any(ToolResultIdMismatchError),
466639
expect.objectContaining({
@@ -516,4 +689,132 @@ describe("validateAndFixToolResultIds", () => {
516689
expect(error.toolUseIds).toEqual(["use-1", "use-2"])
517690
})
518691
})
692+
693+
describe("MissingToolResultError", () => {
694+
it("should create error with correct properties", () => {
695+
const error = new MissingToolResultError(
696+
"Missing tool results detected",
697+
["tool-1", "tool-2"],
698+
["existing-result-1"],
699+
)
700+
701+
expect(error.name).toBe("MissingToolResultError")
702+
expect(error.message).toBe("Missing tool results detected")
703+
expect(error.missingToolUseIds).toEqual(["tool-1", "tool-2"])
704+
expect(error.existingToolResultIds).toEqual(["existing-result-1"])
705+
})
706+
})
707+
708+
describe("telemetry for missing tool_results", () => {
709+
it("should call captureException when tool_results are missing", () => {
710+
const assistantMessage: Anthropic.MessageParam = {
711+
role: "assistant",
712+
content: [
713+
{
714+
type: "tool_use",
715+
id: "tool-123",
716+
name: "read_file",
717+
input: { path: "test.txt" },
718+
},
719+
],
720+
}
721+
722+
const userMessage: Anthropic.MessageParam = {
723+
role: "user",
724+
content: [
725+
{
726+
type: "text",
727+
text: "No tool results here",
728+
},
729+
],
730+
}
731+
732+
validateAndFixToolResultIds(userMessage, [assistantMessage])
733+
734+
expect(TelemetryService.instance.captureException).toHaveBeenCalledTimes(1)
735+
expect(TelemetryService.instance.captureException).toHaveBeenCalledWith(
736+
expect.any(MissingToolResultError),
737+
expect.objectContaining({
738+
missingToolUseIds: ["tool-123"],
739+
existingToolResultIds: [],
740+
toolUseCount: 1,
741+
toolResultCount: 0,
742+
}),
743+
)
744+
})
745+
746+
it("should call captureException twice when both mismatch and missing occur", () => {
747+
const assistantMessage: Anthropic.MessageParam = {
748+
role: "assistant",
749+
content: [
750+
{
751+
type: "tool_use",
752+
id: "tool-1",
753+
name: "read_file",
754+
input: { path: "a.txt" },
755+
},
756+
{
757+
type: "tool_use",
758+
id: "tool-2",
759+
name: "read_file",
760+
input: { path: "b.txt" },
761+
},
762+
],
763+
}
764+
765+
const userMessage: Anthropic.MessageParam = {
766+
role: "user",
767+
content: [
768+
{
769+
type: "tool_result",
770+
tool_use_id: "wrong-id", // Wrong ID (mismatch)
771+
content: "Content",
772+
},
773+
// Missing tool_result for tool-2
774+
],
775+
}
776+
777+
validateAndFixToolResultIds(userMessage, [assistantMessage])
778+
779+
// Should be called twice: once for missing, once for mismatch
780+
expect(TelemetryService.instance.captureException).toHaveBeenCalledTimes(2)
781+
expect(TelemetryService.instance.captureException).toHaveBeenCalledWith(
782+
expect.any(MissingToolResultError),
783+
expect.any(Object),
784+
)
785+
expect(TelemetryService.instance.captureException).toHaveBeenCalledWith(
786+
expect.any(ToolResultIdMismatchError),
787+
expect.any(Object),
788+
)
789+
})
790+
791+
it("should not call captureException for missing when all tool_results exist", () => {
792+
const assistantMessage: Anthropic.MessageParam = {
793+
role: "assistant",
794+
content: [
795+
{
796+
type: "tool_use",
797+
id: "tool-123",
798+
name: "read_file",
799+
input: { path: "test.txt" },
800+
},
801+
],
802+
}
803+
804+
const userMessage: Anthropic.MessageParam = {
805+
role: "user",
806+
content: [
807+
{
808+
type: "tool_result",
809+
tool_use_id: "tool-123",
810+
content: "Content",
811+
},
812+
],
813+
}
814+
815+
validateAndFixToolResultIds(userMessage, [assistantMessage])
816+
817+
expect(TelemetryService.instance.captureException).not.toHaveBeenCalled()
818+
})
819+
})
519820
})

0 commit comments

Comments
 (0)