Skip to content

feat: add core tool search#79823

Merged
steipete merged 5 commits into
mainfrom
feat/tool-search-code-mode
May 10, 2026
Merged

feat: add core tool search#79823
steipete merged 5 commits into
mainfrom
feat/tool-search-code-mode

Conversation

@steipete

@steipete steipete commented May 9, 2026

Copy link
Copy Markdown
Contributor

Summary

This PR adds Tool Search as a core OpenClaw agent/tooling capability for large tool catalogs. Instead of sending every OpenClaw, MCP, and client-provided tool schema to the model up front, OpenClaw can expose one compact bridge and let the model search, describe, and call only the tools it actually needs.

Default model-facing surface:

const hits = await openclaw.tools.search("ticket", { limit: 3 });
const tool = await openclaw.tools.describe(hits[0].id);
return await openclaw.tools.call(tool.id, { title: "Ship it" });

This is deliberately core, not a bundled plugin. The feature rewrites the effective PI tool surface before provider request construction and also needs access to client tools that arrive inside the PI harness path. Plugins can still contribute tools to the catalog; core owns cataloging, compaction, policy preservation, and PI surface shaping.

What This Adds

  • Core code bridge: tool_search_code
  • Core structured fallback tools:
    • tool_search
    • tool_describe
    • tool_call
  • Core config under tools.toolSearch
  • Per-session cataloging for:
    • built-in OpenClaw tools
    • tools registered by OpenClaw plugins
    • MCP tools exposed through OpenClaw
    • client/app tools passed into PI sessions
  • Two model-facing modes:
    • code: expose only tool_search_code
    • tools: expose only structured search/describe/call tools
  • Parent-enforced isolated subprocess execution for model-authored JavaScript.
  • Docs, changelog, config schema/help labels, mock OpenAI scenario support, and a gateway E2E harness with a large fake plugin catalog.

Simplified Config

Public config is now intentionally small because this has not shipped yet:

{
  tools: {
    toolSearch: true,
  },
}

That enables the default code bridge.

Structured fallback mode:

{
  tools: {
    toolSearch: {
      mode: "tools",
    },
  },
}

Disable it:

{
  tools: {
    toolSearch: false,
  },
}

Removed from the public surface before ship:

  • tools.toolSearchCodeMode
  • mode: "both"
  • source-selection toggles for OpenClaw/MCP/client tools
  • public search-limit and timeout knobs

When enabled, Tool Search catalogs every eligible OpenClaw, MCP, and client tool after the normal policy/allowlist/denylist pass. Internal timeout and search limits remain conservative implementation details.

How It Works

During PI run preparation, OpenClaw collects the effective tools for the session as it already does today. If tools.toolSearch is enabled, core creates the control tools, builds a per-session search catalog, and rewrites the prompt-visible tool surface according to mode.

  • mode: "code"
    • removes cataloged target tools from the prompt-visible tool list
    • exposes tool_search_code
    • keeps target tools callable only through the catalog bridge
  • mode: "tools"
    • hides tool_search_code
    • exposes tool_search, tool_describe, and tool_call

Each catalog entry keeps the original tool object and metadata. Calls execute the real OpenClaw tool, not a duplicate implementation. That preserves before-tool-call hooks, tool policies, approvals, loop detection, session routing, and result handling.

The JavaScript bridge exposes only:

openclaw.tools.search(query, options)
openclaw.tools.describe(id)
openclaw.tools.call(id, input)

The model does not receive direct access to the host process, filesystem, network, environment, or plugin internals through this bridge.

Runtime And Security Boundary

The initial implementation used node:vm in the gateway process. Review correctly flagged that as insufficient for model-authored JavaScript and async CPU-bound continuations. The current implementation runs code mode in a short-lived child Node process instead.

The hardened path:

  • starts a fresh child process per tool_search_code call
  • runs the child with Node permission mode
  • passes an empty environment to the child
  • uses a temp working directory
  • communicates with the gateway only over IPC
  • validates bridge messages in the parent
  • allows only search, describe, and call bridge methods
  • enforces the timeout from the parent process
  • kills the child with SIGKILL on timeout
  • fails closed if the current Node runtime does not support the required permission flag

This covers the prior review findings:

  • model-authored code cannot escape the intended bridge with constructor/process tricks
  • async continuations that block the child event loop after an awaited bridge call cannot hang the gateway process

Tool execution still happens in the parent through OpenClaw's normal tool runtime after the bridge request has been validated.

