mirror of
https://bitbucket.org/siakitem/my-pi.git
synced 2026-10-26 18:55:22 +00:00
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>
This commit is contained in:
co-authored by
Devin <158243242+devin-ai-integration[bot]@users.noreply.github.com>
parent
b817d66a11
commit
360d8cffc8
+158
-1
@@ -9,6 +9,7 @@ import { fileURLToPath } from "node:url";
|
||||
|
||||
import {
|
||||
applyToolCatalog,
|
||||
isSubagentCustomPrompt,
|
||||
loadRoutingRules,
|
||||
loadSectionsConfig,
|
||||
loadToolCatalog,
|
||||
@@ -18,6 +19,8 @@ import {
|
||||
resolvePreamblePath,
|
||||
spliceCover,
|
||||
stripJsoncComments,
|
||||
validateRoutingConfig,
|
||||
validateSectionsConfig,
|
||||
type MutablePromptOptions,
|
||||
type RoutingRule,
|
||||
} from "../extensions/system-prompt-config.ts";
|
||||
@@ -74,6 +77,40 @@ test("config/prompt files load with the expected shapes", () => {
|
||||
"chrome_profile_bridge",
|
||||
"tool_routing",
|
||||
]);
|
||||
assert.deepEqual(sections.subagentExclude, ["memory_policy", "chrome_profile_bridge"]);
|
||||
});
|
||||
|
||||
test("routing scope validation: main/subagent accepted, other values rejected", () => {
|
||||
assert.doesNotThrow(() =>
|
||||
validateRoutingConfig({
|
||||
version: 1,
|
||||
rules: [
|
||||
{ text: "both" },
|
||||
{ scope: "main", text: "m" },
|
||||
{ scope: "subagent", text: "s" },
|
||||
],
|
||||
}),
|
||||
);
|
||||
assert.throws(
|
||||
() => validateRoutingConfig({ version: 1, rules: [{ scope: "everyone", text: "x" }] }),
|
||||
/scope must be "main" or "subagent"/,
|
||||
);
|
||||
});
|
||||
|
||||
test("sections.jsonc validation: subagentExclude defaults to [] and rejects bad shapes", () => {
|
||||
assert.deepEqual(validateSectionsConfig({ version: 1, order: ["tool_routing"] }), {
|
||||
version: 1,
|
||||
order: ["tool_routing"],
|
||||
subagentExclude: [],
|
||||
});
|
||||
assert.throws(
|
||||
() => validateSectionsConfig({ version: 1, order: [], subagentExclude: "memory_policy" }),
|
||||
/subagentExclude.*array of section names/,
|
||||
);
|
||||
assert.throws(
|
||||
() => validateSectionsConfig({ version: 1, order: [], subagentExclude: ["Bad Name"] }),
|
||||
/not a valid section name/,
|
||||
);
|
||||
});
|
||||
|
||||
test("readPreamble reads the bundle preamble from config/prompt/", () => {
|
||||
@@ -185,6 +222,40 @@ test("renderRouting honours whenAll, whenAny and unconditional rules", () => {
|
||||
assert.equal(renderRouting([], ["a"]), "");
|
||||
});
|
||||
|
||||
test("renderRouting filters scoped rules by audience, unscoped rules render for both", () => {
|
||||
const rules: RoutingRule[] = [
|
||||
{ text: "both audiences" },
|
||||
{ scope: "main", text: "main only" },
|
||||
{ scope: "subagent", text: "subagent only" },
|
||||
];
|
||||
|
||||
assert.equal(renderRouting(rules, undefined, "main"), "- both audiences\n- main only");
|
||||
assert.equal(renderRouting(rules, undefined, "subagent"), "- both audiences\n- subagent only");
|
||||
// The omitted-audience default stays "main" for callers that predate scopes.
|
||||
assert.equal(renderRouting(rules, undefined), "- both audiences\n- main only");
|
||||
});
|
||||
|
||||
test("isSubagentCustomPrompt matches the <active_agent> contract only on customPrompt", () => {
|
||||
assert.ok(isSubagentCustomPrompt('a\n<active_agent name="Explore"/>'));
|
||||
assert.ok(isSubagentCustomPrompt("<active_agent name='x'/>"));
|
||||
assert.ok(!isSubagentCustomPrompt("plain main-session prompt"));
|
||||
assert.ok(!isSubagentCustomPrompt(undefined));
|
||||
assert.ok(!isSubagentCustomPrompt(42));
|
||||
});
|
||||
|
||||
test("bundled ssh_connect rules differ per audience", () => {
|
||||
const routing = loadRoutingRules();
|
||||
const tools = ["ssh_connect", "ssh_bash"];
|
||||
|
||||
const main = renderRouting(routing.rules, tools, "main");
|
||||
assert.match(main, /the user explicitly names an imported host/);
|
||||
assert.doesNotMatch(main, /the delegated task explicitly names/);
|
||||
|
||||
const subagent = renderRouting(routing.rules, tools, "subagent");
|
||||
assert.match(subagent, /the delegated task explicitly names an imported host/);
|
||||
assert.doesNotMatch(subagent, /the user explicitly names an imported host/);
|
||||
});
|
||||
|
||||
test("bundled routing rules select the same guidance the retired tool-routing extension emitted", () => {
|
||||
const routing = loadRoutingRules();
|
||||
|
||||
@@ -275,12 +346,18 @@ test("spliceCover degenerates to the preamble when the prompt has no sections",
|
||||
|
||||
interface FakePromptOptions extends MutablePromptOptions {
|
||||
forceSystemPrompt?: string;
|
||||
customPrompt?: string;
|
||||
}
|
||||
|
||||
/** Simplified re-render of Pi's prompt from the mutable options object. */
|
||||
function renderPiPrompt(options: FakePromptOptions): string {
|
||||
if (options.forceSystemPrompt !== undefined) return options.forceSystemPrompt;
|
||||
const parts = ["PI DEFAULT PREAMBLE"];
|
||||
// Like the real host: a non-empty customPrompt owns the preamble section.
|
||||
const preamble =
|
||||
typeof options.customPrompt === "string" && options.customPrompt.length > 0
|
||||
? options.customPrompt
|
||||
: "PI DEFAULT PREAMBLE";
|
||||
const parts = [preamble];
|
||||
const snippets = Object.entries(options.toolSnippets ?? {});
|
||||
if (snippets.length > 0) {
|
||||
parts.push(`<tools>\n${snippets.map(([n, s]) => `- ${n}: ${s}`).join("\n")}\n</tools>`);
|
||||
@@ -378,6 +455,86 @@ test("composer degrades to text splicing when an earlier extension froze the pro
|
||||
);
|
||||
});
|
||||
|
||||
test("composer keeps a customPrompt-owned preamble (subagent / SYSTEM.md) intact", () => {
|
||||
const composer = createSystemPromptComposer();
|
||||
const customPrompt = 'You are the Explore agent.\n<active_agent name="Explore"/>';
|
||||
const options: FakePromptOptions = {
|
||||
customPrompt,
|
||||
selectedTools: ["read"],
|
||||
toolSnippets: { read: "reads files" },
|
||||
};
|
||||
const { ctx } = makeUiCtx();
|
||||
const event = {
|
||||
systemPromptOptions: options,
|
||||
systemPrompt: `${customPrompt}\n\n<tools>\n- read: reads files\n</tools>`,
|
||||
};
|
||||
|
||||
const result = composer.handleBeforeAgentStart(event as never, ctx);
|
||||
const output = result?.systemPrompt ?? "";
|
||||
|
||||
assert.ok(output.startsWith(customPrompt), "the agent's own preamble is kept verbatim");
|
||||
assert.ok(
|
||||
output.includes('<active_agent name="Explore"/>'),
|
||||
"the <active_agent> identity the permission system reads survives",
|
||||
);
|
||||
assert.ok(
|
||||
!output.includes(BUNDLE_PREAMBLE),
|
||||
"the bundle preamble must not be spliced over a customPrompt-owned preamble",
|
||||
);
|
||||
assert.match(output, /<tool_routing>/, "routing still degrades to an appended section");
|
||||
});
|
||||
|
||||
test("composer treats an <active_agent> customPrompt as a subagent session", () => {
|
||||
const composer = createSystemPromptComposer();
|
||||
const customPrompt =
|
||||
'You are a subagent.\n<active_agent name="Explore"/>\n# Environment\nWorking directory: /work';
|
||||
const options: FakePromptOptions = {
|
||||
customPrompt,
|
||||
selectedTools: ["read", "ssh_connect"],
|
||||
toolSnippets: { read: "reads files", ssh_connect: "connects" },
|
||||
sections: {
|
||||
memory_policy: "memory guidance for main sessions",
|
||||
chrome_profile_bridge: "chrome bridge preamble",
|
||||
remote_ssh: "remote ssh preamble",
|
||||
},
|
||||
};
|
||||
const { ctx } = makeUiCtx();
|
||||
|
||||
const result = composer.handleBeforeAgentStart(makeEvent(options) as never, ctx);
|
||||
const output = result?.systemPrompt ?? "";
|
||||
|
||||
assert.ok(output.startsWith(customPrompt), "the child preamble stays untouched");
|
||||
assert.doesNotMatch(output, /<memory_policy>/, "subagentExclude removes memory_policy");
|
||||
assert.doesNotMatch(output, /<chrome_profile_bridge>/, "subagentExclude removes chrome_profile_bridge");
|
||||
assert.match(output, /<remote_ssh>/, "unexcluded sections are kept");
|
||||
assert.match(output, /the delegated task explicitly names an imported host/, "subagent-scope routing renders");
|
||||
assert.doesNotMatch(output, /the user explicitly names an imported host/, "main-scope routing stays out");
|
||||
});
|
||||
|
||||
test("a main session keeps excluded sections and main-scope routing even with a literal <active_agent> tag", () => {
|
||||
const composer = createSystemPromptComposer();
|
||||
// The tag appears inside a contributed section (project docs can carry the
|
||||
// literal text) but there is no customPrompt — this is a main session.
|
||||
const options: FakePromptOptions = {
|
||||
selectedTools: ["read", "ssh_connect"],
|
||||
toolSnippets: { read: "reads files", ssh_connect: "connects" },
|
||||
sections: {
|
||||
memory_policy: 'docs mention <active_agent name="Guide"/>',
|
||||
chrome_profile_bridge: "chrome bridge preamble",
|
||||
},
|
||||
};
|
||||
const { ctx } = makeUiCtx();
|
||||
|
||||
const result = composer.handleBeforeAgentStart(makeEvent(options) as never, ctx);
|
||||
const output = result?.systemPrompt ?? "";
|
||||
|
||||
assert.ok(output.startsWith(BUNDLE_PREAMBLE), "the bundle preamble still splices for a main session");
|
||||
assert.match(output, /<memory_policy>/, "main sessions are unaffected by subagentExclude");
|
||||
assert.match(output, /<chrome_profile_bridge>/);
|
||||
assert.match(output, /the user explicitly names an imported host/, "main-scope routing renders");
|
||||
assert.doesNotMatch(output, /the delegated task explicitly names/, "subagent-scope routing stays out");
|
||||
});
|
||||
|
||||
test("composer falls back to text appends when the host exposes no prompt options", () => {
|
||||
const composer = createSystemPromptComposer();
|
||||
const { ctx } = makeUiCtx();
|
||||
|
||||
Reference in New Issue
Block a user