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:
@@ -334,6 +334,17 @@ export function writeCachedCatalog(cachePath: string, catalog: ToolCatalog): voi
|
||||
renameSync(temporaryPath, cachePath);
|
||||
}
|
||||
|
||||
const GROUP_QUERY_ALIASES: Record<string, string[]> = {
|
||||
"context-execution": ["context mode", "run command", "test build logs", "执行命令", "运行测试", "分析日志"],
|
||||
"ssh-connection": ["ssh connect", "remote server connection", "连接服务器", "远程连接"],
|
||||
"ssh-remote-shell": ["ssh shell", "remote command", "remote terminal", "远程命令", "远程终端"],
|
||||
"ssh-remote-files": ["ssh file", "remote read write edit", "远程文件", "远程读写"],
|
||||
"ssh-remote-search": ["ssh search", "remote find grep", "远程搜索", "远程查找"],
|
||||
"chrome-navigation": ["chrome browser navigation", "browser tabs pages", "浏览器导航", "网页导航"],
|
||||
"chrome-interaction": ["chrome browser interaction", "click type form", "浏览器交互", "点击输入"],
|
||||
"chrome-debugging": ["chrome browser debugging", "console network screenshot", "浏览器调试", "控制台网络"],
|
||||
};
|
||||
|
||||
function queryTerms(value: string): Set<string> {
|
||||
const normalized = value.toLowerCase();
|
||||
const terms = new Set(normalized.match(/[\p{L}\p{N}]+/gu) ?? []);
|
||||
@@ -343,24 +354,92 @@ function queryTerms(value: string): Set<string> {
|
||||
return terms;
|
||||
}
|
||||
|
||||
export function rankGroups(catalog: ToolCatalog, query: string): Array<{ group: GroupCard; score: number }> {
|
||||
const querySet = queryTerms(query);
|
||||
const GENERIC_QUERY_TERMS = new Set([
|
||||
"edit",
|
||||
"execute",
|
||||
"fetch",
|
||||
"file",
|
||||
"files",
|
||||
"find",
|
||||
"get",
|
||||
"list",
|
||||
"read",
|
||||
"run",
|
||||
"search",
|
||||
"show",
|
||||
"tool",
|
||||
"tools",
|
||||
"web",
|
||||
"write",
|
||||
"列出",
|
||||
"写入",
|
||||
"工具",
|
||||
"执行",
|
||||
"搜索",
|
||||
"文件",
|
||||
"显示",
|
||||
"查找",
|
||||
"获取",
|
||||
"编辑",
|
||||
"网页",
|
||||
"读取",
|
||||
"运行",
|
||||
]);
|
||||
|
||||
function addTermWeights(target: Map<string, number>, querySet: Set<string>, value: string, weight: number): void {
|
||||
const terms = queryTerms(value);
|
||||
for (const term of querySet) {
|
||||
if (terms.has(term)) target.set(term, Math.max(target.get(term) ?? 0, weight));
|
||||
}
|
||||
}
|
||||
|
||||
export interface RankedGroup {
|
||||
group: GroupCard;
|
||||
score: number;
|
||||
matchedTerms: string[];
|
||||
}
|
||||
|
||||
export function rankGroups(catalog: ToolCatalog, query: string): RankedGroup[] {
|
||||
const normalizedQuery = query.trim().toLowerCase();
|
||||
const querySet = queryTerms(normalizedQuery);
|
||||
return catalog.groups
|
||||
.map((group) => {
|
||||
const cards = catalog.tools.filter((tool) => tool.primaryGroup === group.id);
|
||||
const searchable = [
|
||||
group.id,
|
||||
group.title,
|
||||
group.summary,
|
||||
...group.useWhen,
|
||||
...group.avoidWhen,
|
||||
...cards.flatMap((tool) => [tool.name, tool.summary, ...tool.useWhen, ...tool.keywords]),
|
||||
].join(" ");
|
||||
const terms = queryTerms(searchable);
|
||||
let score = 0;
|
||||
for (const term of querySet) if (terms.has(term)) score += term.length > 1 ? 3 : 1;
|
||||
if (group.id === query.trim().toLowerCase()) score += 100;
|
||||
return { group, score };
|
||||
const aliases = GROUP_QUERY_ALIASES[group.id] ?? [];
|
||||
const termWeights = new Map<string, number>();
|
||||
addTermWeights(termWeights, querySet, group.id, 8);
|
||||
addTermWeights(termWeights, querySet, group.title, 6);
|
||||
addTermWeights(termWeights, querySet, group.summary, 2);
|
||||
addTermWeights(termWeights, querySet, group.useWhen.join(" "), 3);
|
||||
addTermWeights(termWeights, querySet, group.avoidWhen.join(" "), 1);
|
||||
addTermWeights(termWeights, querySet, aliases.join(" "), 5);
|
||||
for (const card of cards) {
|
||||
addTermWeights(termWeights, querySet, card.name, 10);
|
||||
addTermWeights(termWeights, querySet, card.summary, 2);
|
||||
addTermWeights(termWeights, querySet, card.useWhen.join(" "), 3);
|
||||
addTermWeights(termWeights, querySet, card.keywords.join(" "), 5);
|
||||
}
|
||||
let score = [...termWeights].reduce(
|
||||
(total, [term, weight]) => total + (term.length > 1 ? 3 : 1) * weight,
|
||||
0,
|
||||
);
|
||||
if (cards.some((card) => normalizedQuery === card.name.toLowerCase())) score += 800;
|
||||
if (group.id === normalizedQuery) score += 1_000;
|
||||
if (aliases.some((alias) => alias.toLowerCase() === normalizedQuery)) score += 500;
|
||||
return { group, score, matchedTerms: [...termWeights.keys()] };
|
||||
})
|
||||
.sort((left, right) => right.score - left.score || left.group.id.localeCompare(right.group.id));
|
||||
}
|
||||
|
||||
/** Avoid auto-activating a generic, weak, or ambiguous natural-language match. */
|
||||
export function selectConfidentGroup(ranked: RankedGroup[], _query: string): GroupCard | undefined {
|
||||
const [first, second] = ranked;
|
||||
if (!first || first.score < 24) return undefined;
|
||||
if (first.score >= 500) return first.group;
|
||||
const informativeTerms = first.matchedTerms.filter(
|
||||
(term) => term.length > 1 && !GENERIC_QUERY_TERMS.has(term),
|
||||
);
|
||||
if (informativeTerms.length === 0) return undefined;
|
||||
if (second && first.score - second.score < 12) return undefined;
|
||||
return first.group;
|
||||
}
|
||||
|
||||
@@ -1,17 +1,35 @@
|
||||
import { mkdirSync, readFileSync, renameSync, writeFileSync } from "node:fs";
|
||||
import {
|
||||
chmodSync,
|
||||
closeSync,
|
||||
fsyncSync,
|
||||
mkdirSync,
|
||||
openSync,
|
||||
readFileSync,
|
||||
renameSync,
|
||||
rmSync,
|
||||
writeFileSync,
|
||||
} from "node:fs";
|
||||
import { dirname, join } from "node:path";
|
||||
|
||||
export type ToolSearchDefaultResult = "updated" | "unchanged" | "skipped-invalid";
|
||||
|
||||
export const BUNDLE_TOOL_SEARCH_DEFAULTS = {
|
||||
alwaysEnabled: ["codegraph_explore", "lsp_diagnostics"],
|
||||
alwaysEnabled: [
|
||||
"codegraph_explore",
|
||||
"lsp_diagnostics",
|
||||
"ctx_execute",
|
||||
"ctx_execute_file",
|
||||
"ctx_batch_execute",
|
||||
],
|
||||
showToolSearchFooterStatus: false,
|
||||
maxActiveGroups: 3,
|
||||
maxActiveGroups: 5,
|
||||
maxToolsPerGroup: 8,
|
||||
maxDynamicTools: 20,
|
||||
maxDynamicTools: 28,
|
||||
groupOverrides: {},
|
||||
} as const;
|
||||
|
||||
export const BUNDLE_TOOL_SEARCH_DEFAULTS_VERSION = 2;
|
||||
|
||||
export interface ToolSearchConfig {
|
||||
alwaysEnabled: string[];
|
||||
showToolSearchFooterStatus: boolean;
|
||||
@@ -100,12 +118,28 @@ export function ensureToolSearchDefaults(agentDir: string): ToolSearchDefaultRes
|
||||
toolSearch[key] = Array.isArray(value) ? [...value] : isObject(value) ? { ...value } : value;
|
||||
changed = true;
|
||||
}
|
||||
if (existing === undefined) {
|
||||
toolSearch.bundleDefaultsVersion = BUNDLE_TOOL_SEARCH_DEFAULTS_VERSION;
|
||||
changed = true;
|
||||
}
|
||||
if (!changed) return "unchanged";
|
||||
|
||||
settings.toolSearch = toolSearch;
|
||||
mkdirSync(dirname(targetPath), { recursive: true });
|
||||
const temporaryPath = `${targetPath}.my-pi.tmp`;
|
||||
writeFileSync(temporaryPath, `${JSON.stringify(settings, null, 2)}\n`, "utf8");
|
||||
renameSync(temporaryPath, targetPath);
|
||||
const temporaryPath = `${targetPath}.my-pi-${process.pid}-${Math.random().toString(16).slice(2)}.tmp`;
|
||||
let descriptor: number | undefined;
|
||||
try {
|
||||
descriptor = openSync(temporaryPath, "wx", 0o600);
|
||||
writeFileSync(descriptor, `${JSON.stringify(settings, null, 2)}\n`, "utf8");
|
||||
fsyncSync(descriptor);
|
||||
closeSync(descriptor);
|
||||
descriptor = undefined;
|
||||
renameSync(temporaryPath, targetPath);
|
||||
chmodSync(targetPath, 0o600);
|
||||
} catch (error) {
|
||||
if (descriptor !== undefined) closeSync(descriptor);
|
||||
rmSync(temporaryPath, { force: true });
|
||||
throw error;
|
||||
}
|
||||
return "updated";
|
||||
}
|
||||
|
||||
@@ -18,6 +18,7 @@ import {
|
||||
parseGeneratedCatalog,
|
||||
rankGroups,
|
||||
readCachedCatalog,
|
||||
selectConfidentGroup,
|
||||
writeCachedCatalog,
|
||||
type CatalogConstraints,
|
||||
type GroupCard,
|
||||
@@ -32,6 +33,7 @@ import { ensureToolSearchDefaults, readToolSearchConfig } from "./config.ts";
|
||||
|
||||
const TOOL_SEARCH_NAME = "tool_search";
|
||||
const POWERSHELL_TOOL_NAME = "powershell";
|
||||
const MAX_BATCH_GROUPS = 5;
|
||||
const CORE_TOOLS = ["read", "write", "edit", "bash", "grep", "find"];
|
||||
|
||||
type CatalogSource = "bundle" | "cache" | "fallback" | "hybrid" | "model";
|
||||
@@ -102,7 +104,7 @@ function groupDescription(catalog: ToolCatalog, source: CatalogSource): string {
|
||||
})
|
||||
.join("\n");
|
||||
return [
|
||||
"Activate a complete tool group for the current task. Prefer an exact group id from this catalog; use query only when no id clearly matches.",
|
||||
"Activate one or more complete tool groups for the current task. Prefer exact group ids from this catalog; use query only when no id clearly matches.",
|
||||
`Catalog source: ${sourceLabel}.`,
|
||||
"Available groups:",
|
||||
groups || " (no hidden tool groups)",
|
||||
@@ -131,6 +133,7 @@ export default function toolSearchExtension(pi: ExtensionAPI): void {
|
||||
const activeGroups = new Map<string, number>();
|
||||
const pinnedTools = new Set<string>();
|
||||
let bundleState: BundleCatalogResult = { catalog, coveredNames: new Set(), unknownTools: [] };
|
||||
let lastEviction = "none";
|
||||
|
||||
function constraints(): CatalogConstraints {
|
||||
return {
|
||||
@@ -171,12 +174,32 @@ export default function toolSearchExtension(pi: ExtensionAPI): void {
|
||||
return catalog.groups.find((group) => group.id === id);
|
||||
}
|
||||
|
||||
function dynamicToolCount(): number {
|
||||
return unique([...activeGroups.keys()].flatMap((id) => groupById(id)?.tools ?? [])).length;
|
||||
function dynamicToolCount(groupIds: Iterable<string> = activeGroups.keys()): number {
|
||||
return unique([...groupIds].flatMap((id) => groupById(id)?.tools ?? [])).length;
|
||||
}
|
||||
|
||||
function leastRecentlyUsedGroup(): string | undefined {
|
||||
return [...activeGroups].sort((left, right) => left[1] - right[1] || left[0].localeCompare(right[0]))[0]?.[0];
|
||||
function leastRecentlyUsedGroup(
|
||||
groups: Map<string, number> = activeGroups,
|
||||
protectedGroups: ReadonlySet<string> = new Set(),
|
||||
): string | undefined {
|
||||
return [...groups]
|
||||
.filter(([id]) => !protectedGroups.has(id))
|
||||
.sort((left, right) => left[1] - right[1] || left[0].localeCompare(right[0]))[0]?.[0];
|
||||
}
|
||||
|
||||
function currentStatusText(): string {
|
||||
const lru = [...activeGroups]
|
||||
.sort((left, right) => left[1] - right[1] || left[0].localeCompare(right[0]))
|
||||
.map(([id]) => id);
|
||||
return [
|
||||
"Tool Search status",
|
||||
`Catalog: ${catalogSource}`,
|
||||
`Pinned tools: ${pinnedTools.size}`,
|
||||
`Dynamic groups: ${activeGroups.size}/${config.maxActiveGroups}`,
|
||||
`Dynamic tools: ${dynamicToolCount()}/${config.maxDynamicTools}`,
|
||||
`LRU oldest → newest: ${lru.join(" → ") || "none"}`,
|
||||
`Last eviction: ${lastEviction}`,
|
||||
].join("\n");
|
||||
}
|
||||
|
||||
function applyActiveTools(): void {
|
||||
@@ -189,7 +212,7 @@ export default function toolSearchExtension(pi: ExtensionAPI): void {
|
||||
ctx.ui.setStatus(
|
||||
"tool-search",
|
||||
config.showToolSearchFooterStatus
|
||||
? `${activeToolCount} / ${tools.length + 1} tools · ${activeGroups.size} / ${config.maxActiveGroups} groups`
|
||||
? `${activeToolCount} / ${tools.length + pinnedTools.size + 1} tools · ${activeGroups.size} / ${config.maxActiveGroups} groups`
|
||||
: undefined,
|
||||
);
|
||||
}
|
||||
@@ -199,71 +222,147 @@ export default function toolSearchExtension(pi: ExtensionAPI): void {
|
||||
name: TOOL_SEARCH_NAME,
|
||||
label: "Tool Search",
|
||||
description: groupDescription(catalog, catalogSource),
|
||||
promptSnippet: `Activate relevant tool groups on demand; at most ${config.maxActiveGroups} dynamic groups remain active`,
|
||||
promptSnippet: `Activate relevant tool groups on demand; up to ${config.maxActiveGroups} dynamic groups can remain active. Use groups for workflows that need several groups together`,
|
||||
parameters: Type.Object({
|
||||
group: Type.Optional(Type.String({ description: "Exact group id from the tool_search catalog" })),
|
||||
groups: Type.Optional(
|
||||
Type.Array(Type.String({ description: "Exact group id from the tool_search catalog" }), {
|
||||
description: `Exact group ids to activate atomically (maximum ${MAX_BATCH_GROUPS})`,
|
||||
minItems: 1,
|
||||
maxItems: MAX_BATCH_GROUPS,
|
||||
}),
|
||||
),
|
||||
query: Type.Optional(Type.String({ description: "Natural-language task used to rank groups when an exact id is unclear" })),
|
||||
}),
|
||||
async execute(_toolCallId, params, signal, _onUpdate, ctx) {
|
||||
const requestedId = params.group?.trim().toLowerCase();
|
||||
let selected = requestedId ? groupById(requestedId) : undefined;
|
||||
const generation: CatalogGenerationResult = selected
|
||||
? {}
|
||||
: await ensureModelCatalog(ctx, signal);
|
||||
const singleGroup = typeof params.group === "string" ? params.group.trim() : "";
|
||||
const batchGroups = Array.isArray(params.groups) ? params.groups : [];
|
||||
const query = typeof params.query === "string" ? params.query.trim() : "";
|
||||
const hasSingle = singleGroup.length > 0;
|
||||
const hasBatch = batchGroups.length > 0;
|
||||
const hasQuery = query.length > 0;
|
||||
const modes = Number(hasSingle) + Number(hasBatch) + Number(hasQuery);
|
||||
const lines: string[] = [];
|
||||
const emptyDetails = (candidates: Array<{ id: string; score: number }> = []) => ({
|
||||
loadedGroup: undefined,
|
||||
loadedGroups: [] as string[],
|
||||
evictedGroups: [] as string[],
|
||||
activeGroups: [...activeGroups.keys()],
|
||||
candidates,
|
||||
catalogSource,
|
||||
});
|
||||
|
||||
if (modes !== 1) {
|
||||
lines.push("Provide exactly one of group, groups, or query.");
|
||||
return { content: [{ type: "text", text: lines.join("\n") }], details: emptyDetails() };
|
||||
}
|
||||
if (batchGroups.length > MAX_BATCH_GROUPS) {
|
||||
lines.push(`groups accepts at most ${MAX_BATCH_GROUPS} group ids.`);
|
||||
return { content: [{ type: "text", text: lines.join("\n") }], details: emptyDetails() };
|
||||
}
|
||||
|
||||
const requestedIds = unique(
|
||||
(hasBatch ? batchGroups : hasSingle ? [singleGroup] : [])
|
||||
.map((id) => id.trim().toLowerCase())
|
||||
.filter(Boolean),
|
||||
);
|
||||
let selectedGroups = requestedIds.map(groupById).filter((group): group is GroupCard => group !== undefined);
|
||||
const needsCatalog = hasQuery || selectedGroups.length !== requestedIds.length;
|
||||
const generation: CatalogGenerationResult = needsCatalog ? await ensureModelCatalog(ctx, signal) : {};
|
||||
if (generation.notice) lines.push(generation.notice);
|
||||
|
||||
if (!selected && requestedId) selected = groupById(requestedId);
|
||||
const ranked = params.query ? rankGroups(catalog, params.query).slice(0, 3) : [];
|
||||
if (!selected && params.query && (ranked[0]?.score ?? 0) > 0) selected = ranked[0]?.group;
|
||||
selectedGroups = requestedIds.map(groupById).filter((group): group is GroupCard => group !== undefined);
|
||||
const ranked = hasQuery ? rankGroups(catalog, query).slice(0, 3) : [];
|
||||
if (hasQuery) {
|
||||
const selected = selectConfidentGroup(ranked, query);
|
||||
if (selected) selectedGroups = [selected];
|
||||
}
|
||||
|
||||
if (!selected) {
|
||||
const missingIds = requestedIds.filter((id) => !groupById(id));
|
||||
if (selectedGroups.length === 0 || missingIds.length > 0) {
|
||||
const candidates = ranked.length > 0 ? ranked : catalog.groups.slice(0, 5).map((group) => ({ group, score: 0 }));
|
||||
lines.push(
|
||||
params.group ? `Unknown group: ${params.group}` : "No group was activated. Provide an exact group id from the catalog.",
|
||||
missingIds.length > 0
|
||||
? `Unknown groups: ${missingIds.join(", ")}`
|
||||
: hasQuery
|
||||
? "No group was activated because the query match was weak or ambiguous. Use an exact group id from the candidates."
|
||||
: "No group was activated. Provide an exact group id from the catalog.",
|
||||
`Candidates: ${candidates.map(({ group }) => group.id).join(", ") || "none"}`,
|
||||
);
|
||||
return {
|
||||
content: [{ type: "text", text: lines.join("\n") }],
|
||||
details: { loadedGroup: undefined, evictedGroups: [], activeGroups: [...activeGroups.keys()], candidates: candidates.map(({ group, score }) => ({ id: group.id, score })), catalogSource },
|
||||
details: emptyDetails(candidates.map(({ group, score }) => ({ id: group.id, score }))),
|
||||
...(generation.usage ? { usage: generation.usage } : {}),
|
||||
};
|
||||
}
|
||||
|
||||
const groupTools = unique(selected.tools);
|
||||
if (groupTools.length > config.maxDynamicTools) {
|
||||
lines.push(`Group ${selected.id} has ${groupTools.length} tools, exceeding maxDynamicTools=${config.maxDynamicTools}.`);
|
||||
const selectedIds = selectedGroups.map((group) => group.id);
|
||||
const selectedTools = unique(selectedGroups.flatMap((group) => group.tools));
|
||||
if (selectedIds.length > config.maxActiveGroups || selectedTools.length > config.maxDynamicTools) {
|
||||
lines.push(
|
||||
`Requested workflow needs ${selectedIds.length} groups and ${selectedTools.length} tools, exceeding the configured limits of ${config.maxActiveGroups} groups and ${config.maxDynamicTools} tools.`,
|
||||
);
|
||||
return {
|
||||
content: [{ type: "text", text: lines.join("\n") }],
|
||||
details: { loadedGroup: undefined, evictedGroups: [], activeGroups: [...activeGroups.keys()], candidates: [], catalogSource },
|
||||
details: emptyDetails(),
|
||||
...(generation.usage ? { usage: generation.usage } : {}),
|
||||
};
|
||||
}
|
||||
|
||||
const requestedSet = new Set(selectedIds);
|
||||
const nextActive = new Map(activeGroups);
|
||||
for (const id of selectedIds) if (!nextActive.has(id)) nextActive.set(id, Number.MAX_SAFE_INTEGER);
|
||||
const evictedGroups: string[] = [];
|
||||
if (!activeGroups.has(selected.id)) {
|
||||
while (
|
||||
activeGroups.size >= config.maxActiveGroups ||
|
||||
(activeGroups.size > 0 && dynamicToolCount() + groupTools.length > config.maxDynamicTools)
|
||||
) {
|
||||
const evicted = leastRecentlyUsedGroup();
|
||||
if (!evicted) break;
|
||||
activeGroups.delete(evicted);
|
||||
evictedGroups.push(evicted);
|
||||
}
|
||||
const evictionReasons = new Set<string>();
|
||||
while (nextActive.size > config.maxActiveGroups || dynamicToolCount(nextActive.keys()) > config.maxDynamicTools) {
|
||||
if (nextActive.size > config.maxActiveGroups) evictionReasons.add(`group cap ${config.maxActiveGroups}`);
|
||||
if (dynamicToolCount(nextActive.keys()) > config.maxDynamicTools) evictionReasons.add(`tool cap ${config.maxDynamicTools}`);
|
||||
const evicted = leastRecentlyUsedGroup(nextActive, requestedSet);
|
||||
if (!evicted) break;
|
||||
nextActive.delete(evicted);
|
||||
evictedGroups.push(evicted);
|
||||
}
|
||||
activeGroups.set(selected.id, ++clock);
|
||||
applyActiveTools();
|
||||
registerToolSearch();
|
||||
updateStatus(ctx);
|
||||
|
||||
lines.push(`Loaded group: ${selected.id} (${groupTools.join(", ")})`);
|
||||
const previousActive = new Map(activeGroups);
|
||||
const previousClock = clock;
|
||||
const previousLastEviction = lastEviction;
|
||||
activeGroups.clear();
|
||||
for (const [id, recency] of nextActive) activeGroups.set(id, recency);
|
||||
for (const id of selectedIds) activeGroups.set(id, ++clock);
|
||||
if (evictedGroups.length > 0) {
|
||||
lastEviction = `${evictedGroups.join(", ")} (${[...evictionReasons].join("; ")})`;
|
||||
}
|
||||
try {
|
||||
applyActiveTools();
|
||||
registerToolSearch();
|
||||
updateStatus(ctx);
|
||||
} catch (error) {
|
||||
activeGroups.clear();
|
||||
for (const [id, recency] of previousActive) activeGroups.set(id, recency);
|
||||
clock = previousClock;
|
||||
lastEviction = previousLastEviction;
|
||||
try {
|
||||
applyActiveTools();
|
||||
registerToolSearch();
|
||||
updateStatus(ctx);
|
||||
} catch {
|
||||
// Preserve the original host activation failure while retaining rolled-back internal state.
|
||||
}
|
||||
throw error;
|
||||
}
|
||||
|
||||
if (selectedGroups.length === 1) {
|
||||
lines.push(`Loaded group: ${selectedGroups[0]?.id} (${selectedTools.join(", ")})`);
|
||||
} else {
|
||||
lines.push(`Loaded groups: ${selectedIds.join(", ")} (${selectedTools.length} unique tools)`);
|
||||
}
|
||||
if (evictedGroups.length > 0) lines.push(`Evicted least-recently-used: ${evictedGroups.join(", ")}`);
|
||||
lines.push(`Active groups: ${[...activeGroups.keys()].join(", ")}`);
|
||||
return {
|
||||
content: [{ type: "text", text: lines.join("\n") }],
|
||||
details: {
|
||||
loadedGroup: selected.id,
|
||||
loadedGroup: selectedIds.at(-1),
|
||||
loadedGroups: selectedIds,
|
||||
evictedGroups,
|
||||
activeGroups: [...activeGroups.keys()],
|
||||
candidates: ranked.map(({ group, score }) => ({ id: group.id, score })),
|
||||
@@ -379,17 +478,23 @@ export default function toolSearchExtension(pi: ExtensionAPI): void {
|
||||
if (catalogChanged) {
|
||||
activeGroups.clear();
|
||||
clock = 0;
|
||||
lastEviction = "none";
|
||||
attemptedGenerationHash = undefined;
|
||||
installBaseCatalog(true);
|
||||
}
|
||||
|
||||
let capacityChanged = false;
|
||||
const refreshEvictions: string[] = [];
|
||||
const refreshReasons = new Set<string>();
|
||||
while (activeGroups.size > config.maxActiveGroups || dynamicToolCount() > config.maxDynamicTools) {
|
||||
if (activeGroups.size > config.maxActiveGroups) refreshReasons.add(`group cap ${config.maxActiveGroups}`);
|
||||
if (dynamicToolCount() > config.maxDynamicTools) refreshReasons.add(`tool cap ${config.maxDynamicTools}`);
|
||||
const evicted = leastRecentlyUsedGroup();
|
||||
if (!evicted) break;
|
||||
activeGroups.delete(evicted);
|
||||
capacityChanged = true;
|
||||
refreshEvictions.push(evicted);
|
||||
}
|
||||
const capacityChanged = refreshEvictions.length > 0;
|
||||
if (capacityChanged) lastEviction = `${refreshEvictions.join(", ")} (${[...refreshReasons].join("; ")}; settings refresh)`;
|
||||
if (catalogChanged || policyChanged || capacityChanged) {
|
||||
registerToolSearch();
|
||||
applyActiveTools();
|
||||
@@ -397,6 +502,13 @@ export default function toolSearchExtension(pi: ExtensionAPI): void {
|
||||
updateStatus(ctx);
|
||||
}
|
||||
|
||||
pi.registerCommand("tool-search-status", {
|
||||
description: "Show Tool Search catalog, capacity, active-group LRU order, and the latest eviction reason",
|
||||
handler: async (_args, ctx) => {
|
||||
if (ctx.hasUI) ctx.ui.notify(currentStatusText(), "info");
|
||||
},
|
||||
});
|
||||
|
||||
pi.registerCommand("tool-search-rebuild", {
|
||||
description: "Invalidate the generated tool-group catalog; rebuild lazily on the next tool_search call",
|
||||
handler: async (_args, ctx) => {
|
||||
@@ -407,6 +519,7 @@ export default function toolSearchExtension(pi: ExtensionAPI): void {
|
||||
}
|
||||
activeGroups.clear();
|
||||
attemptedGenerationHash = undefined;
|
||||
lastEviction = "none";
|
||||
installBaseCatalog(false);
|
||||
registerToolSearch();
|
||||
applyActiveTools();
|
||||
|
||||
Reference in New Issue
Block a user