Real behavior proof

  • Behavior or issue addressed: Tool Search compacts a large OpenClaw plugin tool catalog while still letting the model discover and call the same target tool through the Gateway.
  • Real environment tested: Crabbox Blacksmith Testbox tbx_01kr7ds1z93cw4d6t3g6x54qed running a real OpenClaw Gateway, mock OpenAI provider, PI harness, and temporary fake plugin with many registered tools.
  • Exact steps or command run after this patch: node --import tsx scripts/tool-search-gateway-e2e.ts && pnpm check:changed && pnpm build.
  • Evidence after fix: Terminal output from Testbox tbx_01kr7ds1z93cw4d6t3g6x54qed showed normal lane status: completed, providerDeclaredToolCount: 56, planned fake_plugin_tool_17, provider raw bytes 78330; Tool Search lane status: completed, providerDeclaredToolCount: 1, planned tool_search_code, provider raw bytes 35133, session log mentions tool_search_code: 20 and fake_plugin_tool_17: 43.
  • Observed result after fix: The same fake plugin tool fake_plugin_tool_17 was called successfully in both normal mode and Tool Search mode, while Tool Search reduced declared provider tools by 55 and provider raw bytes by 43197.
  • What was not tested: No known gaps for this PR path.

The gateway E2E harness sets up a fake plugin catalog with many tools, runs the same request twice, and inspects provider requests plus session logs:

  • normal mode: all fake tools are exposed directly
  • Tool Search mode: only tool_search_code is exposed initially
  • both runs must complete and call the target fake plugin tool
  • session logs must show the actual tool execution path
  • provider payload size and prompt/tool counts are measured

Latest Crabbox/Testbox proof:

  • Testbox: tbx_01kr7ds1z93cw4d6t3g6x54qed
  • Command: node --import tsx scripts/tool-search-gateway-e2e.ts && pnpm check:changed && pnpm build
  • Result: exit 0

E2E metrics from that run:

  • normal lane:
    • status: completed
    • provider requests: 2
    • declared tools: 56
    • planned tool: fake_plugin_tool_17
    • provider raw bytes: 78,330
    • provider system prompt chars: 35,072
    • session log mentions:
      • tool_search_code: 0
      • fake_plugin_tool_17: 28
  • Tool Search lane:
    • status: completed
    • provider requests: 2
    • declared tools: 1
    • planned tool: tool_search_code
    • provider raw bytes: 35,133
    • provider system prompt chars: 32,014
    • session log mentions:
      • tool_search_code: 20
      • fake_plugin_tool_17: 43
  • reduction:
    • declared tools: -55
    • provider raw bytes: -43,197
    • provider system prompt chars: -3,058

This verifies that Tool Search reduces the exposed provider tool surface while still letting the model discover and call the same underlying fake plugin tool through OpenClaw's runtime.

Tests And Verification

Local verification:

pnpm test src/agents/tool-search.test.ts src/agents/pi-tools.create-openclaw-coding-tools.test.ts
pnpm test src/config/schema.test.ts src/config/schema.help.quality.test.ts src/config/schema.hints.test.ts
node --import tsx scripts/tool-search-gateway-e2e.ts
pnpm tsgo:core
pnpm check:docs

Remote verification:

Crabbox/Testbox tbx_01kr7ds1z93cw4d6t3g6x54qed
node --import tsx scripts/tool-search-gateway-e2e.ts && pnpm check:changed && pnpm build

Security regression tests added:

  • rejects host process escape attempts from model-authored code
  • terminates async continuations that enter CPU-bound loops after an awaited bridge call

Docs

New docs page:

The docs cover:

  • when to use Tool Search
  • direct tools vs Tool Search modes
  • config examples
  • runtime boundary
  • E2E validation steps
  • failure behavior and troubleshooting notes

@openclaw-barnacle openclaw-barnacle Bot added docs Improvements or additions to documentation gateway Gateway runtime scripts Repository scripts agents Agent runtime and tooling extensions: qa-lab size: XL maintainer Maintainer-authored PR labels May 9, 2026
@clawsweeper

clawsweeper Bot commented May 9, 2026

Copy link
Copy Markdown
Contributor

Codex review: needs real behavior proof before merge.

Summary
Review failed before ClawSweeper could summarize the requested change.

Reproducibility: unclear. The review failed before ClawSweeper could establish a reproduction path.

Real behavior proof
Not applicable: Real behavior proof was not assessed because the Codex review failed.

Next step before merge
Review did not complete, so no work-lane recommendation was made.

Review details

Best possible solution:

Retry the Codex review after fixing the execution failure.

Do we have a high-confidence way to reproduce the issue?

Unclear. The review failed before ClawSweeper could establish a reproduction path.

Is this the best way to solve the issue?

