diff --git a/CHANGELOG.md b/CHANGELOG.md index 6388cec..e449e9f 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -2,6 +2,7 @@ ## Unreleased +- Stop replaying completed assistant reasoning traces to Command Code while preserving visible text and completed tool calls in follow-up request history. - Add `/commandcode-refresh` and `/commandcode-status` commands for safe model-catalog refreshes and redacted diagnostics. - Bound model discovery to a configurable 10-second timeout so a slow Provider API cannot block pi startup; timed-out discovery uses the validated cache when available. - Normalize Command Code context overflow failures so pi can auto-compact and retry, while leaving unrelated rate-limit and capacity errors unchanged. diff --git a/README.md b/README.md index 1fc9c3a..3c8ff79 100644 --- a/README.md +++ b/README.md @@ -86,6 +86,8 @@ Open `/model` and select one of the models provided by Command Code. Model avail Reasoning metadata is enriched only for models whose Command Code effort support is known. Those models register a model-specific `thinkingLevelMap`, so pi and OMP expose only supported levels. A selected supported level is sent as the documented `params.reasoning_effort` field; `off`, unsupported levels, and newly discovered models without metadata do not add reasoning fields to the request. No prompt instructions are injected. +Reasoning blocks from completed assistant turns remain visible in pi's local session, but are not replayed to Command Code in later requests. Only the assistant's user-visible text and completed tool calls are sent back as history. This matches the current Command Code CLI behavior and prevents prior private reasoning traces from interfering with reasoning on follow-up turns. + List Command Code models from the terminal: ```sh diff --git a/src/converters.ts b/src/converters.ts index eaba961..d2bef47 100644 --- a/src/converters.ts +++ b/src/converters.ts @@ -174,11 +174,6 @@ export function messagesToCC(messages?: readonly MessageLike[]): unknown[] { for (const content of recordArray(message.content)) { if (content.type === "text") { parts.push({ type: "text", text: stringValue(content.text) ?? "" }) - } else if (content.type === "thinking") { - parts.push({ - type: "reasoning", - text: stringValue(content.thinking) ?? "", - }) } else if (content.type === "toolCall") { const toolCallId = stringValue(content.id) ?? "" if (!pairedToolCallIds.has(toolCallId)) continue diff --git a/tests/test-live-e2e.mjs b/tests/test-live-e2e.mjs index d30d759..160882a 100644 --- a/tests/test-live-e2e.mjs +++ b/tests/test-live-e2e.mjs @@ -187,6 +187,51 @@ try { assert.equal(reasoning.code, 0, reasoning.stderr) assert.match(reasoning.stdout, new RegExp(marker)) + console.log("[live-e2e] live multi-turn reasoning history") + const multiTurn = await runRpc(extensionPath, async ({ send, waitFor, events, getStderr }) => { + const countThinkingDeltas = (startIndex) => + events + .slice(startIndex) + .filter( + (event) => + event.type === "message_update" && + event.assistantMessageEvent?.type === "thinking_delta" && + typeof event.assistantMessageEvent.delta === "string" && + event.assistantMessageEvent.delta.length > 0, + ).length + + const firstStart = events.length + send({ + id: "reasoning-turn-1", + type: "prompt", + message: + "Reason step by step before answering. Calculate 37 * 41, then reply with only the number.", + }) + await waitFor( + (event) => event.type === "response" && event.id === "reasoning-turn-1" && event.success, + ) + await waitFor((event) => event.type === "agent_settled") + const firstThinkingDeltas = countThinkingDeltas(firstStart) + + const secondStart = events.length + send({ + id: "reasoning-turn-2", + type: "prompt", + message: + "Now reason step by step again. Add 19 to your previous numeric result, then reply with only the number.", + }) + await waitFor( + (event) => event.type === "response" && event.id === "reasoning-turn-2" && event.success, + ) + await waitFor((event) => event.type === "agent_settled" && events.indexOf(event) >= secondStart) + const secondThinkingDeltas = countThinkingDeltas(secondStart) + + return { firstThinkingDeltas, secondThinkingDeltas, stderr: getStderr() } + }) + assert.ok(multiTurn.firstThinkingDeltas > 0, "first turn should stream reasoning") + assert.ok(multiTurn.secondThinkingDeltas > 0, "follow-up turn should stream fresh reasoning") + assert.doesNotMatch(multiTurn.stderr, /Bearer\s+\S+/i) + console.log("[live-e2e] live runtime refresh/status commands") const runtime = await runRpc(extensionPath, async ({ send, waitFor, getStderr }) => { send({ id: "commands", type: "get_commands" }) diff --git a/tests/test-pure-functions.ts b/tests/test-pure-functions.ts index a6cf530..134c437 100644 --- a/tests/test-pure-functions.ts +++ b/tests/test-pure-functions.ts @@ -499,12 +499,49 @@ describe("messagesToCC()", () => { assert.equal(objectAt(result, ["0", "role"]), "user") assert.equal(objectAt(result, ["1", "role"]), "assistant") - assert.equal(objectAt(result, ["1", "content", "0", "type"]), "reasoning") - assert.equal(objectAt(result, ["1", "content", "2", "type"]), "tool-call") + assert.equal(objectAt(result, ["1", "content", "0", "type"]), "text") + assert.equal(objectAt(result, ["1", "content", "1", "type"]), "tool-call") + assert.equal(objectAt(result, ["1", "content", "2"]), undefined) assert.equal(objectAt(result, ["2", "role"]), "tool") assert.equal(objectAt(result, ["2", "content", "0", "output", "value"]), "hello\nworld") }) + it("drops previous assistant reasoning while preserving text and tool calls", () => { + const result = messagesToCC([ + { role: "user", content: "first question" }, + { + role: "assistant", + content: [ + { type: "thinking", thinking: "private reasoning from turn one" }, + { type: "text", text: "first answer" }, + ], + }, + { role: "user", content: "follow-up question" }, + ]) + + assert.deepEqual(result, [ + { role: "user", content: "first question" }, + { role: "assistant", content: [{ type: "text", text: "first answer" }] }, + { role: "user", content: "follow-up question" }, + ]) + }) + + it("omits assistant turns that contain only previous reasoning", () => { + const result = messagesToCC([ + { role: "user", content: "first question" }, + { + role: "assistant", + content: [{ type: "thinking", thinking: "private reasoning" }], + }, + { role: "user", content: "follow-up question" }, + ]) + + assert.deepEqual(result, [ + { role: "user", content: "first question" }, + { role: "user", content: "follow-up question" }, + ]) + }) + it("drops orphaned tool calls that have no matching tool result", () => { const result = messagesToCC([ { role: "user", content: "edit a file" },