mirror of
https://bitbucket.org/siakitem/my-pi.git
synced 2026-10-26 10:45:22 +00:00
feat(prompt): 重新注册 bash,把"不要用 bash 搜索"写进工具描述
真实会话里模型仍习惯性先调 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>
This commit is contained in:
co-authored by
Devin <158243242+devin-ai-integration[bot]@users.noreply.github.com>
parent
2b21163a18
commit
87940da51a
@@ -58,6 +58,11 @@ test("all package extensions load together without global registration conflicts
|
||||
packageJson.pi.extensions.includes("./extensions/codegraph.ts"),
|
||||
"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(
|
||||
packageJson.pi.extensions.includes("./pi-web-search/index.ts"),
|
||||
"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 mockBin = join(home, "bin");
|
||||
await mkdir(mockBin, { recursive: true });
|
||||
// Probe loaded LAST: records every registered tool (name + exposure) and the
|
||||
// final active set so the test can assert the pinned built-in codemode.
|
||||
// Probe loaded LAST: records every registered tool (name, exposure,
|
||||
// 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 probePath = join(home, "probe.ts");
|
||||
await writeFile(
|
||||
probePath,
|
||||
[
|
||||
'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", () => {',
|
||||
"\t\twriteFileSync(",
|
||||
`\t\t\t${JSON.stringify(probeOutput)},`,
|
||||
"\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\texecuted,",
|
||||
"\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);
|
||||
|
||||
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[];
|
||||
executed?: {
|
||||
text?: string;
|
||||
exitCode?: number;
|
||||
hasOutputSchema?: boolean;
|
||||
registeredDescription?: string;
|
||||
error?: string;
|
||||
};
|
||||
};
|
||||
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");
|
||||
@@ -351,4 +393,31 @@ test("all package extensions load together without global registration conflicts
|
||||
!probe.active.includes("multi_grep"),
|
||||
"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,
|
||||
validateRoutingConfig,
|
||||
validateSectionsConfig,
|
||||
validateToolCatalog,
|
||||
type MutablePromptOptions,
|
||||
type RoutingRule,
|
||||
} from "../extensions/system-prompt-config.ts";
|
||||
@@ -28,7 +29,7 @@ import systemPromptExtension, {
|
||||
createSystemPromptComposer,
|
||||
getSystemPromptDumpPath,
|
||||
} 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_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", () => {
|
||||
assert.deepEqual(validateSectionsConfig({ version: 1, order: ["tool_routing"] }), {
|
||||
version: 1,
|
||||
@@ -608,7 +623,38 @@ test("prompt guard applies catalog description overrides at registration", () =>
|
||||
const override = loadToolCatalog().tools.edit;
|
||||
assert.ok(override && !("inherit" in override) && typeof override.description === "string");
|
||||
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 () => {
|
||||
|
||||
Reference in New Issue
Block a user