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:
云服务部-叶林立
2026-10-20 19:43:22 -07:00
co-authored by Devin <158243242+devin-ai-integration[bot]@users.noreply.github.com>
parent b817d66a11
commit 360d8cffc8
37 changed files with 2837 additions and 2416 deletions
+68 -5
View File
@@ -20,6 +20,8 @@ export interface ToolCatalog {
export interface RoutingRule {
whenAll?: string[];
whenAny?: string[];
/** Which sessions the rule renders for; absent means both. */
scope?: "main" | "subagent";
text: string;
}
@@ -31,6 +33,8 @@ export interface RoutingConfig {
export interface SectionsConfig {
version: 1;
order: string[];
/** Contributed sections removed from subagent (customPrompt) sessions. */
subagentExclude: string[];
}
/**
@@ -64,6 +68,14 @@ export function resolveSectionsPath(): string {
return fileURLToPath(new URL("sections.jsonc", PROMPT_CONFIG_DIR));
}
export function resolveSubagentPreamblePath(): string {
return fileURLToPath(new URL("subagent/preamble.md", PROMPT_CONFIG_DIR));
}
export function resolveSubagentAgentsDir(): string {
return fileURLToPath(new URL("subagent/agents/", PROMPT_CONFIG_DIR));
}
/**
* Strip `//` and `/* ... *\/` comments while respecting string literals, so a
* `//` inside a quoted value (for example `"https://…"`) is never removed.
@@ -185,7 +197,7 @@ function validateToolCatalog(raw: unknown): ToolCatalog {
return { version: 1, tools };
}
const ROUTING_RULE_KEYS = new Set(["whenAll", "whenAny", "text"]);
const ROUTING_RULE_KEYS = new Set(["whenAll", "whenAny", "scope", "text"]);
function validateRoutingRule(index: number, raw: unknown): RoutingRule {
if (!isPlainObject(raw)) {
@@ -205,10 +217,13 @@ function validateRoutingRule(index: number, raw: unknown): RoutingRule {
if ("whenAny" in raw && !isStringArray(raw.whenAny)) {
fail("routing.jsonc", `rule #${index}.whenAny must be an array of strings`);
}
if ("scope" in raw && raw.scope !== "main" && raw.scope !== "subagent") {
fail("routing.jsonc", `rule #${index}.scope must be "main" or "subagent"`);
}
return raw as RoutingRule;
}
function validateRoutingConfig(raw: unknown): RoutingConfig {
export function validateRoutingConfig(raw: unknown): RoutingConfig {
if (!isPlainObject(raw) || raw.version !== 1 || !Array.isArray(raw.rules)) {
fail("routing.jsonc", 'expected { "version": 1, "rules": [ ... ] }');
}
@@ -220,7 +235,7 @@ function validateRoutingConfig(raw: unknown): RoutingConfig {
const SECTION_NAME_PATTERN = /^[a-z][a-z0-9_-]*$/;
function validateSectionsConfig(raw: unknown): SectionsConfig {
export function validateSectionsConfig(raw: unknown): SectionsConfig {
if (!isPlainObject(raw) || raw.version !== 1 || !isStringArray(raw.order)) {
fail("sections.jsonc", 'expected { "version": 1, "order": [ ... ] }');
}
@@ -229,7 +244,19 @@ function validateSectionsConfig(raw: unknown): SectionsConfig {
fail("sections.jsonc", `"${name}" is not a valid section name`);
}
}
return { version: 1, order: raw.order };
if ("subagentExclude" in raw && !isStringArray(raw.subagentExclude)) {
fail("sections.jsonc", `"subagentExclude" must be an array of section names`);
}
for (const name of raw.subagentExclude ?? []) {
if (!SECTION_NAME_PATTERN.test(name) || name === "preamble") {
fail("sections.jsonc", `"subagentExclude" entry "${name}" is not a valid section name`);
}
}
return {
version: 1,
order: raw.order,
subagentExclude: isStringArray(raw.subagentExclude) ? raw.subagentExclude : [],
};
}
export function loadToolCatalog(): ToolCatalog {
@@ -262,6 +289,34 @@ export function readPreamble(): string | undefined {
return trimmed.length > 0 ? trimmed : undefined;
}
/**
* Read the bundle's subagent base preamble (`config/prompt/subagent/preamble.md`),
* re-read on each call so the file is never cached between spawns.
* Blank or missing → `undefined`, which keeps pi-subagents' upstream composition.
*/
export function readSubagentPreamble(): string | undefined {
let raw: string;
try {
raw = readFileSync(resolveSubagentPreamblePath(), "utf8");
} catch {
return undefined;
}
const trimmed = raw.replace(/\s+$/, "");
return trimmed.length > 0 ? trimmed : undefined;
}
/**
* The `<active_agent name="..."/>` tag pi-subagents stamps into every child
* prompt — tested ONLY against `systemPromptOptions.customPrompt`, never the
* rendered prompt: a main session may contain the literal tag via project
* docs and must still be treated as main.
*/
const ACTIVE_AGENT_TAG = /<active_agent\s+name=["'][^"']+["'][^>]*>/i;
export function isSubagentCustomPrompt(customPrompt: unknown): boolean {
return typeof customPrompt === "string" && ACTIVE_AGENT_TAG.test(customPrompt);
}
/**
* Apply the tool catalog to the mutable prompt options: a `snippet` string
* replaces the tool's `<tools>` line (`null` hides it), `guidelines` replaces
@@ -302,11 +357,19 @@ export function applyToolCatalog(options: MutablePromptOptions, catalog: ToolCat
* `whenAny` tool is active (a rule with neither always applies). An absent
* `selectedTools` means every tool counts as active, matching the previous
* tool-routing behavior.
*
* `audience` filters `scope`: rules without a scope render for both "main"
* and "subagent" sessions, scoped rules only for their own audience.
*/
export function renderRouting(rules: RoutingRule[], selectedTools: string[] | undefined): string {
export function renderRouting(
rules: RoutingRule[],
selectedTools: string[] | undefined,
audience: "main" | "subagent" = "main",
): string {
const isActive = (name: string) => selectedTools === undefined || selectedTools.includes(name);
const lines: string[] = [];
for (const rule of rules) {
if (rule.scope !== undefined && rule.scope !== audience) continue;
if (rule.whenAll !== undefined && !rule.whenAll.every(isActive)) continue;
if (rule.whenAny !== undefined && !rule.whenAny.some(isActive)) continue;
lines.push(`- ${rule.text}`);