fix(pi): preserve discovered project prompts
Append Paseo instructions from the integration extension because Pi's append-system-prompt flag suppresses automatic APPEND_SYSTEM.md discovery.
This commit is contained in:
+1
-1
@@ -24,7 +24,7 @@ Paseo tools are not implemented as MCP tools internally. They live in a shared t
|
|||||||
|
|
||||||
Pi is a process-backed provider. Paseo requires the user to have the `pi` binary installed and talks to it through `pi --mode rpc`; the server package does not embed Pi's SDK/runtime packages.
|
Pi is a process-backed provider. Paseo requires the user to have the `pi` binary installed and talks to it through `pi --mode rpc`; the server package does not embed Pi's SDK/runtime packages.
|
||||||
|
|
||||||
Paseo's per-agent and daemon-wide system prompts are passed to Pi with `--append-system-prompt`, so Pi keeps its default coding prompt while receiving Paseo's additional instructions.
|
Paseo's per-agent and daemon-wide system prompts are appended by its generated Pi integration extension. Paseo deliberately does not pass `--append-system-prompt`, because that flag replaces Pi's automatic `APPEND_SYSTEM.md` discovery instead of composing with it.
|
||||||
|
|
||||||
Pi model records expose input capabilities through `model.input`. Only send raw RPC `images` when the current model explicitly includes `"image"` in that list. Text-only Pi/OMP models reject image content and persist the rejected image in JSONL history, so image prompts for those models must be materialized to a local file and passed as a text path hint instead.
|
Pi model records expose input capabilities through `model.input`. Only send raw RPC `images` when the current model explicitly includes `"image"` in that list. Text-only Pi/OMP models reject image content and persist the rejected image in JSONL history, so image prompts for those models must be materialized to a local file and passed as a text path hint instead.
|
||||||
|
|
||||||
|
|||||||
@@ -13,6 +13,7 @@ import { tmpdir } from "node:os";
|
|||||||
import path from "node:path";
|
import path from "node:path";
|
||||||
import pino from "pino";
|
import pino from "pino";
|
||||||
import { setImmediate as waitForImmediate } from "node:timers/promises";
|
import { setImmediate as waitForImmediate } from "node:timers/promises";
|
||||||
|
import { pathToFileURL } from "node:url";
|
||||||
import { describe, expect, onTestFinished, test } from "vitest";
|
import { describe, expect, onTestFinished, test } from "vitest";
|
||||||
|
|
||||||
import type { AgentSession, AgentSessionConfig, AgentStreamEvent } from "../../agent-sdk-types.js";
|
import type { AgentSession, AgentSessionConfig, AgentStreamEvent } from "../../agent-sdk-types.js";
|
||||||
@@ -55,6 +56,26 @@ function readUtf8File(pathname: string): string {
|
|||||||
closeSync(fd);
|
closeSync(fd);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
async function applyPaseoExtensionSystemPrompt(
|
||||||
|
extensionPath: string,
|
||||||
|
systemPrompt: string,
|
||||||
|
): Promise<string | undefined> {
|
||||||
|
const listeners = new Map<string, (event: { systemPrompt: string }) => unknown>();
|
||||||
|
const extension = (await import(pathToFileURL(extensionPath).href)) as {
|
||||||
|
default: (piApi: {
|
||||||
|
on: (event: string, listener: (event: { systemPrompt: string }) => unknown) => void;
|
||||||
|
registerCommand: () => void;
|
||||||
|
}) => void;
|
||||||
|
};
|
||||||
|
extension.default({
|
||||||
|
on: (event, listener) => listeners.set(event, listener),
|
||||||
|
registerCommand: () => undefined,
|
||||||
|
});
|
||||||
|
const result = await listeners.get("before_agent_start")?.({ systemPrompt });
|
||||||
|
return (result as { systemPrompt?: string } | undefined)?.systemPrompt;
|
||||||
|
}
|
||||||
|
|
||||||
async function flushTurnScheduling(): Promise<void> {
|
async function flushTurnScheduling(): Promise<void> {
|
||||||
await waitForImmediate();
|
await waitForImmediate();
|
||||||
}
|
}
|
||||||
@@ -713,11 +734,11 @@ describe("PiRpcAgentSession", () => {
|
|||||||
]);
|
]);
|
||||||
});
|
});
|
||||||
|
|
||||||
test("creates Pi sessions with agent and daemon system prompts appended", async () => {
|
test("appends agent and daemon prompts after Pi's discovered system prompt", async () => {
|
||||||
const pi = new FakePi();
|
const pi = new FakePi();
|
||||||
const client = createClient(pi);
|
const client = createClient(pi);
|
||||||
|
|
||||||
await client.createSession(
|
const session = await client.createSession(
|
||||||
createConfig({
|
createConfig({
|
||||||
systemPrompt: "Agent prompt",
|
systemPrompt: "Agent prompt",
|
||||||
daemonAppendSystemPrompt: "Daemon prompt",
|
daemonAppendSystemPrompt: "Daemon prompt",
|
||||||
@@ -727,7 +748,6 @@ describe("PiRpcAgentSession", () => {
|
|||||||
const actualLaunch = pi.recordedLaunches[0]!;
|
const actualLaunch = pi.recordedLaunches[0]!;
|
||||||
expect(actualLaunch).toMatchObject({
|
expect(actualLaunch).toMatchObject({
|
||||||
cwd: "/tmp/paseo-pi-rpc-test",
|
cwd: "/tmp/paseo-pi-rpc-test",
|
||||||
systemPrompt: "Agent prompt\n\nDaemon prompt",
|
|
||||||
});
|
});
|
||||||
expect(actualLaunch.extensionPaths).toHaveLength(1);
|
expect(actualLaunch.extensionPaths).toHaveLength(1);
|
||||||
expect(actualLaunch.argv).toEqual([
|
expect(actualLaunch.argv).toEqual([
|
||||||
@@ -736,11 +756,15 @@ describe("PiRpcAgentSession", () => {
|
|||||||
"rpc",
|
"rpc",
|
||||||
"--thinking",
|
"--thinking",
|
||||||
"medium",
|
"medium",
|
||||||
"--append-system-prompt",
|
|
||||||
"Agent prompt\n\nDaemon prompt",
|
|
||||||
"--extension",
|
"--extension",
|
||||||
actualLaunch.extensionPaths[0],
|
actualLaunch.extensionPaths[0],
|
||||||
]);
|
]);
|
||||||
|
|
||||||
|
await expect(
|
||||||
|
applyPaseoExtensionSystemPrompt(actualLaunch.extensionPaths[0]!, "Pi project prompt"),
|
||||||
|
).resolves.toBe("Pi project prompt\n\nAgent prompt\n\nDaemon prompt");
|
||||||
|
|
||||||
|
await session.close();
|
||||||
});
|
});
|
||||||
|
|
||||||
test("resumes Pi sessions with daemon system prompts appended", async () => {
|
test("resumes Pi sessions with daemon system prompts appended", async () => {
|
||||||
@@ -769,7 +793,6 @@ describe("PiRpcAgentSession", () => {
|
|||||||
expect(actualLaunch).toMatchObject({
|
expect(actualLaunch).toMatchObject({
|
||||||
cwd: "/workspace/project",
|
cwd: "/workspace/project",
|
||||||
session: "/tmp/native-pi-session",
|
session: "/tmp/native-pi-session",
|
||||||
systemPrompt: "Agent prompt\n\nDaemon prompt",
|
|
||||||
});
|
});
|
||||||
expect(actualLaunch.extensionPaths).toHaveLength(1);
|
expect(actualLaunch.extensionPaths).toHaveLength(1);
|
||||||
expect(actualLaunch.argv).toEqual([
|
expect(actualLaunch.argv).toEqual([
|
||||||
@@ -782,11 +805,12 @@ describe("PiRpcAgentSession", () => {
|
|||||||
"high",
|
"high",
|
||||||
"--session",
|
"--session",
|
||||||
"/tmp/native-pi-session",
|
"/tmp/native-pi-session",
|
||||||
"--append-system-prompt",
|
|
||||||
"Agent prompt\n\nDaemon prompt",
|
|
||||||
"--extension",
|
"--extension",
|
||||||
actualLaunch.extensionPaths[0],
|
actualLaunch.extensionPaths[0],
|
||||||
]);
|
]);
|
||||||
|
await expect(
|
||||||
|
applyPaseoExtensionSystemPrompt(actualLaunch.extensionPaths[0]!, "Pi project prompt"),
|
||||||
|
).resolves.toBe("Pi project prompt\n\nAgent prompt\n\nDaemon prompt");
|
||||||
});
|
});
|
||||||
|
|
||||||
test("updates model and thinking through Pi runtime commands", async () => {
|
test("updates model and thinking through Pi runtime commands", async () => {
|
||||||
|
|||||||
@@ -535,10 +535,6 @@ function buildResumeStartInput(input: {
|
|||||||
session: input.sessionFile,
|
session: input.sessionFile,
|
||||||
model: input.resumeConfig.model,
|
model: input.resumeConfig.model,
|
||||||
thinkingOptionId: normalizePiThinkingOption(input.resumeConfig.thinkingOptionId) ?? undefined,
|
thinkingOptionId: normalizePiThinkingOption(input.resumeConfig.thinkingOptionId) ?? undefined,
|
||||||
systemPrompt: composeSystemPromptParts(
|
|
||||||
input.resumeConfig.config.systemPrompt,
|
|
||||||
input.resumeConfig.config.daemonAppendSystemPrompt,
|
|
||||||
),
|
|
||||||
mcpConfigPath: input.mcpConfig?.path,
|
mcpConfigPath: input.mcpConfig?.path,
|
||||||
extensionPaths: input.paseoExtension ? [input.paseoExtension.path] : undefined,
|
extensionPaths: input.paseoExtension ? [input.paseoExtension.path] : undefined,
|
||||||
};
|
};
|
||||||
@@ -630,7 +626,7 @@ function createPiMcpConfigFile(
|
|||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
|
||||||
function createPiPaseoExtensionFile(): PiTempFile {
|
function createPiPaseoExtensionFile(systemPrompt?: string): PiTempFile {
|
||||||
const dir = mkdtempSync(join(tmpdir(), "paseo-pi-extension-"));
|
const dir = mkdtempSync(join(tmpdir(), "paseo-pi-extension-"));
|
||||||
const filePath = join(dir, "paseo-integration.mjs");
|
const filePath = join(dir, "paseo-integration.mjs");
|
||||||
writeFileSync(
|
writeFileSync(
|
||||||
@@ -680,6 +676,14 @@ function createPiPaseoExtensionFile(): PiTempFile {
|
|||||||
}
|
}
|
||||||
|
|
||||||
export default function paseoIntegration(pi) {
|
export default function paseoIntegration(pi) {
|
||||||
|
${
|
||||||
|
systemPrompt
|
||||||
|
? `pi.on("before_agent_start", async (event) => ({
|
||||||
|
systemPrompt: event.systemPrompt + "\\n\\n" + ${JSON.stringify(systemPrompt)},
|
||||||
|
}));`
|
||||||
|
: ""
|
||||||
|
}
|
||||||
|
|
||||||
pi.on("session_start", async (_event, ctx) => {
|
pi.on("session_start", async (_event, ctx) => {
|
||||||
emitEntryCapture(ctx, "session_start");
|
emitEntryCapture(ctx, "session_start");
|
||||||
});
|
});
|
||||||
@@ -2307,7 +2311,9 @@ export class PiRpcAgentClient implements AgentClient {
|
|||||||
...launchContext?.env,
|
...launchContext?.env,
|
||||||
};
|
};
|
||||||
const mcpConfig = await this.prepareMcpConfig(config.cwd, config.mcpServers, mcpEnv);
|
const mcpConfig = await this.prepareMcpConfig(config.cwd, config.mcpServers, mcpEnv);
|
||||||
const paseoExtension = createPiPaseoExtensionFile();
|
const paseoExtension = createPiPaseoExtensionFile(
|
||||||
|
composeSystemPromptParts(config.systemPrompt, config.daemonAppendSystemPrompt),
|
||||||
|
);
|
||||||
let runtimeSession: PiRuntimeSession;
|
let runtimeSession: PiRuntimeSession;
|
||||||
try {
|
try {
|
||||||
runtimeSession = await this.runtime.startSession({
|
runtimeSession = await this.runtime.startSession({
|
||||||
@@ -2316,10 +2322,6 @@ export class PiRpcAgentClient implements AgentClient {
|
|||||||
thinkingOptionId:
|
thinkingOptionId:
|
||||||
normalizePiThinkingOption(config.thinkingOptionId) ?? DEFAULT_PI_THINKING_LEVEL,
|
normalizePiThinkingOption(config.thinkingOptionId) ?? DEFAULT_PI_THINKING_LEVEL,
|
||||||
noSession: config.internal === true,
|
noSession: config.internal === true,
|
||||||
systemPrompt: composeSystemPromptParts(
|
|
||||||
config.systemPrompt,
|
|
||||||
config.daemonAppendSystemPrompt,
|
|
||||||
),
|
|
||||||
env: launchContext?.env,
|
env: launchContext?.env,
|
||||||
mcpConfigPath: mcpConfig?.path,
|
mcpConfigPath: mcpConfig?.path,
|
||||||
extensionPaths: paseoExtension ? [paseoExtension.path] : undefined,
|
extensionPaths: paseoExtension ? [paseoExtension.path] : undefined,
|
||||||
@@ -2368,7 +2370,12 @@ export class PiRpcAgentClient implements AgentClient {
|
|||||||
resumeConfig.config.mcpServers,
|
resumeConfig.config.mcpServers,
|
||||||
mcpEnv,
|
mcpEnv,
|
||||||
);
|
);
|
||||||
const paseoExtension = createPiPaseoExtensionFile();
|
const paseoExtension = createPiPaseoExtensionFile(
|
||||||
|
composeSystemPromptParts(
|
||||||
|
resumeConfig.config.systemPrompt,
|
||||||
|
resumeConfig.config.daemonAppendSystemPrompt,
|
||||||
|
),
|
||||||
|
);
|
||||||
let runtimeSession: PiRuntimeSession;
|
let runtimeSession: PiRuntimeSession;
|
||||||
try {
|
try {
|
||||||
runtimeSession = await this.runtime.startSession(
|
runtimeSession = await this.runtime.startSession(
|
||||||
|
|||||||
@@ -229,26 +229,6 @@ describe("PiCliRuntime", () => {
|
|||||||
]);
|
]);
|
||||||
});
|
});
|
||||||
|
|
||||||
test("passes an appended system prompt to Pi", async () => {
|
|
||||||
const child = createPiChild();
|
|
||||||
replyToCommands(child, () => ({}));
|
|
||||||
const launches: PiRuntimeLaunch[] = [];
|
|
||||||
const runtime = createRuntime(child, launches);
|
|
||||||
|
|
||||||
await runtime.startSession({
|
|
||||||
cwd: "/workspace/project",
|
|
||||||
systemPrompt: " Use the daemon prompt. ",
|
|
||||||
});
|
|
||||||
|
|
||||||
expect(launches).toEqual([
|
|
||||||
expect.objectContaining({
|
|
||||||
cwd: "/workspace/project",
|
|
||||||
systemPrompt: "Use the daemon prompt.",
|
|
||||||
argv: ["pi", "--mode", "rpc", "--append-system-prompt", "Use the daemon prompt."],
|
|
||||||
}),
|
|
||||||
]);
|
|
||||||
});
|
|
||||||
|
|
||||||
test("delivers events separately from command responses", async () => {
|
test("delivers events separately from command responses", async () => {
|
||||||
const child = createPiChild();
|
const child = createPiChild();
|
||||||
replyToCommands(child, () => ({ models: [] }));
|
replyToCommands(child, () => ({ models: [] }));
|
||||||
|
|||||||
@@ -19,7 +19,6 @@ export interface PiRuntimeLaunch {
|
|||||||
modeId?: string;
|
modeId?: string;
|
||||||
session?: string;
|
session?: string;
|
||||||
noSession?: boolean;
|
noSession?: boolean;
|
||||||
systemPrompt?: string;
|
|
||||||
mcpConfigPath?: string;
|
mcpConfigPath?: string;
|
||||||
extensionPaths?: string[];
|
extensionPaths?: string[];
|
||||||
extraArgs?: string[];
|
extraArgs?: string[];
|
||||||
@@ -34,7 +33,6 @@ export interface PiStartSessionInput {
|
|||||||
modeId?: string;
|
modeId?: string;
|
||||||
session?: string;
|
session?: string;
|
||||||
noSession?: boolean;
|
noSession?: boolean;
|
||||||
systemPrompt?: string;
|
|
||||||
mcpConfigPath?: string;
|
mcpConfigPath?: string;
|
||||||
extensionPaths?: string[];
|
extensionPaths?: string[];
|
||||||
extraArgs?: string[];
|
extraArgs?: string[];
|
||||||
@@ -85,8 +83,7 @@ export function buildPiLaunch(input: {
|
|||||||
const argv = [...command];
|
const argv = [...command];
|
||||||
|
|
||||||
const protocolMode = input.session.protocolMode ?? "rpc";
|
const protocolMode = input.session.protocolMode ?? "rpc";
|
||||||
const systemPrompt = input.session.systemPrompt?.trim();
|
appendPiLaunchArgs(argv, input.session, protocolMode);
|
||||||
appendPiLaunchArgs(argv, input.session, protocolMode, systemPrompt);
|
|
||||||
|
|
||||||
return {
|
return {
|
||||||
cwd: input.session.cwd,
|
cwd: input.session.cwd,
|
||||||
@@ -104,7 +101,6 @@ export function buildPiLaunch(input: {
|
|||||||
modeId: input.session.modeId,
|
modeId: input.session.modeId,
|
||||||
session: input.session.session,
|
session: input.session.session,
|
||||||
noSession: input.session.noSession,
|
noSession: input.session.noSession,
|
||||||
systemPrompt,
|
|
||||||
mcpConfigPath: input.session.mcpConfigPath,
|
mcpConfigPath: input.session.mcpConfigPath,
|
||||||
extensionPaths: input.session.extensionPaths,
|
extensionPaths: input.session.extensionPaths,
|
||||||
extraArgs: input.session.extraArgs,
|
extraArgs: input.session.extraArgs,
|
||||||
@@ -115,7 +111,6 @@ function appendPiLaunchArgs(
|
|||||||
argv: string[],
|
argv: string[],
|
||||||
session: PiStartSessionInput,
|
session: PiStartSessionInput,
|
||||||
protocolMode: "rpc" | "rpc-ui",
|
protocolMode: "rpc" | "rpc-ui",
|
||||||
systemPrompt: string | undefined,
|
|
||||||
): void {
|
): void {
|
||||||
if (!hasModeFlag(argv)) {
|
if (!hasModeFlag(argv)) {
|
||||||
argv.push("--mode", protocolMode);
|
argv.push("--mode", protocolMode);
|
||||||
@@ -134,9 +129,6 @@ function appendPiLaunchArgs(
|
|||||||
} else if (session.session) {
|
} else if (session.session) {
|
||||||
argv.push("--session", session.session);
|
argv.push("--session", session.session);
|
||||||
}
|
}
|
||||||
if (systemPrompt) {
|
|
||||||
argv.push("--append-system-prompt", systemPrompt);
|
|
||||||
}
|
|
||||||
if (session.mcpConfigPath) {
|
if (session.mcpConfigPath) {
|
||||||
argv.push("--mcp-config", session.mcpConfigPath);
|
argv.push("--mcp-config", session.mcpConfigPath);
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -139,6 +139,51 @@ beforeEach((context) => {
|
|||||||
}
|
}
|
||||||
});
|
});
|
||||||
|
|
||||||
|
test(
|
||||||
|
"real Pi daemon composes project and Paseo system prompts",
|
||||||
|
async () => {
|
||||||
|
const cwd = tmpCwd("pi-system-prompts-");
|
||||||
|
|
||||||
|
try {
|
||||||
|
mkdirSync(path.join(cwd, ".pi"), { recursive: true });
|
||||||
|
writeFileSync(
|
||||||
|
path.join(cwd, ".pi", "APPEND_SYSTEM.md"),
|
||||||
|
[
|
||||||
|
"When the user says PASEO_SYSTEM_PROMPT_PROBE, reply with exactly two tokens:",
|
||||||
|
"PROJECT_PROMPT followed by the value of PASEO_PROMPT_TOKEN from later system instructions.",
|
||||||
|
"If no PASEO_PROMPT_TOKEN exists, use MISSING as the second token.",
|
||||||
|
].join("\n"),
|
||||||
|
);
|
||||||
|
|
||||||
|
await withConnectedPiDaemon(async ({ client }) => {
|
||||||
|
const agent = await client.createAgent({
|
||||||
|
cwd,
|
||||||
|
title: "pi-system-prompts",
|
||||||
|
provider: "pi",
|
||||||
|
model: PI_REAL_TEST_MODEL,
|
||||||
|
systemPrompt:
|
||||||
|
"PASEO_PROMPT_TOKEN is PASEO_PROMPT. Follow the project instruction for PASEO_SYSTEM_PROMPT_PROBE.",
|
||||||
|
});
|
||||||
|
|
||||||
|
await client.sendMessage(agent.id, "PASEO_SYSTEM_PROMPT_PROBE");
|
||||||
|
const finish = await client.waitForFinish(agent.id, PI_TEST_TIMEOUT_MS);
|
||||||
|
expect(finish.status).toBe("idle");
|
||||||
|
|
||||||
|
const items = await fetchCanonicalTimeline(client, agent.id);
|
||||||
|
const response = items
|
||||||
|
.filter((item) => item.type === "assistant_message")
|
||||||
|
.map((item) => item.text)
|
||||||
|
.join("")
|
||||||
|
.trim();
|
||||||
|
expect(response).toBe("PROJECT_PROMPT PASEO_PROMPT");
|
||||||
|
});
|
||||||
|
} finally {
|
||||||
|
rmSync(cwd, { recursive: true, force: true });
|
||||||
|
}
|
||||||
|
},
|
||||||
|
PI_TEST_TIMEOUT_MS,
|
||||||
|
);
|
||||||
|
|
||||||
test(
|
test(
|
||||||
"real Pi daemon lists Paseo-handled compact slash commands",
|
"real Pi daemon lists Paseo-handled compact slash commands",
|
||||||
async () => {
|
async () => {
|
||||||
|
|||||||
Reference in New Issue
Block a user