mirror of
https://github.com/getpaseo/paseo.git
synced 2026-07-29 12:01:31 +00:00
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:
@@ -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.
|
||||
|
||||
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.
|
||||
|
||||
|
||||
@@ -13,6 +13,7 @@ import { tmpdir } from "node:os";
|
||||
import path from "node:path";
|
||||
import pino from "pino";
|
||||
import { setImmediate as waitForImmediate } from "node:timers/promises";
|
||||
import { pathToFileURL } from "node:url";
|
||||
import { describe, expect, onTestFinished, test } from "vitest";
|
||||
|
||||
import type { AgentSession, AgentSessionConfig, AgentStreamEvent } from "../../agent-sdk-types.js";
|
||||
@@ -55,6 +56,26 @@ function readUtf8File(pathname: string): string {
|
||||
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> {
|
||||
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 client = createClient(pi);
|
||||
|
||||
await client.createSession(
|
||||
const session = await client.createSession(
|
||||
createConfig({
|
||||
systemPrompt: "Agent prompt",
|
||||
daemonAppendSystemPrompt: "Daemon prompt",
|
||||
@@ -727,7 +748,6 @@ describe("PiRpcAgentSession", () => {
|
||||
const actualLaunch = pi.recordedLaunches[0]!;
|
||||
expect(actualLaunch).toMatchObject({
|
||||
cwd: "/tmp/paseo-pi-rpc-test",
|
||||
systemPrompt: "Agent prompt\n\nDaemon prompt",
|
||||
});
|
||||
expect(actualLaunch.extensionPaths).toHaveLength(1);
|
||||
expect(actualLaunch.argv).toEqual([
|
||||
@@ -736,11 +756,15 @@ describe("PiRpcAgentSession", () => {
|
||||
"rpc",
|
||||
"--thinking",
|
||||
"medium",
|
||||
"--append-system-prompt",
|
||||
"Agent prompt\n\nDaemon prompt",
|
||||
"--extension",
|
||||
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 () => {
|
||||
@@ -769,7 +793,6 @@ describe("PiRpcAgentSession", () => {
|
||||
expect(actualLaunch).toMatchObject({
|
||||
cwd: "/workspace/project",
|
||||
session: "/tmp/native-pi-session",
|
||||
systemPrompt: "Agent prompt\n\nDaemon prompt",
|
||||
});
|
||||
expect(actualLaunch.extensionPaths).toHaveLength(1);
|
||||
expect(actualLaunch.argv).toEqual([
|
||||
@@ -782,11 +805,12 @@ describe("PiRpcAgentSession", () => {
|
||||
"high",
|
||||
"--session",
|
||||
"/tmp/native-pi-session",
|
||||
"--append-system-prompt",
|
||||
"Agent prompt\n\nDaemon prompt",
|
||||
"--extension",
|
||||
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 () => {
|
||||
|
||||
@@ -535,10 +535,6 @@ function buildResumeStartInput(input: {
|
||||
session: input.sessionFile,
|
||||
model: input.resumeConfig.model,
|
||||
thinkingOptionId: normalizePiThinkingOption(input.resumeConfig.thinkingOptionId) ?? undefined,
|
||||
systemPrompt: composeSystemPromptParts(
|
||||
input.resumeConfig.config.systemPrompt,
|
||||
input.resumeConfig.config.daemonAppendSystemPrompt,
|
||||
),
|
||||
mcpConfigPath: input.mcpConfig?.path,
|
||||
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 filePath = join(dir, "paseo-integration.mjs");
|
||||
writeFileSync(
|
||||
@@ -680,6 +676,14 @@ function createPiPaseoExtensionFile(): PiTempFile {
|
||||
}
|
||||
|
||||
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) => {
|
||||
emitEntryCapture(ctx, "session_start");
|
||||
});
|
||||
@@ -2307,7 +2311,9 @@ export class PiRpcAgentClient implements AgentClient {
|
||||
...launchContext?.env,
|
||||
};
|
||||
const mcpConfig = await this.prepareMcpConfig(config.cwd, config.mcpServers, mcpEnv);
|
||||
const paseoExtension = createPiPaseoExtensionFile();
|
||||
const paseoExtension = createPiPaseoExtensionFile(
|
||||
composeSystemPromptParts(config.systemPrompt, config.daemonAppendSystemPrompt),
|
||||
);
|
||||
let runtimeSession: PiRuntimeSession;
|
||||
try {
|
||||
runtimeSession = await this.runtime.startSession({
|
||||
@@ -2316,10 +2322,6 @@ export class PiRpcAgentClient implements AgentClient {
|
||||
thinkingOptionId:
|
||||
normalizePiThinkingOption(config.thinkingOptionId) ?? DEFAULT_PI_THINKING_LEVEL,
|
||||
noSession: config.internal === true,
|
||||
systemPrompt: composeSystemPromptParts(
|
||||
config.systemPrompt,
|
||||
config.daemonAppendSystemPrompt,
|
||||
),
|
||||
env: launchContext?.env,
|
||||
mcpConfigPath: mcpConfig?.path,
|
||||
extensionPaths: paseoExtension ? [paseoExtension.path] : undefined,
|
||||
@@ -2368,7 +2370,12 @@ export class PiRpcAgentClient implements AgentClient {
|
||||
resumeConfig.config.mcpServers,
|
||||
mcpEnv,
|
||||
);
|
||||
const paseoExtension = createPiPaseoExtensionFile();
|
||||
const paseoExtension = createPiPaseoExtensionFile(
|
||||
composeSystemPromptParts(
|
||||
resumeConfig.config.systemPrompt,
|
||||
resumeConfig.config.daemonAppendSystemPrompt,
|
||||
),
|
||||
);
|
||||
let runtimeSession: PiRuntimeSession;
|
||||
try {
|
||||
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 () => {
|
||||
const child = createPiChild();
|
||||
replyToCommands(child, () => ({ models: [] }));
|
||||
|
||||
@@ -19,7 +19,6 @@ export interface PiRuntimeLaunch {
|
||||
modeId?: string;
|
||||
session?: string;
|
||||
noSession?: boolean;
|
||||
systemPrompt?: string;
|
||||
mcpConfigPath?: string;
|
||||
extensionPaths?: string[];
|
||||
extraArgs?: string[];
|
||||
@@ -34,7 +33,6 @@ export interface PiStartSessionInput {
|
||||
modeId?: string;
|
||||
session?: string;
|
||||
noSession?: boolean;
|
||||
systemPrompt?: string;
|
||||
mcpConfigPath?: string;
|
||||
extensionPaths?: string[];
|
||||
extraArgs?: string[];
|
||||
@@ -85,8 +83,7 @@ export function buildPiLaunch(input: {
|
||||
const argv = [...command];
|
||||
|
||||
const protocolMode = input.session.protocolMode ?? "rpc";
|
||||
const systemPrompt = input.session.systemPrompt?.trim();
|
||||
appendPiLaunchArgs(argv, input.session, protocolMode, systemPrompt);
|
||||
appendPiLaunchArgs(argv, input.session, protocolMode);
|
||||
|
||||
return {
|
||||
cwd: input.session.cwd,
|
||||
@@ -104,7 +101,6 @@ export function buildPiLaunch(input: {
|
||||
modeId: input.session.modeId,
|
||||
session: input.session.session,
|
||||
noSession: input.session.noSession,
|
||||
systemPrompt,
|
||||
mcpConfigPath: input.session.mcpConfigPath,
|
||||
extensionPaths: input.session.extensionPaths,
|
||||
extraArgs: input.session.extraArgs,
|
||||
@@ -115,7 +111,6 @@ function appendPiLaunchArgs(
|
||||
argv: string[],
|
||||
session: PiStartSessionInput,
|
||||
protocolMode: "rpc" | "rpc-ui",
|
||||
systemPrompt: string | undefined,
|
||||
): void {
|
||||
if (!hasModeFlag(argv)) {
|
||||
argv.push("--mode", protocolMode);
|
||||
@@ -134,9 +129,6 @@ function appendPiLaunchArgs(
|
||||
} else if (session.session) {
|
||||
argv.push("--session", session.session);
|
||||
}
|
||||
if (systemPrompt) {
|
||||
argv.push("--append-system-prompt", systemPrompt);
|
||||
}
|
||||
if (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(
|
||||
"real Pi daemon lists Paseo-handled compact slash commands",
|
||||
async () => {
|
||||
|
||||
Reference in New Issue
Block a user