@juspay/neurolink 9.61.0 → 9.61.2

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.
@@ -314,7 +314,7 @@ export class BaseProvider {
314
314
  status: (tr.status === "error"
315
315
  ? "failure"
316
316
  : "success"),
317
- result: tr.result,
317
+ result: tr.output ?? tr.result,
318
318
  error: tr.error,
319
319
  }))
320
320
  : undefined,
@@ -441,7 +441,7 @@ export class GenerationHandler {
441
441
  toolExecutions.push({
442
442
  name: toolName,
443
443
  input: toolArgs,
444
- output: trRecord.result ?? "success",
444
+ output: (trRecord.output ?? trRecord.result) ?? "success",
445
445
  });
446
446
  }
447
447
  }
@@ -80,6 +80,7 @@ export declare class RedisConversationMemoryManager implements IConversationMemo
80
80
  [key: string]: unknown;
81
81
  }>, toolResults: Array<{
82
82
  toolCallId?: string;
83
+ output?: unknown;
83
84
  result?: unknown;
84
85
  error?: string;
85
86
  [key: string]: unknown;
@@ -1340,18 +1340,22 @@ User message: "${userMessage}"`;
1340
1340
  const toolCallId = String(toolResult.toolCallId || toolResult.id || "unknown");
1341
1341
  const toolName = toolCallMap.get(toolCallId) ||
1342
1342
  String(toolResult.toolName || "unknown");
1343
+ const toolResultRecord = toolResult;
1344
+ const selectedResultField = "output" in toolResultRecord ? "output" : "result";
1345
+ const toolResultValue = selectedResultField === "output"
1346
+ ? toolResultRecord.output
1347
+ : toolResult.result;
1343
1348
  // Serialize the tool result to string for content field
1344
1349
  let serializedResult;
1345
- if (typeof toolResult.result === "string") {
1346
- serializedResult = toolResult.result;
1350
+ if (typeof toolResultValue === "string") {
1351
+ serializedResult = toolResultValue;
1347
1352
  }
1348
- else if (toolResult.result === undefined ||
1349
- toolResult.result === null) {
1350
- serializedResult = String(toolResult.result ?? "null");
1353
+ else if (toolResultValue === undefined || toolResultValue === null) {
1354
+ serializedResult = String(toolResultValue ?? "null");
1351
1355
  }
1352
1356
  else {
1353
1357
  try {
1354
- serializedResult = JSON.stringify(toolResult.result, null, 2);
1358
+ serializedResult = JSON.stringify(toolResultValue, null, 2);
1355
1359
  }
1356
1360
  catch (serializeError) {
1357
1361
  serializedResult = `[Serialization failed: ${serializeError instanceof Error ? serializeError.message : String(serializeError)}]`;
@@ -1366,7 +1370,7 @@ User message: "${userMessage}"`;
1366
1370
  // The surrogate carries `_meta.neurolinkArtifactId` on the raw result object.
1367
1371
  let artifactId;
1368
1372
  try {
1369
- const rawResult = toolResult.result;
1373
+ const rawResult = toolResultValue;
1370
1374
  if (rawResult && typeof rawResult === "object") {
1371
1375
  const meta = rawResult._meta;
1372
1376
  if (meta && typeof meta === "object") {
@@ -314,7 +314,7 @@ export class BaseProvider {
314
314
  status: (tr.status === "error"
315
315
  ? "failure"
316
316
  : "success"),
317
- result: tr.result,
317
+ result: tr.output ?? tr.result,
318
318
  error: tr.error,
319
319
  }))
320
320
  : undefined,
@@ -441,7 +441,7 @@ export class GenerationHandler {
441
441
  toolExecutions.push({
442
442
  name: toolName,
443
443
  input: toolArgs,
444
- output: trRecord.result ?? "success",
444
+ output: (trRecord.output ?? trRecord.result) ?? "success",
445
445
  });
446
446
  }
447
447
  }
@@ -80,6 +80,7 @@ export declare class RedisConversationMemoryManager implements IConversationMemo
80
80
  [key: string]: unknown;
81
81
  }>, toolResults: Array<{
82
82
  toolCallId?: string;
83
+ output?: unknown;
83
84
  result?: unknown;
84
85
  error?: string;
85
86
  [key: string]: unknown;
@@ -1340,18 +1340,22 @@ User message: "${userMessage}"`;
1340
1340
  const toolCallId = String(toolResult.toolCallId || toolResult.id || "unknown");
1341
1341
  const toolName = toolCallMap.get(toolCallId) ||
1342
1342
  String(toolResult.toolName || "unknown");
1343
+ const toolResultRecord = toolResult;
1344
+ const selectedResultField = "output" in toolResultRecord ? "output" : "result";
1345
+ const toolResultValue = selectedResultField === "output"
1346
+ ? toolResultRecord.output
1347
+ : toolResult.result;
1343
1348
  // Serialize the tool result to string for content field
1344
1349
  let serializedResult;
1345
- if (typeof toolResult.result === "string") {
1346
- serializedResult = toolResult.result;
1350
+ if (typeof toolResultValue === "string") {
1351
+ serializedResult = toolResultValue;
1347
1352
  }
1348
- else if (toolResult.result === undefined ||
1349
- toolResult.result === null) {
1350
- serializedResult = String(toolResult.result ?? "null");
1353
+ else if (toolResultValue === undefined || toolResultValue === null) {
1354
+ serializedResult = String(toolResultValue ?? "null");
1351
1355
  }
1352
1356
  else {
1353
1357
  try {
1354
- serializedResult = JSON.stringify(toolResult.result, null, 2);
1358
+ serializedResult = JSON.stringify(toolResultValue, null, 2);
1355
1359
  }
1356
1360
  catch (serializeError) {
1357
1361
  serializedResult = `[Serialization failed: ${serializeError instanceof Error ? serializeError.message : String(serializeError)}]`;
@@ -1366,7 +1370,7 @@ User message: "${userMessage}"`;
1366
1370
  // The surrogate carries `_meta.neurolinkArtifactId` on the raw result object.
1367
1371
  let artifactId;
1368
1372
  try {
1369
- const rawResult = toolResult.result;
1373
+ const rawResult = toolResultValue;
1370
1374
  if (rawResult && typeof rawResult === "object") {
1371
1375
  const meta = rawResult._meta;
1372
1376
  if (meta && typeof meta === "object") {
@@ -301,12 +301,18 @@ export class MCPToolRegistry extends MCPRegistry {
301
301
  span.setAttribute("tool.arguments_size", argsStr.length);
302
302
  // HITL Safety Check: Request confirmation if required
303
303
  let finalArgs = args;
304
- if (this.hitlManager && this.hitlManager.isEnabled()) {
304
+ const HITLState = context?.hitlState;
305
+ if (!HITLState?.triggered &&
306
+ this.hitlManager &&
307
+ this.hitlManager.isEnabled()) {
305
308
  const requiresConfirmation = this.hitlManager.requiresConfirmation(toolName, args);
306
309
  if (requiresConfirmation) {
307
310
  registryLogger.info(`Tool '${toolName}' requires HITL confirmation`);
308
311
  span.addEvent("tool.hitl_requested");
309
312
  try {
313
+ if (HITLState) {
314
+ HITLState.triggered = true;
315
+ }
310
316
  const confirmationResult = await this.hitlManager.requestConfirmation(toolName, args, {
311
317
  serverId: tool.serverId,
312
318
  sessionId: execContext.sessionId,
@@ -7199,6 +7199,7 @@ Current user's request: ${currentInput}`;
7199
7199
  inputSize: inputStr.length,
7200
7200
  truncatedInput: inputStr.length > 2048 ? inputStr.substring(0, 2048) : inputStr,
7201
7201
  options,
7202
+ hitlState: { triggered: false },
7202
7203
  };
7203
7204
  }
7204
7205
  async executeToolWithSpan(toolName, params, options, executionContext, toolSpan) {
@@ -7305,7 +7306,7 @@ Current user's request: ${currentInput}`;
7305
7306
  circuitBreakerState: prepared.circuitBreaker.getState(),
7306
7307
  });
7307
7308
  const result = await prepared.circuitBreaker.execute(async () => {
7308
- return withRetry(async () => withTimeout(this.executeToolInternal(toolName, params, prepared.finalOptions), prepared.finalOptions.timeout, ErrorFactory.toolTimeout(toolName, prepared.finalOptions.timeout)), {
7309
+ return withRetry(async () => withTimeout(this.executeToolInternal(toolName, params, prepared.finalOptions, executionContext.hitlState), prepared.finalOptions.timeout, ErrorFactory.toolTimeout(toolName, prepared.finalOptions.timeout)), {
7309
7310
  maxAttempts: prepared.finalOptions.maxRetries + 1,
7310
7311
  delayMs: prepared.finalOptions.retryDelayMs,
7311
7312
  isRetriable: isRetriableError,
@@ -7526,7 +7527,7 @@ Current user's request: ${currentInput}`;
7526
7527
  * - Annotations: skip cache for destructive tools, retry safe tools on failure
7527
7528
  * - Middleware: apply global middleware chain before execution
7528
7529
  */
7529
- async executeToolInternal(toolName, params, options) {
7530
+ async executeToolInternal(toolName, params, options, HITLState) {
7530
7531
  const functionTag = "NeuroLink.executeToolInternal";
7531
7532
  // === MCP ENHANCEMENT: Infer annotations for cache/retry decisions ===
7532
7533
  const toolAnnotations = this.getToolAnnotationsForExecution(toolName);
@@ -7645,6 +7646,7 @@ Current user's request: ${currentInput}`;
7645
7646
  const context = {
7646
7647
  ...storedContext,
7647
7648
  ...passedAuthContext,
7649
+ hitlState: HITLState,
7648
7650
  };
7649
7651
  logger.debug(`[Using merged context for unified registry tool:`, {
7650
7652
  toolName,
@@ -232,3 +232,6 @@ export type HITLManager = {
232
232
  on(event: string, listener: (...args: unknown[]) => void): HITLManager;
233
233
  emit(event: string, ...args: unknown[]): boolean;
234
234
  };
235
+ export type HITLExecutionState = {
236
+ triggered: boolean;
237
+ };
@@ -9,6 +9,7 @@ import type { StandardRecord, StringArray, ZodUnknownSchema } from "./aliases.js
9
9
  import type { ValidationError } from "../utils/parameterValidation.js";
10
10
  import type { MCPToolAnnotations } from "./mcp.js";
11
11
  import type { Logger } from "./utilities.js";
12
+ import type { HITLExecutionState } from "./hitl.js";
12
13
  /**
13
14
  * Commonly used Zod schema type aliases for cleaner type declarations
14
15
  */
@@ -48,6 +49,7 @@ export type ExecutionContext<T = StandardRecord> = {
48
49
  timeoutMs?: number;
49
50
  maxRetries?: number;
50
51
  startTime?: number;
52
+ hitlState?: HITLExecutionState;
51
53
  };
52
54
  /**
53
55
  * Cache configuration options
@@ -439,6 +441,7 @@ export type PendingToolExecution = {
439
441
  toolResults: Array<{
440
442
  toolCallId?: string;
441
443
  toolName?: string;
444
+ output?: unknown;
442
445
  result?: unknown;
443
446
  error?: string;
444
447
  timestamp?: Date;
@@ -5,6 +5,7 @@
5
5
  import { ErrorCategory, ErrorSeverity } from "../constants/enums.js";
6
6
  import { logger } from "./logger.js";
7
7
  import { CircuitBreakerOpenError } from "../types/index.js";
8
+ import { HITLTimeoutError } from "../hitl/hitlErrors.js";
8
9
  // Error codes for different scenarios
9
10
  export const ERROR_CODES = {
10
11
  // Tool errors
@@ -950,6 +951,9 @@ export function isRetriableError(error) {
950
951
  if (error instanceof NeuroLinkError) {
951
952
  return error.retriable;
952
953
  }
954
+ if (error instanceof HITLTimeoutError) {
955
+ return false;
956
+ }
953
957
  // Check for common retriable error patterns
954
958
  const retriablePatterns = [
955
959
  /timeout/i,
@@ -8,16 +8,24 @@ import type { ZodUnknownSchema } from "../types/index.js";
8
8
  * - Top-level $ref resolution
9
9
  * - Nested $ref within properties, items, additionalProperties
10
10
  * - $ref within allOf, anyOf, oneOf arrays
11
+ * - Deep $ref paths like "#/definitions/Foo/properties/bar"
11
12
  * - Circular reference detection to prevent infinite loops
12
13
  */
13
- export declare function inlineJsonSchema(schema: Record<string, unknown>, definitions?: Record<string, Record<string, unknown>>, visited?: Set<string>): Record<string, unknown>;
14
+ export declare function inlineJsonSchema(schema: Record<string, unknown>, definitions?: Record<string, Record<string, unknown>>, visited?: Set<string>, rootSchema?: Record<string, unknown>): Record<string, unknown>;
15
+ /**
16
+ * Recursively ensure all nested schemas have a type field.
17
+ * Google Vertex AI requires ALL schema objects (including nested properties) to have a type field.
18
+ * This function walks through the schema tree and adds type:"object" to any object-like schema
19
+ * that's missing its type field.
20
+ */
21
+ export declare function ensureNestedSchemaTypes(schema: Record<string, unknown>): Record<string, unknown>;
14
22
  /**
15
23
  * Convert Zod schema to JSON Schema format for provider APIs.
16
24
  *
17
25
  * Handles three input types:
18
- * 1. Zod schemas (have `_def.typeName`) converted via zod-to-json-schema
19
- * 2. AI SDK `jsonSchema()` wrappers (have `.jsonSchema` property) extracted directly
20
- * 3. Plain JSON Schema objects (have `type`/`properties` but no `_def`) returned as-is
26
+ * 1. Zod schemas (have `_def.typeName`) -- converted via zod-to-json-schema
27
+ * 2. AI SDK `jsonSchema()` wrappers (have `.jsonSchema` property) -- extracted directly
28
+ * 3. Plain JSON Schema objects (have `type`/`properties` but no `_def`) -- returned as-is
21
29
  */
22
30
  export declare function convertZodToJsonSchema(zodSchema: ZodUnknownSchema): object;
23
31
  export declare function normalizeJsonSchemaObject(schema: Record<string, unknown> | undefined | null): Record<string, unknown>;
@@ -2,6 +2,53 @@ import { zodToJsonSchema } from "zod-to-json-schema";
2
2
  import { jsonSchemaToZod } from "json-schema-to-zod";
3
3
  import { z } from "zod";
4
4
  import { logger } from "./logger.js";
5
+ /**
6
+ * Resolve a deep JSON pointer path within a schema.
7
+ * Handles paths like "#/definitions/ToolParameters/properties/foo/properties/bar"
8
+ *
9
+ * Implements RFC 6901 token decoding so property names containing the literal
10
+ * characters "/" or "~" can still be resolved (their escaped forms are "~1"
11
+ * and "~0" respectively). Because "#/..." is the URI-fragment form of a JSON
12
+ * Pointer (RFC 6901 §6), each segment may also be percent-encoded; we decode
13
+ * that first.
14
+ *
15
+ * Order matters: percent-decode → "~1" → "/" → "~0" → "~". Reversing the
16
+ * tilde steps would let "~01" round-trip to "/" instead of the intended "~1".
17
+ */
18
+ function resolveDeepRef(rootSchema, refPath) {
19
+ // Strip the leading "#/" then split + decode each segment per RFC 6901
20
+ const pathParts = refPath
21
+ .replace(/^#\//, "")
22
+ .split("/")
23
+ .map((seg) => safePercentDecode(seg).replace(/~1/g, "/").replace(/~0/g, "~"));
24
+ let current = rootSchema;
25
+ for (const part of pathParts) {
26
+ if (current && typeof current === "object" && part in current) {
27
+ current = current[part];
28
+ }
29
+ else {
30
+ return undefined;
31
+ }
32
+ }
33
+ if (current && typeof current === "object") {
34
+ return current;
35
+ }
36
+ return undefined;
37
+ }
38
+ /**
39
+ * Percent-decode a JSON Pointer segment defensively. Falls back to the raw
40
+ * segment if the input contains a malformed escape sequence (decodeURIComponent
41
+ * throws URIError on those) — better to attempt a literal match than fail the
42
+ * whole resolution.
43
+ */
44
+ function safePercentDecode(segment) {
45
+ try {
46
+ return decodeURIComponent(segment);
47
+ }
48
+ catch {
49
+ return segment;
50
+ }
51
+ }
5
52
  /**
6
53
  * Inline a JSON Schema by recursively resolving all $ref references.
7
54
  * zodToJsonSchema with 'name' option produces schemas with $ref pointing to definitions.
@@ -11,29 +58,45 @@ import { logger } from "./logger.js";
11
58
  * - Top-level $ref resolution
12
59
  * - Nested $ref within properties, items, additionalProperties
13
60
  * - $ref within allOf, anyOf, oneOf arrays
61
+ * - Deep $ref paths like "#/definitions/Foo/properties/bar"
14
62
  * - Circular reference detection to prevent infinite loops
15
63
  */
16
- export function inlineJsonSchema(schema, definitions, visited = new Set()) {
64
+ export function inlineJsonSchema(schema, definitions, visited = new Set(), rootSchema) {
17
65
  // Use definitions from schema if not provided
18
66
  const defs = definitions ||
19
67
  schema.definitions;
68
+ // Keep track of the root schema for deep ref resolution
69
+ const root = rootSchema || schema;
20
70
  // Handle $ref at current level
21
- if (typeof schema.$ref === "string" &&
22
- schema.$ref.startsWith("#/definitions/")) {
23
- const defName = schema.$ref.replace("#/definitions/", "");
71
+ if (typeof schema.$ref === "string" && schema.$ref.startsWith("#/")) {
72
+ const refPath = schema.$ref;
24
73
  // Prevent circular reference infinite loops
25
- if (visited.has(defName)) {
26
- logger.debug(`[SCHEMA-INLINE] Circular reference detected for: ${defName}`);
74
+ if (visited.has(refPath)) {
75
+ logger.debug(`[SCHEMA-INLINE] Circular reference detected for: ${refPath}`);
27
76
  // Return a simple object placeholder for circular refs
28
77
  return { type: "object" };
29
78
  }
30
- if (defs && defs[defName]) {
31
- visited.add(defName);
32
- // Recursively inline the resolved definition
33
- const resolved = inlineJsonSchema({ ...defs[defName] }, defs, visited);
34
- visited.delete(defName);
35
- return resolved;
79
+ // Try simple definition lookup first (for #/definitions/SomeName)
80
+ if (refPath.startsWith("#/definitions/")) {
81
+ const defName = refPath.replace("#/definitions/", "");
82
+ // Check if it's a simple definition name (no slashes after definitions/)
83
+ if (!defName.includes("/") && defs && defs[defName]) {
84
+ visited.add(refPath);
85
+ const resolved = inlineJsonSchema({ ...defs[defName] }, defs, visited, root);
86
+ visited.delete(refPath);
87
+ return resolved;
88
+ }
89
+ }
90
+ // Try deep path resolution for complex paths like
91
+ // #/definitions/ToolParameters/properties/accountPerformance/properties/roas
92
+ const resolved = resolveDeepRef(root, refPath);
93
+ if (resolved) {
94
+ visited.add(refPath);
95
+ const inlined = inlineJsonSchema({ ...resolved }, defs, visited, root);
96
+ visited.delete(refPath);
97
+ return inlined;
36
98
  }
99
+ logger.debug(`[SCHEMA-INLINE] Could not resolve $ref: ${refPath}`);
37
100
  }
38
101
  // Create result without $ref and definitions
39
102
  const result = {};
@@ -47,7 +110,7 @@ export function inlineJsonSchema(schema, definitions, visited = new Set()) {
47
110
  const properties = {};
48
111
  for (const [propName, propSchema] of Object.entries(value)) {
49
112
  if (propSchema && typeof propSchema === "object") {
50
- properties[propName] = inlineJsonSchema(propSchema, defs, visited);
113
+ properties[propName] = inlineJsonSchema(propSchema, defs, visited, root);
51
114
  }
52
115
  else {
53
116
  properties[propName] = propSchema;
@@ -59,32 +122,32 @@ export function inlineJsonSchema(schema, definitions, visited = new Set()) {
59
122
  // Handle array items schema
60
123
  if (Array.isArray(value)) {
61
124
  result[key] = value.map((item) => item && typeof item === "object"
62
- ? inlineJsonSchema(item, defs, visited)
125
+ ? inlineJsonSchema(item, defs, visited, root)
63
126
  : item);
64
127
  }
65
128
  else {
66
- result[key] = inlineJsonSchema(value, defs, visited);
129
+ result[key] = inlineJsonSchema(value, defs, visited, root);
67
130
  }
68
131
  }
69
132
  else if (key === "additionalProperties" &&
70
133
  value &&
71
134
  typeof value === "object") {
72
- result[key] = inlineJsonSchema(value, defs, visited);
135
+ result[key] = inlineJsonSchema(value, defs, visited, root);
73
136
  }
74
137
  else if ((key === "allOf" || key === "anyOf" || key === "oneOf") &&
75
138
  Array.isArray(value)) {
76
139
  // Handle composition schemas
77
140
  result[key] = value.map((item) => item && typeof item === "object"
78
- ? inlineJsonSchema(item, defs, visited)
141
+ ? inlineJsonSchema(item, defs, visited, root)
79
142
  : item);
80
143
  }
81
144
  else if (key === "not" && value && typeof value === "object") {
82
- result[key] = inlineJsonSchema(value, defs, visited);
145
+ result[key] = inlineJsonSchema(value, defs, visited, root);
83
146
  }
84
147
  else if ((key === "if" || key === "then" || key === "else") &&
85
148
  value &&
86
149
  typeof value === "object") {
87
- result[key] = inlineJsonSchema(value, defs, visited);
150
+ result[key] = inlineJsonSchema(value, defs, visited, root);
88
151
  }
89
152
  else {
90
153
  result[key] = value;
@@ -92,13 +155,129 @@ export function inlineJsonSchema(schema, definitions, visited = new Set()) {
92
155
  }
93
156
  return result;
94
157
  }
158
+ /**
159
+ * Recursively ensure all nested schemas have a type field.
160
+ * Google Vertex AI requires ALL schema objects (including nested properties) to have a type field.
161
+ * This function walks through the schema tree and adds type:"object" to any object-like schema
162
+ * that's missing its type field.
163
+ */
164
+ export function ensureNestedSchemaTypes(schema) {
165
+ if (!schema || typeof schema !== "object") {
166
+ return {};
167
+ }
168
+ let result = { ...schema };
169
+ // CRITICAL FIX: Flatten single-item allOf for Google Vertex AI compatibility
170
+ // When we have { allOf: [{ type: "object", ... }], nullable: true }, flatten it to:
171
+ // { type: "object", ..., nullable: true }
172
+ if (result.allOf &&
173
+ Array.isArray(result.allOf) &&
174
+ result.allOf.length === 1 &&
175
+ result.allOf[0] &&
176
+ typeof result.allOf[0] === "object") {
177
+ const innerSchema = result.allOf[0];
178
+ // Only flatten if inner schema has meaningful content (type, properties, items, etc.)
179
+ if (innerSchema.type ||
180
+ innerSchema.properties ||
181
+ innerSchema.items ||
182
+ innerSchema.enum) {
183
+ logger.debug(`[SCHEMA-TYPE-FIX] Flattening single-item allOf with type: ${innerSchema.type}`);
184
+ // Merge: inner schema properties take precedence, except for wrapper's metadata
185
+ const { allOf: _ignored, ...wrapperProps } = result;
186
+ result = {
187
+ ...innerSchema,
188
+ ...wrapperProps, // Keep wrapper's nullable, description, etc.
189
+ };
190
+ // If inner schema had its own nullable/description, restore them
191
+ if (innerSchema.description && !wrapperProps.description) {
192
+ result.description = innerSchema.description;
193
+ }
194
+ }
195
+ }
196
+ // Infer type from structure if missing
197
+ if (!result.type) {
198
+ // If it has properties, it's an object
199
+ if (result.properties) {
200
+ result.type = "object";
201
+ logger.debug(`[SCHEMA-TYPE-FIX] Added type:"object" to schema with properties`);
202
+ }
203
+ // If it has items, it's an array
204
+ else if (result.items) {
205
+ result.type = "array";
206
+ logger.debug(`[SCHEMA-TYPE-FIX] Added type:"array" to schema with items`);
207
+ }
208
+ // If it has enum, infer from enum values — but only when ALL elements
209
+ // share the same primitive type. A mixed enum like [1, "x"] would
210
+ // otherwise be silently narrowed to whatever the first element is.
211
+ else if (result.enum &&
212
+ Array.isArray(result.enum) &&
213
+ result.enum.length > 0) {
214
+ if (result.enum.every((v) => typeof v === "string")) {
215
+ result.type = "string";
216
+ logger.debug(`[SCHEMA-TYPE-FIX] Added type:"string" to schema with enum`);
217
+ }
218
+ else if (result.enum.every((v) => typeof v === "number")) {
219
+ result.type = "number";
220
+ logger.debug(`[SCHEMA-TYPE-FIX] Added type:"number" to schema with enum`);
221
+ }
222
+ // Mixed-type enum: leave result.type unset rather than narrow it.
223
+ }
224
+ // If it has allOf with typed schemas, infer from first item
225
+ else if (result.allOf &&
226
+ Array.isArray(result.allOf) &&
227
+ result.allOf.length > 0) {
228
+ const firstItem = result.allOf[0];
229
+ if (firstItem && firstItem.type) {
230
+ result.type = firstItem.type;
231
+ logger.debug(`[SCHEMA-TYPE-FIX] Inferred type from allOf: ${result.type}`);
232
+ }
233
+ }
234
+ }
235
+ // Recursively process properties
236
+ if (result.properties && typeof result.properties === "object") {
237
+ const properties = {};
238
+ for (const [propName, propSchema] of Object.entries(result.properties)) {
239
+ if (propSchema && typeof propSchema === "object") {
240
+ properties[propName] = ensureNestedSchemaTypes(propSchema);
241
+ }
242
+ else {
243
+ properties[propName] = propSchema;
244
+ }
245
+ }
246
+ result.properties = properties;
247
+ }
248
+ // Recursively process items (for arrays)
249
+ if (result.items && typeof result.items === "object") {
250
+ if (Array.isArray(result.items)) {
251
+ result.items = result.items.map((item) => item && typeof item === "object"
252
+ ? ensureNestedSchemaTypes(item)
253
+ : item);
254
+ }
255
+ else {
256
+ result.items = ensureNestedSchemaTypes(result.items);
257
+ }
258
+ }
259
+ // Recursively process additionalProperties
260
+ if (result.additionalProperties &&
261
+ typeof result.additionalProperties === "object") {
262
+ result.additionalProperties = ensureNestedSchemaTypes(result.additionalProperties);
263
+ }
264
+ // Recursively process allOf, anyOf, oneOf
265
+ for (const key of ["allOf", "anyOf", "oneOf"]) {
266
+ if (result[key] && Array.isArray(result[key])) {
267
+ result[key] = result[key].map((item) => item && typeof item === "object"
268
+ ? ensureNestedSchemaTypes(item)
269
+ : item);
270
+ }
271
+ }
272
+ return result;
273
+ }
95
274
  /**
96
275
  * Convert Zod schema to JSON Schema format for provider APIs.
97
276
  *
98
277
  * Handles three input types:
99
- * 1. Zod schemas (have `_def.typeName`) converted via zod-to-json-schema
100
- * 2. AI SDK `jsonSchema()` wrappers (have `.jsonSchema` property) extracted directly
101
- * 3. Plain JSON Schema objects (have `type`/`properties` but no `_def`) returned as-is
278
+ * 1. Zod schemas (have `_def.typeName`) -- converted via zod-to-json-schema
279
+ * 2. AI SDK `jsonSchema()` wrappers (have `.jsonSchema` property) -- extracted directly
280
+ * 3. Plain JSON Schema objects (have `type`/`properties` but no `_def`) -- returned as-is
102
281
  */
103
282
  export function convertZodToJsonSchema(zodSchema) {
104
283
  const schema = zodSchema;
@@ -110,11 +289,11 @@ export function convertZodToJsonSchema(zodSchema) {
110
289
  schema.jsonSchema !== null &&
111
290
  typeof schema.jsonSchema === "object") {
112
291
  const extracted = schema.jsonSchema;
113
- return ensureTypeField(extracted);
292
+ return ensureNestedSchemaTypes(ensureTypeField(extracted));
114
293
  }
115
294
  // Plain JSON Schema object (from external MCP tools) — no Zod internals
116
295
  if (!isZodSchema(schema)) {
117
- return ensureTypeField(schema);
296
+ return ensureNestedSchemaTypes(ensureTypeField(schema));
118
297
  }
119
298
  // Actual Zod schema — convert via zod-to-json-schema
120
299
  try {
@@ -123,13 +302,15 @@ export function convertZodToJsonSchema(zodSchema) {
123
302
  const zodV3Schema = zodSchema;
124
303
  const jsonSchema = zodToJsonSchema(zodV3Schema, {
125
304
  name: "ToolParameters",
126
- target: "jsonSchema7",
305
+ target: "openApi3", // Use OpenAPI 3.0 for nullable: true instead of anyOf with null (required for Vertex AI)
127
306
  errorMessages: true,
128
307
  });
129
308
  // zodToJsonSchema with 'name' produces { $ref: "#/definitions/ToolParameters", definitions: {...} }
130
- // Inline the $ref to produce a flat schema before passing to ensureTypeField
309
+ // Inline the $ref to produce a flat schema, ensure the root has a type
310
+ // field, then walk the tree so nested objects/arrays/additionalProperties
311
+ // also pick up an inferred type (Vertex/Gemini require it everywhere).
131
312
  const inlined = inlineJsonSchema(jsonSchema);
132
- return ensureTypeField(inlined);
313
+ return ensureNestedSchemaTypes(ensureTypeField(inlined));
133
314
  }
134
315
  catch (error) {
135
316
  logger.warn("Failed to convert Zod schema to JSON Schema", {
@@ -301,12 +301,18 @@ export class MCPToolRegistry extends MCPRegistry {
301
301
  span.setAttribute("tool.arguments_size", argsStr.length);
302
302
  // HITL Safety Check: Request confirmation if required
303
303
  let finalArgs = args;
304
- if (this.hitlManager && this.hitlManager.isEnabled()) {
304
+ const HITLState = context?.hitlState;
305
+ if (!HITLState?.triggered &&
306
+ this.hitlManager &&
307
+ this.hitlManager.isEnabled()) {
305
308
  const requiresConfirmation = this.hitlManager.requiresConfirmation(toolName, args);
306
309
  if (requiresConfirmation) {
307
310
  registryLogger.info(`Tool '${toolName}' requires HITL confirmation`);
308
311
  span.addEvent("tool.hitl_requested");
309
312
  try {
313
+ if (HITLState) {
314
+ HITLState.triggered = true;
315
+ }
310
316
  const confirmationResult = await this.hitlManager.requestConfirmation(toolName, args, {
311
317
  serverId: tool.serverId,
312
318
  sessionId: execContext.sessionId,
package/dist/neurolink.js CHANGED
@@ -7199,6 +7199,7 @@ Current user's request: ${currentInput}`;
7199
7199
  inputSize: inputStr.length,
7200
7200
  truncatedInput: inputStr.length > 2048 ? inputStr.substring(0, 2048) : inputStr,
7201
7201
  options,
7202
+ hitlState: { triggered: false },
7202
7203
  };
7203
7204
  }
7204
7205
  async executeToolWithSpan(toolName, params, options, executionContext, toolSpan) {
@@ -7305,7 +7306,7 @@ Current user's request: ${currentInput}`;
7305
7306
  circuitBreakerState: prepared.circuitBreaker.getState(),
7306
7307
  });
7307
7308
  const result = await prepared.circuitBreaker.execute(async () => {
7308
- return withRetry(async () => withTimeout(this.executeToolInternal(toolName, params, prepared.finalOptions), prepared.finalOptions.timeout, ErrorFactory.toolTimeout(toolName, prepared.finalOptions.timeout)), {
7309
+ return withRetry(async () => withTimeout(this.executeToolInternal(toolName, params, prepared.finalOptions, executionContext.hitlState), prepared.finalOptions.timeout, ErrorFactory.toolTimeout(toolName, prepared.finalOptions.timeout)), {
7309
7310
  maxAttempts: prepared.finalOptions.maxRetries + 1,
7310
7311
  delayMs: prepared.finalOptions.retryDelayMs,
7311
7312
  isRetriable: isRetriableError,
@@ -7526,7 +7527,7 @@ Current user's request: ${currentInput}`;
7526
7527
  * - Annotations: skip cache for destructive tools, retry safe tools on failure
7527
7528
  * - Middleware: apply global middleware chain before execution
7528
7529
  */
7529
- async executeToolInternal(toolName, params, options) {
7530
+ async executeToolInternal(toolName, params, options, HITLState) {
7530
7531
  const functionTag = "NeuroLink.executeToolInternal";
7531
7532
  // === MCP ENHANCEMENT: Infer annotations for cache/retry decisions ===
7532
7533
  const toolAnnotations = this.getToolAnnotationsForExecution(toolName);
@@ -7645,6 +7646,7 @@ Current user's request: ${currentInput}`;
7645
7646
  const context = {
7646
7647
  ...storedContext,
7647
7648
  ...passedAuthContext,
7649
+ hitlState: HITLState,
7648
7650
  };
7649
7651
  logger.debug(`[Using merged context for unified registry tool:`, {
7650
7652
  toolName,