diff --git a/registry/agent/pi/src/adapter.ts b/registry/agent/pi/src/adapter.ts index 6fb8a3bf6d..65615cb54c 100644 --- a/registry/agent/pi/src/adapter.ts +++ b/registry/agent/pi/src/adapter.ts @@ -103,8 +103,21 @@ Object.defineProperty(process, "stdin", { type SessionManagerLike = { inMemory(cwd?: string): unknown; + continueRecent(cwd: string, sessionDir: string): unknown; }; +/** Select persistent Pi sessions only when the adapter is explicitly configured. */ +export function resolveSessionManager( + SessionManager: SessionManagerLike, + cwd: string, + env: Record = process.env, +): unknown { + const sessionDir = env.PI_SESSION_DIR?.trim(); + return sessionDir + ? SessionManager.continueRecent(cwd, sessionDir) + : SessionManager.inMemory(cwd); +} + type ModelLike = { id: string; provider: string; @@ -873,7 +886,7 @@ export class PiSdkAgent implements Agent { const { session } = await __trace.span("createAgentSession", () => createAgentSession({ cwd: params.cwd, - sessionManager: SessionManager.inMemory(params.cwd), + sessionManager: resolveSessionManager(SessionManager, params.cwd), resourceLoader, tools: this.wrapTools( createCodingTools(params.cwd, { diff --git a/registry/agent/pi/tests/adapter.test.mjs b/registry/agent/pi/tests/adapter.test.mjs index e52c4d9185..4aed559627 100644 --- a/registry/agent/pi/tests/adapter.test.mjs +++ b/registry/agent/pi/tests/adapter.test.mjs @@ -6,7 +6,9 @@ import { resolve as resolvePath } from "node:path"; // testing; newSession needs the real Pi SDK, so these drive the translation / // lifecycle logic directly with a mock ACP connection + a fake session. const packageDir = resolvePath(import.meta.dirname, ".."); -const { PiSdkAgent } = await import(resolvePath(packageDir, "dist", "adapter.js")); +const { PiSdkAgent, resolveSessionManager } = await import( + resolvePath(packageDir, "dist", "adapter.js") +); function makeConn(overrides = {}) { return { @@ -26,6 +28,53 @@ function fakeSession(overrides = {}) { }; } +test("PI_SESSION_DIR persists and resumes the most recent Pi session", () => { + const calls = []; + const manager = { + inMemory(cwd) { + calls.push(["inMemory", cwd]); + return "memory"; + }, + continueRecent(cwd, sessionDir) { + calls.push(["continueRecent", cwd, sessionDir]); + return "persistent"; + }, + }; + + assert.equal( + resolveSessionManager(manager, "/workspace", { + PI_SESSION_DIR: " /data/pi-sessions ", + }), + "persistent", + ); + assert.deepEqual(calls, [ + ["continueRecent", "/workspace", "/data/pi-sessions"], + ]); +}); + +test("Pi sessions remain in memory when PI_SESSION_DIR is unset or blank", () => { + const calls = []; + const manager = { + inMemory(cwd) { + calls.push(["inMemory", cwd]); + return "memory"; + }, + continueRecent() { + throw new Error("continueRecent should not be called"); + }, + }; + + assert.equal(resolveSessionManager(manager, "/workspace", {}), "memory"); + assert.equal( + resolveSessionManager(manager, "/workspace", { PI_SESSION_DIR: " " }), + "memory", + ); + assert.deepEqual(calls, [ + ["inMemory", "/workspace"], + ["inMemory", "/workspace"], + ]); +}); + // ── Fix #3: editSnapshots is cleared each turn (no unbounded leak) ── test("pi #3: prompt() clears leaked edit snapshots from a prior aborted turn", async () => { const agent = new PiSdkAgent(makeConn()); diff --git a/website/public/docs/docs/agents/pi.md b/website/public/docs/docs/agents/pi.md index 793955ff5b..6b6c973e82 100644 --- a/website/public/docs/docs/agents/pi.md +++ b/website/public/docs/docs/agents/pi.md @@ -15,6 +15,13 @@ Set the relevant variable on the session's `env`, sourced from your server's env See [LLM Credentials](/docs/llm-credentials), and Pi's [providers docs](https://github.com/badlogic/pi-mono/blob/main/packages/coding-agent/docs/providers.md) for the full list. +## Persistent Pi sessions + +Pi keeps its own conversation state in memory by default. To preserve and resume +the most recent Pi session after the adapter restarts, set `PI_SESSION_DIR` in the +session's `env` to a directory on durable VM storage. Pi writes its session JSONL +files there; an unset or blank value retains the in-memory default. + ## Skills Pi discovers `SKILL.md` files from its skills directory. Write the skill into the VM before creating a session and Pi loads it automatically. @@ -40,4 +47,4 @@ See the [Pi extension documentation](https://github.com/badlogic/pi-mono/tree/ma ## Customizing the agent -Pi is a built-in agent, but it's just a software package under the hood. To ship your own ACP adapter, swap the underlying agent SDK, or register a tweaked Pi build as a new agent, see [Custom Agents](/docs/agents/custom). \ No newline at end of file +Pi is a built-in agent, but it's just a software package under the hood. To ship your own ACP adapter, swap the underlying agent SDK, or register a tweaked Pi build as a new agent, see [Custom Agents](/docs/agents/custom).