mirror of
https://bitbucket.org/siakitem/my-pi.git
synced 2026-10-26 10:45:22 +00:00
真实会话里模型仍习惯性先调 bash grep,被拒后才改用 grep 工具;系统提示词里已有 4 处 提醒,但模型下调用决定时看的是工具 schema 描述,而 Pi 内置 bash 描述对此只字未提。 - 新增 extensions/bash-override.ts:用 Pi 的 createBashToolDefinition 重新注册 bash, 执行、输出结构与 codemode 说明完全沿用上游;shellPath/commandPrefix 按 ctx.cwd 经 SettingsManager 惰性解析,与内置一致 - tools.jsonc 新增 descriptionAppend 字段,由 prompt-guard 注册时拼接到上游描述后 - bash 增加命令→工具对照规则(grep -n / grep -rl / find -name),并提示 文件清单用 $(git ls-files) 传参、不要接 xargs(会触发权限审核) Generated with [Devin](https://devin.ai) Co-Authored-By: Devin <158243242+devin-ai-integration[bot]@users.noreply.github.com>
121 lines
4.5 KiB
TypeScript
121 lines
4.5 KiB
TypeScript
import type { ExtensionAPI } from "@earendil-works/pi-coding-agent";
|
|
|
|
import {
|
|
loadToolCatalog,
|
|
type ToolCatalog,
|
|
type ToolCatalogEntry,
|
|
} from "./system-prompt-config.ts";
|
|
|
|
type AnyMessage = { role?: string; content?: unknown } & Record<string, unknown>;
|
|
|
|
type AnyHandler = (event: never, ctx: never) => unknown;
|
|
|
|
/**
|
|
* The description a tool registers with under its catalog entry: a `description`
|
|
* override replaces the upstream text, a `descriptionAppend` is joined onto
|
|
* whichever base wins (override first, upstream otherwise) with a blank line.
|
|
*/
|
|
export function catalogDescription(description: string, entry: ToolCatalogEntry): string {
|
|
if ("inherit" in entry) return description;
|
|
const base = typeof entry.description === "string" ? entry.description : description;
|
|
return typeof entry.descriptionAppend === "string"
|
|
? `${base.trimEnd()}\n\n${entry.descriptionAppend}`
|
|
: base;
|
|
}
|
|
|
|
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 and
|
|
* `descriptionAppend` suffix from `config/prompt/tools.jsonc` when the
|
|
* catalog carries them (lazily loaded; a catalog error keeps the upstream
|
|
* description). An append composes onto the override when both are set, or
|
|
* onto the upstream description when only the append is set.
|
|
* - `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];
|
|
const description =
|
|
entry === undefined ? tool.description : catalogDescription(tool.description, entry);
|
|
if (description === tool.description) {
|
|
pi.registerTool(tool);
|
|
} else {
|
|
pi.registerTool({ ...tool, description });
|
|
}
|
|
};
|
|
|
|
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;
|
|
},
|
|
});
|
|
}
|