diff --git a/src/wrapper/normalize.mjs b/src/wrapper/normalize.mjs index deae2c4..7e9e7d1 100644 --- a/src/wrapper/normalize.mjs +++ b/src/wrapper/normalize.mjs @@ -1,3 +1,30 @@ +const STOP_REASON_MAP = new Map([ + ["stop", "stop"], + ["end_turn", "stop"], + ["endturn", "stop"], + ["length", "length"], + ["tooluse", "toolUse"], + ["tool_use", "toolUse"], + ["aborted", "aborted"], + ["error", "error"], +]); + +export function normalizeStopReason(value) { + if (typeof value !== "string") { + return { stopReason: undefined, rawStopReason: undefined }; + } + + const rawStopReason = value.trim(); + if (!rawStopReason) { + return { stopReason: undefined, rawStopReason: undefined }; + } + + return { + stopReason: STOP_REASON_MAP.get(rawStopReason.toLowerCase()), + rawStopReason, + }; +} + export function normalizePiEvent(event) { if (event?.type === "tool_execution_start") { return { @@ -14,11 +41,14 @@ export function normalizePiEvent(event) { .join("\n") .trim(); + const { stopReason, rawStopReason } = normalizeStopReason(event.message.stopReason); + return { type: "assistant_text", text, model: event.message.model, - stopReason: event.message.stopReason, + stopReason, + rawStopReason, usage: event.message.usage, }; } diff --git a/src/wrapper/normalize.test.ts b/src/wrapper/normalize.test.ts index 78feff4..1f0df24 100644 --- a/src/wrapper/normalize.test.ts +++ b/src/wrapper/normalize.test.ts @@ -1,6 +1,6 @@ import test from "node:test"; import assert from "node:assert/strict"; -import { normalizePiEvent } from "./normalize.mjs"; +import { normalizePiEvent, normalizeStopReason } from "./normalize.mjs"; test("normalizePiEvent converts tool start events into protocol tool-call records", () => { const normalized = normalizePiEvent({ @@ -16,7 +16,26 @@ test("normalizePiEvent converts tool start events into protocol tool-call record }); }); -test("normalizePiEvent converts assistant message_end into a final-text record", () => { +test("normalizeStopReason maps raw tool-use variants to canonical toolUse", () => { + assert.deepEqual(normalizeStopReason("toolUse"), { + stopReason: "toolUse", + rawStopReason: "toolUse", + }); + + assert.deepEqual(normalizeStopReason("tool_use"), { + stopReason: "toolUse", + rawStopReason: "tool_use", + }); +}); + +test("normalizeStopReason preserves unknown stop reasons without marking them terminal", () => { + assert.deepEqual(normalizeStopReason("custom_reason"), { + stopReason: undefined, + rawStopReason: "custom_reason", + }); +}); + +test("normalizePiEvent converts assistant message_end into canonical final-text record", () => { const normalized = normalizePiEvent({ type: "message_end", message: { @@ -32,7 +51,8 @@ test("normalizePiEvent converts assistant message_end into a final-text record", type: "assistant_text", text: "Final answer", model: "anthropic/claude-sonnet-4-5", - stopReason: "end_turn", + stopReason: "stop", + rawStopReason: "end_turn", usage: { input: 10, output: 5, totalTokens: 15, cost: { total: 0.001 } }, }); });