mirror of
https://bitbucket.org/siakitem/my-pi.git
synced 2026-08-28 08:35:57 +00:00
feat(tool-search): expand dynamic group activation
This commit is contained in:
@@ -3,7 +3,7 @@ import test from "node:test";
|
||||
|
||||
import type { ToolInfo } from "@earendil-works/pi-coding-agent";
|
||||
import { BUNDLE_GROUP_DEFINITIONS, createBundleCatalog } from "../extensions/bundle-groups.ts";
|
||||
import { buildManifestHash } from "../extensions/catalog.ts";
|
||||
import { buildManifestHash, rankGroups, selectConfidentGroup } from "../extensions/catalog.ts";
|
||||
|
||||
function tool(name: string): ToolInfo {
|
||||
return {
|
||||
@@ -38,6 +38,14 @@ test("bundle seed names are unique and cover the declared my-pi tools", () => {
|
||||
assert.equal(result.catalog.tools.find((card) => card.name === "chrome_snapshot")?.primaryGroup, "chrome-navigation");
|
||||
assert.equal(result.catalog.tools.find((card) => card.name === "chrome_click")?.primaryGroup, "chrome-interaction");
|
||||
assert.equal(result.catalog.tools.find((card) => card.name === "chrome_list_network_requests")?.primaryGroup, "chrome-debugging");
|
||||
for (const query of ["read", "read banana", "search", "web search", "查找", "运行", "查找香蕉"]) {
|
||||
assert.equal(selectConfidentGroup(rankGroups(result.catalog, query), query), undefined);
|
||||
}
|
||||
assert.equal(selectConfidentGroup(rankGroups(result.catalog, "ssh_read"), "ssh_read")?.id, "ssh-remote-files");
|
||||
assert.equal(
|
||||
selectConfidentGroup(rankGroups(result.catalog, "remote find grep"), "remote find grep")?.id,
|
||||
"ssh-remote-search",
|
||||
);
|
||||
});
|
||||
|
||||
test("user overrides take priority and unknown tools retain deterministic fallback groups", () => {
|
||||
|
||||
@@ -11,7 +11,9 @@ import {
|
||||
parseGeneratedCatalog,
|
||||
rankGroups,
|
||||
readCachedCatalog,
|
||||
selectConfidentGroup,
|
||||
writeCachedCatalog,
|
||||
type ToolCatalog,
|
||||
} from "../extensions/catalog.ts";
|
||||
|
||||
function tool(name: string, description: string): ToolInfo {
|
||||
@@ -62,6 +64,29 @@ test("validates exact generated assignments and ranks generated metadata", () =>
|
||||
);
|
||||
});
|
||||
|
||||
test("prefers exact tool names and refuses weak or ambiguous query activation", () => {
|
||||
const catalog: ToolCatalog = {
|
||||
version: 1,
|
||||
manifestHash: "test",
|
||||
generatedAt: new Date(0).toISOString(),
|
||||
groups: [
|
||||
{ id: "remote-files", title: "Remote files", summary: "Read remote files", useWhen: [], avoidWhen: [], tools: ["ssh_read"] },
|
||||
{ id: "web-reader", title: "Web reader", summary: "Read web pages", useWhen: [], avoidWhen: [], tools: ["web_read"] },
|
||||
],
|
||||
tools: [
|
||||
{ name: "ssh_read", summary: "Read a remote file", useWhen: [], avoidWhen: [], keywords: ["SSH"], primaryGroup: "remote-files" },
|
||||
{ name: "web_read", summary: "Read a web page", useWhen: [], avoidWhen: [], keywords: ["web"], primaryGroup: "web-reader" },
|
||||
],
|
||||
};
|
||||
|
||||
assert.equal(selectConfidentGroup(rankGroups(catalog, "ssh_read"), "ssh_read")?.id, "remote-files");
|
||||
assert.equal(selectConfidentGroup(rankGroups(catalog, "read"), "read"), undefined);
|
||||
assert.equal(
|
||||
selectConfidentGroup(rankGroups(catalog, "completely unrelated capability"), "completely unrelated capability"),
|
||||
undefined,
|
||||
);
|
||||
});
|
||||
|
||||
test("writes a private cache and rejects a stale manifest hash", async () => {
|
||||
const directory = await mkdtemp(join(tmpdir(), "tool-search-catalog-"));
|
||||
try {
|
||||
|
||||
@@ -1,5 +1,5 @@
|
||||
import assert from "node:assert/strict";
|
||||
import { mkdtemp, readFile, rm, writeFile } from "node:fs/promises";
|
||||
import { mkdtemp, readFile, rm, stat, writeFile } from "node:fs/promises";
|
||||
import { tmpdir } from "node:os";
|
||||
import { join } from "node:path";
|
||||
import test from "node:test";
|
||||
@@ -18,16 +18,25 @@ async function withAgentDir(run: (agentDir: string) => Promise<void>): Promise<v
|
||||
test("writes bundle defaults when toolSearch is absent", async () => {
|
||||
await withAgentDir(async (agentDir) => {
|
||||
assert.equal(ensureToolSearchDefaults(agentDir), "updated");
|
||||
assert.deepEqual(BUNDLE_TOOL_SEARCH_DEFAULTS.alwaysEnabled, [
|
||||
"codegraph_explore",
|
||||
"lsp_diagnostics",
|
||||
"ctx_execute",
|
||||
"ctx_execute_file",
|
||||
"ctx_batch_execute",
|
||||
]);
|
||||
assert.deepEqual(JSON.parse(await readFile(join(agentDir, "settings.json"), "utf8")), {
|
||||
toolSearch: {
|
||||
alwaysEnabled: [...BUNDLE_TOOL_SEARCH_DEFAULTS.alwaysEnabled],
|
||||
showToolSearchFooterStatus: false,
|
||||
maxActiveGroups: 3,
|
||||
maxActiveGroups: 5,
|
||||
maxToolsPerGroup: 8,
|
||||
maxDynamicTools: 20,
|
||||
maxDynamicTools: 28,
|
||||
groupOverrides: {},
|
||||
bundleDefaultsVersion: 2,
|
||||
},
|
||||
});
|
||||
assert.equal((await stat(join(agentDir, "settings.json"))).mode & 0o777, 0o600);
|
||||
});
|
||||
});
|
||||
|
||||
@@ -43,7 +52,7 @@ test("fills missing defaults and preserves explicit settings", async () => {
|
||||
maxActiveGroups: 2,
|
||||
showToolSearchFooterStatus: false,
|
||||
maxToolsPerGroup: 8,
|
||||
maxDynamicTools: 20,
|
||||
maxDynamicTools: 28,
|
||||
groupOverrides: {},
|
||||
},
|
||||
});
|
||||
@@ -67,26 +76,26 @@ test("normalizes invalid runtime values without overwriting the file", async ()
|
||||
assert.deepEqual(readToolSearchConfig(agentDir), {
|
||||
alwaysEnabled: ["one"],
|
||||
showToolSearchFooterStatus: false,
|
||||
maxActiveGroups: 3,
|
||||
maxActiveGroups: 5,
|
||||
maxToolsPerGroup: 4,
|
||||
maxDynamicTools: 20,
|
||||
maxDynamicTools: 28,
|
||||
groupOverrides: { web: ["search"] },
|
||||
});
|
||||
assert.equal(await readFile(path, "utf8"), original);
|
||||
});
|
||||
});
|
||||
|
||||
test("preserves explicit complete configuration", async () => {
|
||||
test("preserves a complete explicit configuration including former defaults", async () => {
|
||||
await withAgentDir(async (agentDir) => {
|
||||
const path = join(agentDir, "settings.json");
|
||||
const original = `${JSON.stringify({
|
||||
toolSearch: {
|
||||
alwaysEnabled: [],
|
||||
showToolSearchFooterStatus: true,
|
||||
maxActiveGroups: 1,
|
||||
maxToolsPerGroup: 2,
|
||||
maxDynamicTools: 2,
|
||||
groupOverrides: { custom: ["one"] },
|
||||
alwaysEnabled: ["codegraph_explore", "lsp_diagnostics"],
|
||||
showToolSearchFooterStatus: false,
|
||||
maxActiveGroups: 3,
|
||||
maxToolsPerGroup: 8,
|
||||
maxDynamicTools: 20,
|
||||
groupOverrides: {},
|
||||
},
|
||||
}, null, 2)}\n`;
|
||||
await writeFile(path, original, "utf8");
|
||||
|
||||
@@ -10,6 +10,7 @@ import toolSearchExtension, { platformToolPolicy } from "../extensions/index.ts"
|
||||
interface ToolSearchResult {
|
||||
details: {
|
||||
loadedGroup?: string;
|
||||
loadedGroups: string[];
|
||||
evictedGroups: string[];
|
||||
activeGroups: string[];
|
||||
catalogSource: string;
|
||||
@@ -20,7 +21,7 @@ interface ToolSearchResult {
|
||||
interface RegisteredTool {
|
||||
execute(
|
||||
id: string,
|
||||
params: { group?: string; query?: string },
|
||||
params: { group?: string; groups?: string[]; query?: string },
|
||||
signal: AbortSignal | undefined,
|
||||
onUpdate: undefined,
|
||||
ctx: ExtensionContext,
|
||||
@@ -67,6 +68,9 @@ test("loads whole groups and evicts the least-recently-used group", async () =>
|
||||
const handlers = new Map<string, (...args: any[]) => unknown>();
|
||||
const registered = new Map<string, RegisteredTool>();
|
||||
const activeCalls: string[][] = [];
|
||||
const commands = new Map<string, (...args: any[]) => unknown>();
|
||||
const notifications: string[] = [];
|
||||
let failActivation = false;
|
||||
const sourceTools = [
|
||||
sourceTool("read", "Read a file"),
|
||||
sourceTool("bash", "Run a command"),
|
||||
@@ -85,13 +89,22 @@ test("loads whole groups and evicts the least-recently-used group", async () =>
|
||||
const tool = definition as RegisteredTool & { name: string };
|
||||
registered.set(tool.name, tool);
|
||||
},
|
||||
registerCommand: () => {},
|
||||
setActiveTools: (names: string[]) => activeCalls.push([...names]),
|
||||
registerCommand: (name: string, definition: { handler: (...args: any[]) => unknown }) => commands.set(name, definition.handler),
|
||||
setActiveTools: (names: string[]) => {
|
||||
if (failActivation) throw new Error("simulated setActiveTools failure");
|
||||
activeCalls.push([...names]);
|
||||
},
|
||||
on: (event: string, handler: (...args: any[]) => unknown) => handlers.set(event, handler),
|
||||
} as unknown as ExtensionAPI;
|
||||
|
||||
toolSearchExtension(api);
|
||||
const ctx = testContext();
|
||||
const ctx = testContext({
|
||||
hasUI: true,
|
||||
ui: {
|
||||
setStatus: () => {},
|
||||
notify: (message: string) => notifications.push(message),
|
||||
} as unknown as ExtensionContext["ui"],
|
||||
});
|
||||
handlers.get("session_start")?.({}, ctx);
|
||||
|
||||
assert.ok(activeCalls.at(-1)?.includes("tool_search"));
|
||||
@@ -113,6 +126,36 @@ test("loads whole groups and evicts the least-recently-used group", async () =>
|
||||
assert.ok(activeCalls.at(-1)?.includes("alpha_one"));
|
||||
assert.ok(activeCalls.at(-1)?.includes("gamma_one"));
|
||||
assert.ok(!activeCalls.at(-1)?.includes("beta_one"));
|
||||
|
||||
const batch = await tool?.execute("call-4", { groups: ["beta", "gamma"] }, undefined, undefined, ctx);
|
||||
assert.deepEqual(batch?.details.loadedGroups, ["beta", "gamma"]);
|
||||
assert.deepEqual(batch?.details.evictedGroups, ["alpha"]);
|
||||
assert.deepEqual(batch?.details.activeGroups, ["gamma", "beta"]);
|
||||
|
||||
const rejected = await tool?.execute(
|
||||
"call-5",
|
||||
{ groups: ["alpha", "beta", "gamma"] },
|
||||
undefined,
|
||||
undefined,
|
||||
ctx,
|
||||
);
|
||||
assert.deepEqual(rejected?.details.loadedGroups, []);
|
||||
assert.deepEqual(rejected?.details.activeGroups, ["gamma", "beta"]);
|
||||
|
||||
await commands.get("tool-search-status")?.("", ctx);
|
||||
assert.match(notifications.at(-1) ?? "", /Dynamic groups: 2\/2/);
|
||||
assert.match(notifications.at(-1) ?? "", /LRU oldest → newest: beta → gamma/);
|
||||
assert.match(notifications.at(-1) ?? "", /Last eviction: alpha \(group cap 2\)/);
|
||||
|
||||
failActivation = true;
|
||||
await assert.rejects(
|
||||
tool!.execute("call-6", { groups: ["alpha", "gamma"] }, undefined, undefined, ctx),
|
||||
/simulated setActiveTools failure/,
|
||||
);
|
||||
failActivation = false;
|
||||
await commands.get("tool-search-status")?.("", ctx);
|
||||
assert.match(notifications.at(-1) ?? "", /LRU oldest → newest: beta → gamma/);
|
||||
assert.match(notifications.at(-1) ?? "", /Last eviction: alpha \(group cap 2\)/);
|
||||
} finally {
|
||||
if (previousAgentDir === undefined) delete process.env.PI_CODING_AGENT_DIR;
|
||||
else process.env.PI_CODING_AGENT_DIR = previousAgentDir;
|
||||
@@ -203,11 +246,15 @@ test("uses the precomputed bundle groups without calling a model", async () => {
|
||||
try {
|
||||
const handlers = new Map<string, (...args: any[]) => unknown>();
|
||||
const registered = new Map<string, RegisteredTool>();
|
||||
const activeCalls: string[][] = [];
|
||||
let modelCalls = 0;
|
||||
const sourceTools = [
|
||||
sourceTool("read", "Read a file"),
|
||||
sourceTool("tavily_web_search", "Search broadly for current web sources"),
|
||||
sourceTool("tavily_web_fetch", "Fetch a selected Tavily source"),
|
||||
sourceTool("ctx_execute", "Run commands in a context sandbox"),
|
||||
sourceTool("ctx_execute_file", "Analyze a file in a context sandbox"),
|
||||
sourceTool("ctx_batch_execute", "Run a bounded command batch"),
|
||||
];
|
||||
const api = {
|
||||
getAllTools: () => sourceTools,
|
||||
@@ -216,7 +263,7 @@ test("uses the precomputed bundle groups without calling a model", async () => {
|
||||
registered.set(tool.name, tool);
|
||||
},
|
||||
registerCommand: () => {},
|
||||
setActiveTools: () => {},
|
||||
setActiveTools: (names: string[]) => activeCalls.push([...names]),
|
||||
on: (event: string, handler: (...args: any[]) => unknown) => handlers.set(event, handler),
|
||||
} as unknown as ExtensionAPI;
|
||||
toolSearchExtension(api);
|
||||
@@ -231,10 +278,21 @@ test("uses the precomputed bundle groups without calling a model", async () => {
|
||||
} as unknown as ExtensionContext["modelRegistry"],
|
||||
});
|
||||
handlers.get("session_start")?.({}, ctx);
|
||||
assert.ok(["ctx_execute", "ctx_execute_file", "ctx_batch_execute"].every((name) => activeCalls.at(-1)?.includes(name)));
|
||||
|
||||
const ambiguous = await registered.get("tool_search")?.execute(
|
||||
"call-1",
|
||||
{ query: "web search" },
|
||||
undefined,
|
||||
undefined,
|
||||
ctx,
|
||||
);
|
||||
assert.equal(ambiguous?.details.loadedGroup, undefined);
|
||||
assert.deepEqual(ambiguous?.details.activeGroups, []);
|
||||
|
||||
const result = await registered.get("tool_search")?.execute(
|
||||
"call-1",
|
||||
{ group: "web-tavily" },
|
||||
"call-2",
|
||||
{ query: "tavily_web_search" },
|
||||
undefined,
|
||||
undefined,
|
||||
ctx,
|
||||
|
||||
Reference in New Issue
Block a user