perf(agents): prime code mode with compact tool ids (#109651)

This commit is contained in:
Peter Steinberger
2026-07-16 23:09:03 -07:00
committed by GitHub
parent c80b236a09
commit c742caee4b
4 changed files with 171 additions and 16 deletions
+20 -5
View File
@@ -39,9 +39,12 @@ identically-named `exec`/`wait` tools.
cannot survive the guest bridge.
- `exec` evaluates model-generated JavaScript or TypeScript in an isolated
QuickJS-WASI worker thread.
- Every catalog-eligible enabled tool (OpenClaw core, plugin, MCP, client) is hidden from
the model prompt and exposed inside the guest program through `ALL_TOOLS`
- Every catalog-eligible enabled tool (OpenClaw core, plugin, MCP, client) is hidden as a
standalone model tool and exposed inside the guest program through `ALL_TOOLS`
and `tools`.
- The `exec` description carries a bounded quick index of exact OpenClaw/plugin
catalog ids and compact input hints. It omits descriptions, full schemas, MCP
entries, and overflow entries; guest-side catalog lookup remains the fallback.
- Guest code searches the hidden catalog, describes a tool's schema, and calls
a tool through the same execution path used by normal agent turns (policy,
approvals, hooks, telemetry all still apply).
@@ -56,8 +59,9 @@ behavior, or model selection.
## Why use it
- Smaller prompt surface: providers get two control tools and only the few
required direct tools instead of dozens or hundreds of full tool schemas.
- Smaller prompt surface: providers get two control tools, a bounded native-tool
index, and only the few required direct tools instead of dozens or hundreds
of full tool schemas.
- Better orchestration: the model can use loops, joins, small transforms,
conditional logic, and parallel nested tool calls inside one code cell.
- Provider neutral: works for OpenClaw, plugin, MCP, and client tools without
@@ -375,7 +379,18 @@ declare function yield_control(reason?: string): Promise<void>;
```
`ALL_TOOLS` is compact metadata for the run-scoped catalog; it does not
contain full schemas by default.
contain full schemas by default. The model-visible `exec` description also
includes a bounded, deterministic subset of exact OpenClaw/plugin ids and
compact input hints so common calls can start without a separate catalog
discovery turn. Descriptions remain deferred so adversarial catalog prose cannot
steer the model. When that index omits a tool, read `ALL_TOOLS` or call
`tools.search(...)` inside the guest program.
Compact entries describe tool inputs, not result schemas, so the index marks
their result shape as `-> ?` (output unknown). When a workflow needs result
fields, the first `exec` must return the raw
`tools.callValue(...)` result unchanged. Filter or map the observed shape only
in a later `exec` call instead of guessing field names.
```typescript
type ToolCatalogEntry = {
+83 -1
View File
@@ -395,6 +395,10 @@ describe("Code Mode", () => {
expect(execTool.description).toContain("`tools.callValue(id: string, args?)`");
expect(execTool.description).toContain("`tools.call(id: string, args?)`");
expect(execTool.description).toContain("Never invent or transform a tool id");
expect(execTool.description).toContain("Quick-index input hints are not output schemas");
expect(execTool.description).toContain("never guess result field names");
expect(execTool.description).toContain("return the raw tool value unchanged");
expect(execTool.description).toContain("filter or map it only in a later exec");
expect(execTool.description).toContain("returns its JSON value directly");
expect(execTool.description).toContain("const hit = ALL_TOOLS.find");
expect(execTool.description).toContain('"javascript" or "typescript"');
@@ -418,6 +422,72 @@ describe("Code Mode", () => {
);
});
it("primes the exec schema with exact native tool ids and compact inputs", () => {
const { config, catalogRef, tools } = createCodeModeHarness();
const compacted = applyCodeModeCatalog({
tools: [
...tools,
pluginTool("zeta_tool", "Description stays deferred."),
pluginTool("alpha_tool", "Another deferred description."),
],
config,
sessionId: "session-code-mode",
sessionKey: "agent:main:main",
runId: "run-code-mode",
catalogRef,
});
const description = compacted.tools[0]?.description ?? "";
expect(description).toContain("descriptions are intentionally deferred");
expect(description).toContain('- "openclaw:fake-code-mode:alpha_tool" { value?: string } -> ?');
expect(description).toContain('- "openclaw:fake-code-mode:zeta_tool" { value?: string } -> ?');
expect(description.indexOf("alpha_tool")).toBeLessThan(description.indexOf("zeta_tool"));
expect(description).not.toContain("Description stays deferred.");
expect(description).not.toContain("Another deferred description.");
});
it("keeps a typical 72-tool catalog fully indexed", () => {
const { config, catalogRef, tools } = createCodeModeHarness();
const catalogTools = Array.from({ length: 72 }, (_, index) =>
pluginTool(`tool_${index.toString().padStart(3, "0")}`, "Deferred", "catalog-owner"),
);
const compacted = applyCodeModeCatalog({
tools: [...tools, ...catalogTools],
config,
sessionId: "session-code-mode",
sessionKey: "agent:main:main",
runId: "run-code-mode",
catalogRef,
});
const description = compacted.tools[0]?.description ?? "";
expect(description).toContain('"openclaw:catalog-owner:tool_071"');
expect(description).not.toContain("additional OpenClaw/plugin tools omitted");
});
it("bounds the model-visible native tool index", () => {
const { config, catalogRef, tools } = createCodeModeHarness();
const pluginId = `fake-${"x".repeat(120)}`;
const catalogTools = Array.from({ length: 100 }, (_, index) =>
pluginTool(`fake_${index.toString().padStart(3, "0")}`, "Deferred", pluginId),
);
const compacted = applyCodeModeCatalog({
tools: [...tools, ...catalogTools],
config,
sessionId: "session-code-mode",
sessionKey: "agent:main:main",
runId: "run-code-mode",
catalogRef,
});
const description = compacted.tools[0]?.description ?? "";
const indexStart = description.indexOf("OpenClaw/plugin tool quick index");
const index = indexStart >= 0 ? description.slice(indexStart) : "";
expect(index.length).toBeLessThanOrEqual(8_000);
expect(index).toContain("additional OpenClaw/plugin tools omitted");
expect(index).not.toContain("fake_099");
});
it("adds registered namespace docs to the model-visible exec schema", () => {
registerTestNamespace({
id: "tickets",
@@ -472,7 +542,16 @@ describe("Code Mode", () => {
const compacted = applyCodeModeCatalog({
tools: [
...tools,
mcpTool({ name: "github__create_issue", serverName: "github", toolName: "create_issue" }),
pluginTool("fake_noop", "Noop"),
mcpTool({
name: "github__create_issue",
serverName: "github",
toolName: "create_issue",
parameters: {
type: "object",
properties: { malicious_prompt: { type: "string" } },
},
}),
],
config,
sessionId: "session-code-mode",
@@ -484,6 +563,9 @@ describe("Code Mode", () => {
const description = compacted.tools[0]?.description ?? "";
expect(description).toContain("API.list(prefix?)");
expect(description).toContain("MCP tools are available only through");
expect(description).toContain('"openclaw:fake-code-mode:fake_noop"');
expect(description).not.toContain("github__create_issue");
expect(description).not.toContain("malicious_prompt");
});
it("validates namespace registrations before exposing globals", () => {
+54 -2
View File
@@ -40,6 +40,7 @@ import type { ToolDefinition } from "./sessions/index.js";
import {
addClientToolsToToolCatalog,
applyToolCatalogCompaction,
compactToolSearchCatalogEntry,
TOOL_CALL_RAW_TOOL_NAME,
TOOL_DESCRIBE_RAW_TOOL_NAME,
TOOL_SEARCH_CODE_MODE_TOOL_NAME,
@@ -67,6 +68,7 @@ const DEFAULT_SNAPSHOT_TTL_SECONDS = 900;
const DEFAULT_SEARCH_LIMIT = 8;
const DEFAULT_MAX_SEARCH_LIMIT = 50;
const MAX_ACTIVE_CODE_MODE_RUNS = 64;
const MAX_CODE_MODE_CATALOG_INDEX_CHARS = 8_000;
type CodeModeLanguage = "javascript" | "typescript";
@@ -1269,6 +1271,54 @@ function telemetry(runtime: ToolSearchRuntime) {
};
}
function renderCodeModeCatalogIndex(lines: readonly string[], total: number): string {
const omitted = total - lines.length;
const footer =
omitted > 0
? `${omitted} additional OpenClaw/plugin tools omitted from this prompt index. Use ALL_TOOLS or tools.search inside exec to find them.`
: "Use these exact ids with tools.callValue; use ALL_TOOLS or tools.search inside exec when lookup is ambiguous.";
return [
"OpenClaw/plugin tool quick index (exact catalog ids and compact input hints; descriptions are intentionally deferred):",
"Each line contains an exact catalog id and compact input hint; `-> ?` means its output schema is unknown.",
"OUTPUT UNKNOWN RULE: the first exec must return that tool's raw value unchanged; filter or map it only in a later exec after observing its shape.",
...lines,
"",
footer,
].join("\n");
}
function formatCodeModeCatalogIndex(catalog: readonly ToolSearchCatalogEntry[]): string {
const lines = catalog
.filter((entry) => entry.source === "openclaw")
.map((entry) => compactToolSearchCatalogEntry(entry))
.toSorted((a, b) => a.id.localeCompare(b.id))
.map((entry) => `- ${JSON.stringify(entry.id)} ${entry.input ?? "unknown"} -> ?`);
if (lines.length === 0) {
return "";
}
const fullIndex = renderCodeModeCatalogIndex(lines, lines.length);
if (fullIndex.length <= MAX_CODE_MODE_CATALOG_INDEX_CHARS) {
return fullIndex;
}
// Prompt bytes and ordering must stay stable for provider prompt caches.
// Truncated entries remain discoverable inside the guest through ALL_TOOLS.
let low = 0;
let high = lines.length;
while (low < high) {
const middle = Math.ceil((low + high) / 2);
if (
renderCodeModeCatalogIndex(lines.slice(0, middle), lines.length).length <=
MAX_CODE_MODE_CATALOG_INDEX_CHARS
) {
low = middle;
} else {
high = middle - 1;
}
}
return renderCodeModeCatalogIndex(lines.slice(0, low), lines.length);
}
function createCodeModeExecDescription(
ctx: CodeModeToolContext,
catalog?: readonly ToolSearchCatalogEntry[],
@@ -1288,12 +1338,14 @@ function createCodeModeExecDescription(
!catalogKnown || namespacePrompt
? " Registered plugin namespaces are available as direct globals and through `namespaces` when their required tools are visible in the run catalog."
: "";
const catalogIndex = catalog ? formatCodeModeCatalogIndex(catalog) : "";
return (
"Run JavaScript or TypeScript in OpenClaw code mode. Use `return` to pass the final value back to the agent; awaited calls without a returned value complete as `null`. Prefer one exec invocation for a complete dependent workflow: select tools, call them, and process their results in the same program. Await prerequisites before later calls; parallelize only independent work. `ALL_TOOLS` is the complete compact catalog with exact ids and input hints. Select from it directly when practical, use `tools.search(query: string, options?)` when lookup is ambiguous, and use `tools.describe(id: string)` only when the compact input hint is insufficient. Never invent or transform a tool id. `tools.callValue(id: string, args?)` executes a tool and returns its JSON value directly; `tools.call(id: string, args?)` preserves the raw `{ tool, result }` envelope. Example: `const hit = ALL_TOOLS.find((entry) => entry.description.includes('weather')) ?? (await tools.search('weather'))[0]; return await tools.callValue(hit.id, {});`. Node.js modules and `require`/`import` are NOT available; for any shell, file, network, or external action, use enabled catalog tools allowed by policy from inside your code." +
"Run JavaScript or TypeScript in OpenClaw code mode. Use `return` to pass the final value back to the agent; awaited calls without a returned value complete as `null`. Quick-index input hints are not output schemas: `output unknown` means never guess result field names. For an unknown output, the first exec must return the raw tool value unchanged with `return await tools.callValue(id, args);`; filter or map it only in a later exec after observing its shape. Prefer one exec invocation only when required result fields are documented: select tools, call them, and process their results in the same program. Await prerequisites before later calls; parallelize only independent work. `ALL_TOOLS` is the complete compact catalog with exact ids and input hints. Select from it directly when practical, use `tools.search(query: string, options?)` when lookup is ambiguous, and use `tools.describe(id: string)` only when the compact input hint is insufficient. Never invent or transform a tool id. `tools.callValue(id: string, args?)` executes a tool and returns its JSON value directly; `tools.call(id: string, args?)` preserves the raw `{ tool, result }` envelope. Example: `const hit = ALL_TOOLS.find((entry) => entry.description.includes('weather')) ?? (await tools.search('weather'))[0]; return await tools.callValue(hit.id, {});`. Node.js modules and `require`/`import` are NOT available; for any shell, file, network, or external action, use enabled catalog tools allowed by policy from inside your code." +
mcpGuidance +
namespaceGuidance +
' The `language` field accepts only "javascript" or "typescript"; do not pass "bash", "shell", or other values.' +
(namespacePrompt ? `\n\n${namespacePrompt}` : "")
(namespacePrompt ? `\n\n${namespacePrompt}` : "") +
(catalogIndex ? `\n\n${catalogIndex}` : "")
);
}
+14 -8
View File
@@ -1261,7 +1261,7 @@ function compactInputHint(parameters: unknown): string {
return `{ ${parts.join("; ")}${omitted ? "; ..." : ""} }`;
}
function compactEntry(entry: ToolSearchCatalogEntry) {
export function compactToolSearchCatalogEntry(entry: ToolSearchCatalogEntry) {
return {
id: entry.id,
source: entry.source,
@@ -1289,7 +1289,9 @@ function formatToolDirectoryIdentifier(value: string | undefined): string | unde
return trimmed && TOOL_DIRECTORY_IDENTIFIER_RE.test(trimmed) ? trimmed : undefined;
}
function formatToolDirectoryEntry(entry: ReturnType<typeof compactEntry>): string | undefined {
function formatToolDirectoryEntry(
entry: ReturnType<typeof compactToolSearchCatalogEntry>,
): string | undefined {
if (entry.source !== "openclaw") {
return undefined;
}
@@ -1312,7 +1314,9 @@ function renderToolSearchCatalogDirectory(lines: string[], total: number): strin
return ["Available deferred-schema tools:", ...lines, "", footer].join("\n");
}
function formatToolSearchCatalogDirectory(entries: Array<ReturnType<typeof compactEntry>>): string {
function formatToolSearchCatalogDirectory(
entries: Array<ReturnType<typeof compactToolSearchCatalogEntry>>,
): string {
if (entries.length === 0) {
return "Available deferred-schema tools: none.";
}
@@ -1648,7 +1652,7 @@ export function estimateToolSchemaDirectoryToolNames(params: {
function describeEntry(entry: ToolSearchCatalogEntry) {
return {
...compactEntry(entry),
...compactToolSearchCatalogEntry(entry),
parameters: entry.parameters ?? {},
};
}
@@ -1879,18 +1883,20 @@ export class ToolSearchRuntime {
.filter((hit) => hit.score > 0)
.toSorted((a, b) => b.score - a.score || a.entry.id.localeCompare(b.entry.id))
.slice(0, limit)
.map((hit) => compactEntry(hit.entry));
.map((hit) => compactToolSearchCatalogEntry(hit.entry));
};
all = (options?: CatalogVisibilityOptions) => {
const catalog = resolveCatalog(this.ctx);
return visibleCatalogEntries(catalog, options).map((entry) => compactEntry(entry));
return visibleCatalogEntries(catalog, options).map((entry) =>
compactToolSearchCatalogEntry(entry),
);
};
namespaceEntries = () => {
const catalog = resolveCatalog(this.ctx);
return catalog.entries.map((entry) =>
Object.assign(compactEntry(entry), {
Object.assign(compactToolSearchCatalogEntry(entry), {
parameters: entry.parameters ?? {},
}),
);
@@ -1984,7 +1990,7 @@ export class ToolSearchRuntime {
onUpdate: options?.onUpdate,
});
return {
tool: compactEntry(entry),
tool: compactToolSearchCatalogEntry(entry),
result,
};
};