refactor(agents): resolve code-mode vs tool-search once for every harness (#115189)

* refactor(agents): share one tool-surface resolver across runner and harness

The code-mode vs tool-search gate decision and the three-way catalog
application were duplicated across the embedded runner and the native
harness bridge, and had already drifted: the harness copy was missing the
skillWorkshopProposalOnly condition and its params object had no field to
express it.

Both surfaces now consume resolveAgentToolSurfacePlan and
applyAgentToolSurfaceCatalog, so the gates and the catalog branch exist
once. Both gate booleans derive from a single shared toolsAvailable
intermediate.

Behavior change: the harness path now honors skillWorkshopProposalOnly.
Proposal-only skill-workshop runs are deliberately narrow single-tool runs,
so code-mode indirection and tool-search catalogs are pure overhead — a
harness-independent reason. Both callers currently pin
agentHarnessRuntimeOverride "openclaw", so this closes a latent fail-open
rather than fixing a live bug.

* fix(agents): let the shared catalog params carry a config-less run

resolveAgentToolSearchRuntimeConfig returns OpenClawConfig | undefined, so
requiring a non-null toolSearchRuntimeConfig broke tsgo:core at both call
sites. Keep the key required so it cannot be silently omitted, but let the
value be undefined as the pre-refactor code already allowed.

* fix(agents): keep the tool-surface module free of dead exports

Importing isCodeModeEngagedForModel and applyToolSchemaDirectoryCatalog from
their defining modules left the code-mode.ts and tool-search.ts re-exports with
no production consumer, which knip rejects. Go back through the barrels, which
is also what both call sites did before this refactor.

The two params types were exported only for the test, so knip's production scan
saw them as dead. Keep them module-local and derive the type in the test.
This commit is contained in:
Peter Steinberger
2026-07-28 09:16:35 -04:00
committed by GitHub
parent 15866acef5
commit fe9893d41c
8 changed files with 321 additions and 126 deletions
+5
View File
@@ -294,6 +294,11 @@ runtime-compatible schema filtering, hidden catalog execution, directory
hydration, and catalog cleanup. Harnesses still own their SDK-specific tool
conversion and native execution callback.
Harnesses that forward embedded attempt params should pass
`skillWorkshopProposalOnly` through. Proposal-only skill-workshop runs are
deliberately narrow single-tool runs, and the runtime keeps them on the raw
tool surface instead of engaging code mode or a tool-search catalog.
### Native Codex harness mode
The bundled `codex` harness is the native Codex mode for embedded OpenClaw
+1
View File
@@ -212,6 +212,7 @@ export async function createCopilotToolBridge(
sessionId: input.sessionId,
sessionKey: attemptParams.sandboxSessionKey ?? attemptParams.sessionKey ?? input.sessionKey,
scheduledToolPolicy: attemptParams.scheduledToolPolicy,
skillWorkshopProposalOnly: attemptParams.skillWorkshopProposalOnly,
sourceReplyDeliveryMode: attemptParams.sourceReplyDeliveryMode,
toolsAllow: attemptParams.toolsAllow,
});
@@ -4,10 +4,8 @@ import { extractModelCompat } from "../../../plugins/provider-model-compat.js";
import { getPluginToolMeta } from "../../../plugins/tools.js";
import { isSubagentSessionKey } from "../../../routing/session-key.js";
import { createOpenClawCodingTools } from "../../agent-tools.js";
import { getActiveAgentRingZeroTools } from "../../agent-tools.ring-zero-context.js";
import { getChannelAgentToolMeta } from "../../channel-tools.js";
import type { CodeModeSkill } from "../../code-mode-skills.js";
import { isCodeModeEngagedForModel, resolveCodeModeConfig } from "../../code-mode.js";
import { resolveConversationCapabilityProfile } from "../../conversation-capability-profile.js";
import {
isLocalModelLeanEnabled,
@@ -17,13 +15,12 @@ import { resolveModelAuthMode } from "../../model-auth.js";
import { supportsModelTools } from "../../model-tool-support.js";
import type { SandboxContext } from "../../sandbox/types.js";
import { isAgentToolRestartSafe } from "../../tool-replay-safety.js";
import { resolveAgentToolSearchRuntimeConfig } from "../../tool-search-runtime-config.js";
import {
createToolSearchCatalogRef,
resolveToolSearchConfig,
type ToolSearchCatalogToolExecutor,
type ToolSearchTargetTranscriptProjection,
} from "../../tool-search.js";
import { resolveAgentToolSurfacePlan } from "../../tool-surface-plan.js";
import type { ComputerContextEpoch } from "../../tools/computer-tool.js";
import type { CronCreatorToolAllowlistEntry } from "../../tools/cron-tool.js";
import { log } from "../logger.js";
@@ -70,7 +67,6 @@ export function prepareEmbeddedAttemptToolBase(params: {
},
);
const toolsEnabled = supportsModelTools(attempt.model);
const ringZeroToolRun = getActiveAgentRingZeroTools().length > 0;
const isRawModelRun = attempt.modelRun === true || attempt.promptMode === "none";
const toolConstructionPlan = resolveEmbeddedAttemptToolConstructionPlan({
disableTools: attempt.disableTools,
@@ -78,31 +74,23 @@ export function prepareEmbeddedAttemptToolBase(params: {
toolsEnabled,
toolsAllow: toolsAllowWithForcedRuntimeTools,
});
const codeModeConfig = resolveCodeModeConfig(attempt.config, params.sessionAgentId);
const toolSearchRuntimeConfig = resolveAgentToolSearchRuntimeConfig({
const {
codeModeControlsEnabled: codeModeControlsEnabledForRun,
toolSearchConfig,
toolSearchControlsEnabled: toolSearchControlsEnabledForRun,
toolSearchRuntimeConfig,
} = resolveAgentToolSurfacePlan({
config: attempt.config,
agentId: params.sessionAgentId,
sessionKey: params.sandboxSessionKey,
forceDirectMessageTool,
model: attempt.model,
toolsEnabled,
disableTools: attempt.disableTools,
isRawModelRun,
skillWorkshopProposalOnly: attempt.skillWorkshopProposalOnly,
toolsAllow: attempt.toolsAllow,
});
const toolSearchConfig = resolveToolSearchConfig(toolSearchRuntimeConfig);
const codeModeControlsEnabledForRun =
toolsEnabled &&
!ringZeroToolRun &&
attempt.disableTools !== true &&
!isRawModelRun &&
attempt.skillWorkshopProposalOnly !== true &&
attempt.toolsAllow?.length !== 0 &&
isCodeModeEngagedForModel(codeModeConfig, attempt.model);
const toolSearchControlsEnabledForRun =
toolsEnabled &&
!ringZeroToolRun &&
attempt.disableTools !== true &&
!isRawModelRun &&
attempt.skillWorkshopProposalOnly !== true &&
attempt.toolsAllow?.length !== 0 &&
!codeModeControlsEnabledForRun &&
toolSearchConfig.enabled;
const effectiveToolsAllow =
toolSearchControlsEnabledForRun && toolsAllowWithForcedRuntimeTools
? [...new Set([...toolsAllowWithForcedRuntimeTools, ...TOOL_SEARCH_CONTROL_ALLOWLIST_NAMES])]
@@ -4,7 +4,6 @@
import type { DiagnosticTraceContext } from "../../../infra/diagnostic-trace-context.js";
import { resolveToolLoopDetectionConfig } from "../../agent-tools.js";
import {
applyCodeModeCatalog,
CODE_MODE_EXEC_TOOL_NAME,
CODE_MODE_WAIT_TOOL_NAME,
createCodeModeTools,
@@ -15,13 +14,12 @@ import { buildEmptyExplicitToolAllowlistError } from "../../tool-allowlist-guard
import { filterRuntimeCompatibleTools } from "../../tool-schema-projection.js";
import { logRuntimeToolSchemaQuarantine } from "../../tool-schema-quarantine.js";
import {
applyToolSchemaDirectoryCatalog,
applyToolSearchCatalog,
TOOL_CALL_RAW_TOOL_NAME,
TOOL_DESCRIBE_RAW_TOOL_NAME,
TOOL_SEARCH_RAW_TOOL_NAME,
type ToolSearchCatalogToolExecutor,
} from "../../tool-search.js";
import { applyAgentToolSurfaceCatalog } from "../../tool-surface-plan.js";
import { log } from "../logger.js";
import type { prepareEmbeddedAttemptBundleTools } from "./attempt-bundle-tools.js";
import { collectAttemptExplicitToolAllowlistSources } from "./attempt-tool-allowlist.js";
@@ -93,45 +91,23 @@ export function prepareEmbeddedAttemptToolCatalog(input: {
codeModeSkills,
})
: [];
// When the message tool is the only reply path it must stay directly visible
// in every search mode; a hidden delivery tool can leave the run mute.
const requiredDirectToolNames = preparedToolBase.forceDirectMessageTool ? ["message"] : [];
const toolSearch = codeModeControlsEnabledForRun
? applyCodeModeCatalog({
tools: [...codeModeTools, ...effectiveTools],
config: attempt.config,
sessionId: attempt.sessionId,
sessionKey: input.sandboxSessionKey,
agentId: input.sessionAgentId,
runId: attempt.runId,
catalogRef: preparedToolBase.toolSearchCatalogRef,
toolHookContext: catalogToolHookContext,
directToolNames: requiredDirectToolNames,
codeModeSkills,
})
: toolSearchConfig.mode === "directory"
? applyToolSchemaDirectoryCatalog({
tools: effectiveTools,
config: toolSearchRuntimeConfig,
sessionId: attempt.sessionId,
sessionKey: input.sandboxSessionKey,
agentId: input.sessionAgentId,
runId: attempt.runId,
catalogRef: preparedToolBase.toolSearchCatalogRef,
toolHookContext: catalogToolHookContext,
directToolNames: requiredDirectToolNames,
})
: applyToolSearchCatalog({
tools: effectiveTools,
config: toolSearchRuntimeConfig,
sessionId: attempt.sessionId,
sessionKey: input.sandboxSessionKey,
agentId: input.sessionAgentId,
runId: attempt.runId,
catalogRef: preparedToolBase.toolSearchCatalogRef,
toolHookContext: catalogToolHookContext,
directToolNames: requiredDirectToolNames,
});
const toolSearch = applyAgentToolSurfaceCatalog({
// `codeModeTools` is empty unless code-mode controls are on, so this stays
// exactly `effectiveTools` for the tool-search branches.
tools: [...codeModeTools, ...effectiveTools],
config: attempt.config,
toolSearchRuntimeConfig,
codeModeControlsEnabled: codeModeControlsEnabledForRun,
toolSearchConfig,
forceDirectMessageTool: preparedToolBase.forceDirectMessageTool,
sessionId: attempt.sessionId,
sessionKey: input.sandboxSessionKey,
agentId: input.sessionAgentId,
runId: attempt.runId,
catalogRef: preparedToolBase.toolSearchCatalogRef,
toolHookContext: catalogToolHookContext,
codeModeSkills,
});
const projectedToolSearchTools = filterLocalModelLeanTools({
tools: toolSearch.tools,
config: attempt.config,
@@ -58,6 +58,27 @@ describe("createAgentHarnessToolSurfaceRuntime", () => {
});
});
it("keeps proposal-only skill workshop runs on the raw harness tool surface", () => {
const rawTools = tools(["skill_workshop"]);
const runtime = createAgentHarnessToolSurfaceRuntime({
config: { tools: { codeMode: true, toolSearch: true } },
executeTool: async () => ({ content: [], details: {} }),
modelToolsEnabled: true,
skillWorkshopProposalOnly: true,
toolsAllow: ["skill_workshop"],
});
try {
expect(runtime.codeModeControlsEnabled).toBe(false);
expect(runtime.toolSearchControlsEnabled).toBe(false);
expect(runtime.compactTools(rawTools).tools).toEqual(rawTools);
expect(runtime.compactTools(rawTools).tools.map((tool) => tool.name)).not.toContain("exec");
expect(runtime.compactTools(rawTools).tools.map((tool) => tool.name)).not.toContain("wait");
} finally {
runtime.cleanup();
}
});
it("filters raw SDK tools but does not refilter prepared constructor output", () => {
const config: OpenClawConfig = {
agents: { defaults: { experimental: { localModelLean: true } } },
+30 -59
View File
@@ -1,14 +1,10 @@
import { messageToolOwnsVisibleReply } from "../../auto-reply/source-reply-delivery-mode.js";
import type { OpenClawConfig } from "../../config/types.openclaw.js";
import type { HookContext } from "../agent-tools.before-tool-call.js";
import { getActiveAgentRingZeroTools } from "../agent-tools.ring-zero-context.js";
import {
CODE_MODE_EXEC_TOOL_NAME,
CODE_MODE_WAIT_TOOL_NAME,
applyCodeModeCatalog,
createCodeModeTools,
isCodeModeEngagedForModel,
resolveCodeModeConfig,
} from "../code-mode.js";
import { resolveConversationCapabilityProfile } from "../conversation-capability-profile.js";
import {
@@ -17,13 +13,9 @@ import {
} from "../local-model-lean.js";
import type { ScheduledToolPolicyContext } from "../scheduled-tool-policy.js";
import { filterRuntimeCompatibleTools } from "../tool-schema-projection.js";
import { resolveAgentToolSearchRuntimeConfig } from "../tool-search-runtime-config.js";
import {
applyToolSchemaDirectoryCatalog,
applyToolSearchCatalog,
clearToolSearchCatalog,
createToolSearchCatalogRef,
resolveToolSearchConfig,
TOOL_CALL_RAW_TOOL_NAME,
TOOL_DESCRIBE_RAW_TOOL_NAME,
TOOL_SEARCH_CODE_MODE_TOOL_NAME,
@@ -31,6 +23,7 @@ import {
type ToolSearchCatalogRef,
type ToolSearchCatalogToolExecutor,
} from "../tool-search.js";
import { applyAgentToolSurfaceCatalog, resolveAgentToolSurfacePlan } from "../tool-surface-plan.js";
import type { AnyAgentTool } from "../tools/common.js";
const TOOL_SEARCH_CONTROL_ALLOWLIST_NAMES = [
@@ -78,27 +71,27 @@ export function createAgentHarnessToolSurfaceRuntime(params: {
sessionKey?: string;
scheduledToolPolicy?: ScheduledToolPolicyContext;
sourceReplyDeliveryMode?: string;
skillWorkshopProposalOnly?: boolean;
toolsAllow?: readonly string[];
}): AgentHarnessToolSurfaceRuntime {
const forceDirectMessageTool = messageToolOwnsVisibleReply(params);
const codeModeConfig = resolveCodeModeConfig(params.config, params.agentId);
const toolSearchRuntimeConfig = resolveAgentToolSearchRuntimeConfig({
const {
codeModeControlsEnabled,
toolSearchControlsEnabled,
toolSearchConfig,
toolSearchRuntimeConfig,
} = resolveAgentToolSurfacePlan({
config: params.config,
agentId: params.agentId,
sessionKey: params.sessionKey,
forceDirectMessageTool,
model: params.model,
toolsEnabled: params.modelToolsEnabled,
disableTools: params.disableTools,
isRawModelRun: params.isRawModelRun === true,
skillWorkshopProposalOnly: params.skillWorkshopProposalOnly,
toolsAllow: params.toolsAllow,
});
const toolSearchConfig = resolveToolSearchConfig(toolSearchRuntimeConfig);
const toolsAvailable =
params.modelToolsEnabled &&
params.disableTools !== true &&
params.isRawModelRun !== true &&
params.toolsAllow?.length !== 0;
const ringZeroToolRun = getActiveAgentRingZeroTools().length > 0;
const codeModeControlsEnabled =
toolsAvailable && !ringZeroToolRun && isCodeModeEngagedForModel(codeModeConfig, params.model);
const toolSearchControlsEnabled =
toolsAvailable && !ringZeroToolRun && !codeModeControlsEnabled && toolSearchConfig.enabled;
const toolSearchCatalogRef =
toolSearchControlsEnabled || codeModeControlsEnabled ? createToolSearchCatalogRef() : undefined;
const runtimeToolAllowlist =
@@ -159,44 +152,22 @@ export function createAgentHarnessToolSurfaceRuntime(params: {
executeTool: params.executeTool,
})
: [];
// When the message tool is the only reply path it must stay directly visible
// in every search mode; a hidden delivery tool can leave the run mute.
const requiredDirectToolNames = forceDirectMessageTool ? ["message"] : [];
const compacted = codeModeControlsEnabled
? applyCodeModeCatalog({
tools: [...codeModeTools, ...effectiveTools],
config: params.config,
sessionId: params.sessionId,
sessionKey: params.sessionKey,
agentId: params.agentId,
runId: params.runId,
catalogRef: toolSearchCatalogRef,
toolHookContext: options.hookContext,
directToolNames: requiredDirectToolNames,
})
: toolSearchConfig.mode === "directory"
? applyToolSchemaDirectoryCatalog({
tools: effectiveTools,
config: toolSearchRuntimeConfig,
sessionId: params.sessionId,
sessionKey: params.sessionKey,
agentId: params.agentId,
runId: params.runId,
catalogRef: toolSearchCatalogRef,
toolHookContext: options.hookContext,
directToolNames: requiredDirectToolNames,
})
: applyToolSearchCatalog({
tools: effectiveTools,
config: toolSearchRuntimeConfig,
sessionId: params.sessionId,
sessionKey: params.sessionKey,
agentId: params.agentId,
runId: params.runId,
catalogRef: toolSearchCatalogRef,
toolHookContext: options.hookContext,
directToolNames: requiredDirectToolNames,
});
const compacted = applyAgentToolSurfaceCatalog({
// `codeModeTools` is empty unless code-mode controls are on, so this stays
// exactly `effectiveTools` for the tool-search branches.
tools: [...codeModeTools, ...effectiveTools],
config: params.config,
toolSearchRuntimeConfig,
codeModeControlsEnabled,
toolSearchConfig,
forceDirectMessageTool,
sessionId: params.sessionId,
sessionKey: params.sessionKey,
agentId: params.agentId,
runId: params.runId,
catalogRef: toolSearchCatalogRef,
toolHookContext: options.hookContext,
});
const projectedCompactedTools = options.localModelLeanApplied
? compacted.tools
: filterLocalModelLeanTools({
+141
View File
@@ -0,0 +1,141 @@
import { describe, expect, it } from "vitest";
import type { OpenClawConfig } from "../config/types.openclaw.js";
import { runWithAgentRingZeroTools } from "./agent-tools.ring-zero-context.js";
import { createCodeModeTools } from "./code-mode.js";
import { createStubTool } from "./test-helpers/agent-tool-stubs.js";
import {
createToolSearchCatalogRef,
TOOL_SEARCH_RAW_TOOL_NAME,
type ToolSearchCatalogToolExecutor,
} from "./tool-search.js";
import { applyAgentToolSurfaceCatalog, resolveAgentToolSurfacePlan } from "./tool-surface-plan.js";
// Params type stays module-local in production; derive it so the test cannot
// keep a public export alive that no production caller needs.
type AgentToolSurfacePlanParams = Parameters<typeof resolveAgentToolSurfacePlan>[0];
const controlsEnabledConfig: OpenClawConfig = {
tools: { codeMode: true, toolSearch: true },
};
const basePlanParams: AgentToolSurfacePlanParams = {
config: controlsEnabledConfig,
forceDirectMessageTool: false,
toolsEnabled: true,
isRawModelRun: false,
};
describe("resolveAgentToolSurfacePlan", () => {
it.each([
{ name: "model tools disabled", overrides: { toolsEnabled: false } },
{ name: "tools disabled for the run", overrides: { disableTools: true } },
{ name: "raw model run", overrides: { isRawModelRun: true } },
{ name: "host-scoped ring-zero run", overrides: {}, ringZero: true },
{ name: "empty explicit allowlist", overrides: { toolsAllow: [] } },
{
name: "proposal-only skill workshop run",
overrides: { skillWorkshopProposalOnly: true },
},
] satisfies Array<{
name: string;
overrides: Partial<AgentToolSurfacePlanParams>;
ringZero?: boolean;
}>)("suppresses both controls for $name", ({ overrides, ringZero }) => {
const resolve = () => resolveAgentToolSurfacePlan({ ...basePlanParams, ...overrides });
const plan = ringZero
? runWithAgentRingZeroTools([createStubTool("openclaw")], resolve)
: resolve();
expect(plan.codeModeControlsEnabled).toBe(false);
expect(plan.toolSearchControlsEnabled).toBe(false);
});
it.each([
{
name: "code mode wins when engaged",
config: { tools: { codeMode: true, toolSearch: true } },
expected: { codeMode: true, toolSearch: false },
},
{
name: "tool search engages when code mode does not",
config: { tools: { codeMode: false, toolSearch: true } },
expected: { codeMode: false, toolSearch: true },
},
] satisfies Array<{
name: string;
config: OpenClawConfig;
expected: { codeMode: boolean; toolSearch: boolean };
}>)("keeps controls mutually exclusive: $name", ({ config, expected }) => {
const plan = resolveAgentToolSurfacePlan({ ...basePlanParams, config });
expect(plan.codeModeControlsEnabled).toBe(expected.codeMode);
expect(plan.toolSearchControlsEnabled).toBe(expected.toolSearch);
expect(plan.codeModeControlsEnabled && plan.toolSearchControlsEnabled).toBe(false);
});
});
describe("applyAgentToolSurfaceCatalog", () => {
const executeTool: ToolSearchCatalogToolExecutor = async () => ({ content: [], details: {} });
it("uses the code-mode catalog when code-mode controls are enabled", () => {
const config: OpenClawConfig = {
tools: { codeMode: true, toolSearch: { enabled: true, mode: "directory" } },
};
const plan = resolveAgentToolSurfacePlan({ ...basePlanParams, config });
const catalogRef = createToolSearchCatalogRef();
const result = applyAgentToolSurfaceCatalog({
tools: [
...createCodeModeTools({ config, catalogRef, executeTool }),
createStubTool("hidden_target"),
],
config,
toolSearchRuntimeConfig: plan.toolSearchRuntimeConfig,
codeModeControlsEnabled: plan.codeModeControlsEnabled,
toolSearchConfig: plan.toolSearchConfig,
forceDirectMessageTool: false,
catalogRef,
});
expect(result.tools.map((tool) => tool.name)).toEqual(["exec", "wait"]);
expect(result.catalogToolCount).toBe(1);
});
it("uses the schema-directory catalog in directory mode", () => {
const config: OpenClawConfig = {
tools: { codeMode: false, toolSearch: { enabled: true, mode: "directory" } },
};
const plan = resolveAgentToolSurfacePlan({ ...basePlanParams, config });
const result = applyAgentToolSurfaceCatalog({
tools: [createStubTool("uncataloged_without_directory_controls")],
config,
toolSearchRuntimeConfig: plan.toolSearchRuntimeConfig,
codeModeControlsEnabled: plan.codeModeControlsEnabled,
toolSearchConfig: plan.toolSearchConfig,
forceDirectMessageTool: false,
catalogRef: createToolSearchCatalogRef(),
});
expect(result.tools.map((tool) => tool.name)).toEqual([
"uncataloged_without_directory_controls",
]);
expect(result.compacted).toBe(false);
});
it("uses the tool-search catalog outside directory mode", () => {
const config: OpenClawConfig = {
tools: { codeMode: false, toolSearch: { enabled: true, mode: "tools" } },
};
const plan = resolveAgentToolSurfacePlan({ ...basePlanParams, config });
const result = applyAgentToolSurfaceCatalog({
tools: [createStubTool(TOOL_SEARCH_RAW_TOOL_NAME), createStubTool("hidden_target")],
config,
toolSearchRuntimeConfig: plan.toolSearchRuntimeConfig,
codeModeControlsEnabled: plan.codeModeControlsEnabled,
toolSearchConfig: plan.toolSearchConfig,
forceDirectMessageTool: false,
catalogRef: createToolSearchCatalogRef(),
});
expect(result.tools.map((tool) => tool.name)).toEqual([TOOL_SEARCH_RAW_TOOL_NAME]);
expect(result.catalogToolCount).toBe(1);
});
});
+92
View File
@@ -0,0 +1,92 @@
import type { OpenClawConfig } from "../config/types.openclaw.js";
import { getActiveAgentRingZeroTools } from "./agent-tools.ring-zero-context.js";
import {
applyCodeModeCatalog,
isCodeModeEngagedForModel,
resolveCodeModeConfig,
} from "./code-mode.js";
import { resolveAgentToolSearchRuntimeConfig } from "./tool-search-runtime-config.js";
import type { ToolSearchConfig } from "./tool-search-types.js";
import {
applyToolSchemaDirectoryCatalog,
applyToolSearchCatalog,
resolveToolSearchConfig,
} from "./tool-search.js";
type AgentToolSurfacePlanParams = {
config?: OpenClawConfig;
agentId?: string;
sessionKey?: string;
forceDirectMessageTool: boolean;
model?: { compat?: unknown };
toolsEnabled: boolean;
disableTools?: boolean;
isRawModelRun: boolean;
skillWorkshopProposalOnly?: boolean;
toolsAllow?: readonly string[];
};
export function resolveAgentToolSurfacePlan(params: AgentToolSurfacePlanParams) {
const codeModeConfig = resolveCodeModeConfig(params.config, params.agentId);
const toolSearchRuntimeConfig = resolveAgentToolSearchRuntimeConfig({
config: params.config,
agentId: params.agentId,
sessionKey: params.sessionKey,
forceDirectMessageTool: params.forceDirectMessageTool,
});
const toolSearchConfig = resolveToolSearchConfig(toolSearchRuntimeConfig);
const toolsAvailable =
params.toolsEnabled &&
getActiveAgentRingZeroTools().length === 0 &&
params.disableTools !== true &&
!params.isRawModelRun &&
// Proposal-only workshop runs are deliberately narrow single-tool runs;
// code-mode indirection and tool-search catalogs are pure overhead.
params.skillWorkshopProposalOnly !== true &&
params.toolsAllow?.length !== 0;
const codeModeControlsEnabled =
toolsAvailable && isCodeModeEngagedForModel(codeModeConfig, params.model);
const toolSearchControlsEnabled =
toolsAvailable && !codeModeControlsEnabled && toolSearchConfig.enabled;
return {
codeModeControlsEnabled,
toolSearchControlsEnabled,
toolSearchConfig,
toolSearchRuntimeConfig,
};
}
type CodeModeCatalogParams = Parameters<typeof applyCodeModeCatalog>[0];
type ApplyAgentToolSurfaceCatalogParams = Omit<CodeModeCatalogParams, "directToolNames"> & {
/** Required key (may be undefined for a config-less run): the tool-search
* branches resolve their mode from this, so omitting it would silently
* downgrade the run to schema defaults. */
toolSearchRuntimeConfig: OpenClawConfig | undefined;
codeModeControlsEnabled: boolean;
toolSearchConfig: ToolSearchConfig;
forceDirectMessageTool: boolean;
};
export function applyAgentToolSurfaceCatalog({
codeModeControlsEnabled,
toolSearchConfig,
toolSearchRuntimeConfig,
forceDirectMessageTool,
...catalogParams
}: ApplyAgentToolSurfaceCatalogParams) {
// When the message tool is the only reply path it must stay directly visible
// in every search mode; a hidden delivery tool can leave the run mute.
const directToolNames = forceDirectMessageTool ? ["message"] : [];
const applyCatalog = codeModeControlsEnabled
? applyCodeModeCatalog
: toolSearchConfig.mode === "directory"
? applyToolSchemaDirectoryCatalog
: applyToolSearchCatalog;
return applyCatalog({
...catalogParams,
// Code mode reads the base config; tool-search modes read the run's
// resolved tool-search runtime config.
config: codeModeControlsEnabled ? catalogParams.config : toolSearchRuntimeConfig,
directToolNames,
});
}