Unclear. Retry the review first so ClawSweeper can evaluate the actual issue and fix direction.

What I checked:

  • failure reason: timeout.
  • codex failure detail: Codex review failed for this PR: spawnSync codex ETIMEDOUT.
  • codex stdout: Per-item Codex failure; continuing with the rest of the shard.

Likely related people:

  • unknown: Codex failed before it could trace repository history. (role: review did not complete; confidence: low)

Remaining risk / open question:

  • No close action taken because the review did not complete.

Codex review notes: model gpt-5.5, reasoning high; reviewed against 421cdd4737dc.

@chatgpt-codex-connector chatgpt-codex-connector Bot left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

💡 Codex Review

Here are some automated review suggestions for this pull request.

Reviewed commit: e0911bd524

ℹ️ About Codex in GitHub

Your team has set up Codex to review pull requests in this repo. Reviews are triggered when you

  • Open a pull request for review
  • Mark a draft as ready
  • Comment "@codex review".

If Codex has suggestions, it will comment; otherwise it will react with 👍.

Codex can also answer questions or update the PR. Try commenting "@codex address that feedback".

Comment thread src/agents/tool-search-code-mode.ts Outdated
Comment on lines +575 to +577
search: runtime.search,
describe: runtime.describe,
call: runtime.call,

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

P0 Badge Prevent VM sandbox escape via host function injection

The sandbox exposes host-created function objects (runtime.search/describe/call) directly to untrusted VM code, which allows escape via Function constructor access (for example through openclaw.tools.search.constructor(...)) and then access to host globals like process. In practice this breaks the claimed isolation and can allow arbitrary host code execution when model-generated code is influenced by untrusted input.

Useful? React with 👍 / 👎.

Comment thread src/agents/tool-search.ts Outdated
toolCallId: string,
input: unknown,
) => Promise<AgentToolResult<unknown>>;
const result = await execute(`tool_search_code:${entry.name}`, input ?? {});

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

P1 Badge Use unique tool call IDs for bridged tool invocations

This forwards a deterministic toolCallId (tool_search_code:${entry.name}) for every call to the same tool, so repeated calls in one code-mode run share an ID. The embedded runner’s client-tool recorder deduplicates by toolCallId, so retries/loops against one client tool can collapse into a single recorded call and lose earlier invocations. Generate a per-invocation ID (e.g., counter/nonce suffix) before dispatching.

Useful? React with 👍 / 👎.

@clawsweeper clawsweeper Bot added the proof: sufficient ClawSweeper judged the real behavior proof convincing. label May 9, 2026

@chatgpt-codex-connector chatgpt-codex-connector Bot left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

💡 Codex Review

Here are some automated review suggestions for this pull request.

Reviewed commit: d2ae6467b6

ℹ️ About Codex in GitHub

Your team has set up Codex to review pull requests in this repo. Reviews are triggered when you

  • Open a pull request for review
  • Mark a draft as ready
  • Comment "@codex review".

If Codex has suggestions, it will comment; otherwise it will react with 👍.

Codex can also answer questions or update the PR. Try commenting "@codex address that feedback".

Comment thread src/agents/tool-search.ts Outdated
Comment on lines +648 to +652
const execute = entry.tool.execute as (
toolCallId: string,
input: unknown,
) => Promise<AgentToolResult<unknown>>;
const result = await execute(`tool_search_code:${entry.name}`, input ?? {});

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

P1 Badge Cancel bridged tool executions when code mode times out

The code-mode timeout only kills the child VM process, but bridged tool calls keep running in the host because runtime.call invokes entry.tool.execute(...) without an AbortSignal. If openclaw.tools.call(...) targets a long-running tool, tool_search_code returns timed out while the underlying tool may continue and still produce side effects (writes, network calls, approvals) after the turn has failed. Please thread a cancellation signal into bridged calls so timeout is fail-stop for both child code and host tool execution.

Useful? React with 👍 / 👎.

