Inject Paseo prompt instructions via first user message
This commit is contained in:
@@ -5,6 +5,7 @@ import {
|
|||||||
type AgentLifecycleStatus,
|
type AgentLifecycleStatus,
|
||||||
} from "../../shared/agent-lifecycle.js";
|
} from "../../shared/agent-lifecycle.js";
|
||||||
import type { Logger } from "pino";
|
import type { Logger } from "pino";
|
||||||
|
import { getSelfIdentificationInstructions } from "./self-identification-instructions.js";
|
||||||
|
|
||||||
import type {
|
import type {
|
||||||
AgentCapabilityFlags,
|
AgentCapabilityFlags,
|
||||||
@@ -1060,6 +1061,15 @@ export class AgentManager {
|
|||||||
normalized.agentControlMcp = this.agentControlMcp;
|
normalized.agentControlMcp = this.agentControlMcp;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
if (
|
||||||
|
normalized.paseoPromptInstructions === undefined &&
|
||||||
|
normalized.agentControlMcp
|
||||||
|
) {
|
||||||
|
normalized.paseoPromptInstructions = getSelfIdentificationInstructions({
|
||||||
|
cwd: normalized.cwd,
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
return normalized;
|
return normalized;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -202,6 +202,14 @@ export type AgentSessionConfig = {
|
|||||||
webSearch?: boolean;
|
webSearch?: boolean;
|
||||||
reasoningEffort?: string;
|
reasoningEffort?: string;
|
||||||
agentControlMcp?: AgentControlMcpConfig;
|
agentControlMcp?: AgentControlMcpConfig;
|
||||||
|
/**
|
||||||
|
* Paseo-owned instructions injected into the first user prompt via
|
||||||
|
* <paseo-instructions>...</paseo-instructions>.
|
||||||
|
*
|
||||||
|
* These MUST NOT be sent via provider system/developer instructions (those are
|
||||||
|
* reserved for provider/session behaviors like resuming).
|
||||||
|
*/
|
||||||
|
paseoPromptInstructions?: string;
|
||||||
extra?: {
|
extra?: {
|
||||||
codex?: AgentMetadata;
|
codex?: AgentMetadata;
|
||||||
claude?: Partial<ClaudeAgentOptions>;
|
claude?: Partial<ClaudeAgentOptions>;
|
||||||
|
|||||||
@@ -32,6 +32,7 @@ import {
|
|||||||
} from "../../utils/worktree.js";
|
} from "../../utils/worktree.js";
|
||||||
import { WaitForAgentTracker } from "./wait-for-agent-tracker.js";
|
import { WaitForAgentTracker } from "./wait-for-agent-tracker.js";
|
||||||
import { NotGitRepoError, renameCurrentBranch } from "../../utils/checkout-git.js";
|
import { NotGitRepoError, renameCurrentBranch } from "../../utils/checkout-git.js";
|
||||||
|
import { injectLeadingPaseoInstructionTag } from "./paseo-instructions-tag.js";
|
||||||
|
|
||||||
export interface AgentMcpServerOptions {
|
export interface AgentMcpServerOptions {
|
||||||
agentManager: AgentManager;
|
agentManager: AgentManager;
|
||||||
@@ -456,8 +457,12 @@ export async function createAgentMcpServer(
|
|||||||
});
|
});
|
||||||
|
|
||||||
if (initialPrompt) {
|
if (initialPrompt) {
|
||||||
|
const initialPromptWithInstructions = injectLeadingPaseoInstructionTag(
|
||||||
|
initialPrompt,
|
||||||
|
snapshot.config.paseoPromptInstructions
|
||||||
|
);
|
||||||
try {
|
try {
|
||||||
agentManager.recordUserMessage(snapshot.id, initialPrompt);
|
agentManager.recordUserMessage(snapshot.id, initialPromptWithInstructions);
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
childLogger.error(
|
childLogger.error(
|
||||||
{ err: error, agentId: snapshot.id },
|
{ err: error, agentId: snapshot.id },
|
||||||
@@ -466,7 +471,7 @@ export async function createAgentMcpServer(
|
|||||||
}
|
}
|
||||||
|
|
||||||
try {
|
try {
|
||||||
startAgentRun(agentManager, snapshot.id, initialPrompt, childLogger);
|
startAgentRun(agentManager, snapshot.id, initialPromptWithInstructions, childLogger);
|
||||||
|
|
||||||
// If not running in background, wait for completion
|
// If not running in background, wait for completion
|
||||||
if (!background) {
|
if (!background) {
|
||||||
|
|||||||
@@ -0,0 +1,45 @@
|
|||||||
|
import { describe, expect, test } from "vitest";
|
||||||
|
|
||||||
|
import {
|
||||||
|
formatPaseoInstructionTag,
|
||||||
|
hasLeadingPaseoInstructionTag,
|
||||||
|
injectLeadingPaseoInstructionTag,
|
||||||
|
stripLeadingPaseoInstructionTag,
|
||||||
|
} from "./paseo-instructions-tag.js";
|
||||||
|
|
||||||
|
describe("paseo instruction tags", () => {
|
||||||
|
test("formatPaseoInstructionTag wraps content", () => {
|
||||||
|
expect(formatPaseoInstructionTag("hello")).toBe(
|
||||||
|
"<paseo-instructions>\nhello\n</paseo-instructions>"
|
||||||
|
);
|
||||||
|
});
|
||||||
|
|
||||||
|
test("hasLeadingPaseoInstructionTag detects leading tag", () => {
|
||||||
|
expect(hasLeadingPaseoInstructionTag("<paseo-instructions>\nX\n</paseo-instructions>")).toBe(
|
||||||
|
true
|
||||||
|
);
|
||||||
|
expect(hasLeadingPaseoInstructionTag("nope <paseo-instructions>")).toBe(false);
|
||||||
|
});
|
||||||
|
|
||||||
|
test("stripLeadingPaseoInstructionTag strips leading tag content", () => {
|
||||||
|
const input = [
|
||||||
|
"<paseo-instructions>",
|
||||||
|
"do the thing",
|
||||||
|
"</paseo-instructions>",
|
||||||
|
"",
|
||||||
|
"Hello world",
|
||||||
|
].join("\n");
|
||||||
|
expect(stripLeadingPaseoInstructionTag(input)).toBe("Hello world");
|
||||||
|
});
|
||||||
|
|
||||||
|
test("injectLeadingPaseoInstructionTag prepends instructions when missing", () => {
|
||||||
|
expect(injectLeadingPaseoInstructionTag("Hello", "do the thing")).toBe(
|
||||||
|
"<paseo-instructions>\ndo the thing\n</paseo-instructions>\n\nHello"
|
||||||
|
);
|
||||||
|
});
|
||||||
|
|
||||||
|
test("injectLeadingPaseoInstructionTag is idempotent when tag already present", () => {
|
||||||
|
const input = "<paseo-instructions>\nX\n</paseo-instructions>\n\nHello";
|
||||||
|
expect(injectLeadingPaseoInstructionTag(input, "do the thing")).toBe(input);
|
||||||
|
});
|
||||||
|
});
|
||||||
@@ -0,0 +1,48 @@
|
|||||||
|
const OPEN_TAG = "<paseo-instructions>";
|
||||||
|
const CLOSE_TAG = "</paseo-instructions>";
|
||||||
|
|
||||||
|
export function formatPaseoInstructionTag(instructions: string): string {
|
||||||
|
return `${OPEN_TAG}\n${instructions}\n${CLOSE_TAG}`;
|
||||||
|
}
|
||||||
|
|
||||||
|
export function hasLeadingPaseoInstructionTag(text: string): boolean {
|
||||||
|
return /^\s*<paseo-instructions>/.test(text);
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Prepend paseo instructions to a prompt exactly once (idempotent by content).
|
||||||
|
* This is intended for agent creation / initial prompt only.
|
||||||
|
*/
|
||||||
|
export function injectLeadingPaseoInstructionTag(
|
||||||
|
prompt: string,
|
||||||
|
instructions: string | null | undefined
|
||||||
|
): string {
|
||||||
|
const normalizedInstructions = instructions?.trim() ?? "";
|
||||||
|
if (!normalizedInstructions) {
|
||||||
|
return prompt;
|
||||||
|
}
|
||||||
|
if (hasLeadingPaseoInstructionTag(prompt)) {
|
||||||
|
return prompt;
|
||||||
|
}
|
||||||
|
return `${formatPaseoInstructionTag(normalizedInstructions)}\n\n${prompt}`;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Remove a leading <paseo-instructions>...</paseo-instructions> block, if present.
|
||||||
|
* The content is treated as internal metadata and is discarded.
|
||||||
|
*/
|
||||||
|
export function stripLeadingPaseoInstructionTag(text: string): string {
|
||||||
|
const leadingMatch = text.match(/^\s*<paseo-instructions>/);
|
||||||
|
if (!leadingMatch || leadingMatch.index !== 0) {
|
||||||
|
return text;
|
||||||
|
}
|
||||||
|
|
||||||
|
const openEnd = leadingMatch[0].length;
|
||||||
|
const closeStart = text.indexOf(CLOSE_TAG, openEnd);
|
||||||
|
if (closeStart === -1) {
|
||||||
|
return text;
|
||||||
|
}
|
||||||
|
|
||||||
|
const closeEnd = closeStart + CLOSE_TAG.length;
|
||||||
|
return text.slice(closeEnd).trimStart();
|
||||||
|
}
|
||||||
@@ -48,7 +48,6 @@ import type {
|
|||||||
PersistedAgentDescriptor,
|
PersistedAgentDescriptor,
|
||||||
} from "../agent-sdk-types.js";
|
} from "../agent-sdk-types.js";
|
||||||
import { getOrchestratorModeInstructions } from "../orchestrator-instructions.js";
|
import { getOrchestratorModeInstructions } from "../orchestrator-instructions.js";
|
||||||
import { getSelfIdentificationInstructions } from "../self-identification-instructions.js";
|
|
||||||
|
|
||||||
const fsPromises = promises;
|
const fsPromises = promises;
|
||||||
|
|
||||||
@@ -802,12 +801,7 @@ class ClaudeAgentSession implements AgentSession {
|
|||||||
systemPrompt: {
|
systemPrompt: {
|
||||||
type: "preset",
|
type: "preset",
|
||||||
preset: "claude_code",
|
preset: "claude_code",
|
||||||
append: [
|
append: [getOrchestratorModeInstructions()].filter(Boolean).join("\n"),
|
||||||
getOrchestratorModeInstructions(),
|
|
||||||
this.currentMode === "plan" ? "" : getSelfIdentificationInstructions({ cwd: this.config.cwd }),
|
|
||||||
]
|
|
||||||
.filter(Boolean)
|
|
||||||
.join("\n"),
|
|
||||||
},
|
},
|
||||||
settingSources: ["user", "project"],
|
settingSources: ["user", "project"],
|
||||||
stderr: (data: string) => {
|
stderr: (data: string) => {
|
||||||
|
|||||||
@@ -0,0 +1,47 @@
|
|||||||
|
import { describe, expect, test } from "vitest";
|
||||||
|
import { mkdtempSync, writeFileSync } from "node:fs";
|
||||||
|
import os from "node:os";
|
||||||
|
import path from "node:path";
|
||||||
|
|
||||||
|
import { __test__ } from "./codex-mcp-agent.js";
|
||||||
|
import type { AgentSessionConfig } from "../agent-sdk-types.js";
|
||||||
|
|
||||||
|
describe("codex developer-instructions vs paseo prompt instructions", () => {
|
||||||
|
test("does not inject Paseo self-identification into developer-instructions", () => {
|
||||||
|
const dir = mkdtempSync(path.join(os.tmpdir(), "codex-rollout-"));
|
||||||
|
const rolloutPath = path.join(dir, "rollout.jsonl");
|
||||||
|
|
||||||
|
const entry = {
|
||||||
|
type: "response_item",
|
||||||
|
payload: {
|
||||||
|
type: "message",
|
||||||
|
role: "user",
|
||||||
|
content: [{ input_text: "hello from history" }],
|
||||||
|
},
|
||||||
|
};
|
||||||
|
|
||||||
|
writeFileSync(rolloutPath, JSON.stringify(entry) + "\n", "utf8");
|
||||||
|
|
||||||
|
const config: AgentSessionConfig = {
|
||||||
|
provider: "codex",
|
||||||
|
cwd: dir,
|
||||||
|
modeId: "auto",
|
||||||
|
};
|
||||||
|
|
||||||
|
const payload = __test__.buildCodexMcpConfig(
|
||||||
|
config,
|
||||||
|
"Hello world",
|
||||||
|
"auto",
|
||||||
|
undefined,
|
||||||
|
rolloutPath
|
||||||
|
);
|
||||||
|
|
||||||
|
const dev = payload["developer-instructions"] ?? "";
|
||||||
|
expect(dev).toContain("<previous_conversation>");
|
||||||
|
expect(dev).toContain("hello from history");
|
||||||
|
expect(dev.toLowerCase()).not.toContain("set_title");
|
||||||
|
expect(dev.toLowerCase()).not.toContain("set_branch");
|
||||||
|
expect(dev.toLowerCase()).not.toContain("you are running under paseo");
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
@@ -789,6 +789,8 @@ describe("CodexMcpAgentClient (MCP integration)", () => {
|
|||||||
session = await client.createSession(config);
|
session = await client.createSession(config);
|
||||||
|
|
||||||
const prompt = [
|
const prompt = [
|
||||||
|
"IMPORTANT: You must execute every step using the exact tool mentioned.",
|
||||||
|
"Do not skip steps, and do not claim completion without running the tools.",
|
||||||
"1. Run the command `printf 'stdout-marker'` using your shell tool.",
|
"1. Run the command `printf 'stdout-marker'` using your shell tool.",
|
||||||
"2. Run the command `printf 'stderr-marker' 1>&2` using your shell tool.",
|
"2. Run the command `printf 'stderr-marker' 1>&2` using your shell tool.",
|
||||||
"3. Use apply_patch to create a new file named tool-create.txt containing only the line 'alpha'.",
|
"3. Use apply_patch to create a new file named tool-create.txt containing only the line 'alpha'.",
|
||||||
|
|||||||
@@ -39,7 +39,6 @@ import type {
|
|||||||
ListPersistedAgentsOptions,
|
ListPersistedAgentsOptions,
|
||||||
PersistedAgentDescriptor,
|
PersistedAgentDescriptor,
|
||||||
} from "../agent-sdk-types.js";
|
} from "../agent-sdk-types.js";
|
||||||
import { getSelfIdentificationInstructions } from "../self-identification-instructions.js";
|
|
||||||
import { curateAgentActivity } from "../activity-curator.js";
|
import { curateAgentActivity } from "../activity-curator.js";
|
||||||
|
|
||||||
type CodexMcpAgentConfig = AgentSessionConfig & { provider: "codex" };
|
type CodexMcpAgentConfig = AgentSessionConfig & { provider: "codex" };
|
||||||
@@ -2759,7 +2758,6 @@ function buildCodexMcpConfig(
|
|||||||
developerInstructions = history;
|
developerInstructions = history;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
const selfIdentificationInstructions = getSelfIdentificationInstructions({ cwd: config.cwd });
|
|
||||||
|
|
||||||
// Build MCP servers configuration
|
// Build MCP servers configuration
|
||||||
const mcpServers: Record<string, CodexMcpServerConfig> = {};
|
const mcpServers: Record<string, CodexMcpServerConfig> = {};
|
||||||
@@ -2833,7 +2831,7 @@ function buildCodexMcpConfig(
|
|||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
const combinedDeveloperInstructions = [developerInstructions, selfIdentificationInstructions]
|
const combinedDeveloperInstructions = [developerInstructions]
|
||||||
.filter(Boolean)
|
.filter(Boolean)
|
||||||
.join("\n\n");
|
.join("\n\n");
|
||||||
|
|
||||||
@@ -3227,7 +3225,7 @@ class CodexMcpAgentSession implements AgentSession {
|
|||||||
const abortController = new AbortController();
|
const abortController = new AbortController();
|
||||||
this.currentAbortController = abortController;
|
this.currentAbortController = abortController;
|
||||||
|
|
||||||
const promptText = await toPromptText(prompt, this.logger);
|
let promptText = await toPromptText(prompt, this.logger);
|
||||||
// NOTE: user_message is NOT emitted here because the agent-manager's
|
// NOTE: user_message is NOT emitted here because the agent-manager's
|
||||||
// recordUserMessage() already handles emitting the user message timeline
|
// recordUserMessage() already handles emitting the user message timeline
|
||||||
// event before calling stream(). Emitting here would cause duplicates.
|
// event before calling stream(). Emitting here would cause duplicates.
|
||||||
@@ -4652,6 +4650,7 @@ export const __test__ = {
|
|||||||
tokenizeCommandArgs,
|
tokenizeCommandArgs,
|
||||||
parseFrontMatter,
|
parseFrontMatter,
|
||||||
expandCodexCustomPrompt,
|
expandCodexCustomPrompt,
|
||||||
|
buildCodexMcpConfig,
|
||||||
isMissingConversationIdError,
|
isMissingConversationIdError,
|
||||||
isMissingConversationIdResponse,
|
isMissingConversationIdResponse,
|
||||||
};
|
};
|
||||||
|
|||||||
@@ -401,64 +401,64 @@ describe("daemon client v2 E2E", () => {
|
|||||||
});
|
});
|
||||||
|
|
||||||
const permissionRequestPromise = waitForSignal(60000, (resolve) => {
|
const permissionRequestPromise = waitForSignal(60000, (resolve) => {
|
||||||
const unsubscribe = ctx.client.on(
|
const unsubscribe = ctx.client.on("agent_permission_request", (message) => {
|
||||||
"agent_permission_request",
|
if (message.type !== "agent_permission_request") {
|
||||||
(message) => {
|
return;
|
||||||
if (message.type !== "agent_permission_request") {
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
if (message.payload.agentId !== agent.id) {
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
resolve(message);
|
|
||||||
}
|
}
|
||||||
);
|
if (message.payload.agentId !== agent.id) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
resolve(message);
|
||||||
|
});
|
||||||
return unsubscribe;
|
return unsubscribe;
|
||||||
});
|
});
|
||||||
|
|
||||||
await ctx.client.sendMessage(
|
|
||||||
agent.id,
|
|
||||||
"Request approval to run the command `printf \"ok\" > permission.txt`."
|
|
||||||
);
|
|
||||||
|
|
||||||
const permission = await ctx.client.waitForPermission(agent.id, 60000);
|
|
||||||
expect(permission).toBeTruthy();
|
|
||||||
expect(permission.id).toBeTruthy();
|
|
||||||
|
|
||||||
const permissionRequest = await permissionRequestPromise;
|
|
||||||
expect(permissionRequest.payload.agentId).toBe(agent.id);
|
|
||||||
|
|
||||||
const permissionResolvedPromise = waitForSignal(60000, (resolve) => {
|
const permissionResolvedPromise = waitForSignal(60000, (resolve) => {
|
||||||
const unsubscribe = ctx.client.on(
|
const unsubscribe = ctx.client.on("agent_permission_resolved", (message) => {
|
||||||
"agent_permission_resolved",
|
if (message.type !== "agent_permission_resolved") {
|
||||||
(message) => {
|
return;
|
||||||
if (message.type !== "agent_permission_resolved") {
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
if (message.payload.agentId !== agent.id) {
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
if (message.payload.requestId !== permission.id) {
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
resolve(message);
|
|
||||||
}
|
}
|
||||||
);
|
if (message.payload.agentId !== agent.id) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
resolve(message);
|
||||||
|
});
|
||||||
return unsubscribe;
|
return unsubscribe;
|
||||||
});
|
});
|
||||||
|
|
||||||
await ctx.client.respondToPermission(agent.id, permission.id, {
|
try {
|
||||||
behavior: "allow",
|
await ctx.client.sendMessage(
|
||||||
});
|
agent.id,
|
||||||
const permissionResolved = await permissionResolvedPromise;
|
[
|
||||||
expect(permissionResolved.payload.requestId).toBe(permission.id);
|
"Use your shell tool to run: `printf \"ok\" > permission.txt`.",
|
||||||
|
"This will require approval. Request permission and wait for approval before continuing.",
|
||||||
|
].join("\n")
|
||||||
|
);
|
||||||
|
|
||||||
const finalState = await ctx.client.waitForAgentIdle(agent.id, 120000);
|
const permission = await ctx.client.waitForPermission(agent.id, 60000);
|
||||||
expect(finalState.status).toBe("idle");
|
expect(permission).toBeTruthy();
|
||||||
expect(existsSync(filePath)).toBe(true);
|
expect(permission.id).toBeTruthy();
|
||||||
|
|
||||||
await ctx.client.deleteAgent(agent.id);
|
const permissionRequest = await permissionRequestPromise;
|
||||||
rmSync(cwd, { recursive: true, force: true });
|
expect(permissionRequest.payload.agentId).toBe(agent.id);
|
||||||
|
|
||||||
|
await ctx.client.respondToPermission(agent.id, permission.id, {
|
||||||
|
behavior: "allow",
|
||||||
|
});
|
||||||
|
|
||||||
|
const permissionResolved = await permissionResolvedPromise;
|
||||||
|
expect(permissionResolved.payload.requestId).toBe(permission.id);
|
||||||
|
|
||||||
|
const finalState = await ctx.client.waitForAgentIdle(agent.id, 120000);
|
||||||
|
expect(finalState.status).toBe("idle");
|
||||||
|
expect(existsSync(filePath)).toBe(true);
|
||||||
|
} finally {
|
||||||
|
// Prevent unhandled rejections if the test fails before promises resolve.
|
||||||
|
await permissionRequestPromise.catch(() => {});
|
||||||
|
await permissionResolvedPromise.catch(() => {});
|
||||||
|
await ctx.client.deleteAgent(agent.id);
|
||||||
|
rmSync(cwd, { recursive: true, force: true });
|
||||||
|
}
|
||||||
},
|
},
|
||||||
180000
|
180000
|
||||||
);
|
);
|
||||||
|
|||||||
@@ -215,9 +215,16 @@ describe("daemon E2E", () => {
|
|||||||
expect(shellCall).toBeDefined();
|
expect(shellCall).toBeDefined();
|
||||||
expect(shellCall?.name).toBe("shell");
|
expect(shellCall?.name).toBe("shell");
|
||||||
expect(shellCall?.input).toBeDefined();
|
expect(shellCall?.input).toBeDefined();
|
||||||
// Command text should be in input.command
|
// Command text should be in input.command (Codex may run setup commands like `pwd` first).
|
||||||
const shellInput = shellCall?.input as { command?: string } | undefined;
|
const shellCalls = toolCalls.filter(
|
||||||
expect(shellInput?.command).toContain("echo");
|
(tc) => tc.type === "tool_call" && tc.name === "shell"
|
||||||
|
);
|
||||||
|
expect(shellCalls.length).toBeGreaterThan(0);
|
||||||
|
expect(
|
||||||
|
shellCalls.some((tc) =>
|
||||||
|
((tc.input as { command?: string } | undefined)?.command ?? "").includes("echo")
|
||||||
|
)
|
||||||
|
).toBe(true);
|
||||||
|
|
||||||
await ctx.client.deleteAgent(agent.id);
|
await ctx.client.deleteAgent(agent.id);
|
||||||
rmSync(cwd, { recursive: true, force: true });
|
rmSync(cwd, { recursive: true, force: true });
|
||||||
|
|||||||
@@ -0,0 +1,36 @@
|
|||||||
|
import { describe, expect, test } from "vitest";
|
||||||
|
|
||||||
|
import { serializeAgentStreamEvent } from "./messages.js";
|
||||||
|
|
||||||
|
describe("serializeAgentStreamEvent", () => {
|
||||||
|
test("strips leading paseo-instructions from user_message timeline items", () => {
|
||||||
|
const event = {
|
||||||
|
type: "timeline",
|
||||||
|
provider: "claude",
|
||||||
|
item: {
|
||||||
|
type: "user_message",
|
||||||
|
text: "<paseo-instructions>\nX\n</paseo-instructions>\n\nHello",
|
||||||
|
messageId: "m1",
|
||||||
|
},
|
||||||
|
} as any;
|
||||||
|
|
||||||
|
const serialized = serializeAgentStreamEvent(event) as any;
|
||||||
|
expect(serialized.item.text).toBe("Hello");
|
||||||
|
expect(serialized.item.messageId).toBe("m1");
|
||||||
|
});
|
||||||
|
|
||||||
|
test("does not strip non-leading tags", () => {
|
||||||
|
const event = {
|
||||||
|
type: "timeline",
|
||||||
|
provider: "claude",
|
||||||
|
item: {
|
||||||
|
type: "user_message",
|
||||||
|
text: "Hello <paseo-instructions>\nX\n</paseo-instructions>",
|
||||||
|
},
|
||||||
|
} as any;
|
||||||
|
|
||||||
|
const serialized = serializeAgentStreamEvent(event) as any;
|
||||||
|
expect(serialized.item.text).toBe(event.item.text);
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
@@ -1,6 +1,7 @@
|
|||||||
import type { ManagedAgent } from "./agent/agent-manager.js";
|
import type { ManagedAgent } from "./agent/agent-manager.js";
|
||||||
import { toAgentPayload } from "./agent/agent-projections.js";
|
import { toAgentPayload } from "./agent/agent-projections.js";
|
||||||
import type { AgentStreamEvent } from "./agent/agent-sdk-types.js";
|
import type { AgentStreamEvent } from "./agent/agent-sdk-types.js";
|
||||||
|
import { stripLeadingPaseoInstructionTag } from "./agent/paseo-instructions-tag.js";
|
||||||
import type {
|
import type {
|
||||||
AgentSnapshotPayload,
|
AgentSnapshotPayload,
|
||||||
AgentStreamEventPayload,
|
AgentStreamEventPayload,
|
||||||
@@ -18,5 +19,21 @@ export function serializeAgentSnapshot(
|
|||||||
export function serializeAgentStreamEvent(
|
export function serializeAgentStreamEvent(
|
||||||
event: AgentStreamEvent
|
event: AgentStreamEvent
|
||||||
): AgentStreamEventPayload {
|
): AgentStreamEventPayload {
|
||||||
return event as AgentStreamEventPayload;
|
if (event.type !== "timeline") {
|
||||||
|
return event as AgentStreamEventPayload;
|
||||||
|
}
|
||||||
|
if (event.item.type !== "user_message") {
|
||||||
|
return event as AgentStreamEventPayload;
|
||||||
|
}
|
||||||
|
const stripped = stripLeadingPaseoInstructionTag(event.item.text);
|
||||||
|
if (stripped === event.item.text) {
|
||||||
|
return event as AgentStreamEventPayload;
|
||||||
|
}
|
||||||
|
return {
|
||||||
|
...event,
|
||||||
|
item: {
|
||||||
|
...event.item,
|
||||||
|
text: stripped,
|
||||||
|
},
|
||||||
|
} as AgentStreamEventPayload;
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -45,6 +45,7 @@ import { StreamableHTTPClientTransport } from "@modelcontextprotocol/sdk/client/
|
|||||||
import { buildProviderRegistry } from "./agent/provider-registry.js";
|
import { buildProviderRegistry } from "./agent/provider-registry.js";
|
||||||
import { AgentManager } from "./agent/agent-manager.js";
|
import { AgentManager } from "./agent/agent-manager.js";
|
||||||
import type { ManagedAgent } from "./agent/agent-manager.js";
|
import type { ManagedAgent } from "./agent/agent-manager.js";
|
||||||
|
import { injectLeadingPaseoInstructionTag } from "./agent/paseo-instructions-tag.js";
|
||||||
import { toAgentPayload } from "./agent/agent-projections.js";
|
import { toAgentPayload } from "./agent/agent-projections.js";
|
||||||
import {
|
import {
|
||||||
StructuredAgentResponseError,
|
StructuredAgentResponseError,
|
||||||
@@ -1317,9 +1318,13 @@ export class Session {
|
|||||||
const trimmedPrompt = initialPrompt?.trim();
|
const trimmedPrompt = initialPrompt?.trim();
|
||||||
if (trimmedPrompt) {
|
if (trimmedPrompt) {
|
||||||
try {
|
try {
|
||||||
|
const initialPromptWithInstructions = injectLeadingPaseoInstructionTag(
|
||||||
|
trimmedPrompt,
|
||||||
|
snapshot.config.paseoPromptInstructions
|
||||||
|
);
|
||||||
await this.handleSendAgentMessage(
|
await this.handleSendAgentMessage(
|
||||||
snapshot.id,
|
snapshot.id,
|
||||||
trimmedPrompt,
|
initialPromptWithInstructions,
|
||||||
uuidv4(),
|
uuidv4(),
|
||||||
images
|
images
|
||||||
);
|
);
|
||||||
|
|||||||
Reference in New Issue
Block a user