mirror of
https://bitbucket.org/siakitem/my-pi.git
synced 2026-10-26 18:55: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>
100 lines
3.7 KiB
TypeScript
100 lines
3.7 KiB
TypeScript
import type { ExtensionAPI } from "@earendil-works/pi-coding-agent";
|
|
|
|
import { loadToolCatalog, type ToolCatalog } from "./system-prompt-config.ts";
|
|
|
|
type AnyMessage = { role?: string; content?: unknown } & Record<string, unknown>;
|
|
|
|
type AnyHandler = (event: never, ctx: never) => unknown;
|
|
|
|
export interface PromptGuardOptions {
|
|
/**
|
|
* Post-process the messages a wrapped `context` handler leaves behind.
|
|
* Receives the message list snapshot from before the handler ran and the
|
|
* list after; returns the list the handler's result should report.
|
|
*/
|
|
contextFilter?: (before: AnyMessage[], after: AnyMessage[]) => AnyMessage[];
|
|
}
|
|
|
|
/**
|
|
* Proxy an `ExtensionAPI` for third-party extensions so they cannot bypass the
|
|
* bundle's single system-prompt owner (`extensions/system-prompt.ts`):
|
|
*
|
|
* - `registerTool` applies the tool's `description` override from
|
|
* `config/prompt/tools.jsonc` when the catalog carries one (lazily loaded;
|
|
* a catalog error keeps the upstream description).
|
|
* - `before_agent_start` handlers keep every returned field except
|
|
* `systemPrompt`, which is dropped.
|
|
* - `context` handlers are post-processed through `opts.contextFilter`.
|
|
* - Everything else passes through unchanged.
|
|
*/
|
|
export function guardExtensionApi(pi: ExtensionAPI, opts: PromptGuardOptions = {}): ExtensionAPI {
|
|
let catalogCache: ToolCatalog | null | undefined;
|
|
const catalog = (): ToolCatalog | null => {
|
|
if (catalogCache === undefined) {
|
|
try {
|
|
catalogCache = loadToolCatalog();
|
|
} catch {
|
|
catalogCache = null;
|
|
}
|
|
}
|
|
return catalogCache;
|
|
};
|
|
|
|
const registerTool: ExtensionAPI["registerTool"] = (tool) => {
|
|
const entry = catalog()?.tools[tool.name];
|
|
if (entry !== undefined && !("inherit" in entry) && typeof entry.description === "string") {
|
|
pi.registerTool({ ...tool, description: entry.description });
|
|
} else {
|
|
pi.registerTool(tool);
|
|
}
|
|
};
|
|
|
|
const on: ExtensionAPI["on"] = ((event: string, handler: AnyHandler): unknown => {
|
|
let wrapped = handler;
|
|
if (event === "before_agent_start") {
|
|
wrapped = (async (eventPayload: unknown, ctx: unknown) => {
|
|
const result = await handler(eventPayload as never, ctx as never);
|
|
if (result !== null && typeof result === "object" && "systemPrompt" in result) {
|
|
const rest = { ...(result as Record<string, unknown>) };
|
|
delete rest.systemPrompt;
|
|
return rest;
|
|
}
|
|
return result;
|
|
}) as AnyHandler;
|
|
} else if (event === "context" && opts.contextFilter) {
|
|
const filter = opts.contextFilter;
|
|
wrapped = (async (eventPayload: { messages?: AnyMessage[] }, ctx: unknown) => {
|
|
const messages = Array.isArray(eventPayload.messages) ? eventPayload.messages : [];
|
|
const before = messages.slice();
|
|
const result = await handler(eventPayload as never, ctx as never);
|
|
const resultMessages =
|
|
result !== null &&
|
|
typeof result === "object" &&
|
|
Array.isArray((result as { messages?: unknown }).messages)
|
|
? ((result as { messages: AnyMessage[] }).messages)
|
|
: messages;
|
|
const filtered = filter(before, resultMessages);
|
|
if (filtered === resultMessages) {
|
|
return result;
|
|
}
|
|
return {
|
|
...(result !== null && typeof result === "object" ? (result as Record<string, unknown>) : {}),
|
|
messages: filtered,
|
|
};
|
|
}) as AnyHandler;
|
|
}
|
|
// `on` is overloaded per event name; drop to a loosely typed call and
|
|
// forward whatever the real registration returns (e.g. a disposer).
|
|
return (pi.on as (name: string, h: AnyHandler) => unknown)(event, wrapped);
|
|
}) as ExtensionAPI["on"];
|
|
|
|
return new Proxy(pi, {
|
|
get(target, prop, receiver) {
|
|
if (prop === "registerTool") return registerTool;
|
|
if (prop === "on") return on;
|
|
const value = Reflect.get(target, prop, receiver);
|
|
return typeof value === "function" ? value.bind(target) : value;
|
|
},
|
|
});
|
|
}
|