mirror of
https://bitbucket.org/siakitem/my-pi.git
synced 2026-10-26 10:45:22 +00:00
Compare commits
1
Commits
2b21163a18
...
main
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
87940da51a |
@@ -21,6 +21,7 @@
|
|||||||
| `pi-compaction/` | 压力触发的观察遮蔽(Pi context_edit)、阈值整段摘要、effective token、native compaction |
|
| `pi-compaction/` | 压力触发的观察遮蔽(Pi context_edit)、阈值整段摘要、effective token、native compaction |
|
||||||
| `pi-overflow/` | 新工具结果统一截断:超阈值原文落盘 + 有界视图 + `overflow_query` |
|
| `pi-overflow/` | 新工具结果统一截断:超阈值原文落盘 + 有界视图 + `overflow_query` |
|
||||||
| `extensions/fff-override.ts` | FFF `override` 模式接管 `find`/`grep` 与 `@` 补全,`multi_grep` 按上游保持关闭 |
|
| `extensions/fff-override.ts` | FFF `override` 模式接管 `find`/`grep` 与 `@` 补全,`multi_grep` 按上游保持关闭 |
|
||||||
|
| `extensions/bash-override.ts` | 用 Pi 的 `createBashToolDefinition` 重新注册 `bash`,仅替换模型可见描述(追加 `tools.jsonc` `descriptionAppend`);执行逻辑、输出结构、codemode 说明不变 |
|
||||||
| `extensions/codegraph.ts` | 原生 `codegraph_explore`,自有 stdio JSON-RPC 直连 `codegraph serve`(`codegraph-client.ts`) |
|
| `extensions/codegraph.ts` | 原生 `codegraph_explore`,自有 stdio JSON-RPC 直连 `codegraph serve`(`codegraph-client.ts`) |
|
||||||
| `extensions/hashline.ts` | 部署 `config/pi-hashline-edit.json` 后加载 pi-hashline-edit 覆盖 `read`/`edit`(`hashline-config.ts`) |
|
| `extensions/hashline.ts` | 部署 `config/pi-hashline-edit.json` 后加载 pi-hashline-edit 覆盖 `read`/`edit`(`hashline-config.ts`) |
|
||||||
| `extensions/permission-system.ts` | 直接加载仓库内 `pi-permission-system/src/index.ts` |
|
| `extensions/permission-system.ts` | 直接加载仓库内 `pi-permission-system/src/index.ts` |
|
||||||
|
|||||||
@@ -215,6 +215,10 @@ Pi 的 `find` 和 `grep`;`multi_grep` 上游标注为有害、默认关闭,w
|
|||||||
删除 `PI_FFF_MULTIGREP` 环境变量,使其永远无法注册。RTK 对所有 `grep` 工具结果直接跳过,
|
删除 `PI_FFF_MULTIGREP` 环境变量,使其永远无法注册。RTK 对所有 `grep` 工具结果直接跳过,
|
||||||
也默认关闭 Bash 命令改写,因此不会把 `rg`、`grep`、`find` 或 `fd` 改写到 RTK。
|
也默认关闭 Bash 命令改写,因此不会把 `rg`、`grep`、`find` 或 `fd` 改写到 RTK。
|
||||||
|
|
||||||
|
`extensions/bash-override.ts` 用 Pi 的 `createBashToolDefinition` 重新注册内置 `bash`,
|
||||||
|
把「不要用 bash 搜索」的警告经 `tools.jsonc` 的 `descriptionAppend` 写进工具 schema,
|
||||||
|
让模型在调用时就看到替代工具;执行逻辑、输出结构和 codemode 的 `tools.bash` 说明完全沿用上游定义。
|
||||||
|
|
||||||
### 输出压缩归 RTK
|
### 输出压缩归 RTK
|
||||||
|
|
||||||
RTK 保留 Bash、read、build、test、lint 和 Git 输出压缩。默认 `read` 的有损压缩
|
RTK 保留 Bash、read、build、test、lint 和 Git 输出压缩。默认 `read` 的有损压缩
|
||||||
|
|||||||
@@ -30,7 +30,7 @@
|
|||||||
},
|
},
|
||||||
{
|
{
|
||||||
"whenAny": ["bash"],
|
"whenAny": ["bash"],
|
||||||
"text": "Use bash for builds, tests, git, package scripts, and other programs. Never run grep, rg, find, fd, or git grep in bash — standalone, piped, inside a loop, or in $(…). Search files with the grep/find tools; when a program needs a file list (wc, node, tsc), pass $(git ls-files '<glob>')."
|
"text": "Use bash for builds, tests, git, package scripts, and other programs. Never run grep, rg, find, fd, or git grep in bash — standalone, piped, inside a loop, or in $(…). Search files with the grep/find tools; when a program needs a file list (wc, node, tsc), pass $(git ls-files '<glob>') as arguments rather than piping into xargs."
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
"whenAll": ["bash", "overflow_query"],
|
"whenAll": ["bash", "overflow_query"],
|
||||||
|
|||||||
@@ -2,17 +2,26 @@
|
|||||||
// Bundle-authoritative tool guidance, owned by extensions/system-prompt.ts.
|
// Bundle-authoritative tool guidance, owned by extensions/system-prompt.ts.
|
||||||
//
|
//
|
||||||
// Every tool the bundle can expose must have an entry. Per field:
|
// Every tool the bundle can expose must have an entry. Per field:
|
||||||
// "snippet" — replaces the tool's one-line <tools> entry (null hides it)
|
// "snippet" — replaces the tool's one-line <tools> entry (null hides it)
|
||||||
// "guidelines" — replaces the tool's <rules> lines
|
// "guidelines" — replaces the tool's <rules> lines
|
||||||
// "description" — replaces the tool description at registration; only
|
// "description" — replaces the tool description at registration
|
||||||
// applied to third-party tools loaded through a bundle
|
// "descriptionAppend" — appended to the description at registration (on top
|
||||||
// adapter (in-repo tools edit their own source instead)
|
// of "description" when both are set)
|
||||||
|
// "description"/"descriptionAppend" only apply to tools registered through a
|
||||||
|
// bundle adapter (prompt-guard), which includes the bash override and the
|
||||||
|
// third-party wrappers; other in-repo tools edit their own source instead.
|
||||||
// An absent field keeps the upstream text. "inherit": true means the
|
// An absent field keeps the upstream text. "inherit": true means the
|
||||||
// upstream text was reviewed against this bundle and is kept as-is.
|
// upstream text was reviewed against this bundle and is kept as-is.
|
||||||
"version": 1,
|
"version": 1,
|
||||||
"tools": {
|
"tools": {
|
||||||
"bash": {
|
"bash": {
|
||||||
"snippet": "Run shell commands for builds, tests, git, package scripts, and other programs; search files with the find/grep tools, not bash"
|
"snippet": "Run shell commands for builds, tests, git, package scripts, and other programs; search files with the find/grep tools, not bash",
|
||||||
|
"descriptionAppend": "Not for searching files: use the grep tool for file contents and the find tool for paths (e.g. `grep -n X src/*.ts` → grep {pattern: \"X\", path: \"src/*.ts\"}); grep, rg, find, fd, and git grep are denied here. When a program needs many files, pass $(git ls-files '<glob>') as arguments instead of piping into xargs, which triggers a permission review.",
|
||||||
|
"guidelines": [
|
||||||
|
"You can inspect PI_* environment variables for current model and session details.",
|
||||||
|
"Search commands are denied in bash; call the tools instead: `grep -n X src/*.ts` → grep {pattern: \"X\", path: \"src/*.ts\"}; `grep -rl X src` → grep {pattern: \"X\", path: \"src/\"} and take the file names; `find . -name '*.ts'` → find {pattern: \"\", path: \"**/*.ts\"}.",
|
||||||
|
"When a program needs many files (wc, node, tsc), pass $(git ls-files '<glob>') as arguments; piping into xargs triggers a permission review."
|
||||||
|
]
|
||||||
},
|
},
|
||||||
"read": { "inherit": true },
|
"read": { "inherit": true },
|
||||||
"edit": {
|
"edit": {
|
||||||
|
|||||||
@@ -25,6 +25,7 @@
|
|||||||
## 各 wrapper 约定
|
## 各 wrapper 约定
|
||||||
|
|
||||||
- `fff-override.ts`:强制 FFF 官方 `override` 模式,统一接管 `find`、`grep` 和 `@` 补全;`multi_grep` 按上游保持关闭(加载前删除 `PI_FFF_MULTIGREP` 环境变量)。FFF 独占本地字面搜索,远端服务器搜索由 `ssh_find` / `ssh_grep` 负责,RTK 不得接管搜索工具(见 `pi-rtk-optimizer/AGENTS.md`)。显式 CLI flag 仍遵循 FFF 官方优先级。
|
- `fff-override.ts`:强制 FFF 官方 `override` 模式,统一接管 `find`、`grep` 和 `@` 补全;`multi_grep` 按上游保持关闭(加载前删除 `PI_FFF_MULTIGREP` 环境变量)。FFF 独占本地字面搜索,远端服务器搜索由 `ssh_find` / `ssh_grep` 负责,RTK 不得接管搜索工具(见 `pi-rtk-optimizer/AGENTS.md`)。显式 CLI flag 仍遵循 FFF 官方优先级。
|
||||||
|
- `bash-override.ts`:用 `createBashToolDefinition` 重新注册内置 `bash`,目的只是让 `tools.jsonc` 的 `descriptionAppend` 落到模型可见描述里(prompt-guard 在注册时拼接)。ExtensionContext 不暴露 session settings,因此 `shellPath`/`commandPrefix` 在首次 execute 时按 `ctx.cwd` 经 `SettingsManager.create(ctx.cwd, getAgentDir())` 惰性解析并按 cwd 缓存,metadata 来自 `process.cwd()` 的定义;执行、输出结构与 codemode 说明完全委托上游定义,不得改动上游执行逻辑,也不得用 prepareLoadout 或 provider-payload 改写实现同样效果。
|
||||||
- `codegraph.ts` + `codegraph-client.ts`:注册原生 `codegraph_explore` 工具,按索引根惰性常驻 `codegraph serve --mcp -p <root>` 子进程,经组合包自有 stdio 逐行 JSON-RPC 客户端直连(不经 shell、不带 pi-mcp-adapter 或 MCP SDK);serve 进程内置文件监听自动同步索引,无结果缓存。子进程 10 分钟无调用自动关闭、会话结束统一关闭;调用中死亡换新客户端透明重试一次;其启动的上游 daemon(`.codegraph/daemon.sock`)退出后可多驻留 ≤5 分钟,属上游行为,组合包不管理。
|
- `codegraph.ts` + `codegraph-client.ts`:注册原生 `codegraph_explore` 工具,按索引根惰性常驻 `codegraph serve --mcp -p <root>` 子进程,经组合包自有 stdio 逐行 JSON-RPC 客户端直连(不经 shell、不带 pi-mcp-adapter 或 MCP SDK);serve 进程内置文件监听自动同步索引,无结果缓存。子进程 10 分钟无调用自动关闭、会话结束统一关闭;调用中死亡换新客户端透明重试一次;其启动的上游 daemon(`.codegraph/daemon.sock`)退出后可多驻留 ≤5 分钟,属上游行为,组合包不管理。
|
||||||
- CodeGraph 是机器级依赖,`install.sh`/`update.sh` 经官方安装器管理 `codegraph` CLI,组合包不内置其运行时。安装、升级和普通启动流程不得执行 `codegraph init`;只有 `codegraph_explore` 在未初始化项目上被调用、且用户在 `ctx.ui.confirm` 中明确确认后才可 `codegraph init` 当前目录,拒绝记入会话级 declined 不再重复询问;init 失败必须如实报告并让模型回退内置工具;不得改动 CodeGraph 更新或遥测设置。CLI 缺失、init 被拒或服务调用失败都不得抛出,必须以文本结果打回并引导模型改用 `find`/`grep`/`read`/LSP。已移除的 `plugin-init.ts`(原 `/plugin_init`)职责已并入本文件的惰性确认,不得恢复。
|
- CodeGraph 是机器级依赖,`install.sh`/`update.sh` 经官方安装器管理 `codegraph` CLI,组合包不内置其运行时。安装、升级和普通启动流程不得执行 `codegraph init`;只有 `codegraph_explore` 在未初始化项目上被调用、且用户在 `ctx.ui.confirm` 中明确确认后才可 `codegraph init` 当前目录,拒绝记入会话级 declined 不再重复询问;init 失败必须如实报告并让模型回退内置工具;不得改动 CodeGraph 更新或遥测设置。CLI 缺失、init 被拒或服务调用失败都不得抛出,必须以文本结果打回并引导模型改用 `find`/`grep`/`read`/LSP。已移除的 `plugin-init.ts`(原 `/plugin_init`)职责已并入本文件的惰性确认,不得恢复。
|
||||||
- `hashline.ts` + `hashline-config.ts`:部署组合包权威 `config/pi-hashline-edit.json` 后加载 `pi-hashline-edit`;默认以 2 字符行哈希覆盖内置 `read` / `edit`,`grep` 与 `replaceText` 均关闭。它保证锚定编辑而不负责压缩,大文件上下文节省必须来自先定位/隔离分析、再做最小范围 `offset/limit` 读取。
|
- `hashline.ts` + `hashline-config.ts`:部署组合包权威 `config/pi-hashline-edit.json` 后加载 `pi-hashline-edit`;默认以 2 字符行哈希覆盖内置 `read` / `edit`,`grep` 与 `replaceText` 均关闭。它保证锚定编辑而不负责压缩,大文件上下文节省必须来自先定位/隔离分析、再做最小范围 `offset/limit` 读取。
|
||||||
|
|||||||
@@ -0,0 +1,38 @@
|
|||||||
|
import {
|
||||||
|
createBashToolDefinition,
|
||||||
|
type ExtensionAPI,
|
||||||
|
getAgentDir,
|
||||||
|
SettingsManager,
|
||||||
|
} from "@earendil-works/pi-coding-agent";
|
||||||
|
import { guardExtensionApi } from "./prompt-guard.ts";
|
||||||
|
|
||||||
|
type BashDefinition = ReturnType<typeof createBashToolDefinition>;
|
||||||
|
|
||||||
|
/** Re-register Pi's bash so tools.jsonc can append to its model-facing description; execution stays upstream. */
|
||||||
|
export default function bashOverrideExtension(pi: ExtensionAPI): void {
|
||||||
|
const sessionDefinitions = new Map<string, BashDefinition>();
|
||||||
|
const sessionDefinition = (cwd: string): BashDefinition => {
|
||||||
|
let definition = sessionDefinitions.get(cwd);
|
||||||
|
if (definition === undefined) {
|
||||||
|
const settings = SettingsManager.create(cwd, getAgentDir());
|
||||||
|
definition = createBashToolDefinition(cwd, {
|
||||||
|
shellPath: settings.getShellPath(),
|
||||||
|
commandPrefix: settings.getShellCommandPrefix(),
|
||||||
|
});
|
||||||
|
sessionDefinitions.set(cwd, definition);
|
||||||
|
}
|
||||||
|
return definition;
|
||||||
|
};
|
||||||
|
|
||||||
|
guardExtensionApi(pi).registerTool({
|
||||||
|
...sessionDefinition(process.cwd()),
|
||||||
|
execute: (toolCallId, params, signal, onUpdate, ctx) =>
|
||||||
|
sessionDefinition(ctx?.cwd ?? process.cwd()).execute(
|
||||||
|
toolCallId,
|
||||||
|
params,
|
||||||
|
signal,
|
||||||
|
onUpdate,
|
||||||
|
ctx,
|
||||||
|
),
|
||||||
|
});
|
||||||
|
}
|
||||||
@@ -1,11 +1,28 @@
|
|||||||
import type { ExtensionAPI } from "@earendil-works/pi-coding-agent";
|
import type { ExtensionAPI } from "@earendil-works/pi-coding-agent";
|
||||||
|
|
||||||
import { loadToolCatalog, type ToolCatalog } from "./system-prompt-config.ts";
|
import {
|
||||||
|
loadToolCatalog,
|
||||||
|
type ToolCatalog,
|
||||||
|
type ToolCatalogEntry,
|
||||||
|
} from "./system-prompt-config.ts";
|
||||||
|
|
||||||
type AnyMessage = { role?: string; content?: unknown } & Record<string, unknown>;
|
type AnyMessage = { role?: string; content?: unknown } & Record<string, unknown>;
|
||||||
|
|
||||||
type AnyHandler = (event: never, ctx: never) => 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 {
|
export interface PromptGuardOptions {
|
||||||
/**
|
/**
|
||||||
* Post-process the messages a wrapped `context` handler leaves behind.
|
* Post-process the messages a wrapped `context` handler leaves behind.
|
||||||
@@ -19,9 +36,11 @@ export interface PromptGuardOptions {
|
|||||||
* Proxy an `ExtensionAPI` for third-party extensions so they cannot bypass the
|
* Proxy an `ExtensionAPI` for third-party extensions so they cannot bypass the
|
||||||
* bundle's single system-prompt owner (`extensions/system-prompt.ts`):
|
* bundle's single system-prompt owner (`extensions/system-prompt.ts`):
|
||||||
*
|
*
|
||||||
* - `registerTool` applies the tool's `description` override from
|
* - `registerTool` applies the tool's `description` override and
|
||||||
* `config/prompt/tools.jsonc` when the catalog carries one (lazily loaded;
|
* `descriptionAppend` suffix from `config/prompt/tools.jsonc` when the
|
||||||
* a catalog error keeps the upstream description).
|
* 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
|
* - `before_agent_start` handlers keep every returned field except
|
||||||
* `systemPrompt`, which is dropped.
|
* `systemPrompt`, which is dropped.
|
||||||
* - `context` handlers are post-processed through `opts.contextFilter`.
|
* - `context` handlers are post-processed through `opts.contextFilter`.
|
||||||
@@ -42,10 +61,12 @@ export function guardExtensionApi(pi: ExtensionAPI, opts: PromptGuardOptions = {
|
|||||||
|
|
||||||
const registerTool: ExtensionAPI["registerTool"] = (tool) => {
|
const registerTool: ExtensionAPI["registerTool"] = (tool) => {
|
||||||
const entry = catalog()?.tools[tool.name];
|
const entry = catalog()?.tools[tool.name];
|
||||||
if (entry !== undefined && !("inherit" in entry) && typeof entry.description === "string") {
|
const description =
|
||||||
pi.registerTool({ ...tool, description: entry.description });
|
entry === undefined ? tool.description : catalogDescription(tool.description, entry);
|
||||||
} else {
|
if (description === tool.description) {
|
||||||
pi.registerTool(tool);
|
pi.registerTool(tool);
|
||||||
|
} else {
|
||||||
|
pi.registerTool({ ...tool, description });
|
||||||
}
|
}
|
||||||
};
|
};
|
||||||
|
|
||||||
|
|||||||
@@ -10,7 +10,12 @@ import { fileURLToPath } from "node:url";
|
|||||||
/** Entry in `config/prompt/tools.jsonc`. */
|
/** Entry in `config/prompt/tools.jsonc`. */
|
||||||
export type ToolCatalogEntry =
|
export type ToolCatalogEntry =
|
||||||
| { inherit: true }
|
| { inherit: true }
|
||||||
| { snippet?: string | null; guidelines?: string[]; description?: string };
|
| {
|
||||||
|
snippet?: string | null;
|
||||||
|
guidelines?: string[];
|
||||||
|
description?: string;
|
||||||
|
descriptionAppend?: string;
|
||||||
|
};
|
||||||
|
|
||||||
export interface ToolCatalog {
|
export interface ToolCatalog {
|
||||||
version: 1;
|
version: 1;
|
||||||
@@ -152,7 +157,7 @@ function isStringArray(value: unknown): value is string[] {
|
|||||||
return Array.isArray(value) && value.every((entry) => typeof entry === "string");
|
return Array.isArray(value) && value.every((entry) => typeof entry === "string");
|
||||||
}
|
}
|
||||||
|
|
||||||
const TOOL_ENTRY_KEYS = new Set(["inherit", "snippet", "guidelines", "description"]);
|
const TOOL_ENTRY_KEYS = new Set(["inherit", "snippet", "guidelines", "description", "descriptionAppend"]);
|
||||||
|
|
||||||
function validateToolEntry(name: string, raw: unknown): ToolCatalogEntry {
|
function validateToolEntry(name: string, raw: unknown): ToolCatalogEntry {
|
||||||
if (!isPlainObject(raw)) {
|
if (!isPlainObject(raw)) {
|
||||||
@@ -170,9 +175,9 @@ function validateToolEntry(name: string, raw: unknown): ToolCatalogEntry {
|
|||||||
return { inherit: true };
|
return { inherit: true };
|
||||||
}
|
}
|
||||||
const hasField =
|
const hasField =
|
||||||
"snippet" in raw || "guidelines" in raw || "description" in raw;
|
"snippet" in raw || "guidelines" in raw || "description" in raw || "descriptionAppend" in raw;
|
||||||
if (!hasField) {
|
if (!hasField) {
|
||||||
fail("tools.jsonc", `tool "${name}" needs "inherit": true or at least one of snippet/guidelines/description`);
|
fail("tools.jsonc", `tool "${name}" needs "inherit": true or at least one of snippet/guidelines/description/descriptionAppend`);
|
||||||
}
|
}
|
||||||
if ("snippet" in raw && raw.snippet !== null && typeof raw.snippet !== "string") {
|
if ("snippet" in raw && raw.snippet !== null && typeof raw.snippet !== "string") {
|
||||||
fail("tools.jsonc", `tool "${name}".snippet must be a string or null`);
|
fail("tools.jsonc", `tool "${name}".snippet must be a string or null`);
|
||||||
@@ -183,10 +188,13 @@ function validateToolEntry(name: string, raw: unknown): ToolCatalogEntry {
|
|||||||
if ("description" in raw && typeof raw.description !== "string") {
|
if ("description" in raw && typeof raw.description !== "string") {
|
||||||
fail("tools.jsonc", `tool "${name}".description must be a string`);
|
fail("tools.jsonc", `tool "${name}".description must be a string`);
|
||||||
}
|
}
|
||||||
|
if ("descriptionAppend" in raw && typeof raw.descriptionAppend !== "string") {
|
||||||
|
fail("tools.jsonc", `tool "${name}".descriptionAppend must be a string`);
|
||||||
|
}
|
||||||
return raw as ToolCatalogEntry;
|
return raw as ToolCatalogEntry;
|
||||||
}
|
}
|
||||||
|
|
||||||
function validateToolCatalog(raw: unknown): ToolCatalog {
|
export function validateToolCatalog(raw: unknown): ToolCatalog {
|
||||||
if (!isPlainObject(raw) || raw.version !== 1 || !isPlainObject(raw.tools)) {
|
if (!isPlainObject(raw) || raw.version !== 1 || !isPlainObject(raw.tools)) {
|
||||||
fail("tools.jsonc", 'expected { "version": 1, "tools": { ... } }');
|
fail("tools.jsonc", 'expected { "version": 1, "tools": { ... } }');
|
||||||
}
|
}
|
||||||
@@ -321,8 +329,8 @@ export function isSubagentCustomPrompt(customPrompt: unknown): boolean {
|
|||||||
* Apply the tool catalog to the mutable prompt options: a `snippet` string
|
* Apply the tool catalog to the mutable prompt options: a `snippet` string
|
||||||
* replaces the tool's `<tools>` line (`null` hides it), `guidelines` replaces
|
* replaces the tool's `<tools>` line (`null` hides it), `guidelines` replaces
|
||||||
* the tool's `<rules>` lines; `{inherit:true}` entries are left untouched.
|
* the tool's `<rules>` lines; `{inherit:true}` entries are left untouched.
|
||||||
* `description` is not applied here — it is a registration-time concern handled
|
* `description` and `descriptionAppend` are not applied here — they are
|
||||||
* by `extensions/prompt-guard.ts`.
|
* registration-time concerns handled by `extensions/prompt-guard.ts`.
|
||||||
*
|
*
|
||||||
* Returns the selected tool names that have no catalog entry at all.
|
* Returns the selected tool names that have no catalog entry at all.
|
||||||
*/
|
*/
|
||||||
|
|||||||
@@ -123,6 +123,7 @@
|
|||||||
"extensions": [
|
"extensions": [
|
||||||
"./pi-overflow/capture.ts",
|
"./pi-overflow/capture.ts",
|
||||||
"./extensions/fff-override.ts",
|
"./extensions/fff-override.ts",
|
||||||
|
"./extensions/bash-override.ts",
|
||||||
"./pi-web-search/index.ts",
|
"./pi-web-search/index.ts",
|
||||||
"./extensions/codegraph.ts",
|
"./extensions/codegraph.ts",
|
||||||
"./pi-rtk-optimizer/index.ts",
|
"./pi-rtk-optimizer/index.ts",
|
||||||
|
|||||||
@@ -58,6 +58,11 @@ test("all package extensions load together without global registration conflicts
|
|||||||
packageJson.pi.extensions.includes("./extensions/codegraph.ts"),
|
packageJson.pi.extensions.includes("./extensions/codegraph.ts"),
|
||||||
"the bundle must expose the native codegraph_explore tool",
|
"the bundle must expose the native codegraph_explore tool",
|
||||||
);
|
);
|
||||||
|
assert.equal(
|
||||||
|
packageJson.pi.extensions.indexOf("./extensions/bash-override.ts"),
|
||||||
|
packageJson.pi.extensions.indexOf("./extensions/fff-override.ts") + 1,
|
||||||
|
"the bash description override must register right after fff-override",
|
||||||
|
);
|
||||||
assert.ok(
|
assert.ok(
|
||||||
packageJson.pi.extensions.includes("./pi-web-search/index.ts"),
|
packageJson.pi.extensions.includes("./pi-web-search/index.ts"),
|
||||||
"the bundle must load the unified pi-web-search tools",
|
"the bundle must load the unified pi-web-search tools",
|
||||||
@@ -292,21 +297,51 @@ test("all package extensions load together without global registration conflicts
|
|||||||
const home = await mkdtemp(join(tmpdir(), "my-pi-extension-load-"));
|
const home = await mkdtemp(join(tmpdir(), "my-pi-extension-load-"));
|
||||||
const mockBin = join(home, "bin");
|
const mockBin = join(home, "bin");
|
||||||
await mkdir(mockBin, { recursive: true });
|
await mkdir(mockBin, { recursive: true });
|
||||||
// Probe loaded LAST: records every registered tool (name + exposure) and the
|
// Probe loaded LAST: records every registered tool (name, exposure,
|
||||||
// final active set so the test can assert the pinned built-in codemode.
|
// description, source) and the final active set so the test can assert the
|
||||||
|
// pinned built-in codemode. Its async setup also runs the bash override's
|
||||||
|
// registered definition end to end — executing `echo hi` through the same
|
||||||
|
// delegation path a model call would use.
|
||||||
const probeOutput = join(home, "probe-tools.json");
|
const probeOutput = join(home, "probe-tools.json");
|
||||||
const probePath = join(home, "probe.ts");
|
const probePath = join(home, "probe.ts");
|
||||||
await writeFile(
|
await writeFile(
|
||||||
probePath,
|
probePath,
|
||||||
[
|
[
|
||||||
'import { writeFileSync } from "node:fs";',
|
'import { writeFileSync } from "node:fs";',
|
||||||
"export default function (pi: any): void {",
|
`import bashOverrideExtension from ${JSON.stringify(join(repositoryRoot, "extensions", "bash-override.ts"))};`,
|
||||||
|
"export default async function (pi: any): Promise<void> {",
|
||||||
|
"\tlet executed: unknown;",
|
||||||
|
"\ttry {",
|
||||||
|
"\t\tlet registered: any;",
|
||||||
|
"\t\tbashOverrideExtension({ registerTool: (tool: any) => { registered = tool; } } as any);",
|
||||||
|
"\t\tconst result = await registered.execute(",
|
||||||
|
'\t\t\t"probe-1",',
|
||||||
|
'\t\t\t{ command: "echo hi" },',
|
||||||
|
"\t\t\tundefined,",
|
||||||
|
"\t\t\tundefined,",
|
||||||
|
`\t\t\t{ cwd: ${JSON.stringify(home)}, sessionManager: { getSessionId: () => "probe-session", getSessionFile: () => undefined } } as any,`,
|
||||||
|
"\t\t);",
|
||||||
|
"\t\texecuted = {",
|
||||||
|
"\t\t\ttext: result?.content?.[0]?.text,",
|
||||||
|
"\t\t\texitCode: result?.structuredContent?.exit_code,",
|
||||||
|
"\t\t\thasOutputSchema: result?.structuredContent !== undefined,",
|
||||||
|
"\t\t\tregisteredDescription: registered?.description,",
|
||||||
|
"\t\t};",
|
||||||
|
"\t} catch (error) {",
|
||||||
|
"\t\texecuted = { error: String(error) };",
|
||||||
|
"\t}",
|
||||||
'\tpi.on("session_start", () => {',
|
'\tpi.on("session_start", () => {',
|
||||||
"\t\twriteFileSync(",
|
"\t\twriteFileSync(",
|
||||||
`\t\t\t${JSON.stringify(probeOutput)},`,
|
`\t\t\t${JSON.stringify(probeOutput)},`,
|
||||||
"\t\t\tJSON.stringify({",
|
"\t\t\tJSON.stringify({",
|
||||||
"\t\t\t\tall: pi.getAllTools().map((tool: any) => ({ name: tool.name, exposure: tool.exposure })),",
|
"\t\t\t\tall: pi.getAllTools().map((tool: any) => ({",
|
||||||
|
"\t\t\t\t\tname: tool.name,",
|
||||||
|
"\t\t\t\t\texposure: tool.exposure,",
|
||||||
|
"\t\t\t\t\tdescription: tool.description,",
|
||||||
|
"\t\t\t\t\tsourcePath: tool.sourceInfo?.path,",
|
||||||
|
"\t\t\t\t})),",
|
||||||
"\t\t\t\tactive: pi.getActiveTools(),",
|
"\t\t\t\tactive: pi.getActiveTools(),",
|
||||||
|
"\t\t\t\texecuted,",
|
||||||
"\t\t\t}),",
|
"\t\t\t}),",
|
||||||
"\t\t);",
|
"\t\t);",
|
||||||
"\t});",
|
"\t});",
|
||||||
@@ -337,8 +372,15 @@ test("all package extensions load together without global registration conflicts
|
|||||||
assert.doesNotMatch(`${result.stdout}\n${result.stderr}`, /Failed to load extension|conflicts with/u);
|
assert.doesNotMatch(`${result.stdout}\n${result.stderr}`, /Failed to load extension|conflicts with/u);
|
||||||
|
|
||||||
const probe = JSON.parse(await readFile(probeOutput, "utf8")) as {
|
const probe = JSON.parse(await readFile(probeOutput, "utf8")) as {
|
||||||
all: { name: string; exposure?: string }[];
|
all: { name: string; exposure?: string; description?: string; sourcePath?: string }[];
|
||||||
active: string[];
|
active: string[];
|
||||||
|
executed?: {
|
||||||
|
text?: string;
|
||||||
|
exitCode?: number;
|
||||||
|
hasOutputSchema?: boolean;
|
||||||
|
registeredDescription?: string;
|
||||||
|
error?: string;
|
||||||
|
};
|
||||||
};
|
};
|
||||||
const codemode = probe.all.find((tool) => tool.name === "codemode");
|
const codemode = probe.all.find((tool) => tool.name === "codemode");
|
||||||
assert.equal(codemode?.exposure, "model-only", "Pi's built-in codemode must load with model-only exposure");
|
assert.equal(codemode?.exposure, "model-only", "Pi's built-in codemode must load with model-only exposure");
|
||||||
@@ -351,4 +393,31 @@ test("all package extensions load together without global registration conflicts
|
|||||||
!probe.active.includes("multi_grep"),
|
!probe.active.includes("multi_grep"),
|
||||||
"multi_grep must not appear in the active tool set",
|
"multi_grep must not appear in the active tool set",
|
||||||
);
|
);
|
||||||
|
const bash = probe.all.find((tool) => tool.name === "bash");
|
||||||
|
assert.ok(bash, "the bundle must expose the bash tool");
|
||||||
|
assert.ok(
|
||||||
|
bash.description?.startsWith("Execute a bash command in the current working directory."),
|
||||||
|
"the bash override must keep Pi's upstream description prefix",
|
||||||
|
);
|
||||||
|
assert.ok(
|
||||||
|
bash.description?.endsWith("which triggers a permission review."),
|
||||||
|
"the registered bash description must end with the tools.jsonc descriptionAppend",
|
||||||
|
);
|
||||||
|
assert.ok(
|
||||||
|
(bash.sourcePath ?? "").endsWith("extensions/bash-override.ts"),
|
||||||
|
`bash must be registered by extensions/bash-override.ts, got ${bash.sourcePath}`,
|
||||||
|
);
|
||||||
|
const executed = probe.executed ?? {};
|
||||||
|
assert.equal(executed.error, undefined, `bash override execute must not throw: ${executed.error}`);
|
||||||
|
assert.equal(executed.exitCode, 0, "echo hi must exit 0 through the delegated definition");
|
||||||
|
assert.match(executed.text ?? "", /hi/);
|
||||||
|
assert.equal(
|
||||||
|
executed.hasOutputSchema,
|
||||||
|
true,
|
||||||
|
"the bash override must keep the upstream outputSchema (structuredContent for codemode)",
|
||||||
|
);
|
||||||
|
assert.ok(
|
||||||
|
executed.registeredDescription?.includes("Not for searching files"),
|
||||||
|
"the catalog descriptionAppend must reach the registered bash description",
|
||||||
|
);
|
||||||
});
|
});
|
||||||
|
|||||||
@@ -21,6 +21,7 @@ import {
|
|||||||
stripJsoncComments,
|
stripJsoncComments,
|
||||||
validateRoutingConfig,
|
validateRoutingConfig,
|
||||||
validateSectionsConfig,
|
validateSectionsConfig,
|
||||||
|
validateToolCatalog,
|
||||||
type MutablePromptOptions,
|
type MutablePromptOptions,
|
||||||
type RoutingRule,
|
type RoutingRule,
|
||||||
} from "../extensions/system-prompt-config.ts";
|
} from "../extensions/system-prompt-config.ts";
|
||||||
@@ -28,7 +29,7 @@ import systemPromptExtension, {
|
|||||||
createSystemPromptComposer,
|
createSystemPromptComposer,
|
||||||
getSystemPromptDumpPath,
|
getSystemPromptDumpPath,
|
||||||
} from "../extensions/system-prompt.ts";
|
} from "../extensions/system-prompt.ts";
|
||||||
import { guardExtensionApi } from "../extensions/prompt-guard.ts";
|
import { catalogDescription, guardExtensionApi } from "../extensions/prompt-guard.ts";
|
||||||
import { BUNDLE_GROUP_DEFINITIONS } from "../pi-tool-search/extensions/bundle-groups.ts";
|
import { BUNDLE_GROUP_DEFINITIONS } from "../pi-tool-search/extensions/bundle-groups.ts";
|
||||||
import { BUNDLE_TOOL_SEARCH_DEFAULTS } from "../pi-tool-search/extensions/config.ts";
|
import { BUNDLE_TOOL_SEARCH_DEFAULTS } from "../pi-tool-search/extensions/config.ts";
|
||||||
|
|
||||||
@@ -93,6 +94,20 @@ test("routing scope validation: main/subagent accepted, other values rejected",
|
|||||||
);
|
);
|
||||||
});
|
});
|
||||||
|
|
||||||
|
test("tools.jsonc validation: descriptionAppend accepted as a string, rejected otherwise", () => {
|
||||||
|
assert.doesNotThrow(() =>
|
||||||
|
validateToolCatalog({ version: 1, tools: { bash: { descriptionAppend: "extra" } } }),
|
||||||
|
);
|
||||||
|
assert.throws(
|
||||||
|
() => validateToolCatalog({ version: 1, tools: { bash: { descriptionAppend: 5 } } }),
|
||||||
|
/descriptionAppend must be a string/,
|
||||||
|
);
|
||||||
|
assert.throws(
|
||||||
|
() => validateToolCatalog({ version: 1, tools: { bash: { descriptionAppend: true } } }),
|
||||||
|
/descriptionAppend must be a string/,
|
||||||
|
);
|
||||||
|
});
|
||||||
|
|
||||||
test("sections.jsonc validation: subagentExclude defaults to [] and rejects bad shapes", () => {
|
test("sections.jsonc validation: subagentExclude defaults to [] and rejects bad shapes", () => {
|
||||||
assert.deepEqual(validateSectionsConfig({ version: 1, order: ["tool_routing"] }), {
|
assert.deepEqual(validateSectionsConfig({ version: 1, order: ["tool_routing"] }), {
|
||||||
version: 1,
|
version: 1,
|
||||||
@@ -608,7 +623,38 @@ test("prompt guard applies catalog description overrides at registration", () =>
|
|||||||
const override = loadToolCatalog().tools.edit;
|
const override = loadToolCatalog().tools.edit;
|
||||||
assert.ok(override && !("inherit" in override) && typeof override.description === "string");
|
assert.ok(override && !("inherit" in override) && typeof override.description === "string");
|
||||||
assert.equal(tools[0].description, override.description);
|
assert.equal(tools[0].description, override.description);
|
||||||
assert.equal(tools[1].description, "upstream grep", "inherit entries keep the upstream description");
|
assert.equal(tools[1].description, "upstream grep", "entries without description fields keep the upstream description");
|
||||||
|
});
|
||||||
|
|
||||||
|
test("prompt guard appends the catalog descriptionAppend to the upstream description", () => {
|
||||||
|
const { pi, tools } = makeRecordingPi();
|
||||||
|
const guarded = guardExtensionApi(pi as unknown as ExtensionAPI);
|
||||||
|
|
||||||
|
guarded.registerTool({ name: "bash", description: "upstream bash" } as never);
|
||||||
|
guarded.registerTool({ name: "read", description: "upstream read" } as never);
|
||||||
|
|
||||||
|
const bashEntry = loadToolCatalog().tools.bash;
|
||||||
|
assert.ok(bashEntry && !("inherit" in bashEntry) && typeof bashEntry.descriptionAppend === "string");
|
||||||
|
assert.equal(tools[0].description, `upstream bash\n\n${bashEntry.descriptionAppend}`);
|
||||||
|
assert.equal(tools[1].description, "upstream read", "inherit entries keep the upstream description");
|
||||||
|
});
|
||||||
|
|
||||||
|
test("catalogDescription composes override, append, and inherit", () => {
|
||||||
|
assert.equal(
|
||||||
|
catalogDescription("upstream", { description: "override", descriptionAppend: "append" }),
|
||||||
|
"override\n\nappend",
|
||||||
|
"an append sits on top of the description override when both are set",
|
||||||
|
);
|
||||||
|
assert.equal(
|
||||||
|
catalogDescription("upstream \n", { descriptionAppend: "append" }),
|
||||||
|
"upstream\n\nappend",
|
||||||
|
"trailing whitespace on the base is trimmed before the blank line",
|
||||||
|
);
|
||||||
|
assert.equal(
|
||||||
|
catalogDescription("upstream", { inherit: true }),
|
||||||
|
"upstream",
|
||||||
|
"inherit entries are untouched",
|
||||||
|
);
|
||||||
});
|
});
|
||||||
|
|
||||||
test("prompt guard strips a returned systemPrompt but keeps other fields", async () => {
|
test("prompt guard strips a returned systemPrompt but keeps other fields", async () => {
|
||||||
|
|||||||
Reference in New Issue
Block a user