Files
my-pi/pi-subagents/test/prompts.test.ts
T
云服务部-叶林立andDevin <158243242+devin-ai-integration[bot]@users.noreply.github.com> 360d8cffc8 feat(pi-subagents): 重新启用子 Agent 并使用独立的子 Agent 提示词
- 根包恢复 file:./pi-subagents 依赖与 extensions/subagents.ts 加载
  (排在 permission-system 之后、Tool Search 之前),并刷新根锁文件;
  Agent / get_subagent_result / steer_subagent 权限设为 allow,
  子 Agent 内的实际操作仍逐个经权限系统并转发主会话。
- 新增 config/prompt/subagent/:执行者角色 preamble 与组合包自带的
  general-purpose / Explore / Plan 定义(替换上游内置,去掉 bash find
  冲突规则与写死的 Haiku 模型)。
- pi-subagents 新增 basePrompt / bundledAgentsDir / excludedExtensionPaths
  选项:子 Agent 不再嵌入父会话提示词,身份标记置于开头;子会话排除
  pi-ask-user。未传选项时保持上游行为。
- system-prompt.ts:customPrompt 自带开场白时不再覆盖(修复子 Agent 角色
  与 <active_agent> 身份被抹掉);仅凭 customPrompt 上的 <active_agent>
  识别子会话,routing 规则支持 scope,sections 支持 subagentExclude
  (子会话去掉 memory_policy 与 chrome_profile_bridge)。
- pi-hermes-memory:子会话只读,记忆/skill 写入工具返回错误,后台整理、
  纠错检测、会话 flush 与索引写入全部跳过。

