mirror of
https://bitbucket.org/siakitem/my-pi.git
synced 2026-10-26 10:45:22 +00:00
多个扩展各自整段改写系统提示词并按加载顺序接力,任一扩展定死提示词后 其余只能做字符串手术,且各上游的工具说明与组合包实际工具格局冲突。 - 新增 extensions/system-prompt.ts 作为唯一出口(加载列表最后),按 config/prompt/(preamble.md、tools.jsonc、routing.jsonc、sections.jsonc) 组合开场白、工具简介/规则与 <tool_routing> 分区;替代原封面与 tool-routing 扩展,/dump-system-prompt 输出最终提示词。 - pi-chrome、pi-ssh、pi-hermes-memory 改为写命名分区,pi-rtk-optimizer 改写 promptGuidelines,pi-permission-system 直接过滤结构化 skills, 均不再返回 systemPrompt;pi-chrome 未授权时不再每轮冻结提示词。 - 新增 prompt-guard 代理加载第三方扩展(Hashline、FFF、LSP、 context-mode):拦截直接改写提示词并按目录覆盖工具说明;context-mode 去掉与组合包路由冲突的锚点段,保留记忆与恢复快照。 - 消除冲突:bash 简介不再建议 grep/find;Hashline edit 不再称可用 grep 锚点、不再介绍已关闭的 replace_text;context-mode 读写/网页路由提示。 - 守门测试:工具目录覆盖率、禁用用法检查、context-mode 上游原文锁定; AGENTS.md 新增「系统提示词单一所有权」。 Generated with [Devin](https://devin.ai) Co-Authored-By: Devin <158243242+devin-ai-integration[bot]@users.noreply.github.com>
163 lines
5.8 KiB
TypeScript
163 lines
5.8 KiB
TypeScript
import type { BeforeAgentStartEvent, ExtensionAPI, ExtensionContext } from "@earendil-works/pi-coding-agent";
|
|
import { mkdir, writeFile } from "node:fs/promises";
|
|
import { dirname, resolve } from "node:path";
|
|
|
|
import {
|
|
applyToolCatalog,
|
|
loadRoutingRules,
|
|
loadSectionsConfig,
|
|
loadToolCatalog,
|
|
orderSections,
|
|
readPreamble,
|
|
renderRouting,
|
|
spliceCover,
|
|
type MutablePromptOptions,
|
|
type RoutingConfig,
|
|
type SectionsConfig,
|
|
type ToolCatalog,
|
|
} from "./system-prompt-config.ts";
|
|
|
|
const DUMP_RELATIVE_PATH = ".pi-debug/effective-system-prompt.md";
|
|
|
|
export function getSystemPromptDumpPath(cwd: string): string {
|
|
return resolve(cwd, DUMP_RELATIVE_PATH);
|
|
}
|
|
|
|
/** The options object as Pi 1.3 normalizes it; the vendored types predate `sections`/`forceSystemPrompt`. */
|
|
type RuntimePromptOptions = MutablePromptOptions & { forceSystemPrompt?: string };
|
|
|
|
/**
|
|
* Injectable seams so the composer can be exercised under `node --test`
|
|
* without touching the real `config/prompt/` files.
|
|
*/
|
|
export interface SystemPromptComposerDeps {
|
|
loadToolCatalog?: () => ToolCatalog;
|
|
loadRoutingRules?: () => RoutingConfig;
|
|
loadSectionsConfig?: () => SectionsConfig;
|
|
readPreamble?: () => string | undefined;
|
|
}
|
|
|
|
export interface SystemPromptComposer {
|
|
handleBeforeAgentStart: (
|
|
event: BeforeAgentStartEvent,
|
|
ctx: ExtensionContext,
|
|
) => { systemPrompt: string } | undefined;
|
|
getLastComposed: () => string | undefined;
|
|
}
|
|
|
|
/**
|
|
* The bundle's single system-prompt owner. It is the only extension allowed to
|
|
* return `systemPrompt`; every other bundle or third-party extension must
|
|
* contribute structurally through `event.systemPromptOptions`.
|
|
*
|
|
* Per turn it applies `config/prompt/tools.jsonc` (tool snippets/guidelines),
|
|
* renders `config/prompt/routing.jsonc` into a `<tool_routing>` section, orders
|
|
* contributed sections per `config/prompt/sections.jsonc`, then reads the
|
|
* re-rendered prompt and splices in `config/prompt/preamble.md` before
|
|
* returning the final text.
|
|
*
|
|
* If an earlier extension violated the contract by setting
|
|
* `forceSystemPrompt`, the prompt is already frozen: the composer degrades to
|
|
* text splicing (preamble + appended `<tool_routing>` when absent) and warns
|
|
* once. Config load/validation failures warn once and leave Pi's default
|
|
* prompt untouched — the handler never throws.
|
|
*/
|
|
export function createSystemPromptComposer(deps: SystemPromptComposerDeps = {}): SystemPromptComposer {
|
|
const loadCatalog = deps.loadToolCatalog ?? loadToolCatalog;
|
|
const loadRouting = deps.loadRoutingRules ?? loadRoutingRules;
|
|
const loadSections = deps.loadSectionsConfig ?? loadSectionsConfig;
|
|
const loadPreamble = deps.readPreamble ?? readPreamble;
|
|
|
|
let lastComposed: string | undefined;
|
|
let forcedNotified = false;
|
|
let configErrorNotified = false;
|
|
const notifiedMissingSets = new Set<string>();
|
|
|
|
function handleBeforeAgentStart(
|
|
event: BeforeAgentStartEvent,
|
|
ctx: ExtensionContext,
|
|
): { systemPrompt: string } | undefined {
|
|
const options = event.systemPromptOptions as unknown as RuntimePromptOptions | undefined;
|
|
const forced = options !== undefined && options.forceSystemPrompt !== undefined;
|
|
if (forced && ctx.hasUI && !forcedNotified) {
|
|
forcedNotified = true;
|
|
ctx.ui.notify(
|
|
"检测到其他扩展直接改写了系统提示词(违反单一入口规则),本轮降级为文本拼接。",
|
|
"warning",
|
|
);
|
|
}
|
|
|
|
let catalog: ToolCatalog;
|
|
let routing: RoutingConfig;
|
|
let sections: SectionsConfig;
|
|
let preamble: string | undefined;
|
|
try {
|
|
catalog = loadCatalog();
|
|
routing = loadRouting();
|
|
sections = loadSections();
|
|
preamble = loadPreamble();
|
|
} catch (error) {
|
|
if (ctx.hasUI && !configErrorNotified) {
|
|
configErrorNotified = true;
|
|
const detail = error instanceof Error ? error.message : String(error);
|
|
ctx.ui.notify(`系统提示词配置加载失败:${detail}`, "warning");
|
|
}
|
|
return undefined;
|
|
}
|
|
|
|
let routingText = "";
|
|
if (options) {
|
|
const missing = applyToolCatalog(options, catalog);
|
|
if (missing.length > 0 && ctx.hasUI) {
|
|
const key = [...missing].sort().join("\n");
|
|
if (!notifiedMissingSets.has(key)) {
|
|
notifiedMissingSets.add(key);
|
|
ctx.ui.notify(`以下工具未在 config/prompt/tools.jsonc 登记:${missing.join(", ")}`, "warning");
|
|
}
|
|
}
|
|
routingText = renderRouting(routing.rules, options.selectedTools);
|
|
options.sections ??= {};
|
|
if (routingText.length > 0) {
|
|
options.sections.tool_routing = routingText;
|
|
}
|
|
options.sections = orderSections(options.sections, sections.order);
|
|
} else {
|
|
routingText = renderRouting(routing.rules, undefined);
|
|
}
|
|
|
|
let rendered = event.systemPrompt;
|
|
if (routingText.length > 0 && !rendered.includes("<tool_routing>")) {
|
|
// Degrade path: the prompt was frozen by forceSystemPrompt, so the
|
|
// contributed section cannot render — append it as plain text.
|
|
rendered += `\n\n<tool_routing>\n${routingText}\n</tool_routing>`;
|
|
}
|
|
|
|
const final = preamble ? spliceCover(rendered, preamble) : rendered;
|
|
lastComposed = final;
|
|
return { systemPrompt: final };
|
|
}
|
|
|
|
return {
|
|
handleBeforeAgentStart,
|
|
getLastComposed: () => lastComposed,
|
|
};
|
|
}
|
|
|
|
export default function systemPromptExtension(pi: ExtensionAPI): void {
|
|
const composer = createSystemPromptComposer();
|
|
|
|
pi.on("before_agent_start", (event, ctx) => composer.handleBeforeAgentStart(event, ctx));
|
|
|
|
pi.registerCommand("dump-system-prompt", {
|
|
description: "Write the effective Pi system prompt to .pi-debug/effective-system-prompt.md",
|
|
handler: async (_args, ctx) => {
|
|
const outputPath = getSystemPromptDumpPath(ctx.cwd);
|
|
const effectivePrompt = composer.getLastComposed() ?? ctx.getSystemPrompt();
|
|
|
|
await mkdir(dirname(outputPath), { recursive: true });
|
|
await writeFile(outputPath, effectivePrompt, "utf8");
|
|
ctx.ui.notify(`System prompt written to ${outputPath} (${effectivePrompt.length} chars)`, "info");
|
|
},
|
|
});
|
|
}
|