From da6a81d3986d00f9648384e68de04c720cc3a96b Mon Sep 17 00:00:00 2001 From: Mario Zechner Date: Mon, 20 Apr 2026 15:09:50 +0200 Subject: [PATCH] fix(coding-agent): reuse session thinking for compaction closes #3438 --- packages/coding-agent/CHANGELOG.md | 1 + .../coding-agent/src/core/agent-session.ts | 2 + .../src/core/compaction/compaction.ts | 28 ++++++++--- .../test/compaction-summary-reasoning.test.ts | 48 +++++++++++++++++-- 4 files changed, 69 insertions(+), 10 deletions(-) diff --git a/packages/coding-agent/CHANGELOG.md b/packages/coding-agent/CHANGELOG.md index 1f4f7a33..8de772b7 100644 --- a/packages/coding-agent/CHANGELOG.md +++ b/packages/coding-agent/CHANGELOG.md @@ -9,6 +9,7 @@ ### Fixed - Fixed missing `@sinclair/typebox` runtime dependency in `@mariozechner/pi-coding-agent`, so strict pnpm installs no longer fail with `ERR_MODULE_NOT_FOUND` when starting `pi` ([#3434](https://github.com/badlogic/pi-mono/issues/3434)) +- Fixed `/compact` to reuse the session thinking level for compaction summaries instead of forcing `high`, avoiding invalid reasoning-effort errors on `github-copilot/claude-opus-4.7` sessions configured for `medium` thinking ([#3438](https://github.com/badlogic/pi-mono/issues/3438)) - Fixed shared/exported plain-text tool output to preserve indentation instead of collapsing leading whitespace in the web share page ([#3440](https://github.com/badlogic/pi-mono/issues/3440)) - Fixed skill resolution to dedupe symlinked aliases by canonical path, so `pi config` no longer shows duplicate skill entries when `~/.pi/agent/skills` points to `~/.agents/skills` ([#3405](https://github.com/badlogic/pi-mono/issues/3405)) - Fixed OpenRouter request attribution to include Pi app headers (`HTTP-Referer: https://pi.dev`, `X-OpenRouter-Title: pi`, `X-OpenRouter-Categories: cli-agent`) when sessions are created through the coding-agent SDK and install telemetry is enabled ([#3414](https://github.com/badlogic/pi-mono/issues/3414)) diff --git a/packages/coding-agent/src/core/agent-session.ts b/packages/coding-agent/src/core/agent-session.ts index 29b8d7a3..f2df3688 100644 --- a/packages/coding-agent/src/core/agent-session.ts +++ b/packages/coding-agent/src/core/agent-session.ts @@ -1667,6 +1667,7 @@ export class AgentSession { headers, customInstructions, this._compactionAbortController.signal, + this.thinkingLevel, ); summary = result.summary; firstKeptEntryId = result.firstKeptEntryId; @@ -1931,6 +1932,7 @@ export class AgentSession { headers, undefined, this._autoCompactionAbortController.signal, + this.thinkingLevel, ); summary = compactResult.summary; firstKeptEntryId = compactResult.firstKeptEntryId; diff --git a/packages/coding-agent/src/core/compaction/compaction.ts b/packages/coding-agent/src/core/compaction/compaction.ts index 247e1012..58504248 100644 --- a/packages/coding-agent/src/core/compaction/compaction.ts +++ b/packages/coding-agent/src/core/compaction/compaction.ts @@ -5,7 +5,7 @@ * and after compaction the session is reloaded. */ -import type { AgentMessage } from "@mariozechner/pi-agent-core"; +import type { AgentMessage, ThinkingLevel } from "@mariozechner/pi-agent-core"; import type { AssistantMessage, Model, Usage } from "@mariozechner/pi-ai"; import { completeSimple } from "@mariozechner/pi-ai"; import { @@ -536,6 +536,7 @@ export async function generateSummary( signal?: AbortSignal, customInstructions?: string, previousSummary?: string, + thinkingLevel?: ThinkingLevel, ): Promise { const maxTokens = Math.floor(0.8 * reserveTokens); @@ -565,9 +566,10 @@ export async function generateSummary( }, ]; - const completionOptions = model.reasoning - ? { maxTokens, signal, apiKey, headers, reasoning: "high" as const } - : { maxTokens, signal, apiKey, headers }; + const completionOptions = + model.reasoning && thinkingLevel && thinkingLevel !== "off" + ? { maxTokens, signal, apiKey, headers, reasoning: thinkingLevel } + : { maxTokens, signal, apiKey, headers }; const response = await completeSimple( model, @@ -719,6 +721,7 @@ export async function compact( headers?: Record, customInstructions?: string, signal?: AbortSignal, + thinkingLevel?: ThinkingLevel, ): Promise { const { firstKeptEntryId, @@ -747,9 +750,18 @@ export async function compact( signal, customInstructions, previousSummary, + thinkingLevel, ) : Promise.resolve("No prior history."), - generateTurnPrefixSummary(turnPrefixMessages, model, settings.reserveTokens, apiKey, headers, signal), + generateTurnPrefixSummary( + turnPrefixMessages, + model, + settings.reserveTokens, + apiKey, + headers, + signal, + thinkingLevel, + ), ]); // Merge into single summary summary = `${historyResult}\n\n---\n\n**Turn Context (split turn):**\n\n${turnPrefixResult}`; @@ -764,6 +776,7 @@ export async function compact( signal, customInstructions, previousSummary, + thinkingLevel, ); } @@ -793,6 +806,7 @@ async function generateTurnPrefixSummary( apiKey: string, headers?: Record, signal?: AbortSignal, + thinkingLevel?: ThinkingLevel, ): Promise { const maxTokens = Math.floor(0.5 * reserveTokens); // Smaller budget for turn prefix const llmMessages = convertToLlm(messages); @@ -809,7 +823,9 @@ async function generateTurnPrefixSummary( const response = await completeSimple( model, { systemPrompt: SUMMARIZATION_SYSTEM_PROMPT, messages: summarizationMessages }, - { maxTokens, signal, apiKey, headers }, + model.reasoning && thinkingLevel && thinkingLevel !== "off" + ? { maxTokens, signal, apiKey, headers, reasoning: thinkingLevel } + : { maxTokens, signal, apiKey, headers }, ); if (response.stopReason === "error") { diff --git a/packages/coding-agent/test/compaction-summary-reasoning.test.ts b/packages/coding-agent/test/compaction-summary-reasoning.test.ts index 3e8fe66c..4e56dbba 100644 --- a/packages/coding-agent/test/compaction-summary-reasoning.test.ts +++ b/packages/coding-agent/test/compaction-summary-reasoning.test.ts @@ -56,18 +56,58 @@ describe("generateSummary reasoning options", () => { completeSimpleMock.mockResolvedValue(mockSummaryResponse); }); - it("sets reasoning=high for reasoning-capable models", async () => { - await generateSummary(messages, createModel(true), 2000, "test-key"); + it("uses the provided thinking level for reasoning-capable models", async () => { + await generateSummary( + messages, + createModel(true), + 2000, + "test-key", + undefined, + undefined, + undefined, + undefined, + "medium", + ); expect(completeSimpleMock).toHaveBeenCalledTimes(1); expect(completeSimpleMock.mock.calls[0][2]).toMatchObject({ - reasoning: "high", + reasoning: "medium", apiKey: "test-key", }); }); + it("does not set reasoning when thinking is off", async () => { + await generateSummary( + messages, + createModel(true), + 2000, + "test-key", + undefined, + undefined, + undefined, + undefined, + "off", + ); + + expect(completeSimpleMock).toHaveBeenCalledTimes(1); + expect(completeSimpleMock.mock.calls[0][2]).toMatchObject({ + apiKey: "test-key", + }); + expect(completeSimpleMock.mock.calls[0][2]).not.toHaveProperty("reasoning"); + }); + it("does not set reasoning for non-reasoning models", async () => { - await generateSummary(messages, createModel(false), 2000, "test-key"); + await generateSummary( + messages, + createModel(false), + 2000, + "test-key", + undefined, + undefined, + undefined, + undefined, + "medium", + ); expect(completeSimpleMock).toHaveBeenCalledTimes(1); expect(completeSimpleMock.mock.calls[0][2]).toMatchObject({