diff --git a/AGENTS.md b/AGENTS.md index 765f376b8..12387d5dd 100644 --- a/AGENTS.md +++ b/AGENTS.md @@ -35,7 +35,7 @@ - `packages/tui` (`node:test`): `node --test test/specific.test.ts` - If you create or modify a test file, run it and iterate on test or implementation until it passes. - For `packages/coding-agent/test/suite/`, use `test/suite/harness.ts` + the faux provider. No real provider APIs, keys, or paid tokens. -- Put issue-specific regressions under `packages/coding-agent/test/suite/regressions/` named `-.test.ts`. +- When regressions tests for fixing a github issue, add a comment with the github issue number next to the test. - For ad-hoc scripts, `write` them to a temp file (e.g. `/tmp`), run, edit if needed, remove when done. Don't embed multi-line scripts in `bash` commands. - Never commit unless the user asks. diff --git a/package-lock.json b/package-lock.json index 55638099a..b1ea489c3 100644 --- a/package-lock.json +++ b/package-lock.json @@ -5329,11 +5329,11 @@ }, "packages/agent": { "name": "@earendil-works/pi-agent-core", - "version": "0.84.3", + "version": "0.84.4", "license": "MIT", "dependencies": { - "@earendil-works/pi-ai": "^0.84.3", - "@earendil-works/pi-telemetry": "^0.84.3", + "@earendil-works/pi-ai": "^0.84.4", + "@earendil-works/pi-telemetry": "^0.84.4", "diff": "8.0.4", "ignore": "7.0.5", "typebox": "1.3.7", @@ -5358,12 +5358,12 @@ }, "packages/ai": { "name": "@earendil-works/pi-ai", - "version": "0.84.3", + "version": "0.84.4", "license": "MIT", "dependencies": { "@anthropic-ai/sdk": "0.91.1", "@aws-sdk/client-bedrock-runtime": "3.1048.0", - "@earendil-works/pi-telemetry": "^0.84.3", + "@earendil-works/pi-telemetry": "^0.84.4", "@google/genai": "1.52.0", "@smithy/node-http-handler": "4.7.3", "http-proxy-agent": "7.0.2", @@ -5386,10 +5386,10 @@ }, "packages/client": { "name": "@earendil-works/pi-client", - "version": "0.84.3", + "version": "0.84.4", "license": "MIT", "dependencies": { - "@earendil-works/pi-protocol": "^0.84.3" + "@earendil-works/pi-protocol": "^0.84.4" }, "devDependencies": { "shx": "0.4.0", @@ -5401,14 +5401,14 @@ }, "packages/coding-agent": { "name": "@earendil-works/pi-coding-agent", - "version": "0.84.3", + "version": "0.84.4", "license": "MIT", "dependencies": { - "@earendil-works/pi-agent-core": "^0.84.3", - "@earendil-works/pi-ai": "^0.84.3", - "@earendil-works/pi-client": "^0.84.3", - "@earendil-works/pi-protocol": "^0.84.3", - "@earendil-works/pi-tui": "^0.84.3", + "@earendil-works/pi-agent-core": "^0.84.4", + "@earendil-works/pi-ai": "^0.84.4", + "@earendil-works/pi-client": "^0.84.4", + "@earendil-works/pi-protocol": "^0.84.4", + "@earendil-works/pi-tui": "^0.84.4", "@silvia-odwyer/photon-node": "0.3.4", "chalk": "5.6.2", "cross-spawn": "7.0.6", @@ -5447,32 +5447,32 @@ }, "packages/coding-agent/examples/extensions/custom-provider-anthropic": { "name": "pi-extension-custom-provider-anthropic", - "version": "0.84.3", + "version": "0.84.4", "dependencies": { "@anthropic-ai/sdk": "0.52.0" } }, "packages/coding-agent/examples/extensions/custom-provider-gitlab-duo": { "name": "pi-extension-custom-provider-gitlab-duo", - "version": "0.84.3" + "version": "0.84.4" }, "packages/coding-agent/examples/extensions/gondolin": { "name": "pi-extension-gondolin", - "version": "0.84.3", + "version": "0.84.4", "dependencies": { "@earendil-works/gondolin": "0.12.0" } }, "packages/coding-agent/examples/extensions/sandbox": { "name": "pi-extension-sandbox", - "version": "1.14.3", + "version": "1.14.4", "dependencies": { "@anthropic-ai/sandbox-runtime": "0.0.26" } }, "packages/coding-agent/examples/extensions/with-deps": { "name": "pi-extension-with-deps", - "version": "0.84.3", + "version": "0.84.4", "dependencies": { "ms": "2.1.3" }, @@ -5526,10 +5526,10 @@ }, "packages/evals": { "name": "@earendil-works/pi-evals", - "version": "0.84.3", + "version": "0.84.4", "devDependencies": { - "@earendil-works/pi-ai": "^0.84.3", - "@earendil-works/pi-coding-agent": "^0.84.3", + "@earendil-works/pi-ai": "^0.84.4", + "@earendil-works/pi-coding-agent": "^0.84.4", "@types/node": "22.19.19", "shx": "0.4.0", "typescript": "5.9.3", @@ -5539,7 +5539,7 @@ }, "packages/protocol": { "name": "@earendil-works/pi-protocol", - "version": "0.84.3", + "version": "0.84.4", "license": "MIT", "dependencies": { "typebox": "1.3.7" @@ -5554,11 +5554,11 @@ }, "packages/server": { "name": "@earendil-works/pi-server", - "version": "0.84.3", + "version": "0.84.4", "license": "MIT", "dependencies": { - "@earendil-works/pi-agent-core": "^0.84.3", - "@earendil-works/pi-protocol": "^0.84.3" + "@earendil-works/pi-agent-core": "^0.84.4", + "@earendil-works/pi-protocol": "^0.84.4" }, "devDependencies": { "shx": "0.4.0", @@ -5570,11 +5570,11 @@ }, "packages/session-backends/sqlite-node": { "name": "@earendil-works/pi-session-backend-sqlite-node", - "version": "0.84.3", + "version": "0.84.4", "license": "MIT", "dependencies": { - "@earendil-works/pi-agent-core": "^0.84.3", - "@earendil-works/pi-ai": "^0.84.3" + "@earendil-works/pi-agent-core": "^0.84.4", + "@earendil-works/pi-ai": "^0.84.4" }, "devDependencies": { "@vitest/coverage-v8": "4.1.9", @@ -5586,7 +5586,7 @@ }, "packages/telemetry": { "name": "@earendil-works/pi-telemetry", - "version": "0.84.3", + "version": "0.84.4", "license": "MIT", "devDependencies": { "@types/node": "22.19.19", @@ -5598,7 +5598,7 @@ }, "packages/tui": { "name": "@earendil-works/pi-tui", - "version": "0.84.3", + "version": "0.84.4", "license": "MIT", "dependencies": { "get-east-asian-width": "1.6.0", diff --git a/packages/agent/CHANGELOG.md b/packages/agent/CHANGELOG.md index 79ca2f5ad..ac4f59532 100644 --- a/packages/agent/CHANGELOG.md +++ b/packages/agent/CHANGELOG.md @@ -2,20 +2,11 @@ ## [Unreleased] +## [0.84.4] - 2026-08-28 + ### Breaking Changes -- Replaced the unfinished harness runtime and record-log session model with the shared entry/register/usage, operation-state, session, search, and public harness contracts specified for the durable harness. -- Added the required replay-stable invocation metadata argument to `AgentHarnessTool.execute()`. -- Required storage backends to return exact post-commit `SessionStats` in `CommitResult.stats`. - -### Added - -- Added scheduler-facing harness execution primitives (`accept`, fenced `drive`, `requestAbort`, and `inspectExecution`) plus replay-stable tool invocation identities. -- Added isolated harness execution primitives for assistant streaming and durable-boundary tool phases. -- Added direct-drive foundation primitives for split effect admission, deterministic commit gating, invocation-fenced progress writes, and injectable session id generation. -- Added durable `AgentHarness` prompt acceptance and minimal no-tool execution with recovery, lifecycle events, usage settlement, and terminal cleanup. -- Added assistant request recovery, durable retry waits, deferred suspension, and missing-identity suspension to `AgentHarness`. -- Added durable tool planning, context-bound sequential and parallel execution, ordered result settlement, and safe tool replay recovery to `AgentHarness`. +- Changed `prepareNextTurn` and `prepareNextTurnWithContext` to run only after `shouldStopAfterTurn` and queued-message checks determine that the agent loop will start another assistant turn. They no longer run after final or terminating turns; move end-of-run work to `agent_end` handling ([#6879](https://github.com/earendil-works/pi/issues/6879)). ### Fixed diff --git a/packages/agent/package.json b/packages/agent/package.json index 297a760d4..00a486566 100644 --- a/packages/agent/package.json +++ b/packages/agent/package.json @@ -1,6 +1,6 @@ { "name": "@earendil-works/pi-agent-core", - "version": "0.84.3", + "version": "0.84.4", "description": "General-purpose agent with transport abstraction, state management, and attachment support", "type": "module", "main": "./dist/index.js", @@ -55,8 +55,8 @@ "prepublishOnly": "npm run build" }, "dependencies": { - "@earendil-works/pi-ai": "^0.84.3", - "@earendil-works/pi-telemetry": "^0.84.3", + "@earendil-works/pi-ai": "^0.84.4", + "@earendil-works/pi-telemetry": "^0.84.4", "diff": "8.0.4", "ignore": "7.0.5", "typebox": "1.3.7", diff --git a/packages/agent/src/agent-loop.ts b/packages/agent/src/agent-loop.ts index a251fede0..7a9d3516b 100644 --- a/packages/agent/src/agent-loop.ts +++ b/packages/agent/src/agent-loop.ts @@ -19,6 +19,7 @@ import type { AgentTool, AgentToolCall, AgentToolResult, + PrepareNextTurnContext, StreamFn, } from "./types.ts"; @@ -162,7 +163,7 @@ async function runLoop( ): Promise { let currentContext = initialContext; let config = initialConfig; - let firstTurn = true; + let lastCompletedTurn: PrepareNextTurnContext | undefined; // Check for steering messages at start (user may have typed while waiting) let pendingMessages: AgentMessage[] = (await config.getSteeringMessages?.()) || []; @@ -172,10 +173,28 @@ async function runLoop( // Inner loop: process tool calls and steering messages while (hasMoreToolCalls || pendingMessages.length > 0) { - if (!firstTurn) { + if (lastCompletedTurn) { + const nextTurnSnapshot = await config.prepareNextTurn?.(lastCompletedTurn); + if (nextTurnSnapshot) { + currentContext = nextTurnSnapshot.context ?? currentContext; + config = { + ...config, + model: nextTurnSnapshot.model ?? config.model, + reasoning: + nextTurnSnapshot.thinkingLevel === undefined + ? config.reasoning + : nextTurnSnapshot.thinkingLevel === "off" + ? undefined + : nextTurnSnapshot.thinkingLevel, + }; + } + // Preparation can be long-running (for example, compaction). Pick up steering + // queued while it ran. Only poll again if the earlier poll returned nothing; + // otherwise one-at-a-time mode would deliver two messages in this turn. + if (pendingMessages.length === 0) { + pendingMessages = (await config.getSteeringMessages?.()) || []; + } await emit({ type: "turn_start" }); - } else { - firstTurn = false; } // Process pending messages (inject before next assistant response) @@ -223,35 +242,14 @@ async function runLoop( await emit({ type: "turn_end", message, toolResults }); - const nextTurnContext = { + lastCompletedTurn = { message, toolResults, context: currentContext, newMessages, }; - const nextTurnSnapshot = await config.prepareNextTurn?.(nextTurnContext); - if (nextTurnSnapshot) { - currentContext = nextTurnSnapshot.context ?? currentContext; - config = { - ...config, - model: nextTurnSnapshot.model ?? config.model, - reasoning: - nextTurnSnapshot.thinkingLevel === undefined - ? config.reasoning - : nextTurnSnapshot.thinkingLevel === "off" - ? undefined - : nextTurnSnapshot.thinkingLevel, - }; - } - if ( - await config.shouldStopAfterTurn?.({ - message, - toolResults, - context: currentContext, - newMessages, - }) - ) { + if (await config.shouldStopAfterTurn?.(lastCompletedTurn)) { await emit({ type: "agent_end", messages: newMessages }); return; } diff --git a/packages/agent/src/types.ts b/packages/agent/src/types.ts index ad9d5cc57..eebb5dc05 100644 --- a/packages/agent/src/types.ts +++ b/packages/agent/src/types.ts @@ -214,6 +214,7 @@ export interface AgentLoopConfig extends SimpleStreamOptions { * * If it returns true, the loop emits `agent_end` and exits before polling steering or follow-up queues, * without starting another LLM call. The current assistant response and any tool executions finish normally. + * This callback sees the completed-turn context and runs before `prepareNextTurn`. * * Use this to request a graceful stop after the current turn, e.g. before context gets too full. * @@ -222,8 +223,8 @@ export interface AgentLoopConfig extends SimpleStreamOptions { shouldStopAfterTurn?: (context: ShouldStopAfterTurnContext) => boolean | Promise; /** - * Called after `turn_end` and before the loop decides whether another provider request should start. - * Return replacement context/model/thinking state to affect the next turn in this run. + * Called after `turn_end` when the loop will continue, immediately before the next turn starts. + * Return replacement context/model/thinking state to affect that turn. * Return undefined to keep using the current context/config. */ prepareNextTurn?: ( diff --git a/packages/agent/test/agent-loop.test.ts b/packages/agent/test/agent-loop.test.ts index e5b2f1db8..d3255dfd0 100644 --- a/packages/agent/test/agent-loop.test.ts +++ b/packages/agent/test/agent-loop.test.ts @@ -1048,11 +1048,13 @@ describe("agentLoop with AgentMessage", () => { tools: [tool], }; let convertedSecondTurnSystemPrompt = ""; + let prepareCalls = 0; let prepared = false; const config: AgentLoopConfig = { model: createModel(), convertToLlm: identityConverter, prepareNextTurn: async ({ context: currentContext }) => { + prepareCalls++; if (prepared) return undefined; prepared = true; return { @@ -1098,6 +1100,7 @@ describe("agentLoop with AgentMessage", () => { } expect(llmCalls).toBe(2); + expect(prepareCalls).toBe(1); expect(convertedSecondTurnSystemPrompt).toBe("second prompt"); }); diff --git a/packages/ai/CHANGELOG.md b/packages/ai/CHANGELOG.md index 3cafae8c6..fdfac3d25 100644 --- a/packages/ai/CHANGELOG.md +++ b/packages/ai/CHANGELOG.md @@ -2,10 +2,20 @@ ## [Unreleased] +## [0.84.4] - 2026-08-28 + +### Added + +- Added the experimental vision-capable `deepseek-v4-flash-vision-exp` model to the DeepSeek catalog. + ### Fixed - Fixed OpenAI-compatible Chat Completions ignoring an explicitly requested `toolChoice` when no tools are defined. - Fixed thinking signature serialization to run once after the signature is complete ([#8671](https://github.com/earendil-works/pi/pull/8671)). +- Fixed fragmented Mistral tool calls splitting when continuation chunks omit the tool-call ID ([#8387](https://github.com/earendil-works/pi/issues/8387)). +- Fixed OpenAI-compatible reasoning replay to merge consecutive streamed text and summary `reasoning_details` deltas. +- Fixed the Cloudflare AI Gateway catalog to include supported `workers-ai/*` passthrough models omitted by models.dev. +- Fixed OpenRouter reasoning controls by deriving `off` support and available effort levels from OpenRouter's model metadata, preventing reasoning-mandatory models from receiving `effort: "none"` ([#8614](https://github.com/earendil-works/pi/pull/8614) by [@davidbrai](https://github.com/davidbrai)). ## [0.84.3] - 2026-08-24 diff --git a/packages/ai/package.json b/packages/ai/package.json index 69137f244..39bef0939 100644 --- a/packages/ai/package.json +++ b/packages/ai/package.json @@ -1,6 +1,6 @@ { "name": "@earendil-works/pi-ai", - "version": "0.84.3", + "version": "0.84.4", "description": "Unified LLM API with automatic model discovery and provider configuration", "type": "module", "main": "./dist/index.js", @@ -66,7 +66,7 @@ "dependencies": { "@anthropic-ai/sdk": "0.91.1", "@aws-sdk/client-bedrock-runtime": "3.1048.0", - "@earendil-works/pi-telemetry": "^0.84.3", + "@earendil-works/pi-telemetry": "^0.84.4", "@google/genai": "1.52.0", "@smithy/node-http-handler": "4.7.3", "http-proxy-agent": "7.0.2", diff --git a/packages/ai/src/api/mistral-conversations.ts b/packages/ai/src/api/mistral-conversations.ts index b9d291f8e..821e39b21 100644 --- a/packages/ai/src/api/mistral-conversations.ts +++ b/packages/ai/src/api/mistral-conversations.ts @@ -561,7 +561,7 @@ async function consumeChatStream( let currentBlock: TextContent | ThinkingContent | null = null; const blocks = output.content; const blockIndex = () => blocks.length - 1; - const toolBlocksByKey = new Map(); + const toolBlocksByKey = new Map(); const finishCurrentBlock = (block?: typeof currentBlock) => { if (!block) return; @@ -690,7 +690,7 @@ async function consumeChatStream( toolCall.id && toolCall.id !== "null" ? toolCall.id : deriveMistralToolCallId(`toolcall:${toolCall.index ?? 0}`, 0); - const key = `${callId}:${toolCall.index || 0}`; + const key = toolCall.index ?? callId; const existingIndex = toolBlocksByKey.get(key); let block: (ToolCall & { partialArgs?: string }) | undefined; diff --git a/packages/ai/src/image-models.generated.ts b/packages/ai/src/image-models.generated.ts index 80bb8f77d..be577a3a4 100644 --- a/packages/ai/src/image-models.generated.ts +++ b/packages/ai/src/image-models.generated.ts @@ -245,6 +245,21 @@ export const IMAGE_MODELS = { cacheWrite: 0, }, } satisfies ImagesModel<"openrouter-images">, + "meta/muse-image": { + id: "meta/muse-image", + name: "Meta: Muse Image", + api: "openrouter-images", + provider: "openrouter", + baseUrl: "https://openrouter.ai/api/v1", + input: ["text", "image"], + output: ["image"], + cost: { + input: 0, + output: 0, + cacheRead: 0, + cacheWrite: 0, + }, + } satisfies ImagesModel<"openrouter-images">, "microsoft/mai-image-2.5": { id: "microsoft/mai-image-2.5", name: "Microsoft: MAI-Image-2.5", @@ -485,6 +500,66 @@ export const IMAGE_MODELS = { cacheWrite: 0, }, } satisfies ImagesModel<"openrouter-images">, + "recraft/recraft-v4-styles": { + id: "recraft/recraft-v4-styles", + name: "Recraft: Recraft V4 Styles", + api: "openrouter-images", + provider: "openrouter", + baseUrl: "https://openrouter.ai/api/v1", + input: ["text", "image"], + output: ["image"], + cost: { + input: 0, + output: 0, + cacheRead: 0, + cacheWrite: 0, + }, + } satisfies ImagesModel<"openrouter-images">, + "recraft/recraft-v4-styles-pro": { + id: "recraft/recraft-v4-styles-pro", + name: "Recraft: Recraft V4 Styles Pro", + api: "openrouter-images", + provider: "openrouter", + baseUrl: "https://openrouter.ai/api/v1", + input: ["text", "image"], + output: ["image"], + cost: { + input: 0, + output: 0, + cacheRead: 0, + cacheWrite: 0, + }, + } satisfies ImagesModel<"openrouter-images">, + "recraft/recraft-v4-styles-pro-vector": { + id: "recraft/recraft-v4-styles-pro-vector", + name: "Recraft: Recraft V4 Styles Pro Vector", + api: "openrouter-images", + provider: "openrouter", + baseUrl: "https://openrouter.ai/api/v1", + input: ["text", "image"], + output: ["image"], + cost: { + input: 0, + output: 0, + cacheRead: 0, + cacheWrite: 0, + }, + } satisfies ImagesModel<"openrouter-images">, + "recraft/recraft-v4-styles-vector": { + id: "recraft/recraft-v4-styles-vector", + name: "Recraft: Recraft V4 Styles Vector", + api: "openrouter-images", + provider: "openrouter", + baseUrl: "https://openrouter.ai/api/v1", + input: ["text", "image"], + output: ["image"], + cost: { + input: 0, + output: 0, + cacheRead: 0, + cacheWrite: 0, + }, + } satisfies ImagesModel<"openrouter-images">, "recraft/recraft-v4-vector": { id: "recraft/recraft-v4-vector", name: "Recraft: Recraft V4 Vector", diff --git a/packages/ai/test/mistral-http-transport.test.ts b/packages/ai/test/mistral-http-transport.test.ts index 3622fb56c..b4282ffe7 100644 --- a/packages/ai/test/mistral-http-transport.test.ts +++ b/packages/ai/test/mistral-http-transport.test.ts @@ -235,7 +235,7 @@ describe("Mistral HTTP transport", () => { ]); }); - it("parses native thinking, text, tool calls, and cached-token usage", async () => { + it("parses native thinking, text, fragmented tool calls, and cached-token usage", async () => { const model = getModel("mistral", "mistral-large-latest"); const context: Context = { messages: [{ role: "user", content: "hello", timestamp: 1 }], @@ -292,9 +292,8 @@ describe("Mistral HTTP transport", () => { delta: { tool_calls: [ { - id: "abc123456", index: 0, - function: { name: "lookup", arguments: '"pi"}' }, + function: { name: "", arguments: '"pi"}' }, }, ], }, diff --git a/packages/client/CHANGELOG.md b/packages/client/CHANGELOG.md index 726fa233d..bd05c0cbd 100644 --- a/packages/client/CHANGELOG.md +++ b/packages/client/CHANGELOG.md @@ -2,6 +2,8 @@ ## [Unreleased] +## [0.84.4] - 2026-08-28 + ## [0.84.3] - 2026-08-24 ## [0.84.2] - 2026-08-14 diff --git a/packages/client/package.json b/packages/client/package.json index 5ca649b91..b0e63d94e 100644 --- a/packages/client/package.json +++ b/packages/client/package.json @@ -1,6 +1,6 @@ { "name": "@earendil-works/pi-client", - "version": "0.84.3", + "version": "0.84.4", "description": "Transport-neutral client for remote pi sessions over framed CBOR bytes", "type": "module", "main": "./dist/index.js", @@ -47,7 +47,7 @@ "node": ">=22.19.0" }, "dependencies": { - "@earendil-works/pi-protocol": "^0.84.3" + "@earendil-works/pi-protocol": "^0.84.4" }, "devDependencies": { "shx": "0.4.0", diff --git a/packages/coding-agent/CHANGELOG.md b/packages/coding-agent/CHANGELOG.md index 86138d6c6..690c38e89 100644 --- a/packages/coding-agent/CHANGELOG.md +++ b/packages/coding-agent/CHANGELOG.md @@ -2,9 +2,21 @@ ## [Unreleased] +## [0.84.4] - 2026-08-28 + +### New Features + +- **Terminal capability overrides** — Override detected terminal hyperlink, image, and truecolor support. See [Capability Overrides](docs/terminal-setup.md#capability-overrides). +- **Extension UI prompt events** — Integrations can distinguish active agent work from time spent waiting for `ctx.ui` prompts. See [Extension UI prompt events](docs/extensions.md#ui_prompt_start--ui_prompt_end). +- **RPC queue clearing** — Retrieve and clear queued steering and follow-up messages with `clear_queue`. See [RPC `clear_queue`](docs/rpc.md#clear_queue). +- **Fullscreen selection copy controls** — Disable automatic selection copying in fullscreen mode and use Ctrl+X to copy the active selection. See [UI & Display](docs/settings.md#ui--display). +- **DeepSeek V4 Flash Vision (experimental)** — Use the vision-capable model through the built-in DeepSeek provider. See [API Keys](docs/providers.md#api-keys). + ### Added -- Added `ui_prompt_start` and `ui_prompt_end` extension events so host integrations can distinguish active agent work from waiting on user-facing `ctx.ui` prompts ([#5329](https://github.com/earendil-works/pi/issues/5329)). +- Added `ui_prompt_start` and `ui_prompt_end` extension events so host integrations can distinguish active agent work from waiting on user-facing `ctx.ui` prompts ([#8355](https://github.com/earendil-works/pi/pull/8355) by [@cristinaponcela](https://github.com/cristinaponcela)). +- Added `detectSupportedImageMimeTypeFromFile()` to the public library exports ([#8600](https://github.com/earendil-works/pi/pull/8600) by [@xl0](https://github.com/xl0)). +- Added inherited experimental vision-capable `deepseek-v4-flash-vision-exp` model support. - Added transcript usage notices for compaction and branch summaries when cache miss notices are enabled. - Added RPC `clear_queue` to retrieve and remove queued steering and follow-up messages ([#8432](https://github.com/earendil-works/pi/issues/8432)). - Added environment variables and advanced settings for overriding auto-detected terminal hyperlink, image, and truecolor capabilities ([#8665](https://github.com/earendil-works/pi/issues/8665)). @@ -17,7 +29,18 @@ - Fixed resumed sessions corrupting the next appended entry when their JSONL file lacks a trailing newline ([#8345](https://github.com/earendil-works/pi/issues/8345)). - Fixed extension messages sent with `triggerTurn: false` while the agent is running being inserted between a tool call and its result, which made providers that validate message order reject the replayed history. They are now appended once the turn's tool results are in ([#8537](https://github.com/earendil-works/pi/issues/8537)). - Fixed compaction and branch summaries forcing `toolChoice: "none"` ([#8649](https://github.com/earendil-works/pi/issues/8649), [#8638](https://github.com/earendil-works/pi/issues/8638)). +- Fixed large tool results crossing the auto-compaction threshold being sent to the provider before compaction. Pi now compacts between tool execution and the next assistant response in the same run, and restores interactive progress when that run resumes ([#6879](https://github.com/earendil-works/pi/issues/6879)). - Fixed Google Vertex requests failing with `HttpsProxyAgent is not a constructor` when the bundled Node.js runtime uses an HTTP(S) proxy ([#8610](https://github.com/earendil-works/pi/issues/8610)). +- Fixed saving a default model from a non-empty model scope so it remains available in that scope. +- Fixed inherited `@` file autocomplete ranking to prefer direct and shallower matches over similarly ranked nested paths ([#8669](https://github.com/earendil-works/pi/pull/8669)). +- Fixed inherited OpenAI-compatible streams serializing thinking signatures repeatedly during streaming ([#8671](https://github.com/earendil-works/pi/pull/8671)). +- Fixed inherited main-screen rendering crashing when image-heavy output exceeded V8's string length limit ([#8028](https://github.com/earendil-works/pi/issues/8028)). +- Fixed inherited fullscreen double-click word selection splitting paths and kebab-case tokens on `/` and `-` ([#8676](https://github.com/earendil-works/pi/pull/8676)). +- Fixed inherited Cloudflare AI Gateway catalogs omitting supported `workers-ai/*` passthrough models. +- Fixed inherited OpenAI-compatible reasoning replay to merge consecutive streamed text and summary `reasoning_details` deltas. +- Fixed inherited OpenRouter reasoning controls so reasoning-mandatory models do not receive `effort: "none"` ([#8614](https://github.com/earendil-works/pi/pull/8614) by [@davidbrai](https://github.com/davidbrai)). +- Fixed inherited OpenAI-compatible Chat Completions ignoring an explicitly requested `toolChoice` when no tools are defined. +- Fixed inherited fragmented Mistral tool calls splitting when continuation chunks omit the tool-call ID ([#8387](https://github.com/earendil-works/pi/issues/8387)). ## [0.84.3] - 2026-08-24 @@ -86,7 +109,7 @@ - Added `session_compact_failed` extension events so compaction failures and aborts expose their reason, retry state, source, and error message to handlers ([#8175](https://github.com/earendil-works/pi/issues/8175)). - Fixed truncated compaction and branch summaries being persisted when generation reaches its output token limit ([#7048](https://github.com/earendil-works/pi/issues/7048)). - Fixed npm package update checks treating older registry versions as available updates, preventing `pi update` from downgrading already-newer installed packages ([#8226](https://github.com/earendil-works/pi/issues/8226)). -- Fixed built-in llama.cpp models disappearing from `/model` when `/llama` refreshed a configured server under `PI_OFFLINE`, and included idle-slept `sleeping` router models plus autoloadable unloaded presets in the selectable catalog ([#8167](https://github.com/earendil-works/pi/issues/8167)). +- Fixed built-in llama.cpp models disappearing from `/model` when `/llama` refreshed a configured server under `PI_OFFLINE`, and included idle-slept `sleeping` router models plus autoloadable unloaded presets in the selectable catalog ([#8558](https://github.com/earendil-works/pi/pull/8558) by [@cristinaponcela](https://github.com/cristinaponcela)). - Fixed `pi.registerFlag()` accepting default values that do not match the declared flag type ([#8064](https://github.com/earendil-works/pi/issues/8064)). - Fixed Z.AI Coding Plan defaults referencing the removed GLM-5.1 model ([#8096](https://github.com/earendil-works/pi/issues/8096)). - Fixed repeated ambiguous truncated-response recovery being mislabeled as context overflow ([#8130](https://github.com/earendil-works/pi/issues/8130)). diff --git a/packages/coding-agent/docs/compaction.md b/packages/coding-agent/docs/compaction.md index 3a9dff1c6..c3896f4e6 100644 --- a/packages/coding-agent/docs/compaction.md +++ b/packages/coding-agent/docs/compaction.md @@ -34,6 +34,8 @@ contextTokens > contextWindow - reserveTokens By default, `reserveTokens` is 16384 tokens (configurable in `~/.pi/agent/settings.json` or `/.pi/settings.json`). This leaves room for the LLM's response. +During a multi-turn agent run, Pi checks this threshold after tools finish and their results are appended, before starting the next assistant response. If the threshold is crossed, Pi compacts inside the same agent run and resumes with the summary and retained messages. It skips this between-turn check when the completed tool batch terminates the run and no queued message requires another response. Pi also checks the threshold before a new user prompt and after a low-level agent run ends. + You can also trigger manually with `/compact [instructions]`, where optional instructions focus the summary. ### How It Works diff --git a/packages/coding-agent/examples/extensions/custom-provider-anthropic/package-lock.json b/packages/coding-agent/examples/extensions/custom-provider-anthropic/package-lock.json index a5ce47769..ba5aae8b7 100644 --- a/packages/coding-agent/examples/extensions/custom-provider-anthropic/package-lock.json +++ b/packages/coding-agent/examples/extensions/custom-provider-anthropic/package-lock.json @@ -1,12 +1,12 @@ { "name": "pi-extension-custom-provider", - "version": "0.84.3", + "version": "0.84.4", "lockfileVersion": 3, "requires": true, "packages": { "": { "name": "pi-extension-custom-provider", - "version": "0.84.3", + "version": "0.84.4", "dependencies": { "@anthropic-ai/sdk": "^0.52.0" } diff --git a/packages/coding-agent/examples/extensions/custom-provider-anthropic/package.json b/packages/coding-agent/examples/extensions/custom-provider-anthropic/package.json index 9de50e63b..bb67dd877 100644 --- a/packages/coding-agent/examples/extensions/custom-provider-anthropic/package.json +++ b/packages/coding-agent/examples/extensions/custom-provider-anthropic/package.json @@ -1,7 +1,7 @@ { "name": "pi-extension-custom-provider-anthropic", "private": true, - "version": "0.84.3", + "version": "0.84.4", "type": "module", "scripts": { "clean": "echo 'nothing to clean'", diff --git a/packages/coding-agent/examples/extensions/custom-provider-gitlab-duo/package.json b/packages/coding-agent/examples/extensions/custom-provider-gitlab-duo/package.json index 247c4d5b4..f005a861c 100644 --- a/packages/coding-agent/examples/extensions/custom-provider-gitlab-duo/package.json +++ b/packages/coding-agent/examples/extensions/custom-provider-gitlab-duo/package.json @@ -1,7 +1,7 @@ { "name": "pi-extension-custom-provider-gitlab-duo", "private": true, - "version": "0.84.3", + "version": "0.84.4", "type": "module", "scripts": { "clean": "echo 'nothing to clean'", diff --git a/packages/coding-agent/examples/extensions/gondolin/package-lock.json b/packages/coding-agent/examples/extensions/gondolin/package-lock.json index 1beb731ef..1178b7cdb 100644 --- a/packages/coding-agent/examples/extensions/gondolin/package-lock.json +++ b/packages/coding-agent/examples/extensions/gondolin/package-lock.json @@ -1,12 +1,12 @@ { "name": "pi-extension-gondolin", - "version": "0.84.3", + "version": "0.84.4", "lockfileVersion": 3, "requires": true, "packages": { "": { "name": "pi-extension-gondolin", - "version": "0.84.3", + "version": "0.84.4", "dependencies": { "@earendil-works/gondolin": "0.12.0" } diff --git a/packages/coding-agent/examples/extensions/gondolin/package.json b/packages/coding-agent/examples/extensions/gondolin/package.json index eac937af0..7c91de779 100644 --- a/packages/coding-agent/examples/extensions/gondolin/package.json +++ b/packages/coding-agent/examples/extensions/gondolin/package.json @@ -1,7 +1,7 @@ { "name": "pi-extension-gondolin", "private": true, - "version": "0.84.3", + "version": "0.84.4", "type": "module", "scripts": { "clean": "echo 'nothing to clean'", diff --git a/packages/coding-agent/examples/extensions/sandbox/package-lock.json b/packages/coding-agent/examples/extensions/sandbox/package-lock.json index 6a422b32b..fb4742606 100644 --- a/packages/coding-agent/examples/extensions/sandbox/package-lock.json +++ b/packages/coding-agent/examples/extensions/sandbox/package-lock.json @@ -1,12 +1,12 @@ { "name": "pi-extension-sandbox", - "version": "1.14.3", + "version": "1.14.4", "lockfileVersion": 3, "requires": true, "packages": { "": { "name": "pi-extension-sandbox", - "version": "1.14.3", + "version": "1.14.4", "dependencies": { "@anthropic-ai/sandbox-runtime": "^0.0.26" } diff --git a/packages/coding-agent/examples/extensions/sandbox/package.json b/packages/coding-agent/examples/extensions/sandbox/package.json index 2d37b5c82..f0ab54a8f 100644 --- a/packages/coding-agent/examples/extensions/sandbox/package.json +++ b/packages/coding-agent/examples/extensions/sandbox/package.json @@ -1,7 +1,7 @@ { "name": "pi-extension-sandbox", "private": true, - "version": "1.14.3", + "version": "1.14.4", "type": "module", "scripts": { "clean": "echo 'nothing to clean'", diff --git a/packages/coding-agent/examples/extensions/with-deps/package-lock.json b/packages/coding-agent/examples/extensions/with-deps/package-lock.json index b0d0855aa..286adc293 100644 --- a/packages/coding-agent/examples/extensions/with-deps/package-lock.json +++ b/packages/coding-agent/examples/extensions/with-deps/package-lock.json @@ -1,12 +1,12 @@ { "name": "pi-extension-with-deps", - "version": "0.84.3", + "version": "0.84.4", "lockfileVersion": 3, "requires": true, "packages": { "": { "name": "pi-extension-with-deps", - "version": "0.84.3", + "version": "0.84.4", "dependencies": { "ms": "^2.1.3" }, diff --git a/packages/coding-agent/examples/extensions/with-deps/package.json b/packages/coding-agent/examples/extensions/with-deps/package.json index 3532ad7a7..990628784 100644 --- a/packages/coding-agent/examples/extensions/with-deps/package.json +++ b/packages/coding-agent/examples/extensions/with-deps/package.json @@ -1,7 +1,7 @@ { "name": "pi-extension-with-deps", "private": true, - "version": "0.84.3", + "version": "0.84.4", "type": "module", "scripts": { "clean": "echo 'nothing to clean'", diff --git a/packages/coding-agent/install-lock/package-lock.json b/packages/coding-agent/install-lock/package-lock.json index 44ab528ce..4c48ca4c7 100644 --- a/packages/coding-agent/install-lock/package-lock.json +++ b/packages/coding-agent/install-lock/package-lock.json @@ -1,14 +1,14 @@ { "name": "@earendil-works/pi-coding-agent-install", - "version": "0.84.3", + "version": "0.84.4", "lockfileVersion": 3, "requires": true, "packages": { "": { "name": "@earendil-works/pi-coding-agent-install", - "version": "0.84.3", + "version": "0.84.4", "dependencies": { - "@earendil-works/pi-coding-agent": "0.84.3" + "@earendil-works/pi-coding-agent": "0.84.4" }, "engines": { "node": ">=22.19.0" @@ -450,12 +450,12 @@ } }, "node_modules/@earendil-works/pi-agent-core": { - "version": "0.84.3", - "resolved": "https://registry.npmjs.org/@earendil-works/pi-agent-core/-/pi-agent-core-0.84.3.tgz", + "version": "0.84.4", + "resolved": "https://registry.npmjs.org/@earendil-works/pi-agent-core/-/pi-agent-core-0.84.4.tgz", "license": "MIT", "dependencies": { - "@earendil-works/pi-ai": "^0.84.3", - "@earendil-works/pi-telemetry": "^0.84.3", + "@earendil-works/pi-ai": "^0.84.4", + "@earendil-works/pi-telemetry": "^0.84.4", "diff": "8.0.4", "ignore": "7.0.5", "typebox": "1.3.7", @@ -466,13 +466,13 @@ } }, "node_modules/@earendil-works/pi-ai": { - "version": "0.84.3", - "resolved": "https://registry.npmjs.org/@earendil-works/pi-ai/-/pi-ai-0.84.3.tgz", + "version": "0.84.4", + "resolved": "https://registry.npmjs.org/@earendil-works/pi-ai/-/pi-ai-0.84.4.tgz", "license": "MIT", "dependencies": { "@anthropic-ai/sdk": "0.91.1", "@aws-sdk/client-bedrock-runtime": "3.1048.0", - "@earendil-works/pi-telemetry": "^0.84.3", + "@earendil-works/pi-telemetry": "^0.84.4", "@google/genai": "1.52.0", "@smithy/node-http-handler": "4.7.3", "http-proxy-agent": "7.0.2", @@ -489,26 +489,26 @@ } }, "node_modules/@earendil-works/pi-client": { - "version": "0.84.3", - "resolved": "https://registry.npmjs.org/@earendil-works/pi-client/-/pi-client-0.84.3.tgz", + "version": "0.84.4", + "resolved": "https://registry.npmjs.org/@earendil-works/pi-client/-/pi-client-0.84.4.tgz", "license": "MIT", "dependencies": { - "@earendil-works/pi-protocol": "^0.84.3" + "@earendil-works/pi-protocol": "^0.84.4" }, "engines": { "node": ">=22.19.0" } }, "node_modules/@earendil-works/pi-coding-agent": { - "version": "0.84.3", - "resolved": "https://registry.npmjs.org/@earendil-works/pi-coding-agent/-/pi-coding-agent-0.84.3.tgz", + "version": "0.84.4", + "resolved": "https://registry.npmjs.org/@earendil-works/pi-coding-agent/-/pi-coding-agent-0.84.4.tgz", "license": "MIT", "dependencies": { - "@earendil-works/pi-agent-core": "^0.84.3", - "@earendil-works/pi-ai": "^0.84.3", - "@earendil-works/pi-client": "^0.84.3", - "@earendil-works/pi-protocol": "^0.84.3", - "@earendil-works/pi-tui": "^0.84.3", + "@earendil-works/pi-agent-core": "^0.84.4", + "@earendil-works/pi-ai": "^0.84.4", + "@earendil-works/pi-client": "^0.84.4", + "@earendil-works/pi-protocol": "^0.84.4", + "@earendil-works/pi-tui": "^0.84.4", "@silvia-odwyer/photon-node": "0.3.4", "chalk": "5.6.2", "cross-spawn": "7.0.6", @@ -536,8 +536,8 @@ } }, "node_modules/@earendil-works/pi-protocol": { - "version": "0.84.3", - "resolved": "https://registry.npmjs.org/@earendil-works/pi-protocol/-/pi-protocol-0.84.3.tgz", + "version": "0.84.4", + "resolved": "https://registry.npmjs.org/@earendil-works/pi-protocol/-/pi-protocol-0.84.4.tgz", "license": "MIT", "dependencies": { "typebox": "1.3.7" @@ -547,16 +547,16 @@ } }, "node_modules/@earendil-works/pi-telemetry": { - "version": "0.84.3", - "resolved": "https://registry.npmjs.org/@earendil-works/pi-telemetry/-/pi-telemetry-0.84.3.tgz", + "version": "0.84.4", + "resolved": "https://registry.npmjs.org/@earendil-works/pi-telemetry/-/pi-telemetry-0.84.4.tgz", "license": "MIT", "engines": { "node": ">=22.19.0" } }, "node_modules/@earendil-works/pi-tui": { - "version": "0.84.3", - "resolved": "https://registry.npmjs.org/@earendil-works/pi-tui/-/pi-tui-0.84.3.tgz", + "version": "0.84.4", + "resolved": "https://registry.npmjs.org/@earendil-works/pi-tui/-/pi-tui-0.84.4.tgz", "license": "MIT", "dependencies": { "get-east-asian-width": "1.6.0", diff --git a/packages/coding-agent/install-lock/package.json b/packages/coding-agent/install-lock/package.json index e7cf5a2af..7bd56d54e 100644 --- a/packages/coding-agent/install-lock/package.json +++ b/packages/coding-agent/install-lock/package.json @@ -1,10 +1,10 @@ { "name": "@earendil-works/pi-coding-agent-install", - "version": "0.84.3", + "version": "0.84.4", "private": true, "description": "Lockfile root used by the Pi installer and updater.", "dependencies": { - "@earendil-works/pi-coding-agent": "0.84.3" + "@earendil-works/pi-coding-agent": "0.84.4" }, "overrides": { "protobufjs": "7.6.5", diff --git a/packages/coding-agent/npm-shrinkwrap.json b/packages/coding-agent/npm-shrinkwrap.json index f65c23df9..cb2f59077 100644 --- a/packages/coding-agent/npm-shrinkwrap.json +++ b/packages/coding-agent/npm-shrinkwrap.json @@ -1,19 +1,19 @@ { "name": "@earendil-works/pi-coding-agent", - "version": "0.84.3", + "version": "0.84.4", "lockfileVersion": 3, "requires": true, "packages": { "": { "name": "@earendil-works/pi-coding-agent", - "version": "0.84.3", + "version": "0.84.4", "license": "MIT", "dependencies": { - "@earendil-works/pi-agent-core": "^0.84.3", - "@earendil-works/pi-ai": "^0.84.3", - "@earendil-works/pi-client": "^0.84.3", - "@earendil-works/pi-protocol": "^0.84.3", - "@earendil-works/pi-tui": "^0.84.3", + "@earendil-works/pi-agent-core": "^0.84.4", + "@earendil-works/pi-ai": "^0.84.4", + "@earendil-works/pi-client": "^0.84.4", + "@earendil-works/pi-protocol": "^0.84.4", + "@earendil-works/pi-tui": "^0.84.4", "@silvia-odwyer/photon-node": "0.3.4", "chalk": "5.6.2", "cross-spawn": "7.0.6", @@ -476,12 +476,12 @@ } }, "node_modules/@earendil-works/pi-agent-core": { - "version": "0.84.3", - "resolved": "https://registry.npmjs.org/@earendil-works/pi-agent-core/-/pi-agent-core-0.84.3.tgz", + "version": "0.84.4", + "resolved": "https://registry.npmjs.org/@earendil-works/pi-agent-core/-/pi-agent-core-0.84.4.tgz", "license": "MIT", "dependencies": { - "@earendil-works/pi-ai": "^0.84.3", - "@earendil-works/pi-telemetry": "^0.84.3", + "@earendil-works/pi-ai": "^0.84.4", + "@earendil-works/pi-telemetry": "^0.84.4", "diff": "8.0.4", "ignore": "7.0.5", "typebox": "1.3.7", @@ -492,13 +492,13 @@ } }, "node_modules/@earendil-works/pi-ai": { - "version": "0.84.3", - "resolved": "https://registry.npmjs.org/@earendil-works/pi-ai/-/pi-ai-0.84.3.tgz", + "version": "0.84.4", + "resolved": "https://registry.npmjs.org/@earendil-works/pi-ai/-/pi-ai-0.84.4.tgz", "license": "MIT", "dependencies": { "@anthropic-ai/sdk": "0.91.1", "@aws-sdk/client-bedrock-runtime": "3.1048.0", - "@earendil-works/pi-telemetry": "^0.84.3", + "@earendil-works/pi-telemetry": "^0.84.4", "@google/genai": "1.52.0", "@smithy/node-http-handler": "4.7.3", "http-proxy-agent": "7.0.2", @@ -515,19 +515,19 @@ } }, "node_modules/@earendil-works/pi-client": { - "version": "0.84.3", - "resolved": "https://registry.npmjs.org/@earendil-works/pi-client/-/pi-client-0.84.3.tgz", + "version": "0.84.4", + "resolved": "https://registry.npmjs.org/@earendil-works/pi-client/-/pi-client-0.84.4.tgz", "license": "MIT", "dependencies": { - "@earendil-works/pi-protocol": "^0.84.3" + "@earendil-works/pi-protocol": "^0.84.4" }, "engines": { "node": ">=22.19.0" } }, "node_modules/@earendil-works/pi-protocol": { - "version": "0.84.3", - "resolved": "https://registry.npmjs.org/@earendil-works/pi-protocol/-/pi-protocol-0.84.3.tgz", + "version": "0.84.4", + "resolved": "https://registry.npmjs.org/@earendil-works/pi-protocol/-/pi-protocol-0.84.4.tgz", "license": "MIT", "dependencies": { "typebox": "1.3.7" @@ -537,16 +537,16 @@ } }, "node_modules/@earendil-works/pi-telemetry": { - "version": "0.84.3", - "resolved": "https://registry.npmjs.org/@earendil-works/pi-telemetry/-/pi-telemetry-0.84.3.tgz", + "version": "0.84.4", + "resolved": "https://registry.npmjs.org/@earendil-works/pi-telemetry/-/pi-telemetry-0.84.4.tgz", "license": "MIT", "engines": { "node": ">=22.19.0" } }, "node_modules/@earendil-works/pi-tui": { - "version": "0.84.3", - "resolved": "https://registry.npmjs.org/@earendil-works/pi-tui/-/pi-tui-0.84.3.tgz", + "version": "0.84.4", + "resolved": "https://registry.npmjs.org/@earendil-works/pi-tui/-/pi-tui-0.84.4.tgz", "license": "MIT", "dependencies": { "get-east-asian-width": "1.6.0", diff --git a/packages/coding-agent/package.json b/packages/coding-agent/package.json index 02f45a37b..8aa47351d 100644 --- a/packages/coding-agent/package.json +++ b/packages/coding-agent/package.json @@ -1,6 +1,6 @@ { "name": "@earendil-works/pi-coding-agent", - "version": "0.84.3", + "version": "0.84.4", "description": "Coding agent CLI with read, bash, edit, write tools and session management", "type": "module", "piConfig": { @@ -44,11 +44,11 @@ "prepublishOnly": "npm run clean && npm run build && npm run shrinkwrap" }, "dependencies": { - "@earendil-works/pi-agent-core": "^0.84.3", - "@earendil-works/pi-ai": "^0.84.3", - "@earendil-works/pi-client": "^0.84.3", - "@earendil-works/pi-protocol": "^0.84.3", - "@earendil-works/pi-tui": "^0.84.3", + "@earendil-works/pi-agent-core": "^0.84.4", + "@earendil-works/pi-ai": "^0.84.4", + "@earendil-works/pi-client": "^0.84.4", + "@earendil-works/pi-protocol": "^0.84.4", + "@earendil-works/pi-tui": "^0.84.4", "@silvia-odwyer/photon-node": "0.3.4", "chalk": "5.6.2", "cross-spawn": "7.0.6", diff --git a/packages/coding-agent/src/core/agent-session.ts b/packages/coding-agent/src/core/agent-session.ts index ea69b84ef..27b5ac13f 100644 --- a/packages/coding-agent/src/core/agent-session.ts +++ b/packages/coding-agent/src/core/agent-session.ts @@ -17,6 +17,7 @@ import { readFileSync } from "node:fs"; import { basename, dirname } from "node:path"; import type { Agent, + AgentContext, AgentEvent, AgentMessage, AgentState, @@ -539,6 +540,25 @@ export class AgentSession { }; } + private async _compactBeforeNextAssistantResponse(context: AgentContext): Promise { + const model = this.model; + const settings = this.settingsManager.getCompactionSettings(); + + if ( + !model || + model.contextWindow <= 0 || + !shouldCompact(estimateContextTokens(context.messages).tokens, model.contextWindow, settings) + ) { + return context; + } + + await this._runAutoCompaction("threshold", false); + return { + ...context, + messages: this.agent.state.messages.slice(), + }; + } + private _installAgentNextTurnRefresh(): void { const previousPrepareNextTurnWithContext = this.agent.prepareNextTurnWithContext ?? @@ -546,13 +566,14 @@ export class AgentSession { ? async (_turn: PrepareNextTurnContext, signal?: AbortSignal) => await this.agent.prepareNextTurn?.(signal) : undefined); this.agent.prepareNextTurnWithContext = async (turn, signal) => { - const previousSnapshot = await previousPrepareNextTurnWithContext?.(turn, signal); - const previousContext = previousSnapshot?.context ?? turn.context; + const context = await this._compactBeforeNextAssistantResponse(turn.context); + const previousSnapshot = await previousPrepareNextTurnWithContext?.({ ...turn, context }, signal); + const nextContext = previousSnapshot?.context ?? context; return { ...previousSnapshot, context: { - ...previousContext, + ...nextContext, systemPrompt: this._systemPromptOverride ?? this._baseSystemPrompt, tools: this.agent.state.tools.slice(), }, diff --git a/packages/coding-agent/src/modes/interactive/interactive-mode.ts b/packages/coding-agent/src/modes/interactive/interactive-mode.ts index d5aa6ef60..d5a0a0c10 100644 --- a/packages/coding-agent/src/modes/interactive/interactive-mode.ts +++ b/packages/coding-agent/src/modes/interactive/interactive-mode.ts @@ -2095,6 +2095,16 @@ export class InteractiveMode { } } + private showWorkingStatusIndicator(): void { + this.showStatusIndicator( + new WorkingStatusIndicator( + this.ui, + this.workingMessage ?? this.defaultWorkingMessage, + this.workingIndicatorOptions, + ), + ); + } + private setWorkingVisible(visible: boolean): void { this.workingVisible = visible; if (!visible) { @@ -2103,13 +2113,7 @@ export class InteractiveMode { return; } if (this.session.isStreaming && this.activeStatusIndicator?.kind !== "working") { - this.showStatusIndicator( - new WorkingStatusIndicator( - this.ui, - this.workingMessage ?? this.defaultWorkingMessage, - this.workingIndicatorOptions, - ), - ); + this.showWorkingStatusIndicator(); } this.ui.requestRender(); } @@ -3118,23 +3122,22 @@ export class InteractiveMode { switch (event.type) { case "agent_start": this.pendingTools.clear(); - if (this.settingsManager.getShowTerminalProgress()) { - this.ui.terminal.setProgress(true); - } // Restore main escape handler if retry handler is still active // (retry success event fires later, but we need main handler now) if (this.retryEscapeHandler) { this.defaultEditor.onEscape = this.retryEscapeHandler; this.retryEscapeHandler = undefined; } + break; + + case "turn_start": + if (this.settingsManager.getShowTerminalProgress()) { + this.ui.terminal.setProgress(true); + } if (this.workingVisible) { - this.showStatusIndicator( - new WorkingStatusIndicator( - this.ui, - this.workingMessage ?? this.defaultWorkingMessage, - this.workingIndicatorOptions, - ), - ); + if (this.activeStatusIndicator?.kind !== "working") { + this.showWorkingStatusIndicator(); + } } else { this.clearStatusIndicator(); } diff --git a/packages/coding-agent/test/interactive-mode-compaction.test.ts b/packages/coding-agent/test/interactive-mode-compaction.test.ts index ece2d43ca..378be9303 100644 --- a/packages/coding-agent/test/interactive-mode-compaction.test.ts +++ b/packages/coding-agent/test/interactive-mode-compaction.test.ts @@ -197,6 +197,37 @@ describe("InteractiveMode compaction events", () => { expect(fakeThis.flushCompactionQueue).toHaveBeenCalledWith({ willRetry: false }); }); + test("updates the working state when the same agent run resumes after compaction", async () => { + const fakeThis = { + isInitialized: true, + footer: { invalidate: vi.fn() }, + activeStatusIndicator: undefined, + workingVisible: true, + showWorkingStatusIndicator: vi.fn(), + clearStatusIndicator: vi.fn(), + settingsManager: { getShowTerminalProgress: () => true }, + ui: { requestRender: vi.fn(), terminal: { setProgress: vi.fn() } }, + }; + const handleEvent = Reflect.get(InteractiveMode.prototype, "handleEvent") as ( + this: typeof fakeThis, + event: { type: "turn_start" }, + ) => Promise; + + await handleEvent.call(fakeThis, { type: "turn_start" }); + + expect(fakeThis.ui.terminal.setProgress).toHaveBeenCalledWith(true); + expect(fakeThis.showWorkingStatusIndicator).toHaveBeenCalledTimes(1); + expect(fakeThis.clearStatusIndicator).not.toHaveBeenCalled(); + expect(fakeThis.ui.requestRender).toHaveBeenCalledTimes(1); + + fakeThis.workingVisible = false; + await handleEvent.call(fakeThis, { type: "turn_start" }); + + expect(fakeThis.showWorkingStatusIndicator).toHaveBeenCalledTimes(1); + expect(fakeThis.clearStatusIndicator).toHaveBeenCalledTimes(1); + expect(fakeThis.ui.requestRender).toHaveBeenCalledTimes(2); + }); + test("preserves steering behavior when flushing into an active agent run", async () => { const fakeThis = { compactionQueuedMessages: [{ text: "change direction", mode: "steer" as const }], diff --git a/packages/coding-agent/test/suite/README.md b/packages/coding-agent/test/suite/README.md index cdd25a97f..b6f758dc1 100644 --- a/packages/coding-agent/test/suite/README.md +++ b/packages/coding-agent/test/suite/README.md @@ -8,9 +8,3 @@ Rules: - Do not use real provider APIs, real API keys, network calls, or paid tokens - Keep these tests CI-safe and deterministic - Do not use or extend the legacy `test/test-harness.ts` path unless a missing capability forces it - -Organization: -- Put broad lifecycle and characterization tests directly under `test/suite/` -- Put issue-specific regression tests under `test/suite/regressions/` -- Name regression tests as `-.test.ts` -- Example: `test/suite/regressions/2023-queued-slash-command-followup.test.ts` diff --git a/packages/coding-agent/test/suite/agent-session-compaction.test.ts b/packages/coding-agent/test/suite/agent-session-compaction.test.ts index 7d81358c5..64da8b4ab 100644 --- a/packages/coding-agent/test/suite/agent-session-compaction.test.ts +++ b/packages/coding-agent/test/suite/agent-session-compaction.test.ts @@ -1,12 +1,14 @@ -import type { AgentMessage } from "@earendil-works/pi-agent-core"; +import type { AgentMessage, AgentTool } from "@earendil-works/pi-agent-core"; import { type AssistantMessage, type Context, createAssistantMessageEventStream, fauxAssistantMessage, + fauxToolCall, type Model, type SimpleStreamOptions, } from "@earendil-works/pi-ai"; +import { Type } from "typebox"; import { afterEach, describe, expect, it, vi } from "vitest"; import { estimateTokens } from "../../src/core/compaction/index.ts"; import { createHarness, getUserTexts, type Harness } from "./harness.ts"; @@ -392,6 +394,175 @@ describe("AgentSession compaction characterization", () => { expect(harness.session.getLastAssistantText()).toBe("completed response"); }); + it("compacts after a tool result before the next assistant request in the same run", async () => { + const toolResult = `large-tool-result:${"x".repeat(6800)}`; + const largeTool: AgentTool = { + name: "large_result", + label: "Large result", + description: "Returns enough content to cross the compaction threshold", + parameters: Type.Object({}), + execute: async () => ({ content: [{ type: "text", text: toolResult }], details: {} }), + }; + const order: string[] = []; + const harness = await createHarness({ + models: [{ id: "faux-1", contextWindow: 2600, maxTokens: 100 }], + settings: { compaction: { enabled: true, reserveTokens: 400, keepRecentTokens: 1750 } }, + tools: [largeTool], + extensionFactories: [ + (pi) => { + pi.on("session_before_compact", (event) => { + order.push("compaction"); + return { + compaction: { + summary: "compacted history", + firstKeptEntryId: event.preparation.firstKeptEntryId, + tokensBefore: event.preparation.tokensBefore, + details: {}, + }, + }; + }); + }, + ], + }); + harnesses.push(harness); + let resumedRequest = ""; + harness.setResponses([ + fauxAssistantMessage(`old-history:${"a".repeat(800)}`), + fauxAssistantMessage(`recent-history:${"b".repeat(800)}`), + fauxAssistantMessage(fauxToolCall("large_result", {}), { stopReason: "toolUse" }), + (context) => { + order.push("provider"); + resumedRequest = JSON.stringify(context.messages); + return fauxAssistantMessage("finished after compaction"); + }, + ]); + + await harness.session.prompt("seed old history"); + await harness.session.prompt("seed recent history"); + const agentStartsBefore = harness.eventsOfType("agent_start").length; + await harness.session.prompt("run the large tool"); + + expect(order).toEqual(["compaction", "provider"]); + expect(harness.eventsOfType("agent_start")).toHaveLength(agentStartsBefore + 1); + expect(harness.eventsOfType("compaction_start").at(-1)).toEqual({ + type: "compaction_start", + reason: "threshold", + }); + expect(resumedRequest).toContain("compacted history"); + expect(resumedRequest).toContain("large-tool-result"); + expect(harness.session.getLastAssistantText()).toBe("finished after compaction"); + }); + + it("includes steering queued during compaction in the resumed assistant request", async () => { + const largeTool: AgentTool = { + name: "large_result", + label: "Large result", + description: "Returns enough content to cross the compaction threshold", + parameters: Type.Object({}), + execute: async () => ({ + content: [{ type: "text", text: `large-tool-result:${"x".repeat(6800)}` }], + details: {}, + }), + }; + let markCompactionStarted = () => {}; + const compactionStarted = new Promise((resolve) => { + markCompactionStarted = resolve; + }); + let releaseCompaction = () => {}; + const compactionReleased = new Promise((resolve) => { + releaseCompaction = resolve; + }); + const harness = await createHarness({ + models: [{ id: "faux-1", contextWindow: 2600, maxTokens: 100 }], + settings: { compaction: { enabled: true, reserveTokens: 400, keepRecentTokens: 1750 } }, + tools: [largeTool], + extensionFactories: [ + (pi) => { + pi.on("session_before_compact", async (event) => { + markCompactionStarted(); + await compactionReleased; + return { + compaction: { + summary: "compacted history", + firstKeptEntryId: event.preparation.firstKeptEntryId, + tokensBefore: event.preparation.tokensBefore, + details: {}, + }, + }; + }); + }, + ], + }); + harnesses.push(harness); + let resumedRequest = ""; + harness.setResponses([ + fauxAssistantMessage(`old-history:${"a".repeat(800)}`), + fauxAssistantMessage(`recent-history:${"b".repeat(800)}`), + fauxAssistantMessage(fauxToolCall("large_result", {}), { stopReason: "toolUse" }), + (context) => { + resumedRequest = JSON.stringify(context.messages); + return fauxAssistantMessage("finished after compaction"); + }, + fauxAssistantMessage("finished after delayed steering"), + ]); + + await harness.session.prompt("seed old history"); + await harness.session.prompt("seed recent history"); + const promptPromise = harness.session.prompt("run the large tool"); + await compactionStarted; + await harness.session.steer("change direction"); + releaseCompaction(); + await promptPromise; + + expect(resumedRequest).toContain("change direction"); + expect(harness.faux.state.callCount).toBe(4); + }); + + it("does not compact after a terminating tool result", async () => { + const terminatingTool: AgentTool = { + name: "terminate_with_large_result", + label: "Terminate with large result", + description: "Returns enough content to cross the compaction threshold, then terminates", + parameters: Type.Object({}), + execute: async () => ({ + content: [{ type: "text", text: `large-tool-result:${"x".repeat(6800)}` }], + details: {}, + terminate: true, + }), + }; + const harness = await createHarness({ + models: [{ id: "faux-1", contextWindow: 2600, maxTokens: 100 }], + settings: { compaction: { enabled: true, reserveTokens: 400, keepRecentTokens: 1750 } }, + tools: [terminatingTool], + extensionFactories: [ + (pi) => { + pi.on("session_before_compact", (event) => ({ + compaction: { + summary: "unexpected compaction", + firstKeptEntryId: event.preparation.firstKeptEntryId, + tokensBefore: event.preparation.tokensBefore, + details: {}, + }, + })); + }, + ], + }); + harnesses.push(harness); + harness.setResponses([ + fauxAssistantMessage(`old-history:${"a".repeat(800)}`), + fauxAssistantMessage(`recent-history:${"b".repeat(800)}`), + fauxAssistantMessage(fauxToolCall("terminate_with_large_result", {}), { stopReason: "toolUse" }), + ]); + + await harness.session.prompt("seed old history"); + await harness.session.prompt("seed recent history"); + await harness.session.prompt("run the terminating tool"); + + expect(harness.eventsOfType("compaction_start")).toEqual([]); + expect(harness.sessionManager.getEntries().filter((entry) => entry.type === "compaction")).toEqual([]); + expect(harness.getPendingResponseCount()).toBe(0); + }); + it("does not compact when a length stop reaches the desired output limit", async () => { const harness = await createHarness({ models: [{ id: "faux-1", contextWindow: 1_000_000, maxTokens: 100 }], diff --git a/packages/coding-agent/test/suite/regressions/7253-manual-compact-during-response.test.ts b/packages/coding-agent/test/suite/regressions/7253-manual-compact-during-response.test.ts index 4c0169ec2..937179bf1 100644 --- a/packages/coding-agent/test/suite/regressions/7253-manual-compact-during-response.test.ts +++ b/packages/coding-agent/test/suite/regressions/7253-manual-compact-during-response.test.ts @@ -23,7 +23,7 @@ describe("issue #7253: manual compaction during an active response", () => { } }); - it("runs only the requested manual compaction when the previous turn crossed the threshold", async () => { + it("persists the aborted response before running the requested manual compaction", async () => { let markSecondResponseStarted = () => {}; const secondResponseStarted = new Promise((resolve) => { markSecondResponseStarted = resolve; @@ -34,8 +34,8 @@ describe("issue #7253: manual compaction during an active response", () => { }); const harness = await createHarness({ - models: [{ id: "faux-1", contextWindow: 1000, maxTokens: 100 }], - settings: { compaction: { enabled: true, reserveTokens: 999, keepRecentTokens: 2 } }, + models: [{ id: "faux-1", contextWindow: 1000, maxTokens: 1000 }], + settings: { compaction: { enabled: true, reserveTokens: 200, keepRecentTokens: 2 } }, tools: [createNoopTool()], extensionFactories: [ (pi) => { @@ -56,7 +56,7 @@ describe("issue #7253: manual compaction during an active response", () => { async () => { markSecondResponseStarted(); await secondResponseReleased; - return fauxAssistantMessage("second response"); + return fauxAssistantMessage(`second response:${"x".repeat(4000)}`); }, ]); @@ -70,6 +70,14 @@ describe("issue #7253: manual compaction during an active response", () => { expect(harness.eventsOfType("compaction_start").map((event) => event.reason)).toEqual(["manual"]); expect(harness.eventsOfType("compaction_end").map((event) => event.reason)).toEqual(["manual"]); - expect(harness.sessionManager.getEntries().filter((entry) => entry.type === "compaction")).toHaveLength(1); + const entries = harness.sessionManager.getEntries(); + const abortedResponseIndex = entries.findIndex( + (entry) => + entry.type === "message" && entry.message.role === "assistant" && entry.message.stopReason === "aborted", + ); + const compactionIndex = entries.findIndex((entry) => entry.type === "compaction"); + expect(abortedResponseIndex).toBeGreaterThan(-1); + expect(compactionIndex).toBeGreaterThan(abortedResponseIndex); + expect(entries.filter((entry) => entry.type === "compaction")).toHaveLength(1); }); }); diff --git a/packages/evals/package.json b/packages/evals/package.json index b02e89a8e..0cdd049b7 100644 --- a/packages/evals/package.json +++ b/packages/evals/package.json @@ -1,6 +1,6 @@ { "name": "@earendil-works/pi-evals", - "version": "0.84.3", + "version": "0.84.4", "private": true, "type": "module", "scripts": { @@ -9,8 +9,8 @@ "test": "vitest run --config vitest.test.config.ts" }, "devDependencies": { - "@earendil-works/pi-ai": "^0.84.3", - "@earendil-works/pi-coding-agent": "^0.84.3", + "@earendil-works/pi-ai": "^0.84.4", + "@earendil-works/pi-coding-agent": "^0.84.4", "@types/node": "22.19.19", "shx": "0.4.0", "typescript": "5.9.3", diff --git a/packages/protocol/CHANGELOG.md b/packages/protocol/CHANGELOG.md index 2cf175791..2417925ab 100644 --- a/packages/protocol/CHANGELOG.md +++ b/packages/protocol/CHANGELOG.md @@ -2,9 +2,7 @@ ## [Unreleased] -### Removed - -- Removed the unused manual-action run-result and lane-snapshot wire fields. `PROTOCOL_VERSION` remains unchanged because no shipped producer emitted them. +## [0.84.4] - 2026-08-28 ## [0.84.3] - 2026-08-24 diff --git a/packages/protocol/package.json b/packages/protocol/package.json index 5ac7bf7e7..df057ebfa 100644 --- a/packages/protocol/package.json +++ b/packages/protocol/package.json @@ -1,6 +1,6 @@ { "name": "@earendil-works/pi-protocol", - "version": "0.84.3", + "version": "0.84.4", "description": "Transport-neutral CBOR protocol for remote pi sessions", "type": "module", "main": "./dist/index.js", diff --git a/packages/server/CHANGELOG.md b/packages/server/CHANGELOG.md index ab89c9590..96b942e50 100644 --- a/packages/server/CHANGELOG.md +++ b/packages/server/CHANGELOG.md @@ -2,6 +2,8 @@ ## [Unreleased] +## [0.84.4] - 2026-08-28 + ## [0.84.3] - 2026-08-24 ## [0.84.2] - 2026-08-14 diff --git a/packages/server/package.json b/packages/server/package.json index 54c310288..016b1507c 100644 --- a/packages/server/package.json +++ b/packages/server/package.json @@ -1,6 +1,6 @@ { "name": "@earendil-works/pi-server", - "version": "0.84.3", + "version": "0.84.4", "description": "experimental server package for pi", "type": "module", "main": "./dist/index.js", @@ -47,8 +47,8 @@ "node": ">=22.19.0" }, "dependencies": { - "@earendil-works/pi-agent-core": "^0.84.3", - "@earendil-works/pi-protocol": "^0.84.3" + "@earendil-works/pi-agent-core": "^0.84.4", + "@earendil-works/pi-protocol": "^0.84.4" }, "devDependencies": { "shx": "0.4.0", diff --git a/packages/session-backends/sqlite-node/CHANGELOG.md b/packages/session-backends/sqlite-node/CHANGELOG.md index c2150f7a0..984923a5e 100644 --- a/packages/session-backends/sqlite-node/CHANGELOG.md +++ b/packages/session-backends/sqlite-node/CHANGELOG.md @@ -2,9 +2,7 @@ ## [Unreleased] -### Changed - -- Returned exact post-commit session totals from SQLite storage commits. +## [0.84.4] - 2026-08-28 ## [0.84.3] - 2026-08-24 diff --git a/packages/session-backends/sqlite-node/package.json b/packages/session-backends/sqlite-node/package.json index 004400ce2..5c59a310b 100644 --- a/packages/session-backends/sqlite-node/package.json +++ b/packages/session-backends/sqlite-node/package.json @@ -1,6 +1,6 @@ { "name": "@earendil-works/pi-session-backend-sqlite-node", - "version": "0.84.3", + "version": "0.84.4", "description": "Node sqlite session backend for @earendil-works/pi-agent-core sessions", "type": "module", "main": "./dist/index.js", @@ -35,8 +35,8 @@ "node": ">=22.19.0" }, "dependencies": { - "@earendil-works/pi-ai": "^0.84.3", - "@earendil-works/pi-agent-core": "^0.84.3" + "@earendil-works/pi-ai": "^0.84.4", + "@earendil-works/pi-agent-core": "^0.84.4" }, "devDependencies": { "@vitest/coverage-v8": "4.1.9", diff --git a/packages/telemetry/CHANGELOG.md b/packages/telemetry/CHANGELOG.md index e11e6186a..f80c752ea 100644 --- a/packages/telemetry/CHANGELOG.md +++ b/packages/telemetry/CHANGELOG.md @@ -2,6 +2,8 @@ ## [Unreleased] +## [0.84.4] - 2026-08-28 + ## [0.84.3] - 2026-08-24 ## [0.84.2] - 2026-08-14 diff --git a/packages/telemetry/package.json b/packages/telemetry/package.json index 49ffa4396..bbdee4ea3 100644 --- a/packages/telemetry/package.json +++ b/packages/telemetry/package.json @@ -1,6 +1,6 @@ { "name": "@earendil-works/pi-telemetry", - "version": "0.84.3", + "version": "0.84.4", "description": "Vendor-neutral telemetry contracts and typed schema utilities for pi", "type": "module", "main": "./dist/index.js", diff --git a/packages/tui/CHANGELOG.md b/packages/tui/CHANGELOG.md index 15ae98abc..6a6f69f3e 100644 --- a/packages/tui/CHANGELOG.md +++ b/packages/tui/CHANGELOG.md @@ -2,6 +2,8 @@ ## [Unreleased] +## [0.84.4] - 2026-08-28 + ### Added - Added environment and programmatic overrides for OSC 8 hyperlinks, inline image protocols, and truecolor terminal capabilities ([#8665](https://github.com/earendil-works/pi/issues/8665)). diff --git a/packages/tui/package.json b/packages/tui/package.json index 7e015439b..5ff745224 100644 --- a/packages/tui/package.json +++ b/packages/tui/package.json @@ -1,6 +1,6 @@ { "name": "@earendil-works/pi-tui", - "version": "0.84.3", + "version": "0.84.4", "description": "Terminal User Interface library with differential rendering for efficient text-based applications", "type": "module", "main": "dist/index.js",