fix(agents): skip unreadable tool search catalog entries

This commit is contained in:
Vincent Koc
2026-06-04 09:33:22 +02:00
parent ec3aa5def4
commit 5d1ebe624b
2 changed files with 73 additions and 11 deletions

View File

@@ -158,6 +158,32 @@ describe("Tool Search", () => {
expect(telemetry.callCount).toBe(1);
});
it("skips catalog tools with unreadable schemas without dropping healthy siblings", () => {
const codeTool = fakeTool(TOOL_SEARCH_CODE_MODE_TOOL_NAME, "code mode");
const broken = pluginTool("fake_broken", "Broken target");
Object.defineProperty(broken, "parameters", {
enumerable: true,
get() {
throw new Error("fuzzplugin parameters getter exploded");
},
});
const healthy = pluginTool("fake_healthy", "Healthy target");
const compacted = applyToolSearchCatalog({
tools: [codeTool, broken, healthy],
config: {
tools: {
toolSearch: true,
},
} as never,
sessionId: "session-unreadable-schema",
sessionKey: "agent:main:unreadable-schema",
});
expect(compacted.tools.map((tool) => tool.name)).toEqual([TOOL_SEARCH_CODE_MODE_TOOL_NAME]);
expect(compacted.catalogToolCount).toBe(1);
});
it("scopes catalogs by run id when attempts share a session", async () => {
const runATool = pluginTool("fake_run_a", "Tool visible only to run A");
const runBTool = pluginTool("fake_run_b", "Tool visible only to run B");

View File

@@ -638,9 +638,35 @@ function classifyTool(tool: CatalogTool): {
return { source: "openclaw", sourceName: "core" };
}
function makeCatalogId(tool: CatalogTool, source: CatalogSource, sourceName?: string): string {
function makeCatalogId(name: string, source: CatalogSource, sourceName?: string): string {
const owner = sourceName?.trim() || "core";
return `${source}:${owner}:${tool.name}`;
return `${source}:${owner}:${name}`;
}
type CatalogToolSnapshot = {
name: string;
label?: string;
description: string;
parameters?: unknown;
};
function readCatalogToolSnapshot(tool: CatalogTool): CatalogToolSnapshot | undefined {
try {
const name = tool.name;
if (typeof name !== "string" || !name.trim()) {
return undefined;
}
const label = typeof tool.label === "string" ? tool.label : undefined;
const description = typeof tool.description === "string" ? tool.description : "";
return {
name,
...(label !== undefined ? { label } : {}),
description,
parameters: tool.parameters,
};
} catch {
return undefined;
}
}
function wrapCatalogTool(tool: AnyAgentTool, hookContext?: HookContext): AnyAgentTool {
@@ -654,21 +680,25 @@ function toCatalogEntry(
tool: CatalogTool,
sourceOverride?: CatalogSource,
hookContext?: HookContext,
): ToolSearchCatalogEntry {
): ToolSearchCatalogEntry | undefined {
const snapshot = readCatalogToolSnapshot(tool);
if (!snapshot) {
return undefined;
}
const classified = classifyTool(tool);
const source = sourceOverride ?? classified.source;
const sourceName = sourceOverride === "client" ? "client" : classified.sourceName;
const catalogTool =
source === "client" ? tool : wrapCatalogTool(tool as AnyAgentTool, hookContext);
return {
id: makeCatalogId(tool, source, sourceName),
id: makeCatalogId(snapshot.name, source, sourceName),
source,
sourceName,
...(source === "mcp" && classified.mcp ? { mcp: classified.mcp } : {}),
name: tool.name,
label: tool.label,
description: tool.description ?? "",
parameters: tool.parameters,
name: snapshot.name,
label: snapshot.label,
description: snapshot.description,
parameters: snapshot.parameters,
tool: catalogTool,
};
}
@@ -1292,7 +1322,10 @@ export function applyToolCatalogCompaction(params: {
continue;
}
if (shouldCatalog(tool)) {
catalog.push(toCatalogEntry(tool, undefined, params.toolHookContext));
const entry = toCatalogEntry(tool, undefined, params.toolHookContext);
if (entry) {
catalog.push(entry);
}
continue;
}
visible.push(tool);
@@ -1382,16 +1415,19 @@ export function addClientToolsToToolCatalog(params: {
if (!existing) {
return { tools: params.tools, compacted: false, catalogToolCount: 0 };
}
const entries = params.tools
.map((tool) => toCatalogEntry(tool, "client"))
.filter((entry): entry is ToolSearchCatalogEntry => Boolean(entry));
registerToolSearchCatalog({
sessionId: params.sessionId,
sessionKey: params.sessionKey,
agentId: params.agentId,
runId: params.runId,
catalogRef: params.catalogRef,
entries: params.tools.map((tool) => toCatalogEntry(tool, "client")),
entries,
append: true,
});
return { tools: [], compacted: params.tools.length > 0, catalogToolCount: params.tools.length };
return { tools: [], compacted: entries.length > 0, catalogToolCount: entries.length };
}
function toJsonSafe(value: unknown): unknown {