File size: 6,555 Bytes
e249c6d
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
/**
 * 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<TSchemaType extends TSchema = TSchema, TResult = unknown> = {
  runtimePlan?: AgentRuntimePlan;
  tools: AgentTool<TSchemaType, TResult>[];
  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<TSchemaType, TResult>[],
  ) => 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<TSchemaType extends TSchema = TSchema, TResult = unknown>(
  sourceTools: AgentTool<TSchemaType, TResult>[],
  normalizedTools: AgentTool<TSchemaType, TResult>[],
): AgentTool<TSchemaType, TResult>[] {
  const sourcesByUniqueName = new Map<string, AgentTool<TSchemaType, TResult>>();
  const duplicateNames = new Set<string>();
  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<AgentRuntimeToolPolicyParams<TSchemaType, TResult>, "tools"> & {
    tools: readonly AgentTool<TSchemaType, TResult>[];
  },
): AgentTool<TSchemaType, TResult>[] {
  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,
  });
}