From 5f39c44ec0b06133582d0676e913ef0e50584c2b Mon Sep 17 00:00:00 2001 From: Nathan Flurry Date: Tue, 7 Jul 2026 13:55:45 -0700 Subject: [PATCH] fix(pi): keep appended prompts out of path resolution --- packages/core/tests/pi-extensions.test.ts | 15 +++--- registry/agent/pi/src/adapter.ts | 60 +++++++++++++++++------ registry/agent/pi/tests/adapter.test.mjs | 29 ++++++++++- 3 files changed, 81 insertions(+), 23 deletions(-) diff --git a/packages/core/tests/pi-extensions.test.ts b/packages/core/tests/pi-extensions.test.ts index c67eab5c1..c2d58947c 100644 --- a/packages/core/tests/pi-extensions.test.ts +++ b/packages/core/tests/pi-extensions.test.ts @@ -1,3 +1,4 @@ +import pi from "@agentos-software/pi"; import { describe, expect, test } from "vitest"; import { AgentOs } from "../src/agent-os.js"; import { @@ -6,10 +7,9 @@ import { stopLlmock, } from "./helpers/llmock-helper.js"; -// Pi ships PRE-PACKED as an `/opt/agentos` package and is projected by default -// (run `pnpm pack:agents` first), so this test needs no `software: [pi]` mount -// and no host node_modules access — `createSession("pi")` resolves the packaged -// adapter via `/opt/agentos/bin/pi-sdk-acp`. +// Pi ships as a packed `/opt/agentos` package. Pass the built package explicitly +// so `createSession("pi")` resolves the packaged adapter via +// `/opt/agentos/bin/pi-sdk-acp`. const HOME_DIR = "/home/agentos"; const WORKSPACE_DIR = `${HOME_DIR}/workspace`; const PI_AGENT_DIR = `${HOME_DIR}/.pi/agent`; @@ -32,6 +32,7 @@ function requestIncludesExtensionMarker(req: unknown): boolean { async function createPiVm(mockUrl: string): Promise { return AgentOs.create({ loopbackExemptPorts: [Number(new URL(mockUrl).port)], + software: [pi], }); } @@ -116,9 +117,9 @@ describe("Pi extensions quickstart truth test", () => { expect(response.error).toBeUndefined(); expect(text).toContain(EXPECTED_REPLY); expect(mock.getRequests().length).toBeGreaterThanOrEqual(1); - expect( - mock.getRequests().some(requestIncludesExtensionMarker), - ).toBe(true); + expect(mock.getRequests().some(requestIncludesExtensionMarker)).toBe( + true, + ); } finally { if (sessionId) { vm.closeSession(sessionId); diff --git a/registry/agent/pi/src/adapter.ts b/registry/agent/pi/src/adapter.ts index 6fb8a3bf6..313dd50bc 100644 --- a/registry/agent/pi/src/adapter.ts +++ b/registry/agent/pi/src/adapter.ts @@ -217,6 +217,16 @@ type PiToolLike = { type ExtensionFactoryLike = (api: unknown) => unknown; +type DefaultResourceLoaderOptions = { + cwd?: string; + agentDir?: string; + settingsManager?: SettingsManagerInstanceLike; + appendSystemPrompt?: string; + appendSystemPromptOverride?: (appendSystemPrompt: string[]) => string[]; + extensionFactories?: ExtensionFactoryLike[]; + noExtensions?: boolean; +}; + type PiSessionLike = { readonly sessionId: string; readonly thinkingLevel: string; @@ -235,14 +245,9 @@ type PiSdkRuntime = { AuthStorage: { create(authPath?: string): unknown; }; - DefaultResourceLoader: new (options: { - cwd?: string; - agentDir?: string; - settingsManager?: SettingsManagerInstanceLike; - appendSystemPrompt?: string; - extensionFactories?: ExtensionFactoryLike[]; - noExtensions?: boolean; - }) => MinimalResourceLoaderLike; + DefaultResourceLoader: new ( + options: DefaultResourceLoaderOptions, + ) => MinimalResourceLoaderLike; DEFAULT_THINKING_LEVEL: string; ModelRegistry: new (authStorage: unknown, modelsPath?: string) => { find(provider: string, modelId: string): ModelLike | undefined; @@ -590,6 +595,30 @@ class MinimalResourceLoader implements MinimalResourceLoaderLike { extendResources(_paths: string[]): void {} } +export function buildDefaultResourceLoaderOptions(options: { + cwd: string; + agentDir: string; + extensionFactories: ExtensionFactoryLike[]; + appendSystemPrompt?: string; +}): DefaultResourceLoaderOptions { + const loaderOptions: DefaultResourceLoaderOptions = { + cwd: options.cwd, + agentDir: options.agentDir, + noExtensions: true, + extensionFactories: options.extensionFactories, + }; + + if (options.appendSystemPrompt) { + const prompt = options.appendSystemPrompt; + loaderOptions.appendSystemPromptOverride = (baseAppend) => [ + ...baseAppend, + prompt, + ]; + } + + return loaderOptions; +} + function findInstalledPackageRoot(packageName: string): string | null { const searchPaths = require.resolve.paths(packageName) ?? []; for (const basePath of searchPaths) { @@ -851,13 +880,14 @@ export class PiSdkAgent implements Agent { // extension runtime) is constructed when extensions are actually present. const resourceLoader: MinimalResourceLoaderLike = extensionFactories.length > 0 - ? new DefaultResourceLoader({ - cwd: params.cwd, - agentDir, - noExtensions: true, - extensionFactories, - ...(appendSystemPrompt ? { appendSystemPrompt } : {}), - }) + ? new DefaultResourceLoader( + buildDefaultResourceLoaderOptions({ + cwd: params.cwd, + agentDir, + extensionFactories, + appendSystemPrompt, + }), + ) : new MinimalResourceLoader({ ...(appendSystemPrompt ? { appendSystemPrompt } : {}), }); diff --git a/registry/agent/pi/tests/adapter.test.mjs b/registry/agent/pi/tests/adapter.test.mjs index e52c4d918..acaf246f1 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, buildDefaultResourceLoaderOptions } = await import( + resolvePath(packageDir, "dist", "adapter.js") +); function makeConn(overrides = {}) { return { @@ -26,6 +28,31 @@ function fakeSession(overrides = {}) { }; } +// ── Fix #1650: appended system prompt text must not be treated as a path ── +test("pi #1650: DefaultResourceLoader receives appended prompt via override, not path source", () => { + const appendSystemPrompt = "# agentOS\n\n".repeat(200); + const extensionFactory = () => {}; + const options = buildDefaultResourceLoaderOptions({ + cwd: "/home/agentos/workspace", + agentDir: "/home/agentos/.pi/agent", + extensionFactories: [extensionFactory], + appendSystemPrompt, + }); + + assert.equal( + Object.hasOwn(options, "appendSystemPrompt"), + false, + "long prompt text must not be passed to Pi's path-probing appendSystemPrompt option", + ); + assert.equal(typeof options.appendSystemPromptOverride, "function"); + assert.deepEqual(options.appendSystemPromptOverride(["from-file"]), [ + "from-file", + appendSystemPrompt, + ]); + assert.deepEqual(options.extensionFactories, [extensionFactory]); + assert.equal(options.noExtensions, true); +}); + // ── 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());