mirror of
https://github.com/openclaw/openclaw.git
synced 2026-06-29 11:13:36 +00:00
fix(agents): suggest recovery for unknown tool ids (#93374)
Merged via squash.
Prepared head SHA: bee84e4eb8
Co-authored-by: mushuiyu886 <266724580+mushuiyu886@users.noreply.github.com>
Co-authored-by: vincentkoc <25068+vincentkoc@users.noreply.github.com>
Reviewed-by: @vincentkoc
This commit is contained in:
@@ -779,6 +779,71 @@ describe("Code Mode", () => {
|
||||
expect(ticket.execute).toHaveBeenCalledTimes(1);
|
||||
});
|
||||
|
||||
it("uses tools recovery guidance for guessed tool ids", async () => {
|
||||
const { config, catalogRef, tools: codeModeTools } = createCodeModeHarness();
|
||||
const writeTool = pluginTool("write", "Write a file to the workspace");
|
||||
applyCodeModeCatalog({
|
||||
tools: [...codeModeTools, writeTool],
|
||||
config,
|
||||
sessionId: "session-code-mode",
|
||||
sessionKey: "agent:main:main",
|
||||
runId: "run-code-mode",
|
||||
catalogRef,
|
||||
});
|
||||
|
||||
const details = await runUntilCompleted({
|
||||
execTool: codeModeTools[0],
|
||||
waitTool: codeModeTools[1],
|
||||
code: `
|
||||
try {
|
||||
await tools.call("file_write", {
|
||||
path: "memory/2026-05-22.md",
|
||||
content: "remember this",
|
||||
});
|
||||
return "unexpected success";
|
||||
} catch (error) {
|
||||
return error.message;
|
||||
}
|
||||
`,
|
||||
});
|
||||
|
||||
expect(details.status).toBe("completed");
|
||||
expect(details.value).toBe(
|
||||
"Unknown tool id: file_write. Did you mean: write? Use tools.search to find a tool, tools.describe to inspect it, then tools.call with the exact id or name.",
|
||||
);
|
||||
expect(writeTool.execute).not.toHaveBeenCalled();
|
||||
});
|
||||
|
||||
it("uses tools recovery guidance when no generic Code Mode suggestion matches", async () => {
|
||||
const { config, catalogRef, tools: codeModeTools } = createCodeModeHarness();
|
||||
applyCodeModeCatalog({
|
||||
tools: codeModeTools,
|
||||
config,
|
||||
sessionId: "session-code-mode",
|
||||
sessionKey: "agent:main:main",
|
||||
runId: "run-code-mode",
|
||||
catalogRef,
|
||||
});
|
||||
|
||||
const details = await runUntilCompleted({
|
||||
execTool: codeModeTools[0],
|
||||
waitTool: codeModeTools[1],
|
||||
code: `
|
||||
try {
|
||||
await tools.call("missing_tool", {});
|
||||
return "unexpected success";
|
||||
} catch (error) {
|
||||
return error.message;
|
||||
}
|
||||
`,
|
||||
});
|
||||
|
||||
expect(details.status).toBe("completed");
|
||||
expect(details.value).toBe(
|
||||
"Unknown tool id: missing_tool. Use tools.search to find a tool, tools.describe to inspect it, then tools.call with the exact id or name.",
|
||||
);
|
||||
});
|
||||
|
||||
it("exposes MCP tools only through the MCP namespace", async () => {
|
||||
const { config, catalogRef, tools: codeModeTools } = createCodeModeHarness();
|
||||
const githubCreate = mcpTool({
|
||||
@@ -882,8 +947,10 @@ describe("Code Mode", () => {
|
||||
},
|
||||
searchHits: [],
|
||||
allHasMcp: false,
|
||||
directDescribe: "Unknown tool id: github__create_issue",
|
||||
directCall: "Unknown tool id: github__create_issue",
|
||||
directDescribe:
|
||||
"Unknown tool id: github__create_issue. Use tools.search to find a tool, tools.describe to inspect it, then tools.call with the exact id or name.",
|
||||
directCall:
|
||||
"Unknown tool id: github__create_issue. Use tools.search to find a tool, tools.describe to inspect it, then tools.call with the exact id or name.",
|
||||
hasMcp: true,
|
||||
apiSchemaTitle: "object",
|
||||
apiHeader: expect.stringContaining("function createIssue("),
|
||||
|
||||
@@ -305,13 +305,24 @@ class CodeModeLimitError extends ToolInputError {
|
||||
}
|
||||
}
|
||||
|
||||
function isRuntimeInterruptedError(error: unknown): boolean {
|
||||
return errorMessage(error) === "interrupted";
|
||||
}
|
||||
|
||||
function codeModeFailureCode(error: unknown): CodeModeFailureCode {
|
||||
if (error instanceof CodeModeLimitError) {
|
||||
return error.code;
|
||||
}
|
||||
if (isRuntimeInterruptedError(error)) {
|
||||
return "timeout";
|
||||
}
|
||||
return error instanceof ToolInputError ? "invalid_input" : "internal_error";
|
||||
}
|
||||
|
||||
function codeModeFailureMessage(error: unknown): string {
|
||||
return isRuntimeInterruptedError(error) ? "code mode timeout exceeded" : errorMessage(error);
|
||||
}
|
||||
|
||||
function enforceOutputLimit(output: unknown[], config: CodeModeConfig): void {
|
||||
if (jsonByteLength(output) > config.maxOutputBytes) {
|
||||
throw new CodeModeLimitError("output_limit_exceeded", "code mode output limit exceeded");
|
||||
@@ -505,7 +516,10 @@ async function runBridgeRequest(params: {
|
||||
if (typeof id !== "string") {
|
||||
throw new ToolInputError("describe id must be a string.");
|
||||
}
|
||||
value = await params.runtime.describe(id, { includeMcp: false });
|
||||
value = await params.runtime.describe(id, {
|
||||
includeMcp: false,
|
||||
recoverySurface: "tools",
|
||||
});
|
||||
break;
|
||||
}
|
||||
case "call": {
|
||||
@@ -513,7 +527,10 @@ async function runBridgeRequest(params: {
|
||||
if (typeof id !== "string") {
|
||||
throw new ToolInputError("call id must be a string.");
|
||||
}
|
||||
const described = await params.runtime.describe(id, { includeMcp: false });
|
||||
const described = await params.runtime.describe(id, {
|
||||
includeMcp: false,
|
||||
recoverySurface: "tools",
|
||||
});
|
||||
value = await params.runtime.callExactId(described.id, values[1] ?? {}, {
|
||||
parentToolCallId: params.parentToolCallId,
|
||||
signal: params.signal,
|
||||
@@ -606,24 +623,26 @@ function failedCodeModeWorkerResult(
|
||||
};
|
||||
}
|
||||
|
||||
function isQuickJsInterruptedWorkerError(error: unknown): boolean {
|
||||
return String(error) === "interrupted";
|
||||
}
|
||||
|
||||
function normalizeCodeModeWorkerResult(result: CodeModeWorkerResult): CodeModeWorkerResult {
|
||||
function normalizeCodeModeTimeoutResult<
|
||||
T extends { status: string; code?: unknown; error?: unknown },
|
||||
>(result: T): T {
|
||||
if (
|
||||
result.status === "failed" &&
|
||||
result.code === "timeout" &&
|
||||
isQuickJsInterruptedWorkerError(result.error)
|
||||
!String(result.error).includes("timeout exceeded")
|
||||
) {
|
||||
return {
|
||||
...result,
|
||||
error: "code mode timeout exceeded",
|
||||
};
|
||||
} as T;
|
||||
}
|
||||
return result;
|
||||
}
|
||||
|
||||
function normalizeCodeModeWorkerResult(result: CodeModeWorkerResult): CodeModeWorkerResult {
|
||||
return normalizeCodeModeTimeoutResult(result);
|
||||
}
|
||||
|
||||
async function runCodeModeWorker(
|
||||
workerData: unknown,
|
||||
timeoutMs: number,
|
||||
@@ -855,7 +874,7 @@ async function runExec(params: {
|
||||
} catch (error) {
|
||||
return {
|
||||
status: "failed" as const,
|
||||
error: errorMessage(error),
|
||||
error: codeModeFailureMessage(error),
|
||||
code: codeModeFailureCode(error),
|
||||
output: [],
|
||||
telemetry: telemetry(runtime),
|
||||
@@ -889,7 +908,7 @@ async function runExec(params: {
|
||||
} catch (error) {
|
||||
return {
|
||||
status: "failed" as const,
|
||||
error: errorMessage(error),
|
||||
error: codeModeFailureMessage(error),
|
||||
code: codeModeFailureCode(error),
|
||||
output: [],
|
||||
telemetry: telemetry(runtime),
|
||||
@@ -1094,7 +1113,7 @@ async function runWait(params: {
|
||||
} catch (error) {
|
||||
return {
|
||||
status: "failed" as const,
|
||||
error: errorMessage(error),
|
||||
error: codeModeFailureMessage(error),
|
||||
code: codeModeFailureCode(error),
|
||||
output: state.output,
|
||||
telemetry: telemetry(state.runtime),
|
||||
@@ -1135,14 +1154,16 @@ export function createCodeModeTools(ctx: CodeModeToolContext): AnyAgentTool[] {
|
||||
) => {
|
||||
const input = readCode(args);
|
||||
return jsonResult(
|
||||
await runExec({
|
||||
toolCallId,
|
||||
ctx,
|
||||
code: input.code,
|
||||
language: input.language,
|
||||
signal,
|
||||
onUpdate,
|
||||
}),
|
||||
normalizeCodeModeTimeoutResult(
|
||||
await runExec({
|
||||
toolCallId,
|
||||
ctx,
|
||||
code: input.code,
|
||||
language: input.language,
|
||||
signal,
|
||||
onUpdate,
|
||||
}),
|
||||
),
|
||||
);
|
||||
},
|
||||
} as AnyAgentTool);
|
||||
@@ -1160,13 +1181,15 @@ export function createCodeModeTools(ctx: CodeModeToolContext): AnyAgentTool[] {
|
||||
onUpdate?: AgentToolUpdateCallback,
|
||||
) =>
|
||||
jsonResult(
|
||||
await runWait({
|
||||
toolCallId,
|
||||
ctx,
|
||||
runId: readRunId(args),
|
||||
signal,
|
||||
onUpdate,
|
||||
}),
|
||||
normalizeCodeModeTimeoutResult(
|
||||
await runWait({
|
||||
toolCallId,
|
||||
ctx,
|
||||
runId: readRunId(args),
|
||||
signal,
|
||||
onUpdate,
|
||||
}),
|
||||
),
|
||||
),
|
||||
} as AnyAgentTool);
|
||||
return [execTool, waitTool];
|
||||
|
||||
@@ -6,8 +6,6 @@ import { readFile } from "node:fs/promises";
|
||||
import { createRequire } from "node:module";
|
||||
import { parentPort, workerData } from "node:worker_threads";
|
||||
import { EvalFlags, Intrinsics, JSException, QuickJS, type JSValueHandle } from "quickjs-wasi";
|
||||
import { toCodeModeJsonSafe as toJsonSafe } from "./code-mode-json.js";
|
||||
|
||||
const require = createRequire(import.meta.url);
|
||||
const QUICKJS_WASM_PATH = require.resolve("quickjs-wasi/quickjs.wasm");
|
||||
let quickJsWasmModulePromise: Promise<WebAssembly.Module> | undefined;
|
||||
@@ -162,6 +160,35 @@ function errorMessage(error: unknown): string {
|
||||
return String(error);
|
||||
}
|
||||
|
||||
function toJsonSafe(value: unknown): unknown {
|
||||
if (value === undefined) {
|
||||
return null;
|
||||
}
|
||||
try {
|
||||
const serialized = JSON.stringify(value);
|
||||
return serialized === undefined ? null : (JSON.parse(serialized) as unknown);
|
||||
} catch {
|
||||
if (value instanceof Error) {
|
||||
return { name: value.name, message: value.message };
|
||||
}
|
||||
if (value === null) {
|
||||
return null;
|
||||
}
|
||||
switch (typeof value) {
|
||||
case "string":
|
||||
case "number":
|
||||
case "boolean":
|
||||
return value;
|
||||
case "bigint":
|
||||
case "symbol":
|
||||
case "function":
|
||||
return String(value);
|
||||
default:
|
||||
return Object.prototype.toString.call(value);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
const CONTROLLER_SOURCE = String.raw`
|
||||
(() => {
|
||||
const output = [];
|
||||
@@ -727,10 +754,15 @@ async function main(): Promise<CodeModeWorkerResult> {
|
||||
output: [],
|
||||
};
|
||||
} catch (error) {
|
||||
const timedOut = isQuickJsInterruptedError(error);
|
||||
return {
|
||||
status: "failed",
|
||||
error: errorMessage(error),
|
||||
code: error instanceof CodeModeWorkerFailure ? error.code : "internal_error",
|
||||
error: timedOut ? "code mode timeout exceeded" : errorMessage(error),
|
||||
code: timedOut
|
||||
? "timeout"
|
||||
: error instanceof CodeModeWorkerFailure
|
||||
? error.code
|
||||
: "internal_error",
|
||||
output: error instanceof CodeModeWorkerFailureWithOutput ? error.output : [],
|
||||
};
|
||||
}
|
||||
|
||||
@@ -1288,6 +1288,119 @@ describe("Tool Search", () => {
|
||||
).rejects.toThrow();
|
||||
});
|
||||
|
||||
it("suggests recoverable Tool Search steps for guessed tool ids", async () => {
|
||||
const callTool = fakeTool(TOOL_CALL_RAW_TOOL_NAME, "call");
|
||||
const searchTool = fakeTool(TOOL_SEARCH_RAW_TOOL_NAME, "search");
|
||||
const describeTool = fakeTool(TOOL_DESCRIBE_RAW_TOOL_NAME, "describe");
|
||||
const writeTool = fakeTool("write", "Write a file to the workspace");
|
||||
applyToolSearchCatalog({
|
||||
tools: [callTool, searchTool, describeTool, writeTool],
|
||||
config: { tools: { toolSearch: { mode: "tools" } } } as never,
|
||||
sessionId: "session-guessed-file-write",
|
||||
sessionKey: "agent:main:main",
|
||||
});
|
||||
|
||||
const runtimeTools = createToolSearchTools({
|
||||
sessionId: "session-guessed-file-write",
|
||||
sessionKey: "agent:main:main",
|
||||
config: { tools: { toolSearch: { mode: "tools" } } } as never,
|
||||
});
|
||||
const runtimeCallTool = runtimeTools[3];
|
||||
|
||||
await expect(
|
||||
runtimeCallTool.execute("call-guessed-file-write", {
|
||||
id: "file_write",
|
||||
args: { path: "memory/2026-05-22.md", content: "remember this" },
|
||||
}),
|
||||
).rejects.toThrow(
|
||||
"Unknown tool id: file_write. Did you mean: write? Use tool_search to find a tool, tool_describe to inspect it, then tool_call with the exact id or name.",
|
||||
);
|
||||
expect(writeTool.execute).not.toHaveBeenCalled();
|
||||
});
|
||||
|
||||
it("uses exact ids when recovery suggestions have duplicate names", async () => {
|
||||
const callTool = fakeTool(TOOL_CALL_RAW_TOOL_NAME, "call");
|
||||
const searchTool = fakeTool(TOOL_SEARCH_RAW_TOOL_NAME, "search");
|
||||
const describeTool = fakeTool(TOOL_DESCRIBE_RAW_TOOL_NAME, "describe");
|
||||
const firstWriteTool = pluginTool("write", "Write a file", "first-plugin");
|
||||
const secondWriteTool = pluginTool("write", "Write another file", "second-plugin");
|
||||
applyToolSearchCatalog({
|
||||
tools: [callTool, searchTool, describeTool, firstWriteTool, secondWriteTool],
|
||||
config: { tools: { toolSearch: { mode: "tools" } } } as never,
|
||||
sessionId: "session-duplicate-recovery",
|
||||
sessionKey: "agent:main:main",
|
||||
});
|
||||
|
||||
const runtimeTools = createToolSearchTools({
|
||||
sessionId: "session-duplicate-recovery",
|
||||
sessionKey: "agent:main:main",
|
||||
config: { tools: { toolSearch: { mode: "tools" } } } as never,
|
||||
});
|
||||
|
||||
await expect(
|
||||
runtimeTools[3].execute("call-duplicate-write", {
|
||||
id: "file_write",
|
||||
args: {},
|
||||
}),
|
||||
).rejects.toThrow("Did you mean: openclaw:first-plugin:write, openclaw:second-plugin:write?");
|
||||
});
|
||||
|
||||
it("keeps raw Tool Search recovery guidance when no suggestion matches", async () => {
|
||||
const callTool = fakeTool(TOOL_CALL_RAW_TOOL_NAME, "call");
|
||||
const searchTool = fakeTool(TOOL_SEARCH_RAW_TOOL_NAME, "search");
|
||||
const describeTool = fakeTool(TOOL_DESCRIBE_RAW_TOOL_NAME, "describe");
|
||||
const writeTool = fakeTool("write", "Write a file to the workspace");
|
||||
applyToolSearchCatalog({
|
||||
tools: [callTool, searchTool, describeTool, writeTool],
|
||||
config: { tools: { toolSearch: { mode: "tools" } } } as never,
|
||||
sessionId: "session-missing-raw-tool",
|
||||
sessionKey: "agent:main:main",
|
||||
});
|
||||
|
||||
const runtimeTools = createToolSearchTools({
|
||||
sessionId: "session-missing-raw-tool",
|
||||
sessionKey: "agent:main:main",
|
||||
config: { tools: { toolSearch: { mode: "tools" } } } as never,
|
||||
});
|
||||
const runtimeCallTool = runtimeTools[3];
|
||||
|
||||
await expect(
|
||||
runtimeCallTool.execute("call-missing-raw-tool", {
|
||||
id: "missing_tool",
|
||||
args: {},
|
||||
}),
|
||||
).rejects.toThrow(
|
||||
"Unknown tool id: missing_tool. Use tool_search to find a tool, tool_describe to inspect it, then tool_call with the exact id or name.",
|
||||
);
|
||||
expect(writeTool.execute).not.toHaveBeenCalled();
|
||||
});
|
||||
|
||||
it("preserves code-mode bridge recovery guidance for guessed tool ids", async () => {
|
||||
const codeTool = fakeTool(TOOL_SEARCH_CODE_MODE_TOOL_NAME, "code mode");
|
||||
const writeTool = fakeTool("write", "Write a file to the workspace");
|
||||
applyToolSearchCatalog({
|
||||
tools: [codeTool, writeTool],
|
||||
config: { tools: { toolSearch: true } } as never,
|
||||
sessionId: "session-code-guessed-file-write",
|
||||
sessionKey: "agent:main:main",
|
||||
});
|
||||
|
||||
const [runtimeCodeTool] = createToolSearchTools({
|
||||
sessionId: "session-code-guessed-file-write",
|
||||
sessionKey: "agent:main:main",
|
||||
config: {},
|
||||
});
|
||||
|
||||
await expect(
|
||||
runtimeCodeTool.execute("call-code-guessed-file-write", {
|
||||
code: `return await openclaw.tools.call("file_write", { path: "memory/2026-05-22.md" });`,
|
||||
}),
|
||||
).rejects.toThrow(
|
||||
"Unknown tool id: file_write. Did you mean: write? Use openclaw.tools.search to find a tool, openclaw.tools.describe to inspect it, then openclaw.tools.call with the exact id or name.",
|
||||
);
|
||||
expect(writeTool.execute).not.toHaveBeenCalled();
|
||||
});
|
||||
|
||||
it("preserves code-mode bridge errors from the child process", async () => {
|
||||
const codeTool = fakeTool(TOOL_SEARCH_CODE_MODE_TOOL_NAME, "code mode");
|
||||
applyToolSearchCatalog({
|
||||
@@ -1307,7 +1420,9 @@ describe("Tool Search", () => {
|
||||
runtimeCodeTool.execute("call-missing-tool", {
|
||||
code: `return await openclaw.tools.call("missing_tool", {});`,
|
||||
}),
|
||||
).rejects.toThrow("Unknown tool id: missing_tool");
|
||||
).rejects.toThrow(
|
||||
"Unknown tool id: missing_tool. Use openclaw.tools.search to find a tool, openclaw.tools.describe to inspect it, then openclaw.tools.call with the exact id or name.",
|
||||
);
|
||||
});
|
||||
|
||||
it("does not expose host-realm bridge result objects to model-authored code", async () => {
|
||||
|
||||
@@ -55,6 +55,11 @@ type CatalogTool = AnyAgentTool | ToolDefinition;
|
||||
type CatalogVisibilityOptions = {
|
||||
includeMcp?: boolean;
|
||||
};
|
||||
type UnknownToolRecoverySurface = "raw-tools" | "code-mode" | "tools";
|
||||
type UnknownToolErrorOptions = {
|
||||
exactIdOnly?: boolean;
|
||||
recoverySurface?: UnknownToolRecoverySurface;
|
||||
};
|
||||
|
||||
type ReusableCatalogSnapshot = {
|
||||
entries: ToolSearchCatalogEntry[];
|
||||
@@ -1539,10 +1544,74 @@ function visibleCatalogEntries(
|
||||
: catalog.entries;
|
||||
}
|
||||
|
||||
function tokenizeLookupValue(input: string): Set<string> {
|
||||
return new Set(normalizeStringEntries(input.toLowerCase().split(/[^a-z0-9]+/u)));
|
||||
}
|
||||
|
||||
function scoreUnknownToolSuggestion(needle: string, entry: ToolSearchCatalogEntry): number {
|
||||
const normalizedNeedle = needle.toLowerCase();
|
||||
const name = entry.name.toLowerCase();
|
||||
const id = entry.id.toLowerCase();
|
||||
const label = (entry.label ?? "").toLowerCase();
|
||||
const description = entry.description.toLowerCase();
|
||||
const needleTokens = tokenizeLookupValue(needle);
|
||||
const entryTokens = tokenizeLookupValue(
|
||||
`${entry.name} ${entry.id} ${entry.label ?? ""} ${entry.description}`,
|
||||
);
|
||||
let score = 0;
|
||||
if ((name && normalizedNeedle.includes(name)) || id.includes(normalizedNeedle)) {
|
||||
score += 40;
|
||||
}
|
||||
if (name && needleTokens.has(name)) {
|
||||
score += 40;
|
||||
}
|
||||
for (const token of needleTokens) {
|
||||
if (entryTokens.has(token)) {
|
||||
score += 12;
|
||||
}
|
||||
}
|
||||
if (label.includes(normalizedNeedle) || description.includes(normalizedNeedle)) {
|
||||
score += 8;
|
||||
}
|
||||
return score;
|
||||
}
|
||||
|
||||
function formatUnknownToolIdError(
|
||||
needle: string,
|
||||
entries: readonly ToolSearchCatalogEntry[],
|
||||
options: UnknownToolErrorOptions = {},
|
||||
): string {
|
||||
const nameCounts = new Map<string, number>();
|
||||
for (const entry of entries) {
|
||||
nameCounts.set(entry.name, (nameCounts.get(entry.name) ?? 0) + 1);
|
||||
}
|
||||
const suggestions = uniqueStrings(
|
||||
entries
|
||||
.map((entry) => ({
|
||||
value: options.exactIdOnly || (nameCounts.get(entry.name) ?? 0) > 1 ? entry.id : entry.name,
|
||||
score: scoreUnknownToolSuggestion(needle, entry),
|
||||
}))
|
||||
.filter((candidate) => candidate.score > 0)
|
||||
.toSorted((a, b) => b.score - a.score || a.value.localeCompare(b.value))
|
||||
.map((candidate) => candidate.value),
|
||||
).slice(0, 3);
|
||||
const recoveryText =
|
||||
options.recoverySurface === "code-mode"
|
||||
? "Use openclaw.tools.search to find a tool, openclaw.tools.describe to inspect it, then openclaw.tools.call with the exact id or name."
|
||||
: options.recoverySurface === "tools"
|
||||
? "Use tools.search to find a tool, tools.describe to inspect it, then tools.call with the exact id or name."
|
||||
: "Use tool_search to find a tool, tool_describe to inspect it, then tool_call with the exact id or name.";
|
||||
if (suggestions.length === 0) {
|
||||
return `Unknown tool id: ${needle}. ${recoveryText}`;
|
||||
}
|
||||
return `Unknown tool id: ${needle}. Did you mean: ${suggestions.join(", ")}? ${recoveryText}`;
|
||||
}
|
||||
|
||||
function findEntry(
|
||||
catalog: ToolSearchCatalogSession,
|
||||
id: string,
|
||||
options?: CatalogVisibilityOptions,
|
||||
errorOptions?: UnknownToolErrorOptions,
|
||||
): ToolSearchCatalogEntry {
|
||||
const needle = id.trim();
|
||||
const entries = visibleCatalogEntries(catalog, options);
|
||||
@@ -1556,16 +1625,22 @@ function findEntry(
|
||||
}
|
||||
const namedEntry = namedEntries[0];
|
||||
if (!namedEntry) {
|
||||
throw new ToolInputError(`Unknown tool id: ${needle}`);
|
||||
throw new ToolInputError(formatUnknownToolIdError(needle, entries, errorOptions));
|
||||
}
|
||||
return namedEntry;
|
||||
}
|
||||
|
||||
function findEntryByExactId(catalog: ToolSearchCatalogSession, id: string): ToolSearchCatalogEntry {
|
||||
function findEntryByExactId(
|
||||
catalog: ToolSearchCatalogSession,
|
||||
id: string,
|
||||
errorOptions: UnknownToolErrorOptions = {},
|
||||
): ToolSearchCatalogEntry {
|
||||
const needle = id.trim();
|
||||
const entry = catalog.entries.find((candidate) => candidate.id === needle);
|
||||
if (!entry) {
|
||||
throw new ToolInputError(`Unknown tool id: ${needle}`);
|
||||
throw new ToolInputError(
|
||||
formatUnknownToolIdError(needle, catalog.entries, { ...errorOptions, exactIdOnly: true }),
|
||||
);
|
||||
}
|
||||
return entry;
|
||||
}
|
||||
@@ -1670,10 +1745,10 @@ export class ToolSearchRuntime {
|
||||
);
|
||||
};
|
||||
|
||||
describe = async (id: string, options?: CatalogVisibilityOptions) => {
|
||||
describe = async (id: string, options?: CatalogVisibilityOptions & UnknownToolErrorOptions) => {
|
||||
const catalog = resolveCatalog(this.ctx);
|
||||
catalog.describeCount += 1;
|
||||
return describeEntry(findEntry(catalog, id, options));
|
||||
return describeEntry(findEntry(catalog, id, options, options));
|
||||
};
|
||||
|
||||
call = async (
|
||||
@@ -1683,10 +1758,11 @@ export class ToolSearchRuntime {
|
||||
parentToolCallId?: string;
|
||||
signal?: AbortSignal;
|
||||
onUpdate?: AgentToolUpdateCallback;
|
||||
recoverySurface?: UnknownToolRecoverySurface;
|
||||
},
|
||||
) => {
|
||||
const catalog = resolveCatalog(this.ctx);
|
||||
const entry = findEntry(catalog, id);
|
||||
const entry = findEntry(catalog, id, undefined, options);
|
||||
return await this.callEntry(catalog, entry, input, options);
|
||||
};
|
||||
|
||||
@@ -1697,10 +1773,11 @@ export class ToolSearchRuntime {
|
||||
parentToolCallId?: string;
|
||||
signal?: AbortSignal;
|
||||
onUpdate?: AgentToolUpdateCallback;
|
||||
recoverySurface?: UnknownToolRecoverySurface;
|
||||
},
|
||||
) => {
|
||||
const catalog = resolveCatalog(this.ctx);
|
||||
const entry = findEntryByExactId(catalog, id);
|
||||
const entry = findEntryByExactId(catalog, id, options);
|
||||
return await this.callEntry(catalog, entry, input, options);
|
||||
};
|
||||
|
||||
@@ -2000,14 +2077,17 @@ async function runCodeModeBridgeRequest(
|
||||
if (typeof id !== "string") {
|
||||
throw new ToolInputError("describe id must be a string.");
|
||||
}
|
||||
return await runtime.describe(id);
|
||||
return await runtime.describe(id, { recoverySurface: "code-mode" });
|
||||
}
|
||||
case "call": {
|
||||
const id = values[0];
|
||||
if (typeof id !== "string") {
|
||||
throw new ToolInputError("call id must be a string.");
|
||||
}
|
||||
return await runtime.call(id, values[1] ?? {}, options);
|
||||
return await runtime.call(id, values[1] ?? {}, {
|
||||
...options,
|
||||
recoverySurface: "code-mode",
|
||||
});
|
||||
}
|
||||
}
|
||||
throw new ToolInputError("Unsupported tool_search_code bridge method.");
|
||||
|
||||
Reference in New Issue
Block a user