diff --git a/docs/plugins/sdk-agent-harness.md b/docs/plugins/sdk-agent-harness.md index b6056bef4f4f..0a030da11e81 100644 --- a/docs/plugins/sdk-agent-harness.md +++ b/docs/plugins/sdk-agent-harness.md @@ -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 diff --git a/extensions/copilot/src/tool-bridge.ts b/extensions/copilot/src/tool-bridge.ts index 6ceda16457f6..2b5fb808801b 100644 --- a/extensions/copilot/src/tool-bridge.ts +++ b/extensions/copilot/src/tool-bridge.ts @@ -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, }); diff --git a/src/agents/embedded-agent-runner/run/attempt-tool-base-prepare.ts b/src/agents/embedded-agent-runner/run/attempt-tool-base-prepare.ts index 8b4677e6765b..18efbde39e48 100644 --- a/src/agents/embedded-agent-runner/run/attempt-tool-base-prepare.ts +++ b/src/agents/embedded-agent-runner/run/attempt-tool-base-prepare.ts @@ -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])] diff --git a/src/agents/embedded-agent-runner/run/attempt-tool-catalog.ts b/src/agents/embedded-agent-runner/run/attempt-tool-catalog.ts index e4dd2cc29e5c..cd886c36411e 100644 --- a/src/agents/embedded-agent-runner/run/attempt-tool-catalog.ts +++ b/src/agents/embedded-agent-runner/run/attempt-tool-catalog.ts @@ -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, diff --git a/src/agents/harness/tool-surface-bridge.test.ts b/src/agents/harness/tool-surface-bridge.test.ts index 192a71cbd18f..5ae8c6dc62d6 100644 --- a/src/agents/harness/tool-surface-bridge.test.ts +++ b/src/agents/harness/tool-surface-bridge.test.ts @@ -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 } } }, diff --git a/src/agents/harness/tool-surface-bridge.ts b/src/agents/harness/tool-surface-bridge.ts index fc326c8f1d1b..4665396cb26b 100644 --- a/src/agents/harness/tool-surface-bridge.ts +++ b/src/agents/harness/tool-surface-bridge.ts @@ -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({ diff --git a/src/agents/tool-surface-plan.test.ts b/src/agents/tool-surface-plan.test.ts new file mode 100644 index 000000000000..ee0e234582c9 --- /dev/null +++ b/src/agents/tool-surface-plan.test.ts @@ -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[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; + 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); + }); +}); diff --git a/src/agents/tool-surface-plan.ts b/src/agents/tool-surface-plan.ts new file mode 100644 index 000000000000..5669b3c6cd3b --- /dev/null +++ b/src/agents/tool-surface-plan.ts @@ -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[0]; +type ApplyAgentToolSurfaceCatalogParams = Omit & { + /** 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, + }); +}