Comment thread src/agents/tool-search.ts
Comment on lines +481 to +485
const uniqueCatalogs = [...new Set(sessionCatalogs.values())];
if (uniqueCatalogs.length === 1) {
const catalog = uniqueCatalogs[0];
if (catalog) {
return catalog;

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

P2 Badge Fail closed when no session-scoped catalog is found

When lookup by sessionId/sessionKey/agentId misses, resolveCatalog falls back to the only global catalog in memory. Because OpenClawPluginToolContext fields are optional, a call with missing context can accidentally bind to another run’s catalog instead of failing, which can expose or execute tools outside the current session boundary when one catalog is active. This path should fail closed unless a matching session-scoped key is found.

Useful? React with 👍 / 👎.

@steipete
steipete force-pushed the feat/tool-search-code-mode branch from d2ae646 to b79d433 Compare May 9, 2026 14:33

@chatgpt-codex-connector chatgpt-codex-connector Bot left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

💡 Codex Review

Here are some automated review suggestions for this pull request.

Reviewed commit: b79d433a7b

ℹ️ About Codex in GitHub

Your team has set up Codex to review pull requests in this repo. Reviews are triggered when you

  • Open a pull request for review
  • Mark a draft as ready
  • Comment "@codex review".

If Codex has suggestions, it will comment; otherwise it will react with 👍.

Codex can also answer questions or update the PR. Try commenting "@codex address that feedback".

Comment thread src/agents/tool-search.ts Outdated
Comment on lines +648 to +653
const execute = entry.tool.execute as (
toolCallId: string,
input: unknown,
) => Promise<AgentToolResult<unknown>>;
const result = await execute(`tool_search_code:${entry.name}`, input ?? {});
return {

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

P1 Badge Preserve direct-tool error semantics in bridged calls

ToolSearchRuntime.call invokes catalog tools via entry.tool.execute(...) and lets thrown errors escape, so a single tool failure aborts tool_search_code/tool_call instead of returning a structured tool error result. In the normal direct-tool path, toToolDefinitions wraps execution and converts non-abort failures into status: "error" results (src/agents/pi-tool-definition-adapter.ts:236-263). This regression means transient validation/runtime errors in any cataloged OpenClaw tool can terminate the whole code-mode run rather than giving the model recoverable tool output.

Useful? React with 👍 / 👎.

@steipete
steipete force-pushed the feat/tool-search-code-mode branch from b79d433 to bb900f9 Compare May 9, 2026 14:59
@100yenadmin

100yenadmin commented May 9, 2026

Copy link
Copy Markdown
Contributor

This is a good idea 🚀another way to do this is micro LLM call to 5.4 mini that reads prompt > recommended tool loads and or cache and assigns classes of tools, or regex that recommends tools etc

@jalehman

jalehman commented May 9, 2026

Copy link
Copy Markdown
Contributor

While live-testing this branch with verbose tool-call display enabled, I noticed a UX regression: every bridged call only renders as:

🧩 Tool Search Code

That hides the actual underlying operation (for example, whether the bridge is searching, describing, or calling lcm_grep, exec, sessions_list, etc.). For debugging and trust, verbose mode should expose the underlying tool-search-code operation and, for openclaw.tools.call(...), the target tool name/id when known.

Suggested behavior: keep the compact bridge from the model-facing prompt, but annotate runtime/verbose display with something like:

  • 🧩 Tool Search Code: search("lcm")
  • 🧩 Tool Search Code: describe(openclaw:core:lcm_grep)
  • 🧩 Tool Search Code → lcm_grep or 🧩 Tool Search Code: call(openclaw:core:lcm_grep)

I’m going to try a local patch against the live-tested branch now.

@steipete
steipete force-pushed the feat/tool-search-code-mode branch from bb900f9 to 6b44455 Compare May 9, 2026 22:17

@chatgpt-codex-connector chatgpt-codex-connector Bot left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

💡 Codex Review

Here are some automated review suggestions for this pull request.

Reviewed commit: 6b444552ba

ℹ️ About Codex in GitHub

Your team has set up Codex to review pull requests in this repo. Reviews are triggered when you

  • Open a pull request for review
  • Mark a draft as ready
  • Comment "@codex review".

If Codex has suggestions, it will comment; otherwise it will react with 👍.

Codex can also answer questions or update the PR. Try commenting "@codex address that feedback".

Comment thread src/agents/pi-tools.ts Outdated
Comment on lines +713 to +714
includeOpenClawTools && resolveToolSearchCodeModeConfig(options?.config).enabled
? createToolSearchCodeModeTools({

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

P1 Badge Recognize tool_search control tools in allowlist planning

Only adding Tool Search Code Mode control tools when includeOpenClawTools is true makes explicit allowlists like tools.allow: ["tool_search_code"] impossible to satisfy: the tool-construction plan classifies that name as non-core, so core tools are not built and this branch never runs. In practice the runtime logs tool_search_code as unknown and can produce an empty callable tool set even with tools.toolSearchCodeMode.enabled=true, which breaks the documented rollout path for restricted allowlists.

Useful? React with 👍 / 👎.

Comment on lines +1132 to +1134
let effectiveTools = [...tools, ...filteredBundledTools];
const toolSearchCodeMode = applyToolSearchCodeModeCatalog({
tools: effectiveTools,

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

P2 Badge Preserve core-name conflict checks before catalog compaction

Compacting effectiveTools before client-tool conflict validation removes hidden core tools from the later coreBuiltinToolNames set, so name collisions that were previously rejected can now slip through in code mode (for example a client tool named read). That creates ambiguous name-based resolution in the catalog bridge and can route calls to the wrong implementation depending on catalog ordering, instead of failing early with the existing conflict error.

Useful? React with 👍 / 👎.

@pashpashpash pashpashpash left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I want to request changes before this merges.

The PI-side idea makes sense. PI has a large flat tool catalog, so hiding that catalog behind search, describe, call, and code controls is a reasonable compaction strategy. The problem is that the current implementation is not isolated to PI.

Specifically: this PR adds tool_search_code, tool_search, tool_describe, and tool_call from shared createOpenClawCodingTools when tools.toolSearchCodeMode.enabled is true. The Codex app-server adapter also calls that same factory when building Codex dynamic tools, then passes the result into the Codex dynamic tool bridge. So with this config enabled, Codex harness mode can receive the OpenClaw tool-search-code controls too.

That is the part I do not want to merge.

I also checked the Codex source, and this is more than just a duplicate search tool. Codex already has native code mode. codex-rs/code-mode/src/lib.rs exposes the public exec and wait tools. codex-rs/code-mode/src/description.rs describes exec as JavaScript in a fresh V8 isolate with nested tools available on the global tools object, plus ALL_TOOLS for enabled nested tool metadata. codex-rs/core/src/tools/code_mode/mod.rs wires that through Codex's own tool router, and codex-rs/core/tests/suite/code_mode.rs has coverage for code mode calling hidden/deferred dynamic tools. The app-server docs also say deferred dynamic tools stay callable by runtime features such as code_mode and searchable through tool_search.

So the Codex harness already has the elegant version of this idea: native code mode can compose tool calls, including deferred dynamic tools, inside Codex's own runtime and tool router. We should lean into that. OpenClaw should supply product capabilities through Codex dynamic tools; Codex should own the code-mode and tool-search interaction model around those tools.

The current PR risks layering a second OpenClaw JavaScript bridge on top of Codex's native one. Worse, it is a different bridge: tool_search_code runs model-authored JavaScript in a Node subprocess with openclaw.tools.search/describe/call, while Codex code mode runs in Codex's V8 runtime with its own nested-tool host. That gives the model two ways to do the same conceptual thing, with different semantics, different names, and a wider debug/security surface.

The elegant boundary should be:

  • PI gets PI-specific catalog compaction because PI lacks Codex's native code/tool-search surface.
  • Codex gets OpenClaw dynamic tools for product integrations: messaging, sessions, cron, gateway, media, plugin tools, and similar capabilities.
  • Codex does not get an extra OpenClaw tool_search_code or generic tool_search wrapper. It should use Codex's native exec, wait, deferred dynamic tools, and tool-search semantics.

Please make this feature PI-only. A clean version would either create these controls inside the PI embedded runner, or add an explicit PI-only option to createOpenClawCodingTools that defaults off and is never passed by the Codex app-server adapter.

Please also add a regression test for the Codex path: enabling tools.toolSearchCodeMode must not expose tool_search_code, tool_search, tool_describe, or tool_call through Codex dynamic tools.

Once that separation is clear, I am happy to look again.

@steipete steipete changed the title feat: add tool search code mode feat: add core tool search May 9, 2026

@chatgpt-codex-connector chatgpt-codex-connector Bot left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

💡 Codex Review

Here are some automated review suggestions for this pull request.

Reviewed commit: 4f43bce801

ℹ️ About Codex in GitHub

Your team has set up Codex to review pull requests in this repo. Reviews are triggered when you

  • Open a pull request for review
  • Mark a draft as ready
  • Comment "@codex review".

If Codex has suggestions, it will comment; otherwise it will react with 👍.

Codex can also answer questions or update the PR. Try commenting "@codex address that feedback".

Comment thread src/agents/tool-search.ts
if (!process.allowedNodeEnvironmentFlags.has("--permission")) {
throw new ToolInputError("tool_search_code requires a Node runtime with --permission support.");
}
return ["--permission", "--input-type=module", "--eval", TOOL_SEARCH_CODE_MODE_CHILD_SOURCE];

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

P1 Badge Block outbound network access in code-mode child

The child process is launched with only --permission, which does not sandbox networking, so model-authored code can still open sockets/fetch URLs directly from tool_search_code. In this mode, tool outputs returned by openclaw.tools.call(...) can be exfiltrated over the network without going through OpenClaw tool policy/approval surfaces, which breaks the intended isolation boundary for untrusted model code.

Useful? React with 👍 / 👎.

Comment thread src/agents/tool-search.ts Outdated
if (!id || !method) {
return;
}
void runCodeModeBridgeRequest(params.runtime, method, message.args)

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

P1 Badge Stop processing bridge calls after result settlement

The parent continues dispatching bridge messages even after a result message has already settled the tool call, so model code can fire openclaw.tools.call(...) without awaiting it and still trigger host-side tool execution after tool_search_code has returned. This creates post-completion side effects (and potential writes/network calls) that are detached from the reported tool result and run lifecycle.

Useful? React with 👍 / 👎.

@steipete steipete added the proof: supplied External PR includes structured after-fix real behavior proof. label May 9, 2026

@chatgpt-codex-connector chatgpt-codex-connector Bot left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

💡 Codex Review

Here are some automated review suggestions for this pull request.

Reviewed commit: 501099f9f4

ℹ️ About Codex in GitHub

Your team has set up Codex to review pull requests in this repo. Reviews are triggered when you

  • Open a pull request for review
  • Mark a draft as ready
  • Comment "@codex review".

If Codex has suggestions, it will comment; otherwise it will react with 👍.

Codex can also answer questions or update the PR. Try commenting "@codex address that feedback".

modelAuthMode: resolveModelAuthMode(model.provider, params.config, undefined, {
workspaceDir: effectiveWorkspace,
}),
includeToolSearchControls: true,

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

P2 Badge Keep Tool Search controls out of compaction until cataloged

compactEmbeddedPiSessionDirectOnce now enables Tool Search control tools for the compaction session, but this code path never runs applyToolSearchCatalog (or otherwise registers a session catalog) before those tools are exposed to the model. If the compaction model emits tool_search_code/tool_search during a compaction turn, resolveCatalog in tool-search.ts fails with “catalog is unavailable,” which can abort compaction instead of recovering context. Either compact/catalog tools in this path as well, or do not include Tool Search controls here.

Useful? React with 👍 / 👎.

@clawsweeper clawsweeper Bot removed the proof: sufficient ClawSweeper judged the real behavior proof convincing. label May 9, 2026

@TurboTheTurtle TurboTheTurtle left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Current-head follow-up on fdb22b8: I would still hold merge for two correctness issues that are visible in the latest diff.

  • src/agents/tool-search.ts: after runCodeModeChild settles a result, the parent can still process later bridge messages from the child. A model body can fire openclaw.tools.call(...) without awaiting it, return successfully, and still cause host-side tool execution after the reported tool_search_code result. The bridge should stop accepting work once the call is settled, and this should have a regression test with a fire-and-forget bridged call.

  • src/agents/pi-embedded-runner/compact.ts: compactEmbeddedPiSessionDirectOnce now passes includeToolSearchControls: true, but that compaction path does not register a Tool Search catalog before exposing those controls. If a compaction turn emits tool_search_code or the structured search tools, the tool fails with “catalog is unavailable” and can abort the compaction flow. Either keep these controls out of compaction, or register the same catalog used by normal embedded attempts.

I skipped older feedback that the latest branch appears to have addressed, such as the default-off shared factory behavior and unique bridged tool call ids.

@clawsweeper clawsweeper Bot added the proof: sufficient ClawSweeper judged the real behavior proof convincing. label May 9, 2026

@chatgpt-codex-connector chatgpt-codex-connector Bot left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

💡 Codex Review

Here are some automated review suggestions for this pull request.

Reviewed commit: fb4d50e097

ℹ️ About Codex in GitHub

Your team has set up Codex to review pull requests in this repo. Reviews are triggered when you

  • Open a pull request for review
  • Mark a draft as ready
  • Comment "@codex review".

If Codex has suggestions, it will comment; otherwise it will react with 👍.

Codex can also answer questions or update the PR. Try commenting "@codex address that feedback".

Comment thread src/agents/tool-search.ts
Comment on lines +746 to +748
const runtime = new ToolSearchRuntime(params.ctx, params.config);
const logs: string[] = [];
const value = await runCodeModeChild({

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

P1 Badge Preserve unique bridged tool IDs across code-tool invocations

Fresh evidence: although toolCallId now includes a sequence suffix, runCodeMode creates a new ToolSearchRuntime for every tool_search_code execution, so the counter restarts at 1 each time. In a single embedded run, repeated tool_search_code calls against the same catalog tool can reuse IDs like tool_search_code:<name>:1, and the client-tool slot recorder deduplicates by toolCallId (src/agents/pi-embedded-runner/run/attempt.ts:1610-1614), which can collapse multiple client-tool invocations into one recorded slot.

Useful? React with 👍 / 👎.

Comment thread src/agents/tool-search.ts
Comment on lines +898 to +901
void runCodeModeBridgeRequest(params.runtime, method, message.args, {
signal: params.signal,
onUpdate: params.onUpdate,
})

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

P1 Badge Cancel in-flight bridge calls once code mode settles

Fresh evidence for the prior post-settlement side-effect concern: the bridge object is thenable (then/catch/finally), so Promise.resolve(openclaw.tools.call(...)); return ...; still starts a host bridge call without awaiting it. When the child then sends result, this path only stops sending bridge-result messages but does not abort the already-started host execution, so tool side effects can continue after tool_search_code has already returned.

Useful? React with 👍 / 👎.

@chatgpt-codex-connector chatgpt-codex-connector Bot left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

💡 Codex Review

Here are some automated review suggestions for this pull request.

Reviewed commit: 5e613f9578

ℹ️ About Codex in GitHub

Your team has set up Codex to review pull requests in this repo. Reviews are triggered when you

  • Open a pull request for review
  • Mark a draft as ready
  • Comment "@codex review".

If Codex has suggestions, it will comment; otherwise it will react with 👍.

Codex can also answer questions or update the PR. Try commenting "@codex address that feedback".

Comment thread src/agents/pi-tools.ts Outdated
Comment on lines +717 to +720
const toolSearchTools =
includeOpenClawTools &&
options?.includeToolSearchControls === true &&
resolveToolSearchConfig(options?.config).enabled

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

P2 Badge Decouple Tool Search controls from OpenClaw core-tool gating

Tool Search controls are only created when includeOpenClawTools is true, so runs that intentionally allow only plugin tools (for example tools.allow: ["group:plugins"]) never get tool_search_code/fallback controls and therefore never compact the catalog even with tools.toolSearch enabled. In run/attempt, compaction depends on those controls being present, so this silently disables the feature for a common restricted-allowlist shape instead of honoring the config.

Useful? React with 👍 / 👎.

Comment thread src/agents/tool-search.ts
Comment on lines +637 to +639
const entry = catalog.entries.find(
(candidate) => candidate.id === needle || candidate.name === needle,
);

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

P2 Badge Reject ambiguous tool-name lookups in catalog calls

Name-based resolution accepts either id or name, but findEntry returns the first matching entry when multiple catalog entries share a name (for example client and plugin tools with the same function name). Because this silently picks one implementation by sort order, tool_call/tool_search_code can invoke the wrong tool whenever the model passes a name instead of an id. The lookup should fail on ambiguous names (or require ids) to avoid misrouting execution.

Useful? React with 👍 / 👎.

@chatgpt-codex-connector chatgpt-codex-connector Bot left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

💡 Codex Review

Here are some automated review suggestions for this pull request.

Reviewed commit: d339eb0305

ℹ️ About Codex in GitHub

Your team has set up Codex to review pull requests in this repo. Reviews are triggered when you

  • Open a pull request for review
  • Mark a draft as ready
  • Comment "@codex review".

If Codex has suggestions, it will comment; otherwise it will react with 👍.

Codex can also answer questions or update the PR. Try commenting "@codex address that feedback".

callableToolNames: buildCallableToolNamesForEmptyAllowlistCheck({
effectiveToolNames: effectiveTools.map((tool) => tool.name),
autoAddedToolSearchControlNames,
toolSearchCatalogToolCount: toolSearch.catalogToolCount,

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

P1 Badge Count compacted client tools before empty-allowlist precheck

The empty explicit allowlist precheck runs before client tools are compacted into the Tool Search catalog, and it only uses toolSearch.catalogToolCount (which at this point counts non-client tools). In runs where the explicit allowlist is present and callable tools are client-only, auto-added Tool Search controls are stripped from callableToolNames and this precheck can incorrectly report “No callable tools remain,” causing prompt submission to be skipped even though client tools become callable later via addClientToolsToToolSearchCatalog.

Useful? React with 👍 / 👎.

@chatgpt-codex-connector chatgpt-codex-connector Bot left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

💡 Codex Review

Here are some automated review suggestions for this pull request.

Reviewed commit: d295efc444

ℹ️ About Codex in GitHub

Your team has set up Codex to review pull requests in this repo. Reviews are triggered when you

  • Open a pull request for review
  • Mark a draft as ready
  • Comment "@codex review".

If Codex has suggestions, it will comment; otherwise it will react with 👍.

Codex can also answer questions or update the PR. Try commenting "@codex address that feedback".

});
const startupTrace = createGatewayStartupTrace();
const startupConfigModulePromise = import("./server-startup-config.js");
const reloadHandlersModulePromise = import("./server-reload-handlers.js");

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

P2 Badge Guard preloaded reload-handler import against unhandled rejection

Starting import("./server-reload-handlers.js") eagerly here creates a promise that has no rejection handler until much later (await reloadHandlersModulePromise), so if that module load fails early (for example due to a module-evaluation error in this path), Node treats it as an unhandled rejection and can terminate the process before gateway startup error handling runs. Please either defer the import to the await site or attach a rejection handler immediately when creating the preloaded promise.

Useful? React with 👍 / 👎.

@clawsweeper clawsweeper Bot removed the proof: sufficient ClawSweeper judged the real behavior proof convincing. label May 10, 2026
@steipete
steipete force-pushed the feat/tool-search-code-mode branch from 7a33648 to b78604a Compare May 10, 2026 06:55
@scoootscooob

scoootscooob commented May 10, 2026

Copy link
Copy Markdown
Member

I think this PR needs one boundary-contract fix before we safely enable Tool Search for ClawBench or other external transcript consumers. I opened the Codex-side OpenClaw PR for the same contract here: #80155. That PR keeps Codex dynamic tool search from changing the verifier-facing transcript shape. The defensive ClawBench patch was only a stopgap; the durable fix belongs in OpenClaw rather than in benchmark adapters.

The important contract: Tool Search should be a model-visible prompt compaction layer only. Public OpenClaw surfaces should still look as if the agent called the real tool directly. If the public transcript/session/trajectory surface mainly shows tool_search_code or tool_call, downstream verifiers and trajectory scoring stop seeing actual tool families like browser, exec, read, write, and edit. That is the same class of problem we hit with Codex dynamic tool search: the underlying work may happen, but the verifier-facing shape no longer matches the normal tool-call contract, so scoring/utilization can be wrong or all-zero.

Concrete request:

  1. Keep two tool surfaces:

    • logical/effective tools: the real allowed tools after policy/allowlist. This should feed tools.effective, system reports, trajectory metadata, benchmark clients, and external consumers.
    • prompt-visible tools: the compact bridge tools (tool_search_code or tool_search/tool_describe/tool_call) used only for the provider prompt.
  2. At the transcript/event boundary, expose the underlying bridged call as canonical OpenClaw tool shape. For example, if the bridge calls browser, public session/transcript consumers should see something like:

{
  "type": "toolCall",
  "id": "target-call-id",
  "name": "browser",
  "input": { "...": "..." },
  "bridgedBy": "tool_search_code",
  "parentToolCallId": "bridge-call-id"
}

paired with:

{
  "role": "toolResult",
  "toolCallId": "target-call-id",
  "toolName": "browser",
  "content": []
}

It is fine to retain the bridge call internally, but public transcript/session/trajectory consumers need to see the actual target tool call/result, not only the control tool.

  1. Add tests that assert direct mode and Tool Search mode are equivalent from the public consumer perspective:
    • sessions.get / session JSONL / gateway transcript includes the real target tool name.
    • Code mode and structured mode both expose the target tool call/result canonically.
    • tools.effective still reports real tools, not only bridge tools.
    • The E2E should parse canonical toolCall / toolResult entries and assert the target tool appears there; raw substring mentions in logs are not enough.

The short version: Tool Search can change what OpenClaw sends to the model, but it should not change the external OpenClaw tool-call contract.

@steipete
steipete merged commit 15b26be into main May 10, 2026
111 checks passed
@steipete
steipete deleted the feat/tool-search-code-mode branch May 10, 2026 07:21
@steipete

Copy link
Copy Markdown
Contributor Author

Landed via rebase merge.

  • Head before merge: cbb23e5
  • Merge commit: 15b26be
  • Verification: CI passed on cbb23e5, including check, check-lint, check-docs, check-prod-types, check-test-types, CodeQL security shards, and Real behavior proof.
  • Local fix proof before final push: pnpm exec oxfmt --check --threads=1 src/agents/tool-search.ts src/agents/tool-search.test.ts; node scripts/run-oxlint.mjs --tsconfig config/tsconfig/oxlint.core.json src/agents/tool-search.ts src/agents/tool-search.test.ts; pnpm test src/agents/tool-search.test.ts.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

agents Agent runtime and tooling docs Improvements or additions to documentation extensions: codex extensions: qa-lab gateway Gateway runtime maintainer Maintainer-authored PR proof: supplied External PR includes structured after-fix real behavior proof. scripts Repository scripts size: XL

Projects

None yet

Development

Successfully merging this pull request may close these issues.

6 participants