mirror of
https://github.com/openclaw/openclaw.git
synced 2026-08-12 21:53:00 -06:00
perf(agents): prime code mode with compact tool ids (#109651)
This commit is contained in:
committed by
GitHub
parent
c80b236a09
commit
c742caee4b
@@ -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 = {
|
||||
|
||||
@@ -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
@@ -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}` : "")
|
||||
);
|
||||
}
|
||||
|
||||
|
||||
@@ -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,
|
||||
};
|
||||
};
|
||||
|
||||
Reference in New Issue
Block a user