Generated with [Devin](https://devin.ai)

Co-Authored-By: Devin <158243242+devin-ai-integration[bot]@users.noreply.github.com>
2026-10-20 19:43:22 -07:00

549 lines
20 KiB
TypeScript

import { beforeEach, describe, expect, it } from "vitest";
import { getAgentConfig, registerAgents } from "../src/agent-types.js";
import { buildAgentPrompt } from "../src/prompts.js";
import type { AgentConfig, EnvInfo } from "../src/types.js";
const env: EnvInfo = {
isGitRepo: true,
branch: "main",
platform: "darwin",
};
const envNoGit: EnvInfo = {
isGitRepo: false,
branch: "",
platform: "linux",
};
// Initialize default agents
beforeEach(() => {
registerAgents(new Map());
});
function getDefaultConfig(name: string): AgentConfig {
return getAgentConfig(name)!;
}
describe("buildAgentPrompt", () => {
it("includes cwd and git info", () => {
const config = getDefaultConfig("general-purpose");
const prompt = buildAgentPrompt(config, "/workspace", env);
expect(prompt).toContain("/workspace");
expect(prompt).toContain("Branch: main");
expect(prompt).toContain("darwin");
});
it("handles non-git repos", () => {
const config = getDefaultConfig("Explore");
const prompt = buildAgentPrompt(config, "/workspace", envNoGit);
expect(prompt).toContain("Not a git repository");
expect(prompt).not.toContain("Branch:");
});
it("Explore prompt is read-only", () => {
const config = getDefaultConfig("Explore");
const prompt = buildAgentPrompt(config, "/workspace", env);
expect(prompt).toContain("READ-ONLY");
expect(prompt).toContain("file search specialist");
});
it("Plan prompt is read-only", () => {
const config = getDefaultConfig("Plan");
const prompt = buildAgentPrompt(config, "/workspace", env);
expect(prompt).toContain("READ-ONLY");
expect(prompt).toContain("software architect");
});
it("general-purpose uses append mode (parent twin)", () => {
const config = getDefaultConfig("general-purpose");
const parentPrompt = "You are a parent coding agent with full powers.";
const prompt = buildAgentPrompt(config, "/workspace", env, parentPrompt);
expect(prompt).toContain("parent coding agent with full powers");
expect(prompt).toContain("<sub_agent_context>");
expect(prompt).not.toContain("<inherited_system_prompt>");
expect(prompt).not.toContain("READ-ONLY");
// Empty systemPrompt means no <agent_instructions> section
expect(prompt).not.toContain("<agent_instructions>");
});
it("general-purpose without parent prompt falls back to generic base", () => {
const config = getDefaultConfig("general-purpose");
const prompt = buildAgentPrompt(config, "/workspace", env);
expect(prompt).toContain("general-purpose coding agent");
expect(prompt).not.toContain("READ-ONLY");
});
it("append mode with parent prompt includes parent + custom instructions", () => {
const config: AgentConfig = {
name: "appender",
description: "Appender",
builtinToolNames: [],
extensions: true,
skills: true,
systemPrompt: "Extra custom instructions here.",
promptMode: "append",
inheritContext: false,
runInBackground: false,
isolated: false,
};
const parentPrompt = "You are a parent coding agent with special powers.";
const prompt = buildAgentPrompt(config, "/workspace", env, parentPrompt);
expect(prompt).toContain("/workspace");
expect(prompt).toContain("parent coding agent with special powers");
expect(prompt).toContain("<sub_agent_context>");
expect(prompt).not.toContain("<inherited_system_prompt>");
expect(prompt).toContain("<agent_instructions>");
expect(prompt).toContain("Extra custom instructions here.");
});
it("append mode without parent prompt falls back to generic base", () => {
const config: AgentConfig = {
name: "appender",
description: "Appender",
builtinToolNames: [],
extensions: true,
skills: true,
systemPrompt: "Extra custom instructions here.",
promptMode: "append",
inheritContext: false,
runInBackground: false,
isolated: false,
};
const prompt = buildAgentPrompt(config, "/workspace", env);
expect(prompt).toContain("/workspace");
expect(prompt).toContain("general-purpose coding agent");
expect(prompt).toContain("Extra custom instructions here.");
});
it("append mode with empty systemPrompt is a pure parent clone", () => {
const config: AgentConfig = {
name: "clone",
description: "Clone",
builtinToolNames: [],
extensions: true,
skills: true,
systemPrompt: "",
promptMode: "append",
inheritContext: false,
runInBackground: false,
isolated: false,
};
const parentPrompt = "You are a parent coding agent.";
const prompt = buildAgentPrompt(config, "/workspace", env, parentPrompt);
expect(prompt).toContain("parent coding agent");
expect(prompt).toContain("<sub_agent_context>");
expect(prompt).not.toContain("<inherited_system_prompt>");
expect(prompt).not.toContain("<agent_instructions>");
});
it("replace mode uses config systemPrompt directly", () => {
const config: AgentConfig = {
name: "custom",
description: "Custom",
builtinToolNames: [],
extensions: true,
skills: true,
systemPrompt: "You are a specialized agent.",
promptMode: "replace",
inheritContext: false,
runInBackground: false,
isolated: false,
};
const prompt = buildAgentPrompt(config, "/workspace", env);
expect(prompt).toContain("You are a specialized agent.");
expect(prompt).toContain("/workspace");
expect(prompt).toContain("You are a pi coding agent sub-agent");
});
it("replace mode ignores parent prompt", () => {
const config: AgentConfig = {
name: "standalone",
description: "Standalone",
builtinToolNames: [],
extensions: true,
skills: true,
systemPrompt: "You are a standalone agent.",
promptMode: "replace",
inheritContext: false,
runInBackground: false,
isolated: false,
};
const prompt = buildAgentPrompt(config, "/workspace", env, "SECRET parent prompt content");
expect(prompt).toContain("You are a standalone agent.");
expect(prompt).not.toContain("SECRET parent prompt content");
expect(prompt).not.toContain("<sub_agent_context>");
});
it("append mode bridge contains tool reminders", () => {
const config = getDefaultConfig("general-purpose");
const prompt = buildAgentPrompt(config, "/workspace", env, "Parent prompt.");
expect(prompt).toContain("Use the read tool instead of cat");
expect(prompt).toContain("Use the edit tool instead of sed");
expect(prompt).toContain("Use the grep tool instead of");
});
it("append mode without parent prompt still has bridge", () => {
const config: AgentConfig = {
name: "no-parent",
description: "No parent",
builtinToolNames: [],
extensions: true,
skills: true,
systemPrompt: "Extra stuff.",
promptMode: "append",
inheritContext: false,
runInBackground: false,
isolated: false,
};
const prompt = buildAgentPrompt(config, "/workspace", env);
expect(prompt).toContain("<sub_agent_context>");
expect(prompt).not.toContain("<inherited_system_prompt>");
expect(prompt).toContain("Use the read tool instead of cat");
expect(prompt).toContain("general-purpose coding agent");
expect(prompt).toContain("Extra stuff.");
});
it("injects memory block in replace mode", () => {
const config: AgentConfig = {
name: "mem-agent",
description: "Memory Agent",
builtinToolNames: [],
extensions: true,
skills: true,
systemPrompt: "You are a memory agent.",
promptMode: "replace",
inheritContext: false,
runInBackground: false,
isolated: false,
};
const extras = { memoryBlock: "# Agent Memory\nYou have persistent memory at /tmp/mem/" };
const prompt = buildAgentPrompt(config, "/workspace", env, undefined, extras);
expect(prompt).toContain("You are a memory agent.");
expect(prompt).toContain("Agent Memory");
expect(prompt).toContain("persistent memory");
});
it("injects memory block in append mode", () => {
const config: AgentConfig = {
name: "mem-append",
description: "Memory Append",
builtinToolNames: [],
extensions: true,
skills: true,
systemPrompt: "Custom instructions.",
promptMode: "append",
inheritContext: false,
runInBackground: false,
isolated: false,
};
const extras = { memoryBlock: "# Agent Memory\nPersistent memory here." };
const prompt = buildAgentPrompt(config, "/workspace", env, "Parent prompt.", extras);
expect(prompt).toContain("<sub_agent_context>");
expect(prompt).toContain("Agent Memory");
expect(prompt).toContain("Custom instructions.");
});
it("injects preloaded skill blocks", () => {
const config: AgentConfig = {
name: "skill-agent",
description: "Skill Agent",
builtinToolNames: [],
extensions: true,
skills: true,
systemPrompt: "You are a skill agent.",
promptMode: "replace",
inheritContext: false,
runInBackground: false,
isolated: false,
};
const extras = {
skillBlocks: [
{ name: "api-conventions", content: "Use REST endpoints." },
{ name: "error-handling", content: "Handle errors gracefully." },
],
};
const prompt = buildAgentPrompt(config, "/workspace", env, undefined, extras);
expect(prompt).toContain("Preloaded Skill: api-conventions");
expect(prompt).toContain("Use REST endpoints.");
expect(prompt).toContain("Preloaded Skill: error-handling");
expect(prompt).toContain("Handle errors gracefully.");
});
it("injects both memory and skills", () => {
const config: AgentConfig = {
name: "full-agent",
description: "Full Agent",
builtinToolNames: [],
extensions: true,
skills: true,
systemPrompt: "Full agent.",
promptMode: "replace",
inheritContext: false,
runInBackground: false,
isolated: false,
};
const extras = {
memoryBlock: "# Memory\nRemember this.",
skillBlocks: [{ name: "skill1", content: "Skill content." }],
};
const prompt = buildAgentPrompt(config, "/workspace", env, undefined, extras);
expect(prompt).toContain("# Memory");
expect(prompt).toContain("Preloaded Skill: skill1");
});
it("no extras means no extra sections", () => {
const config: AgentConfig = {
name: "plain",
description: "Plain",
builtinToolNames: [],
extensions: true,
skills: true,
systemPrompt: "Plain agent.",
promptMode: "replace",
inheritContext: false,
runInBackground: false,
isolated: false,
};
const prompt = buildAgentPrompt(config, "/workspace", env);
expect(prompt).not.toContain("Agent Memory");
expect(prompt).not.toContain("Preloaded Skill");
});
describe("active_agent tag", () => {
it("tag is present at start of prompt in replace mode", () => {
const config: AgentConfig = {
name: "my-agent",
description: "Test",
builtinToolNames: [],
extensions: true,
skills: true,
systemPrompt: "You are a test agent.",
promptMode: "replace",
inheritContext: false,
runInBackground: false,
isolated: false,
};
const prompt = buildAgentPrompt(config, "/workspace", env);
expect(prompt).toMatch(/^<active_agent name="my-agent"\/>/);
});
it("tag follows the cacheable inherited prefix in append mode", () => {
const config: AgentConfig = {
name: "my-agent",
description: "Test",
builtinToolNames: [],
extensions: true,
skills: true,
systemPrompt: "Custom instructions.",
promptMode: "append",
inheritContext: false,
runInBackground: false,
isolated: false,
};
const prompt = buildAgentPrompt(config, "/workspace", env, "Parent prompt.");
// Parent prompt must form the verbatim, cacheable byte prefix.
expect(prompt.startsWith("Parent prompt.")).toBe(true);
// The varying tag follows the static <sub_agent_context> bridge.
const ctxIdx = prompt.indexOf("<sub_agent_context>");
const tagIdx = prompt.indexOf('<active_agent name="my-agent"/>');
expect(ctxIdx).toBeGreaterThan(-1);
expect(tagIdx).toBeGreaterThan(ctxIdx);
});
it("tag uses agent name verbatim", () => {
const config: AgentConfig = {
name: "Some Agent With Spaces",
description: "Test",
builtinToolNames: [],
extensions: true,
skills: true,
systemPrompt: "Test.",
promptMode: "replace",
inheritContext: false,
runInBackground: false,
isolated: false,
};
const prompt = buildAgentPrompt(config, "/workspace", env);
expect(prompt).toContain('<active_agent name="Some Agent With Spaces"/>');
});
it("tag appears before the env block in both modes", () => {
for (const promptMode of ["replace", "append"] as const) {
const config: AgentConfig = {
name: "test-agent",
description: "Test",
builtinToolNames: [],
extensions: true,
skills: true,
systemPrompt: "Test.",
promptMode,
inheritContext: false,
runInBackground: false,
isolated: false,
};
const prompt = buildAgentPrompt(config, "/workspace", env, "Parent.");
const tagIndex = prompt.indexOf('<active_agent name="test-agent"/>');
const envIndex = prompt.indexOf("# Environment");
expect(tagIndex).toBeLessThan(envIndex);
}
});
});
// #187: the inherited parent prompt names the main checkout as cwd, so a
// worktree agent needs to be told which of the two paths is really its own.
describe("worktree isolation block", () => {
function worktreeConfig(promptMode: "append" | "replace"): AgentConfig {
return {
name: "test-agent",
description: "Test",
builtinToolNames: [],
extensions: true,
skills: true,
systemPrompt: "Custom instructions.",
promptMode,
inheritContext: false,
runInBackground: false,
isolated: false,
};
}
it("is absent without a worktree base", () => {
for (const promptMode of ["replace", "append"] as const) {
const prompt = buildAgentPrompt(worktreeConfig(promptMode), "/wt/copy", env, "Parent.", {});
expect(prompt).not.toContain("<worktree_isolation>");
}
});
it("names the parent checkout and follows the env block in both modes", () => {
for (const promptMode of ["replace", "append"] as const) {
const prompt = buildAgentPrompt(worktreeConfig(promptMode), "/wt/copy", env, "Parent.", {
worktreeBase: "/repo",
});
expect(prompt).toContain("isolated git worktree copy of /repo");
expect(prompt).toContain("never in /repo, even if other instructions name that path");
expect(prompt.indexOf("<worktree_isolation>")).toBeGreaterThan(prompt.indexOf("Working directory: /wt/copy"));
}
});
it("stays out of the cacheable inherited prefix", () => {
const prompt = buildAgentPrompt(worktreeConfig("append"), "/wt/copy", env, "Parent prompt.", {
worktreeBase: "/repo",
});
expect(prompt.startsWith("Parent prompt.")).toBe(true);
expect(prompt.indexOf("<worktree_isolation>")).toBeGreaterThan(prompt.indexOf("<sub_agent_context>"));
});
});
describe("host-owned base prompt", () => {
const BASE = "You are a subagent operating inside pi. The human user does not see your messages.";
function baseConfig(promptMode: "append" | "replace", systemPrompt = "Custom instructions."): AgentConfig {
return {
name: "bundled-agent",
description: "Bundled",
builtinToolNames: [],
extensions: true,
skills: true,
systemPrompt,
promptMode,
inheritContext: false,
runInBackground: false,
isolated: false,
};
}
it("append mode: replaces parent prompt and drops the bridge, keeps env/instructions/tag", () => {
const prompt = buildAgentPrompt(
baseConfig("append"),
"/workspace",
env,
"SECRET parent prompt content",
undefined,
BASE,
);
// No inherited parent prefix to keep cacheable, so the tag leads the
// prompt — same position as replace mode — and lets early session hooks
// identify the child before before_agent_start.
expect(prompt.startsWith('<active_agent name="bundled-agent"/>')).toBe(true);
expect(prompt).not.toContain("SECRET parent prompt content");
expect(prompt).not.toContain("<sub_agent_context>");
expect(prompt).not.toContain("You are a pi coding agent sub-agent");
expect(prompt).toContain("Working directory: /workspace");
expect(prompt).toContain("<agent_instructions>");
expect(prompt).toContain("Custom instructions.");
expect(prompt.match(/<active_agent name="bundled-agent"\/>/g)).toHaveLength(1);
const order = [
'<active_agent name="bundled-agent"/>',
BASE,
"# Environment",
"<agent_instructions>",
].map((s) => prompt.indexOf(s));
expect(order.every((i) => i >= 0)).toBe(true);
expect(order).toEqual([...order].sort((a, b) => a - b));
});
it("replace mode: drops the sub-agent header, keeps env + config prompt + tag", () => {
const prompt = buildAgentPrompt(
baseConfig("replace", "You are a read-only search agent."),
"/workspace",
env,
"SECRET parent prompt content",
undefined,
BASE,
);
expect(prompt).not.toContain("You are a pi coding agent sub-agent");
expect(prompt).not.toContain("SECRET parent prompt content");
expect(prompt).not.toContain("<sub_agent_context>");
expect(prompt).toContain("Working directory: /workspace");
expect(prompt).toContain("You are a read-only search agent.");
expect(prompt.match(/<active_agent name="bundled-agent"\/>/g)).toHaveLength(1);
// The tag leads, then base, then env, then the config's system prompt.
const order = [
'<active_agent name="bundled-agent"/>',
BASE,
"# Environment",
"You are a read-only search agent.",
].map((s) => prompt.indexOf(s));
expect(order.every((i) => i >= 0)).toBe(true);
expect(order).toEqual([...order].sort((a, b) => a - b));
});
it("keeps worktree and extras blocks", () => {
const prompt = buildAgentPrompt(
baseConfig("append"),
"/wt/copy",
env,
undefined,
{ worktreeBase: "/repo", memoryBlock: "# Agent Memory\nRemember this." },
BASE,
);
expect(prompt).toContain("isolated git worktree copy of /repo");
expect(prompt).toContain("# Agent Memory");
expect(prompt).not.toContain("<sub_agent_context>");
});
it("empty agent systemPrompt yields no agent_instructions in append mode", () => {
const prompt = buildAgentPrompt(
baseConfig("append", ""),
"/workspace",
env,
"Parent.",
undefined,
BASE,
);
expect(prompt).not.toContain("<agent_instructions>");
});
it("blank base keeps upstream composition byte-for-byte", () => {
for (const promptMode of ["append", "replace"] as const) {
const config = baseConfig(promptMode);
for (const blank of [undefined, "", " \n "]) {
expect(buildAgentPrompt(config, "/workspace", env, "Parent.", undefined, blank)).toBe(
buildAgentPrompt(config, "/workspace", env, "Parent."),
);
}
}
});
});
});