@botbotgo/agent-harness 0.0.400 → 0.0.401

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
@@ -1,6 +1,6 @@
1
1
  import path from "node:path";
2
2
  import { createAsyncSubAgentMiddleware, createFilesystemMiddleware, createMemoryMiddleware, createPatchToolCallsMiddleware, createSkillsMiddleware, createSummarizationMiddleware, createSubAgentMiddleware, FilesystemBackend, StateBackend, } from "deepagents";
3
- import { createAgent, humanInTheLoopMiddleware, todoListMiddleware } from "langchain";
3
+ import { AIMessage, createAgent, createMiddleware, humanInTheLoopMiddleware, todoListMiddleware, ToolMessage } from "langchain";
4
4
  import { sanitizeVisibleText, tryParseJson, wrapResolvedModel, } from "./parsing/output-parsing.js";
5
5
  import { salvageJsonToolCalls } from "./parsing/output-tool-args.js";
6
6
  import { extractMessageText } from "../utils/message-content.js";
@@ -27,7 +27,7 @@ export { computeRemainingTimeoutMs, isRetryableProviderError, resolveBindingTime
27
27
  import { getBindingAdapterKind, getBindingBuiltinToolsConfig, getBindingDeepAgentSubagents, getBindingExecutionParams, getBindingExecutionKind, getBindingFilesystemConfig, getBindingMemorySources, getBindingPrimaryModel, getBindingSkills, getBindingSubagents, getBindingToolCount, getBindingPrimaryTools, getBindingSystemPrompt, isDeepAgentBinding, isLangChainBinding, } from "./support/compiled-binding.js";
28
28
  class DelegatedExecutionNoToolEvidenceError extends Error {
29
29
  constructor(agentId) {
30
- super(`Delegated agent ${agentId} completed without tool execution evidence.`);
30
+ super(`Delegated agent ${agentId} lacked non-planning tool evidence.`);
31
31
  this.name = "DelegatedExecutionNoToolEvidenceError";
32
32
  }
33
33
  }
@@ -38,11 +38,245 @@ function hasDelegatedExecutionToolEvidence(result) {
38
38
  return executedToolResults.some((toolResult) => (toolResult.isError !== true
39
39
  && !isPlanToolName(toolResult.toolName)));
40
40
  }
41
+ function buildDelegatedPlanEvidenceBlocker(agentId) {
42
+ return [
43
+ "Status: blocked",
44
+ "Summary:",
45
+ `- Delegated agent ${agentId} ended before producing the required TODO plan evidence.`,
46
+ "",
47
+ "Blockers:",
48
+ "- The delegated run did not expose a valid planning trace, so the framework cannot treat the task as complete.",
49
+ "",
50
+ "Next Actions:",
51
+ "- Retry with the same request or inspect the delegated agent configuration and model/tool-call behavior.",
52
+ ].join("\n");
53
+ }
54
+ function buildDelegatedExecutionEvidenceBlocker(agentId) {
55
+ return [
56
+ "Status: blocked",
57
+ "Summary:",
58
+ `- Delegated agent ${agentId} did not return any non-planning tool evidence after retry.`,
59
+ "",
60
+ "Blockers:",
61
+ "- The TODO board alone is not execution evidence.",
62
+ "- The framework cannot mark the delegated task complete without a non-planning tool result or an explicit blocker from that tool path.",
63
+ "",
64
+ "Next Actions:",
65
+ "- Retry the request or inspect the delegated agent's model/tool-call behavior.",
66
+ ].join("\n");
67
+ }
68
+ function normalizePlanToolName(toolName) {
69
+ return typeof toolName === "string" ? toolName.trim().toLowerCase().replace(/[\s-]+/gu, "_") : "";
70
+ }
41
71
  function isPlanToolName(toolName) {
42
- return toolName === "write_todos"
43
- || toolName === "read_todos"
44
- || toolName === "tool_call_write_todos"
45
- || toolName === "tool_call_read_todos";
72
+ const normalized = normalizePlanToolName(toolName);
73
+ return normalized === "write_todos"
74
+ || normalized === "read_todos"
75
+ || normalized === "tool_call_write_todos"
76
+ || normalized === "tool_call_read_todos";
77
+ }
78
+ function readConfiguredToolName(value) {
79
+ if (typeof value !== "object" || value === null) {
80
+ return "";
81
+ }
82
+ const typed = value;
83
+ return typeof typed.name === "string" ? typed.name.trim() : "";
84
+ }
85
+ function createBootstrapTodoPlan(toolNames) {
86
+ const evidenceToolName = toolNames.find((toolName) => !isPlanToolName(toolName));
87
+ const contents = evidenceToolName
88
+ ? [
89
+ `Run ${evidenceToolName} for the requested evidence`,
90
+ `Inspect the ${evidenceToolName} result and extract concrete findings`,
91
+ "Update TODO status from the observed evidence",
92
+ "Return the final answer grounded in tool output",
93
+ ]
94
+ : [
95
+ "Identify the concrete evidence needed for this request",
96
+ "Collect and inspect the available evidence",
97
+ "Update TODO status from the observed evidence",
98
+ "Return the final answer grounded in evidence",
99
+ ];
100
+ return contents.map((content, index) => ({
101
+ content,
102
+ status: index === 0 ? "in_progress" : "pending",
103
+ }));
104
+ }
105
+ function readMessageContentText(message) {
106
+ if (typeof message !== "object" || message === null) {
107
+ return "";
108
+ }
109
+ const content = message.content;
110
+ if (typeof content === "string") {
111
+ return content.trim();
112
+ }
113
+ if (!Array.isArray(content)) {
114
+ return "";
115
+ }
116
+ return content
117
+ .map((part) => typeof part === "object" && part !== null && typeof part.text === "string"
118
+ ? part.text
119
+ : "")
120
+ .join("")
121
+ .trim();
122
+ }
123
+ function parseToolCallArgs(value) {
124
+ if (typeof value === "object" && value !== null && !Array.isArray(value)) {
125
+ return value;
126
+ }
127
+ if (typeof value !== "string" || value.trim().length === 0) {
128
+ return {};
129
+ }
130
+ try {
131
+ const parsed = JSON.parse(value);
132
+ return typeof parsed === "object" && parsed !== null && !Array.isArray(parsed)
133
+ ? parsed
134
+ : {};
135
+ }
136
+ catch {
137
+ return {};
138
+ }
139
+ }
140
+ function readMessageToolCalls(message) {
141
+ if (typeof message !== "object" || message === null) {
142
+ return [];
143
+ }
144
+ const typed = message;
145
+ const raw = Array.isArray(typed.tool_calls) ? typed.tool_calls
146
+ : Array.isArray(typed.kwargs?.tool_calls) ? typed.kwargs.tool_calls
147
+ : Array.isArray(typed.additional_kwargs?.tool_calls) ? typed.additional_kwargs.tool_calls
148
+ : Array.isArray(typed.kwargs?.additional_kwargs?.tool_calls) ? typed.kwargs.additional_kwargs.tool_calls
149
+ : Array.isArray(typed.lc_kwargs?.tool_calls) ? typed.lc_kwargs.tool_calls
150
+ : Array.isArray(typed.lc_kwargs?.additional_kwargs?.tool_calls) ? typed.lc_kwargs.additional_kwargs.tool_calls
151
+ : [];
152
+ return raw
153
+ .map((toolCall) => {
154
+ if (typeof toolCall !== "object" || toolCall === null) {
155
+ return null;
156
+ }
157
+ const call = toolCall;
158
+ const name = typeof call.name === "string"
159
+ ? call.name
160
+ : typeof call.function?.name === "string"
161
+ ? call.function.name
162
+ : undefined;
163
+ const args = parseToolCallArgs(call.args ?? call.function?.arguments);
164
+ return {
165
+ ...(typeof call.id === "string" ? { id: call.id } : {}),
166
+ ...(name ? { name } : {}),
167
+ args,
168
+ };
169
+ })
170
+ .filter((toolCall) => toolCall !== null);
171
+ }
172
+ function todoToolCallIsTerminal(toolCall) {
173
+ const todos = toolCall.args?.todos;
174
+ if (!Array.isArray(todos) || todos.length === 0) {
175
+ return false;
176
+ }
177
+ return todos.every((todo) => {
178
+ if (typeof todo !== "object" || todo === null || typeof todo.status !== "string") {
179
+ return false;
180
+ }
181
+ const status = todo.status.trim().toLowerCase();
182
+ return status !== "pending" && status !== "in_progress";
183
+ });
184
+ }
185
+ function createTodoPlanGuardMiddleware(options = {}) {
186
+ return createMiddleware({
187
+ name: "harnessTodoPlanGuard",
188
+ wrapToolCall: ((request, handler) => {
189
+ const toolName = typeof request.toolCall?.name === "string"
190
+ ? request.toolCall.name
191
+ : typeof request.tool?.name === "string"
192
+ ? request.tool.name
193
+ : "";
194
+ const messages = Array.isArray(request.state?.messages) ? request.state.messages : [];
195
+ const hasNonPlanToolResult = messages.some((message) => {
196
+ if (typeof message !== "object" || message === null) {
197
+ return false;
198
+ }
199
+ const typed = message;
200
+ const messageType = typeof typed.type === "string"
201
+ ? typed.type
202
+ : typeof typed._getType === "function"
203
+ ? String(typed._getType())
204
+ : "";
205
+ if (messageType !== "tool" && typeof typed.tool_call_id !== "string") {
206
+ return false;
207
+ }
208
+ const resultToolName = typeof typed.name === "string" ? typed.name : "";
209
+ return resultToolName.length > 0 && !isPlanToolName(resultToolName);
210
+ });
211
+ if (options.requiresPlan === true
212
+ && !hasNonPlanToolResult
213
+ && isPlanToolName(toolName)
214
+ && normalizePlanToolName(toolName).includes("write_todos")
215
+ && todoToolCallIsTerminal({ args: parseToolCallArgs(request.toolCall?.args) })) {
216
+ return new ToolMessage({
217
+ content: "Error: write_todos cannot mark every todo as terminal before any non-planning evidence tool returns. Keep one todo in_progress and the remaining todos pending until evidence tools return.",
218
+ tool_call_id: typeof request.toolCall?.id === "string" ? request.toolCall.id : `write-todos-tool-guard-${Math.random().toString(36).slice(2, 10)}`,
219
+ status: "error",
220
+ });
221
+ }
222
+ return handler(request);
223
+ }),
224
+ afterModel: (state) => {
225
+ if (!Array.isArray(state.messages) || state.messages.length === 0) {
226
+ return;
227
+ }
228
+ const hasNonPlanToolResult = state.messages.some((message) => {
229
+ if (typeof message !== "object" || message === null) {
230
+ return false;
231
+ }
232
+ const typed = message;
233
+ const messageType = typeof typed.type === "string"
234
+ ? typed.type
235
+ : typeof typed._getType === "function"
236
+ ? String(typed._getType())
237
+ : "";
238
+ if (messageType !== "tool" && typeof typed.tool_call_id !== "string") {
239
+ return false;
240
+ }
241
+ const toolName = typeof typed.name === "string" ? typed.name : "";
242
+ return toolName.length > 0 && !isPlanToolName(toolName);
243
+ });
244
+ if (hasNonPlanToolResult) {
245
+ return;
246
+ }
247
+ const lastAiMessage = [...state.messages].reverse().find((message) => readMessageToolCalls(message).length > 0);
248
+ const lastToolCalls = readMessageToolCalls(lastAiMessage);
249
+ if (!lastAiMessage && options.requiresPlan === true) {
250
+ const latestMessage = state.messages.at(-1);
251
+ const hasVisibleContent = readMessageContentText(latestMessage).length > 0;
252
+ if (!hasVisibleContent) {
253
+ return {
254
+ messages: [new AIMessage({
255
+ content: "",
256
+ tool_calls: [{
257
+ id: `write-todos-bootstrap-${Math.random().toString(36).slice(2, 10)}`,
258
+ name: "write_todos",
259
+ args: { todos: createBootstrapTodoPlan(options.toolNames ?? []) },
260
+ type: "tool_call",
261
+ }],
262
+ })],
263
+ };
264
+ }
265
+ }
266
+ const writeTodosCalls = lastToolCalls.filter((toolCall) => isPlanToolName(toolCall.name));
267
+ const prematureCompletedCalls = writeTodosCalls.filter(todoToolCallIsTerminal);
268
+ if (prematureCompletedCalls.length === 0) {
269
+ return;
270
+ }
271
+ return {
272
+ messages: prematureCompletedCalls.map((toolCall, index) => new ToolMessage({
273
+ content: "Error: write_todos cannot mark every todo as terminal before any non-planning evidence tool returns. Keep one todo in_progress and the remaining todos pending until evidence tools return.",
274
+ tool_call_id: toolCall.id ?? `write-todos-plan-guard-${index}`,
275
+ status: "error",
276
+ })),
277
+ };
278
+ },
279
+ });
46
280
  }
47
281
  function shouldUseConfigurableDeepAgentAssembly(binding) {
48
282
  return getBindingExecutionKind(binding) === "deepagent";
@@ -160,25 +394,21 @@ function parseCompactRouterSelection(value, subagentNames) {
160
394
  }
161
395
  function inferCompactRouterSelectionFromRequest(requestText, subagents) {
162
396
  const normalized = requestText.toLowerCase();
397
+ const requestTokens = extractRouterMatchTokens(normalized);
163
398
  const score = (subagent) => {
164
399
  const name = subagent.name.toLowerCase();
165
400
  const description = (subagent.description ?? "").toLowerCase();
401
+ const descriptionTokens = extractRouterMatchTokens(`${name} ${description}`);
166
402
  let value = 0;
167
403
  if (normalized.includes(name))
168
404
  value += 4;
169
- const keywordGroups = {
170
- k8s: ["k8s", "kubernetes", "kubectl", "cluster", "pod", "node", "节点", "集群", "调度"],
171
- software: ["code", "repo", "implementation", "debug", "代码", "仓库", "实现", "配置"],
172
- ops: ["ci", "cd", "github actions", "disk", "storage", "deploy", "磁盘", "存储", "运维"],
173
- qa: ["test", "coverage", "regression", "验证", "测试", "回归"],
174
- release: ["release", "publish", "version", "tag", "发版", "发布", "版本"],
175
- research: ["research", "web", "latest", "调查资料", "外部", "搜索"],
176
- secretary: ["summary", "transcript", "youtube", "brief", "摘要", "讲稿", "转写"],
177
- };
178
- const keywords = keywordGroups[name] ?? [];
179
- for (const keyword of keywords) {
180
- if (normalized.includes(keyword))
181
- value += description.includes(keyword) || name.includes(keyword) ? 3 : 1;
405
+ for (const token of requestTokens) {
406
+ if (token === name) {
407
+ value += 4;
408
+ }
409
+ else if (descriptionTokens.has(token)) {
410
+ value += token.length > 2 ? 2 : 1;
411
+ }
182
412
  }
183
413
  return value;
184
414
  };
@@ -191,6 +421,16 @@ function inferCompactRouterSelectionFromRequest(requestText, subagents) {
191
421
  }
192
422
  return ranked[0].name;
193
423
  }
424
+ function extractRouterMatchTokens(value) {
425
+ const tokens = new Set();
426
+ for (const match of value.matchAll(/[\p{L}\p{N}_-]+/gu)) {
427
+ const token = match[0].toLowerCase();
428
+ if (token.length >= 2) {
429
+ tokens.add(token);
430
+ }
431
+ }
432
+ return tokens;
433
+ }
194
434
  function isDelegationOnlyDeepAgentBinding(binding) {
195
435
  return isDeepAgentBinding(binding)
196
436
  && getBindingSubagents(binding).length > 0
@@ -679,8 +919,14 @@ export class AgentRuntimeAdapter {
679
919
  const inlineSubagents = input.resolvedSubagents.filter((subagent) => !("graphId" in subagent));
680
920
  const asyncSubagents = input.resolvedSubagents.filter((subagent) => "graphId" in subagent);
681
921
  const subagents = inlineSubagents;
922
+ const requiresPlan = binding.harnessRuntime.executionContract?.requiresPlan === true;
923
+ const resolvedToolNames = input.resolvedTools.map(readConfiguredToolName).filter((name) => name.length > 0);
682
924
  const middleware = [
683
925
  ...(builtinTools.todos === false ? [] : [todoListMiddleware()]),
926
+ ...(builtinTools.todos === false ? [] : [createTodoPlanGuardMiddleware({
927
+ requiresPlan,
928
+ toolNames: resolvedToolNames,
929
+ })]),
684
930
  ...(input.resolvedSkills.length > 0 ? [createSkillsMiddleware({
685
931
  backend,
686
932
  sources: resolveDeepAgentSkillSourceRootPaths({
@@ -1074,7 +1320,7 @@ export class AgentRuntimeAdapter {
1074
1320
  }
1075
1321
  if (selectedBinding.harnessRuntime.executionContract?.requiresPlan === true
1076
1322
  && !hasDelegatedPlanEvidence(delegatedResult)) {
1077
- const output = "runtime_error=Delegated agent ended before producing required plan evidence.";
1323
+ const output = buildDelegatedPlanEvidenceBlocker(selectedBinding.agent.id);
1078
1324
  return {
1079
1325
  toolOutput: output,
1080
1326
  delegatedSubagentType: subagentType,
@@ -1087,7 +1333,7 @@ export class AgentRuntimeAdapter {
1087
1333
  };
1088
1334
  }
1089
1335
  if (targetRequiresExecutionToolEvidence && !hasDelegatedExecutionToolEvidence(delegatedResult)) {
1090
- const output = `runtime_error=Delegated agent ${selectedBinding.agent.id} completed without tool execution evidence.`;
1336
+ const output = buildDelegatedExecutionEvidenceBlocker(selectedBinding.agent.id);
1091
1337
  return {
1092
1338
  toolOutput: output,
1093
1339
  delegatedSubagentType: subagentType,
@@ -1397,7 +1643,7 @@ export class AgentRuntimeAdapter {
1397
1643
  }
1398
1644
  if (selectedBinding.harnessRuntime.executionContract?.requiresPlan === true
1399
1645
  && !hasDelegatedPlanEvidence(delegatedResult)) {
1400
- const output = "runtime_error=Delegated agent ended before producing required plan evidence.";
1646
+ const output = buildDelegatedPlanEvidenceBlocker(selectedBinding.agent.id);
1401
1647
  delegatedResult = {
1402
1648
  ...delegatedResult,
1403
1649
  state: "failed",
@@ -1406,7 +1652,7 @@ export class AgentRuntimeAdapter {
1406
1652
  };
1407
1653
  }
1408
1654
  if (targetRequiresExecutionToolEvidence && !hasDelegatedExecutionToolEvidence(delegatedResult)) {
1409
- const output = `runtime_error=Delegated agent ${selectedBinding.agent.id} completed without tool execution evidence.`;
1655
+ const output = buildDelegatedExecutionEvidenceBlocker(selectedBinding.agent.id);
1410
1656
  delegatedResult = {
1411
1657
  ...delegatedResult,
1412
1658
  state: "failed",
@@ -2,6 +2,7 @@ import { resolveBindingTimeout, resolveProviderRetryPolicy, resolveStreamIdleTim
2
2
  import { toolRequiresRuntimeApproval } from "../adapter/tool/tool-hitl.js";
3
3
  import { getBindingPrimaryTools } from "../support/compiled-binding.js";
4
4
  import { compiledToolHasInputSchema } from "./tool-schema.js";
5
+ import { projectBindingToolGatewayPolicy } from "./tool-gateway/index.js";
5
6
  export function getWorkspaceBinding(workspace, agentId) {
6
7
  return workspace.bindings.get(agentId);
7
8
  }
@@ -36,6 +37,7 @@ export function projectBindingToolExecutionPolicy(binding) {
36
37
  const retryableToolCount = projectedTools.filter((tool) => tool.retryable).length;
37
38
  return {
38
39
  agentId: binding.agent.id,
40
+ gateway: projectBindingToolGatewayPolicy(binding),
39
41
  invokeTimeoutMs: resolveBindingTimeout(binding),
40
42
  streamIdleTimeoutMs: resolveStreamIdleTimeout(binding) ?? 60_000,
41
43
  providerRetries: resolveProviderRetryPolicy(binding),
@@ -0,0 +1,2 @@
1
+ export * from "./policy.js";
2
+ export * from "./validation.js";
@@ -0,0 +1,2 @@
1
+ export * from "./policy.js";
2
+ export * from "./validation.js";
@@ -0,0 +1,2 @@
1
+ import type { CompiledAgentBinding, RuntimeToolExecutionPolicy } from "../../../contracts/types.js";
2
+ export declare function projectBindingToolGatewayPolicy(binding: CompiledAgentBinding): RuntimeToolExecutionPolicy["gateway"];
@@ -0,0 +1,45 @@
1
+ import { toolRequiresRuntimeApproval } from "../../adapter/tool/tool-hitl.js";
2
+ import { getBindingPrimaryTools } from "../../support/compiled-binding.js";
3
+ import { compiledToolHasInputSchema } from "../tool-schema.js";
4
+ export function projectBindingToolGatewayPolicy(binding) {
5
+ const tools = getBindingPrimaryTools(binding).map((tool) => {
6
+ const hasInputSchema = compiledToolHasInputSchema(tool);
7
+ const requiresApproval = toolRequiresRuntimeApproval(tool);
8
+ return {
9
+ toolId: tool.id,
10
+ name: tool.name,
11
+ retryable: tool.retryable === true,
12
+ hasInputSchema,
13
+ requiresApproval,
14
+ gatewayMode: requiresApproval ? "approval-gated" : hasInputSchema ? "schema-first" : "best-effort",
15
+ modelRole: "propose",
16
+ runtimeRole: requiresApproval
17
+ ? "request-approval"
18
+ : hasInputSchema
19
+ ? "validate-and-execute"
20
+ : "execute-with-runtime-checks",
21
+ };
22
+ });
23
+ const schemaBoundToolCount = tools.filter((tool) => tool.hasInputSchema).length;
24
+ const approvalRequiredToolCount = tools.filter((tool) => tool.requiresApproval).length;
25
+ return {
26
+ layer: "tool-gateway",
27
+ toolScope: {
28
+ source: "agent-binding",
29
+ exposedToolCount: tools.length,
30
+ schemaBoundToolCount,
31
+ approvalRequiredToolCount,
32
+ },
33
+ validation: {
34
+ strategy: "schema-first",
35
+ runtimeValidationRequired: tools.some((tool) => !tool.hasInputSchema || tool.requiresApproval),
36
+ strictProviderSchemaPreferred: tools.length > 0,
37
+ },
38
+ correction: {
39
+ invalidArguments: "structured-error-retry",
40
+ maxModelRetries: 2,
41
+ highRiskInvalidArguments: "approval-or-deny",
42
+ },
43
+ tools,
44
+ };
45
+ }
@@ -0,0 +1,33 @@
1
+ export type ToolGatewayValidationIssue = {
2
+ path: string;
3
+ message: string;
4
+ expected?: string;
5
+ received?: string;
6
+ };
7
+ export type ToolGatewayValidationResult = {
8
+ ok: true;
9
+ input: unknown;
10
+ } | {
11
+ ok: false;
12
+ error: ToolGatewayInvalidArgumentsResult;
13
+ };
14
+ export type ToolGatewayInvalidArgumentsResult = {
15
+ isError: true;
16
+ code: "INVALID_ARGUMENTS";
17
+ toolName: string;
18
+ message: string;
19
+ retryable: boolean;
20
+ requiresApproval: boolean;
21
+ validationErrors: ToolGatewayValidationIssue[];
22
+ };
23
+ export declare function createInvalidToolArgumentsResult(input: {
24
+ toolName: string;
25
+ requiresApproval: boolean;
26
+ issues: ToolGatewayValidationIssue[];
27
+ }): ToolGatewayInvalidArgumentsResult;
28
+ export declare function validateToolGatewayInput(input: {
29
+ toolName: string;
30
+ schema: unknown;
31
+ args: unknown;
32
+ requiresApproval?: boolean;
33
+ }): ToolGatewayValidationResult;
@@ -0,0 +1,176 @@
1
+ function isRecord(value) {
2
+ return typeof value === "object" && value !== null && !Array.isArray(value);
3
+ }
4
+ function readJsonSchemaType(schema) {
5
+ if (!isRecord(schema)) {
6
+ return [];
7
+ }
8
+ const type = schema.type;
9
+ if (typeof type === "string") {
10
+ return [type];
11
+ }
12
+ return Array.isArray(type) ? type.filter((entry) => typeof entry === "string") : [];
13
+ }
14
+ function typeMatches(value, allowedTypes) {
15
+ if (allowedTypes.length === 0) {
16
+ return true;
17
+ }
18
+ if (value === null) {
19
+ return allowedTypes.includes("null");
20
+ }
21
+ return allowedTypes.some((type) => {
22
+ if (type === "integer") {
23
+ return Number.isInteger(value);
24
+ }
25
+ if (type === "array") {
26
+ return Array.isArray(value);
27
+ }
28
+ if (type === "object") {
29
+ return isRecord(value);
30
+ }
31
+ return typeof value === type;
32
+ });
33
+ }
34
+ function toPath(path) {
35
+ if (path.length === 0) {
36
+ return "$";
37
+ }
38
+ let rendered = "$";
39
+ for (const part of path) {
40
+ rendered = typeof part === "number" ? `${rendered}[${part}]` : `${rendered}.${part}`;
41
+ }
42
+ return rendered;
43
+ }
44
+ function zodIssues(error) {
45
+ const issues = isRecord(error) && Array.isArray(error.issues) ? error.issues : [];
46
+ return issues
47
+ .map((issue) => {
48
+ if (!isRecord(issue)) {
49
+ return null;
50
+ }
51
+ const path = Array.isArray(issue.path) ? issue.path.filter((part) => typeof part === "string" || typeof part === "number") : [];
52
+ const message = typeof issue.message === "string" ? issue.message : "Invalid value";
53
+ return {
54
+ path: toPath(path),
55
+ message,
56
+ ...(typeof issue.expected === "string" ? { expected: issue.expected } : {}),
57
+ ...(typeof issue.received === "string" ? { received: issue.received } : {}),
58
+ };
59
+ })
60
+ .filter((issue) => issue !== null);
61
+ }
62
+ function validateJsonSchemaObject(schema, input) {
63
+ const issues = [];
64
+ const rootTypes = readJsonSchemaType(schema);
65
+ if (!typeMatches(input, rootTypes.length > 0 ? rootTypes : ["object"])) {
66
+ return [{
67
+ path: "$",
68
+ message: "Tool input must match the declared schema root type.",
69
+ expected: rootTypes.join("|") || "object",
70
+ received: Array.isArray(input) ? "array" : input === null ? "null" : typeof input,
71
+ }];
72
+ }
73
+ if (!isRecord(input)) {
74
+ return issues;
75
+ }
76
+ const properties = isRecord(schema.properties) ? schema.properties : {};
77
+ const required = Array.isArray(schema.required)
78
+ ? schema.required.filter((entry) => typeof entry === "string")
79
+ : [];
80
+ for (const key of required) {
81
+ if (!(key in input)) {
82
+ issues.push({
83
+ path: `$.${key}`,
84
+ message: "Required tool argument is missing.",
85
+ });
86
+ }
87
+ }
88
+ for (const [key, value] of Object.entries(input)) {
89
+ const fieldSchema = properties[key];
90
+ if (!fieldSchema) {
91
+ if (schema.additionalProperties === false) {
92
+ issues.push({
93
+ path: `$.${key}`,
94
+ message: "Unexpected tool argument.",
95
+ });
96
+ }
97
+ continue;
98
+ }
99
+ const fieldTypes = readJsonSchemaType(fieldSchema);
100
+ if (!typeMatches(value, fieldTypes)) {
101
+ issues.push({
102
+ path: `$.${key}`,
103
+ message: "Tool argument has the wrong type.",
104
+ ...(fieldTypes.length > 0 ? { expected: fieldTypes.join("|") } : {}),
105
+ received: Array.isArray(value) ? "array" : value === null ? "null" : typeof value,
106
+ });
107
+ }
108
+ if (isRecord(fieldSchema) && Array.isArray(fieldSchema.enum) && !fieldSchema.enum.includes(value)) {
109
+ issues.push({
110
+ path: `$.${key}`,
111
+ message: "Tool argument must be one of the declared enum values.",
112
+ });
113
+ }
114
+ }
115
+ return issues;
116
+ }
117
+ export function createInvalidToolArgumentsResult(input) {
118
+ return {
119
+ isError: true,
120
+ code: "INVALID_ARGUMENTS",
121
+ toolName: input.toolName,
122
+ message: `Tool ${input.toolName} received invalid arguments. Correct the arguments to match the declared schema before retrying.`,
123
+ retryable: !input.requiresApproval,
124
+ requiresApproval: input.requiresApproval,
125
+ validationErrors: input.issues.length > 0
126
+ ? input.issues
127
+ : [{ path: "$", message: "Tool input failed schema validation." }],
128
+ };
129
+ }
130
+ export function validateToolGatewayInput(input) {
131
+ const schema = input.schema;
132
+ const requiresApproval = input.requiresApproval === true;
133
+ if (schema && typeof schema.safeParse === "function") {
134
+ const parsed = schema.safeParse(input.args);
135
+ if (parsed.success) {
136
+ return { ok: true, input: parsed.data };
137
+ }
138
+ return {
139
+ ok: false,
140
+ error: createInvalidToolArgumentsResult({
141
+ toolName: input.toolName,
142
+ requiresApproval,
143
+ issues: zodIssues(parsed.error),
144
+ }),
145
+ };
146
+ }
147
+ if (schema && typeof schema.parse === "function") {
148
+ try {
149
+ return { ok: true, input: schema.parse(input.args) };
150
+ }
151
+ catch (error) {
152
+ return {
153
+ ok: false,
154
+ error: createInvalidToolArgumentsResult({
155
+ toolName: input.toolName,
156
+ requiresApproval,
157
+ issues: zodIssues(error),
158
+ }),
159
+ };
160
+ }
161
+ }
162
+ if (isRecord(input.schema) && (input.schema.type === "object" || isRecord(input.schema.properties))) {
163
+ const issues = validateJsonSchemaObject(input.schema, input.args);
164
+ if (issues.length > 0) {
165
+ return {
166
+ ok: false,
167
+ error: createInvalidToolArgumentsResult({
168
+ toolName: input.toolName,
169
+ requiresApproval,
170
+ issues,
171
+ }),
172
+ };
173
+ }
174
+ }
175
+ return { ok: true, input: input.args };
176
+ }