Skip to content
Draft
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
15 changes: 8 additions & 7 deletions packages/core/tests/pi-extensions.test.ts
Original file line number Diff line number Diff line change
@@ -1,3 +1,4 @@
import pi from "@agentos-software/pi";
import { describe, expect, test } from "vitest";
import { AgentOs } from "../src/agent-os.js";
import {
Expand All @@ -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`;
Expand All @@ -32,6 +32,7 @@ function requestIncludesExtensionMarker(req: unknown): boolean {
async function createPiVm(mockUrl: string): Promise<AgentOs> {
return AgentOs.create({
loopbackExemptPorts: [Number(new URL(mockUrl).port)],
software: [pi],
});
}

Expand Down Expand Up @@ -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);
Expand Down
60 changes: 45 additions & 15 deletions registry/agent/pi/src/adapter.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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;
Expand All @@ -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;
Expand Down Expand Up @@ -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) {
Expand Down Expand Up @@ -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 } : {}),
});
Expand Down
29 changes: 28 additions & 1 deletion registry/agent/pi/tests/adapter.test.mjs
Original file line number Diff line number Diff line change
Expand Up @@ -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 {
Expand All @@ -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());
Expand Down
Loading