mirror of
https://github.com/openclaw/openclaw.git
synced 2026-08-12 21:53:00 -06:00
refactor(agents): centralize message-tool and direct-visibility predicates (#114720)
* refactor(agents): centralize message-tool and direct-visibility predicates * refactor(agents): unexport classifyTool and drop redundant union
This commit is contained in:
committed by
GitHub
parent
6d9b2d4d66
commit
f4058528e5
@@ -377,6 +377,9 @@ const CODEX_OPENCLAW_DYNAMIC_TOOL_NAMESPACE = "openclaw";
|
||||
// Keep OpenClaw control-path tools directly callable even when Codex tool_search
|
||||
// is unavailable or resolves a connector-only universe. Developer instructions
|
||||
// still steer normal Codex subagents to native spawn_agent.
|
||||
// sessions_yield is normally routed by its catalogMode "direct-only" before
|
||||
// this set is consulted; the name entry stays as the metadata-independent
|
||||
// contract that control-path tools remain directly callable.
|
||||
const ALWAYS_DIRECT_DYNAMIC_TOOL_NAMES = new Set([
|
||||
"agents_list",
|
||||
"sessions_spawn",
|
||||
|
||||
@@ -9,6 +9,7 @@ import type {
|
||||
TaskSuggestionDeliveryMode,
|
||||
} from "../auto-reply/get-reply-options.types.js";
|
||||
import { HEARTBEAT_RESPONSE_TOOL_NAME } from "../auto-reply/heartbeat-tool-response.js";
|
||||
import { messageToolOwnsVisibleReply } from "../auto-reply/source-reply-delivery-mode.js";
|
||||
import type { ChatType } from "../channels/chat-type.js";
|
||||
import type { InboundEventKind } from "../channels/inbound-event/kind.js";
|
||||
import type { ModelCompatConfig } from "../config/types.models.js";
|
||||
@@ -560,9 +561,7 @@ function createOpenClawCodingToolsInternal(options?: OpenClawCodingToolsOptions)
|
||||
sourceReplyDeliveryMode: options?.sourceReplyDeliveryMode,
|
||||
});
|
||||
const runtimeProfileAlsoAllow = [
|
||||
...(options?.forceMessageTool || options?.sourceReplyDeliveryMode === "message_tool_only"
|
||||
? ["message"]
|
||||
: []),
|
||||
...(options && messageToolOwnsVisibleReply(options) ? ["message"] : []),
|
||||
...(runtimeToolAllowlistIncludesMessage ? ["message"] : []),
|
||||
...(forceHeartbeatTool ? [HEARTBEAT_RESPONSE_TOOL_NAME] : []),
|
||||
...toolSearchControlAllowlist,
|
||||
|
||||
@@ -1,3 +1,4 @@
|
||||
import { messageToolOwnsVisibleReply } from "../../../auto-reply/source-reply-delivery-mode.js";
|
||||
import type { DiagnosticTraceContext } from "../../../infra/diagnostic-trace-context.js";
|
||||
import { extractModelCompat } from "../../../plugins/provider-model-compat.js";
|
||||
import { getPluginToolMeta } from "../../../plugins/tools.js";
|
||||
@@ -57,8 +58,7 @@ export function prepareEmbeddedAttemptToolBase(params: {
|
||||
toolSearchCatalogExecutor: ToolSearchCatalogToolExecutor;
|
||||
}) {
|
||||
const { attempt } = params;
|
||||
const forceDirectMessageTool =
|
||||
attempt.forceMessageTool === true || attempt.sourceReplyDeliveryMode === "message_tool_only";
|
||||
const forceDirectMessageTool = messageToolOwnsVisibleReply(attempt);
|
||||
const toolsAllowWithForcedRuntimeTools = mergeForcedEmbeddedAttemptToolsAllow(
|
||||
attempt.toolsAllow,
|
||||
{
|
||||
@@ -337,6 +337,7 @@ export function prepareEmbeddedAttemptToolBase(params: {
|
||||
computerContextEpoch,
|
||||
cronCreatorToolAllowlist,
|
||||
effectiveToolsAllow,
|
||||
forceDirectMessageTool,
|
||||
inheritedToolAllowlist,
|
||||
localModelLeanEnabled,
|
||||
localModelLeanPreserveToolNames,
|
||||
|
||||
@@ -93,10 +93,7 @@ export function prepareEmbeddedAttemptToolCatalog(input: {
|
||||
: [];
|
||||
// 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 =
|
||||
attempt.forceMessageTool === true || attempt.sourceReplyDeliveryMode === "message_tool_only"
|
||||
? ["message"]
|
||||
: [];
|
||||
const requiredDirectToolNames = preparedToolBase.forceDirectMessageTool ? ["message"] : [];
|
||||
const toolSearch = codeModeControlsEnabledForRun
|
||||
? applyCodeModeCatalog({
|
||||
tools: [...codeModeTools, ...effectiveTools],
|
||||
|
||||
@@ -1,3 +1,4 @@
|
||||
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";
|
||||
@@ -79,8 +80,7 @@ export function createAgentHarnessToolSurfaceRuntime(params: {
|
||||
sourceReplyDeliveryMode?: string;
|
||||
toolsAllow?: readonly string[];
|
||||
}): AgentHarnessToolSurfaceRuntime {
|
||||
const forceDirectMessageTool =
|
||||
params.forceMessageTool === true || params.sourceReplyDeliveryMode === "message_tool_only";
|
||||
const forceDirectMessageTool = messageToolOwnsVisibleReply(params);
|
||||
const codeModeConfig = resolveCodeModeConfig(params.config, params.agentId);
|
||||
const toolSearchRuntimeConfig = resolveAgentToolSearchRuntimeConfig({
|
||||
config: params.config,
|
||||
|
||||
@@ -3,6 +3,7 @@
|
||||
* Removes high-latency or channel-dependent tools for local models while
|
||||
* preserving explicitly required delivery tools.
|
||||
*/
|
||||
import { messageToolOwnsVisibleReply } from "../auto-reply/source-reply-delivery-mode.js";
|
||||
import type { OpenClawConfig } from "../config/types.openclaw.js";
|
||||
import { normalizeAgentId, parseAgentSessionKey } from "../routing/session-key.js";
|
||||
import { resolveAgentConfig, resolveDefaultAgentId } from "./agent-scope-config.js";
|
||||
@@ -44,7 +45,7 @@ export function resolveLocalModelLeanPreserveToolNames(params?: {
|
||||
sourceReplyDeliveryMode?: string;
|
||||
}): string[] {
|
||||
const names = [...(params?.toolNames ?? [])];
|
||||
if (params?.forceMessageTool || params?.sourceReplyDeliveryMode === "message_tool_only") {
|
||||
if (params && messageToolOwnsVisibleReply(params)) {
|
||||
names.push("message");
|
||||
}
|
||||
return [...new Set(names)];
|
||||
|
||||
@@ -6,6 +6,7 @@ import {
|
||||
rewrapToolWithBeforeToolCallHook,
|
||||
wrapToolWithBeforeToolCallHook,
|
||||
} from "./agent-tools.before-tool-call.js";
|
||||
import { isCoreCodingSurfaceToolName } from "./core-tool-factory-descriptors.js";
|
||||
import type { ToolDefinition } from "./sessions/index.js";
|
||||
import { compactToolInputHint, compactToolOutputHint } from "./tool-schema-hints.js";
|
||||
import {
|
||||
@@ -223,7 +224,7 @@ function rememberReusableCatalog(key: string | undefined, catalog: ToolSearchCat
|
||||
}
|
||||
}
|
||||
|
||||
export function classifyTool(tool: CatalogTool): {
|
||||
function classifyTool(tool: CatalogTool): {
|
||||
source: CatalogSource;
|
||||
sourceName?: string;
|
||||
mcp?: PluginToolMcpMeta;
|
||||
@@ -285,6 +286,24 @@ function shouldCatalogTool(tool: AnyAgentTool): boolean {
|
||||
return !TOOL_SEARCH_CONTROL_TOOL_NAMES.has(tool.name) && tool.catalogMode !== "direct-only";
|
||||
}
|
||||
|
||||
/**
|
||||
* Core file/shell primitives and caller-required names (e.g. message when it is
|
||||
* the only reply path) stay visible while remaining searchable. Both must
|
||||
* resolve to trusted OpenClaw tools: an MCP lookalike must never become a
|
||||
* direct delivery or core-coding tool.
|
||||
*/
|
||||
export function isDirectVisibleCatalogTool(
|
||||
tool: AnyAgentTool,
|
||||
directToolNames: ReadonlySet<string>,
|
||||
): boolean {
|
||||
const classified = classifyTool(tool);
|
||||
return (
|
||||
classified.source === "openclaw" &&
|
||||
(directToolNames.has(tool.name) ||
|
||||
(isCoreCodingSurfaceToolName(tool.name) && classified.sourceName === "core"))
|
||||
);
|
||||
}
|
||||
|
||||
export function registerHeadlessToolSearchCatalog(params: {
|
||||
catalogRef: ToolSearchCatalogRef;
|
||||
tools: readonly AnyAgentTool[];
|
||||
|
||||
@@ -1,10 +1,9 @@
|
||||
import { normalizeStringEntries } from "@openclaw/normalization-core/string-normalization";
|
||||
import { truncateUtf16Safe } from "@openclaw/normalization-core/utf16-slice";
|
||||
import { isCoreCodingSurfaceToolName } from "./core-tool-factory-descriptors.js";
|
||||
import {
|
||||
applyToolCatalogCompaction,
|
||||
classifyTool,
|
||||
collectUniqueCatalogToolNames,
|
||||
isDirectVisibleCatalogTool,
|
||||
resolveCatalog,
|
||||
visibleCatalogEntries,
|
||||
} from "./tool-search-catalog.js";
|
||||
@@ -63,19 +62,10 @@ export function applyToolSchemaDirectoryCatalog(params: {
|
||||
...params,
|
||||
enabled: config.enabled,
|
||||
isVisibleControlTool: (tool) => TOOL_SCHEMA_DIRECTORY_CONTROL_TOOL_NAMES.has(tool.name),
|
||||
// Required names must resolve to trusted OpenClaw tools; an MCP lookalike
|
||||
// must never become a direct delivery or core-coding tool.
|
||||
isVisibleCatalogTool: (tool) => {
|
||||
if (!uniqueCatalogToolNames.has(tool.name)) {
|
||||
return false;
|
||||
}
|
||||
const classified = classifyTool(tool);
|
||||
return (
|
||||
classified.source === "openclaw" &&
|
||||
(directToolNames.has(tool.name) ||
|
||||
(isCoreCodingSurfaceToolName(tool.name) && classified.sourceName === "core"))
|
||||
);
|
||||
},
|
||||
// The unique-name gate defers any cross-source name collision before the
|
||||
// shared trust check runs.
|
||||
isVisibleCatalogTool: (tool) =>
|
||||
uniqueCatalogToolNames.has(tool.name) && isDirectVisibleCatalogTool(tool, directToolNames),
|
||||
});
|
||||
}
|
||||
|
||||
|
||||
@@ -2389,7 +2389,10 @@ describe("Tool Search", () => {
|
||||
|
||||
const config = {
|
||||
tools: {
|
||||
toolSearch: { enabled: true, mode: "code", codeTimeoutMs: 100 },
|
||||
// Generous timeout: the child process must have started the bridged call
|
||||
// before the deadline fires, or the abort assertion races process spawn
|
||||
// latency under machine load.
|
||||
toolSearch: { enabled: true, mode: "code", codeTimeoutMs: 1500 },
|
||||
},
|
||||
} as never;
|
||||
applyToolSearchCatalog({
|
||||
|
||||
@@ -3,13 +3,12 @@ import { normalizeStringEntries } from "@openclaw/normalization-core/string-norm
|
||||
import { Type } from "typebox";
|
||||
import type { OpenClawConfig } from "../config/types.openclaw.js";
|
||||
import type { HookContext } from "./agent-tools.before-tool-call.js";
|
||||
import { isCoreCodingSurfaceToolName } from "./core-tool-factory-descriptors.js";
|
||||
import type { AgentToolResult, AgentToolUpdateCallback } from "./runtime/index.js";
|
||||
import type { ToolDefinition } from "./sessions/index.js";
|
||||
import {
|
||||
addClientToolsToToolCatalog,
|
||||
applyToolCatalogCompaction,
|
||||
classifyTool,
|
||||
isDirectVisibleCatalogTool,
|
||||
reusableCatalogSnapshots,
|
||||
resolveCatalog,
|
||||
sessionCatalogs,
|
||||
@@ -112,18 +111,7 @@ export function applyToolSearchCatalog(params: {
|
||||
isVisibleControlTool: (tool) =>
|
||||
TOOL_SEARCH_CONTROL_TOOL_NAMES.has(tool.name) &&
|
||||
shouldExposeControlTool(tool.name, config.mode),
|
||||
// Core file/shell primitives and caller-required names (e.g. message when it
|
||||
// is the only reply path) stay visible while remaining searchable. Required
|
||||
// names must resolve to trusted OpenClaw tools; an MCP lookalike must never
|
||||
// become a direct delivery or core-coding tool.
|
||||
isVisibleCatalogTool: (tool) => {
|
||||
const classified = classifyTool(tool);
|
||||
return (
|
||||
classified.source === "openclaw" &&
|
||||
(directToolNames.has(tool.name) ||
|
||||
(isCoreCodingSurfaceToolName(tool.name) && classified.sourceName === "core"))
|
||||
);
|
||||
},
|
||||
isVisibleCatalogTool: (tool) => isDirectVisibleCatalogTool(tool, directToolNames),
|
||||
});
|
||||
}
|
||||
|
||||
|
||||
@@ -0,0 +1,15 @@
|
||||
/** Canonical predicate for message-tool-owned visible replies. */
|
||||
|
||||
/**
|
||||
* True when the visible source reply must flow through the message tool, either
|
||||
* because the run forces it or because the delivery mode is message_tool_only.
|
||||
* Consumers use this to keep the message tool visible/preserved: hiding the only
|
||||
* reply path leaves the run mute. The mode is accepted as plain string because
|
||||
* harness callers carry it untyped; only "message_tool_only" is meaningful here.
|
||||
*/
|
||||
export function messageToolOwnsVisibleReply(params: {
|
||||
forceMessageTool?: boolean;
|
||||
sourceReplyDeliveryMode?: string;
|
||||
}): boolean {
|
||||
return params.forceMessageTool === true || params.sourceReplyDeliveryMode === "message_tool_only";
|
||||
}
|
||||
Reference in New Issue
Block a user