diff --git a/src/hooks/tool-pair-validator/hook.test.ts b/src/hooks/tool-pair-validator/hook.test.ts index dd2a1fc3b..dbd0724bd 100644 --- a/src/hooks/tool-pair-validator/hook.test.ts +++ b/src/hooks/tool-pair-validator/hook.test.ts @@ -21,6 +21,7 @@ type TestPart = { content?: string | Array<{ type: "text"; text: string }> text?: string synthetic?: boolean + state?: { status?: string; output?: string } } type TestMessage = { @@ -57,6 +58,35 @@ describe("createToolPairValidatorHook", () => { ]) }) + it("leaves terminal OpenCode tool parts unchanged", async () => { + //#given + const messages = [ + { + info: { role: "assistant" }, + parts: [ + { type: "tool", callID: "call_completed", state: { status: "completed", output: "OK" } }, + { type: "tool", callID: "call_error", state: { status: "error", output: "File not found" } }, + ], + }, + { info: { role: "assistant" }, parts: [{ type: "text", text: "final answer" }] }, + ] satisfies TestMessage[] + + //#when + await runTransform(messages) + + //#then + expect(messages).toEqual([ + { + info: { role: "assistant" }, + parts: [ + { type: "tool", callID: "call_completed", state: { status: "completed", output: "OK" } }, + { type: "tool", callID: "call_error", state: { status: "error", output: "File not found" } }, + ], + }, + { info: { role: "assistant" }, parts: [{ type: "text", text: "final answer" }] }, + ]) + }) + it("injects a missing tool_result into the next user message", async () => { //#given const messages = [ diff --git a/src/hooks/tool-pair-validator/hook.ts b/src/hooks/tool-pair-validator/hook.ts index da63504e5..9394d0d10 100644 --- a/src/hooks/tool-pair-validator/hook.ts +++ b/src/hooks/tool-pair-validator/hook.ts @@ -5,6 +5,7 @@ import { log } from "../../shared/logger" const TOOL_RESULT_PLACEHOLDER = "Tool output unavailable (context compacted)" const TOOL_RESULT_RECOVERY_CONTINUATION = "Recovered missing tool results. Continue from the repaired tool output." +const TERMINAL_OPENCODE_TOOL_STATUSES = new Set(["completed", "error"]) type ToolUsePart = { type: "tool_use" @@ -46,6 +47,24 @@ type MessagesTransformHook = { ) => Promise } +function isRecord(value: unknown): value is Record { + return typeof value === "object" && value !== null +} + +function isTerminalOpenCodeToolPart(part: TransformPart): boolean { + const candidate = part as { type?: unknown; callID?: unknown; state?: unknown } + if (candidate.type !== "tool" || typeof candidate.callID !== "string" || candidate.callID.length === 0) { + return false + } + + if (!isRecord(candidate.state)) { + return false + } + + const status = candidate.state["status"] + return typeof status === "string" && TERMINAL_OPENCODE_TOOL_STATUSES.has(status) +} + function getToolUseID(part: TransformPart): string | null { const candidate = part as { type?: unknown; id?: unknown; callID?: unknown } @@ -54,7 +73,7 @@ function getToolUseID(part: TransformPart): string | null { } if (candidate.type === "tool" && typeof candidate.callID === "string" && candidate.callID.length > 0) { - return candidate.callID + return isTerminalOpenCodeToolPart(part) ? null : candidate.callID } return null