/** * Applies runtime-plan or provider fallback tool schema policy. The helpers * normalize tool schemas, preserve owner metadata across cloned definitions, * and emit provider diagnostics. */ import type { TSchema } from "typebox"; import type { OpenClawConfig } from "../../config/types.openclaw.js"; import type { ProviderRuntimePluginHandle } from "../../plugins/provider-hook-runtime.js"; import type { ProviderRuntimeModel } from "../../plugins/provider-runtime-model.types.js"; import { copyAgentToolMetadata } from "../agent-tool-metadata.js"; import { logProviderToolSchemaDiagnostics, normalizeProviderToolSchemas, } from "../embedded-agent-runner/tool-schema-runtime.js"; import type { AgentTool } from "../runtime/index.js"; import { filterProviderNormalizableTools, type RuntimeToolSchemaDiagnostic, } from "../tool-schema-projection.js"; import type { AnyAgentTool } from "../tools/common.js"; import type { AgentRuntimePlan } from "./types.js"; type AgentRuntimeToolPolicyParams = { runtimePlan?: AgentRuntimePlan; tools: AgentTool[]; provider: string; config?: OpenClawConfig; workspaceDir?: string; env?: NodeJS.ProcessEnv; modelId?: string; modelApi?: string | null; model?: ProviderRuntimeModel; runtimeHandle?: ProviderRuntimePluginHandle; allowProviderRuntimePluginLoad?: boolean; /** * Invoked on every normalization, including with an empty list, so * consumers can observe the all-clear and retire stale quarantine state. */ onPreNormalizationSchemaDiagnostics?: ( diagnostics: readonly RuntimeToolSchemaDiagnostic[], tools: readonly AgentTool[], ) => void; }; /** Builds the provider/runtime context passed into runtime-plan tool hooks. */ function runtimePlanToolContext(params: { workspaceDir?: string; modelApi?: string | null; model?: ProviderRuntimeModel; }) { return { workspaceDir: params.workspaceDir, modelApi: params.modelApi ?? undefined, model: params.model, }; } // Normalizers may return cloned tool definitions. Preserve owner and private // execution metadata so downstream dispatch keeps the same policy and result contract. function copyRuntimeToolMetadata(source: AgentTool, target: AgentTool): void { if (source === target) { return; } const { catalogMode, outputSchema, hideFromChannelProgress } = source as AnyAgentTool; Object.assign(target, { ...(catalogMode ? { catalogMode } : {}), ...(outputSchema !== undefined ? { outputSchema } : {}), ...(hideFromChannelProgress === true ? { hideFromChannelProgress } : {}), }); copyAgentToolMetadata(source as never, target as never); } // Duplicate names cannot be matched by map lookup alone, so same-index matches // take precedence and unique-name fallback covers cloned arrays. function preserveRuntimeToolMetadata( sourceTools: AgentTool[], normalizedTools: AgentTool[], ): AgentTool[] { const sourcesByUniqueName = new Map>(); const duplicateNames = new Set(); for (const source of sourceTools) { const name = source.name; if (sourcesByUniqueName.has(name)) { duplicateNames.add(name); sourcesByUniqueName.delete(name); continue; } if (!duplicateNames.has(name)) { sourcesByUniqueName.set(name, source); } } for (const [index, target] of normalizedTools.entries()) { const indexedSource = sourceTools[index]; const source = indexedSource?.name === target.name ? indexedSource : sourcesByUniqueName.get(target.name); if (source) { copyRuntimeToolMetadata(source, target); } } return normalizedTools; } /** Normalizes tool schemas through a runtime plan or provider fallback policy. */ export function normalizeAgentRuntimeTools< TSchemaType extends TSchema = TSchema, TResult = unknown, >( params: Omit, "tools"> & { tools: readonly AgentTool[]; }, ): AgentTool[] { const planContext = runtimePlanToolContext(params); const normalizableToolProjection = filterProviderNormalizableTools(params.tools); params.onPreNormalizationSchemaDiagnostics?.( normalizableToolProjection.diagnostics, params.tools, ); // Projection owns this fresh array, so normalizers can mutate it without changing raw input. const normalizableTools = normalizableToolProjection.tools; const planNormalized = params.runtimePlan?.tools.normalize(normalizableTools, planContext); // Empty fallback input cannot gain provider-specific schema changes. Avoid loading a provider // runtime just to return the same empty list; runtime plans still receive their normal callback. const normalized = planNormalized ?? (normalizableTools.length === 0 ? normalizableTools : normalizeProviderToolSchemas({ tools: normalizableTools, provider: params.provider, config: params.config, workspaceDir: params.workspaceDir, env: params.env ?? process.env, modelId: params.modelId, modelApi: params.modelApi, model: params.model, runtimeHandle: params.runtimeHandle, allowRuntimePluginLoad: params.allowProviderRuntimePluginLoad, })); // Provider collection views wrap the results of map, including host-owned tool wrappers. // Own the assembly array so later wrapping keeps its metadata; retain fenced tool elements. const normalizedTools = Array.isArray(normalized) ? Array.from(normalized) : normalizableTools; return preserveRuntimeToolMetadata(normalizableTools, normalizedTools); } /** Emits runtime-plan or provider fallback diagnostics for normalized tools. */ export function logAgentRuntimeToolDiagnostics(params: AgentRuntimeToolPolicyParams): void { const planContext = runtimePlanToolContext(params); if (params.runtimePlan) { params.runtimePlan.tools.logDiagnostics(params.tools, planContext); return; } logProviderToolSchemaDiagnostics({ tools: params.tools, provider: params.provider, config: params.config, workspaceDir: params.workspaceDir, env: params.env ?? process.env, modelId: params.modelId, modelApi: params.modelApi, model: params.model, runtimeHandle: params.runtimeHandle, }); }