@juspay/neurolink 13.5.1 → 13.6.1

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.
@@ -152,7 +152,9 @@ export declare abstract class BaseProvider implements AIProvider {
152
152
  private sortToolRecord;
153
153
  /**
154
154
  * On-demand discovery (`tools.discovery: true`): defer external MCP tool
155
- * schemas behind one `search_tools` meta-tool. Built-in tools, per-call
155
+ * schemas — or, with `tools.discoveryScope: "all"`, every non-built-in
156
+ * tool — behind one `search_tools` meta-tool, except names matching
157
+ * `tools.discoveryAlwaysVisible`. Built-in tools, per-call
156
158
  * tools, explicitly whitelisted tools, and session-pinned (previously
157
159
  * discovered) tools always stay hot. No-op when discovery is off — with a
158
160
  * one-time WARN when the catalog is large enough that selection accuracy
@@ -1246,7 +1246,9 @@ export class BaseProvider {
1246
1246
  }
1247
1247
  /**
1248
1248
  * On-demand discovery (`tools.discovery: true`): defer external MCP tool
1249
- * schemas behind one `search_tools` meta-tool. Built-in tools, per-call
1249
+ * schemas — or, with `tools.discoveryScope: "all"`, every non-built-in
1250
+ * tool — behind one `search_tools` meta-tool, except names matching
1251
+ * `tools.discoveryAlwaysVisible`. Built-in tools, per-call
1250
1252
  * tools, explicitly whitelisted tools, and session-pinned (previously
1251
1253
  * discovered) tools always stay hot. No-op when discovery is off — with a
1252
1254
  * one-time WARN when the catalog is large enough that selection accuracy
@@ -1273,8 +1275,13 @@ export class BaseProvider {
1273
1275
  }
1274
1276
  return tools;
1275
1277
  }
1276
- const externalTools = this.neurolink?.getExternalMCPTools() ?? [];
1277
- if (externalTools.length === 0) {
1278
+ // "external" defers only attached MCP servers' tools; "all" defers every
1279
+ // tool in the record except the built-ins.
1280
+ const builtinNames = new Set(Object.keys(this.directTools ?? {}));
1281
+ const candidateNames = policy.discoveryScope === "all"
1282
+ ? Object.keys(tools).filter((name) => !builtinNames.has(name))
1283
+ : (this.neurolink?.getExternalMCPTools() ?? []).map((t) => t.name);
1284
+ if (candidateNames.length === 0) {
1278
1285
  return tools;
1279
1286
  }
1280
1287
  // Session pinning requires a caller-provided sessionId. Without one there
@@ -1305,12 +1312,15 @@ export class BaseProvider {
1305
1312
  explicitPatterns.push(toolChoice.toolName);
1306
1313
  }
1307
1314
  const explicitMatcher = explicitPatterns.length > 0 ? toolNameMatcher(explicitPatterns) : null;
1308
- const deferrableNames = externalTools
1309
- .map((t) => t.name)
1310
- .filter((name) => name in tools &&
1315
+ const alwaysVisiblePatterns = policy.discoveryAlwaysVisible ?? [];
1316
+ const alwaysVisibleMatcher = alwaysVisiblePatterns.length > 0
1317
+ ? toolNameMatcher(alwaysVisiblePatterns)
1318
+ : null;
1319
+ const deferrableNames = candidateNames.filter((name) => name in tools &&
1311
1320
  !perCallNames.has(name) &&
1312
- !(explicitMatcher ? explicitMatcher(name) : false));
1313
- return partitionToolsForDiscovery(tools, {
1321
+ !(explicitMatcher ? explicitMatcher(name) : false) &&
1322
+ !(alwaysVisibleMatcher ? alwaysVisibleMatcher(name) : false));
1323
+ const partitioned = partitionToolsForDiscovery(tools, {
1314
1324
  deferrableNames,
1315
1325
  pinnedNames,
1316
1326
  onHydrate: (names) => {
@@ -1319,6 +1329,16 @@ export class BaseProvider {
1319
1329
  }
1320
1330
  },
1321
1331
  });
1332
+ // The meta-tool is built after the event wrappers ran, so without this
1333
+ // a search_tools call never reaches tool:start / tool:end listeners.
1334
+ const metaTool = partitioned["search_tools"];
1335
+ if (isDiscoveryMetaTool(metaTool)) {
1336
+ partitioned["search_tools"] =
1337
+ this.toolsManager.wrapExternalToolsWithEvents({
1338
+ search_tools: metaTool,
1339
+ })["search_tools"];
1340
+ }
1341
+ return partitioned;
1322
1342
  }
1323
1343
  static warnedLargeCatalog = false;
1324
1344
  warnLargeCatalogOnce(toolCount) {
@@ -6,6 +6,7 @@ import { logger } from "../../utils/logger.js";
6
6
  import { TOOL_EVENTS_WRAPPED } from "../../types/index.js";
7
7
  import { getKeyCount } from "../../utils/transformationUtils.js";
8
8
  import { extractIsErrorText } from "../../utils/toolResultStatus.js";
9
+ import { createToolExecutionId } from "../../utils/toolExecutionId.js";
9
10
  import { convertJsonSchemaToZod } from "../../utils/schemaConversion.js";
10
11
  import { generateToolOutputPreview, DEFAULT_MAX_PREVIEW_BYTES, RETRIEVE_CONTEXT_TOOL_NAME, } from "../../context/toolOutputLimits.js";
11
12
  /**
@@ -512,6 +513,12 @@ export class ToolsManager {
512
513
  const idField = typeof toolCallId === "string" && toolCallId.length > 0
513
514
  ? { toolCallId }
514
515
  : {};
516
+ // Same per-invocation id executeTool gives registered tools, so a
517
+ // listener can pair this start with its end.
518
+ const eventIds = {
519
+ ...idField,
520
+ executionId: createToolExecutionId(toolName),
521
+ };
515
522
  return withSpan({
516
523
  name: toolSpanName(toolName),
517
524
  tracer: tracers.mcp,
@@ -521,7 +528,7 @@ export class ToolsManager {
521
528
  const startTime = Date.now();
522
529
  this.emitToolEvent("tool:start", toolName, {
523
530
  input: params,
524
- ...idField,
531
+ ...eventIds,
525
532
  });
526
533
  let result;
527
534
  try {
@@ -533,7 +540,7 @@ export class ToolsManager {
533
540
  error: errorMsg,
534
541
  success: false,
535
542
  responseTime: Date.now() - startTime,
536
- ...idField,
543
+ ...eventIds,
537
544
  });
538
545
  throw error;
539
546
  }
@@ -549,7 +556,7 @@ export class ToolsManager {
549
556
  success: isErrorText === undefined,
550
557
  ...(isErrorText !== undefined ? { error: isErrorText } : {}),
551
558
  responseTime: Date.now() - startTime,
552
- ...idField,
559
+ ...eventIds,
553
560
  });
554
561
  return result;
555
562
  });
@@ -84,6 +84,8 @@ export declare const toPrepareStepRecord: (step: {
84
84
  export declare const hasNativeDoGenerate: (value: unknown) => value is {
85
85
  doGenerate: (options: Record<string, unknown>) => Promise<Record<string, unknown>>;
86
86
  };
87
+ /** A tool's inputSchema with its lazy (function) form, if any, resolved. */
88
+ export declare const resolveToolInputSchema: (schema: Tool["inputSchema"]) => Exclude<Tool["inputSchema"], (...args: never[]) => unknown>;
87
89
  /**
88
90
  * Spell a JSON Schema into the conversation's system turn.
89
91
  *
@@ -17,6 +17,7 @@ import { observeGeneration } from "../telemetry/observation.js";
17
17
  import { StepSpans, withGeneration } from "../telemetry/withSpan.js";
18
18
  import { logger } from "../utils/logger.js";
19
19
  import { guardToolExecutor } from "./toolExecutionGuards.js";
20
+ import { resolveLiveTool } from "../tools/toolDiscovery.js";
20
21
  const DEFAULT_TOOL_CHOICE_STEPS = 1;
21
22
  /**
22
23
  * A choice that compels a tool call: `"required"` or a named tool, in either
@@ -74,9 +75,9 @@ const warnUndeclaredForcedTool = (toolName, step, source) => {
74
75
  };
75
76
  /**
76
77
  * The names this request actually carries, in the record shape
77
- * `declaredTools` takes. The loop sends `args.tools`, built once by the
78
- * caller; `args.toolsRecord` can hold more (discovery hydrates into it, and
79
- * this loop never re-declares), so only `tools` answers "is it offered".
78
+ * `declaredTools` takes. `args.toolsRecord` can hold more than is declared
79
+ * (discovery hydrates into it), so only the sent declarations answer "is it
80
+ * offered".
80
81
  * Null prototype: a tool may be named `__proto__` or `constructor`.
81
82
  */
82
83
  const declaredToolRecord = (tools) => {
@@ -336,6 +337,8 @@ const parseToolInput = (raw) => {
336
337
  return { input: {}, error: "arguments were not valid JSON" };
337
338
  }
338
339
  };
340
+ /** A tool's inputSchema with its lazy (function) form, if any, resolved. */
341
+ export const resolveToolInputSchema = (schema) => typeof schema === "function" ? resolveToolInputSchema(schema()) : schema;
339
342
  /**
340
343
  * Validate parsed input against the tool's own schema when it exposes one.
341
344
  *
@@ -346,8 +349,13 @@ const parseToolInput = (raw) => {
346
349
  * validator this loop has no business carrying.
347
350
  */
348
351
  const validateToolInput = (tool, input) => {
349
- const schema = tool?.inputSchema;
350
- if (typeof schema?.safeParse !== "function") {
352
+ const schema = tool?.inputSchema
353
+ ? resolveToolInputSchema(tool.inputSchema)
354
+ : undefined;
355
+ if (!schema ||
356
+ typeof schema !== "object" ||
357
+ !("safeParse" in schema) ||
358
+ typeof schema.safeParse !== "function") {
351
359
  return undefined;
352
360
  }
353
361
  const result = schema.safeParse(input);
@@ -478,11 +486,28 @@ async function runGenerateSteps(args, toolExecutionSummaries, stepSpans) {
478
486
  // answer is that result — not a thrown turn.
479
487
  let preReask;
480
488
  const hasTools = Boolean(args.tools && args.tools.length > 0);
489
+ // The declarations actually sent. Starts as the caller's list and grows when
490
+ // a tool joins toolsRecord mid-turn (discovery loads tools into it).
491
+ const liveTools = args.tools ? [...args.tools] : undefined;
492
+ const declaredNames = declaredToolRecord(args.tools);
481
493
  // Completed-step records for `prepareStep`; one per step that returned.
482
494
  const stepRecords = [];
483
495
  for (let step = 0; step < args.maxSteps; step++) {
484
496
  steps = step + 1;
485
497
  stepSpans.begin(steps);
498
+ if (liveTools && args.declareTool) {
499
+ const loaded = [];
500
+ for (const name of Object.keys(args.toolsRecord)) {
501
+ if (!declaredNames[name]) {
502
+ liveTools.push(args.declareTool(name, args.toolsRecord[name]));
503
+ declaredNames[name] = true;
504
+ loaded.push(name);
505
+ }
506
+ }
507
+ if (loaded.length > 0) {
508
+ logger.debug(`[nativeGenerateLoop] ${loaded.length} tool(s) hydrated mid-turn via discovery: ${loaded.join(", ")}`);
509
+ }
510
+ }
486
511
  // The forced-choice window and the caller's hook are resolved BEFORE the
487
512
  // re-ask override below: the re-ask has to win, since its whole point is
488
513
  // a request with tools disabled.
@@ -498,7 +523,7 @@ async function runGenerateSteps(args, toolExecutionSummaries, stepSpans) {
498
523
  steps: stepRecords,
499
524
  maxSteps: args.maxSteps,
500
525
  model: args.modelId,
501
- declaredTools: declaredToolRecord(args.tools),
526
+ declaredTools: declaredToolRecord(liveTools),
502
527
  ...(args.abortSignal ? { abortSignal: args.abortSignal } : {}),
503
528
  })
504
529
  : undefined;
@@ -519,7 +544,7 @@ async function runGenerateSteps(args, toolExecutionSummaries, stepSpans) {
519
544
  }
520
545
  const runThisStep = () => generateObservedStep(args, {
521
546
  prompt: args.conversation,
522
- ...(args.tools && args.tools.length > 0 ? { tools: args.tools } : {}),
547
+ ...(liveTools && liveTools.length > 0 ? { tools: liveTools } : {}),
523
548
  // The v3 call option is an OBJECT — `{ type: "none" }`. Passing the
524
549
  // bare string "none" type-checks against `unknown` and is then
525
550
  // dropped by every converter that switches on `choice.type`, so the
@@ -688,7 +713,9 @@ async function runGenerateSteps(args, toolExecutionSummaries, stepSpans) {
688
713
  const startTime = new Date();
689
714
  const parsed = parseToolInput(call.input);
690
715
  const input = parsed.input;
691
- const tool = args.toolsRecord[name];
716
+ // resolveLiveTool also loads a deferred tool the model called by its
717
+ // catalog name without searching first.
718
+ const tool = resolveLiveTool(args.toolsRecord, name);
692
719
  let output;
693
720
  let failure;
694
721
  const rejection = parsed.error ??
package/dist/neurolink.js CHANGED
@@ -76,6 +76,7 @@ import { SpanSerializer } from "./observability/utils/spanSerializer.js";
76
76
  import { endSpanWhenDropped, withSpan } from "./telemetry/withSpan.js";
77
77
  import { getActiveTraceContext } from "./telemetry/traceContext.js";
78
78
  import { calculateCost } from "./utils/pricing.js";
79
+ import { createToolExecutionId } from "./utils/toolExecutionId.js";
79
80
  import { acquireTelemetry, ensureTelemetry, flushOpenTelemetry, getLangfuseHealthStatus, isOpenTelemetryInitialized, releaseTelemetry, setLangfuseContext, traceMetadataFromContext, } from "./services/server/ai/observability/instrumentation.js";
80
81
  import { clearChecklistState, createChecklistTools, getChecklistState, resolveChecklistSessionId, } from "./agent/taskChecklist.js";
81
82
  import { cancelDelegates as cancelBackgroundDelegates, collectDelegates as collectBackgroundDelegates, configureDelegation, createDelegationTools, spawnDelegate as spawnBackgroundDelegate, } from "./agent/backgroundDelegation.js";
@@ -10599,7 +10600,7 @@ Current user's request: ${currentInput}`;
10599
10600
  * @returns executionId for tracking this specific execution
10600
10601
  */
10601
10602
  emitToolStart(toolName, input, startTime = Date.now()) {
10602
- const executionId = `${toolName}-${startTime}-${Math.random().toString(36).substr(2, 9)}`;
10603
+ const executionId = createToolExecutionId(toolName, startTime);
10603
10604
  // Create execution context for tracking
10604
10605
  const context = {
10605
10606
  executionId,
@@ -36,7 +36,7 @@ import { NoOutputGeneratedError } from "../utils/generationErrors.js";
36
36
  import { buildNoOutputSentinel, stampNoOutputSpan, } from "../utils/noOutputSentinel.js";
37
37
  import { composeAbortSignalsScoped, createTimeoutController, mergeAbortSignals, TimeoutError, } from "../utils/timeout.js";
38
38
  import { resolveRequestKind } from "../core/resolveRequestKind.js";
39
- import { appendJsonSchemaInstruction, hasNativeDoGenerate, resolveStepToolChoice, runNativeGenerateLoop, toPrepareStepRecord, } from "../core/nativeGenerateLoop.js";
39
+ import { appendJsonSchemaInstruction, hasNativeDoGenerate, resolveToolInputSchema, resolveStepToolChoice, runNativeGenerateLoop, toPrepareStepRecord, } from "../core/nativeGenerateLoop.js";
40
40
  import { resolveToolExecutionRecords, toolCallsFromSummaries, } from "../core/toolExecutionRecorder.js";
41
41
  import { convertZodToJsonSchema } from "../utils/schemaConversion.js";
42
42
  import { coerceJsonToSchema, recoverScalarRoot, schemaAccepts, } from "../utils/json/coerce.js";
@@ -925,18 +925,19 @@ export class OpenAIChatCompletionsProvider extends BaseProvider {
925
925
  ? options.tools || (await this.getAllTools())
926
926
  : {};
927
927
  // v3 tool shape — the same one doGenerate already converts internally.
928
+ // Also handed to the loop, which declares tools discovery loads mid-turn.
929
+ const declareTool = (name, tool) => {
930
+ return {
931
+ type: "function",
932
+ name,
933
+ description: tool.description ?? "",
934
+ inputSchema: tool.inputSchema
935
+ ? convertZodToJsonSchema(resolveToolInputSchema(tool.inputSchema))
936
+ : { type: "object", properties: {} },
937
+ };
938
+ };
928
939
  const v3Tools = shouldUseTools
929
- ? Object.entries(toolsRecord).map(([name, t]) => {
930
- const tool = t;
931
- return {
932
- type: "function",
933
- name,
934
- description: tool.description ?? "",
935
- inputSchema: (tool.inputSchema
936
- ? convertZodToJsonSchema(tool.inputSchema)
937
- : { type: "object", properties: {} }),
938
- };
939
- })
940
+ ? Object.entries(toolsRecord).map(([name, t]) => declareTool(name, t))
940
941
  : undefined;
941
942
  const hasTools = !!v3Tools && v3Tools.length > 0;
942
943
  // Structured output rides response_format, which is what Output.object did
@@ -964,6 +965,14 @@ export class OpenAIChatCompletionsProvider extends BaseProvider {
964
965
  // model has already given. Dropping them is what makes `response_format`
965
966
  // legal again on a vendor that refuses the two together.
966
967
  withTools = true) => {
968
+ // What this pass sends: the initial declarations plus any the loop adds
969
+ // for tools discovery loads mid-turn, so the guard counts them too.
970
+ const passTools = withTools && v3Tools ? [...v3Tools] : undefined;
971
+ const declarePassTool = (name, tool) => {
972
+ const declaration = declareTool(name, tool);
973
+ passTools?.push(declaration);
974
+ return declaration;
975
+ };
967
976
  return runNativeGenerateLoop({
968
977
  doGenerate,
969
978
  provider: this.providerName,
@@ -983,12 +992,14 @@ export class OpenAIChatCompletionsProvider extends BaseProvider {
983
992
  // the estimate past the reclaim threshold and drop older tool
984
993
  // exchanges out of a reformat request that would have fit.
985
994
  getFixedOverheadTokens: () => estimateTokens(serializeForEstimate({
986
- ...(withTools && v3Tools ? { tools: v3Tools } : {}),
995
+ ...(passTools ? { tools: passTools } : {}),
987
996
  responseFormat: format,
988
997
  }), this.providerName),
989
998
  }),
990
999
  conversation: conv,
991
- ...(withTools && v3Tools ? { tools: v3Tools } : {}),
1000
+ ...(passTools
1001
+ ? { tools: [...passTools], declareTool: declarePassTool }
1002
+ : {}),
992
1003
  toolsRecord,
993
1004
  ...(withTools && hasTools && options.toolChoice
994
1005
  ? { toolChoice: resolveToolChoice(options, toolsRecord, true) }
@@ -244,7 +244,8 @@ export function resolveLiveTool(record, name) {
244
244
  function buildSearchTool(entries, allTools, hotRecord, onHydrate) {
245
245
  const catalog = renderCatalog(entries);
246
246
  const searchTool = createAISDKTool({
247
- description: `Search and load additional tools on demand. ${entries.length} more tools are available but not yet loaded:\n` +
247
+ description: `Search and load additional tools on demand. ${entries.length} more tools are available but not yet loaded. ` +
248
+ `Before telling the user you cannot do something, check this catalog: if a listed tool fits, call it directly by its exact name, or call search_tools with a short description of the task.\n` +
248
249
  `${catalog}\n` +
249
250
  `Call this with a task description or an exact tool name; matching tools are loaded and become directly callable.`,
250
251
  inputSchema: z.object({
@@ -186,8 +186,28 @@ export function resolveToolPolicy(input) {
186
186
  }
187
187
  // --- discovery -----------------------------------------------------------
188
188
  const discovery = instanceConfig?.discovery === true;
189
+ let discoveryScope = "external";
190
+ let discoveryAlwaysVisible = [];
189
191
  if (discovery) {
190
192
  sources.push("tools.discovery");
191
- }
192
- return { enabled, include, includeBound, exclude, discovery, sources };
193
+ const configScope = instanceConfig?.discoveryScope;
194
+ if (configScope === "all") {
195
+ discoveryScope = "all";
196
+ }
197
+ else if (configScope !== undefined && configScope !== "external") {
198
+ warnOnce("shape:tools.discoveryScope", `[ToolPolicy] Ignoring unknown tools.discoveryScope; using "external"`, { received: configScope });
199
+ }
200
+ discoveryAlwaysVisible =
201
+ normalizeConfigList(instanceConfig?.discoveryAlwaysVisible, "tools.discoveryAlwaysVisible", false) ?? [];
202
+ }
203
+ return {
204
+ enabled,
205
+ include,
206
+ includeBound,
207
+ exclude,
208
+ discovery,
209
+ discoveryScope,
210
+ discoveryAlwaysVisible,
211
+ sources,
212
+ };
193
213
  }
@@ -443,9 +443,40 @@ export type ToolConfig = {
443
443
  * full. Note: the instance-level `include` list deliberately does NOT
444
444
  * force tools hot — it scopes the catalog, and discovery defers within
445
445
  * that scope (a scoped-but-large catalog is exactly where deferral pays).
446
+ *
447
+ * What the model and your listeners see once discovery is on: the
448
+ * `search_tools` description ends with an instruction to check the catalog
449
+ * before saying a task cannot be done; each `search_tools` call emits
450
+ * `tool:start` and `tool:end` (paired by `executionId`) and is traced and
451
+ * observed like any other tool call; and the events of every wrapped
452
+ * external tool carry an `executionId`.
446
453
  * Default: false.
447
454
  */
448
455
  discovery?: boolean;
456
+ /**
457
+ * Which tools `discovery` may defer.
458
+ * - `"external"`: tools of servers attached with `addExternalMCPServer`.
459
+ * - `"all"`: every tool in the request's record except the built-in tools,
460
+ * so tools registered with `registerTool`/`registerTools` and in-process
461
+ * MCP tools are deferred as well. That includes the tools NeuroLink
462
+ * registers for its own features through `registerTool()`, such as
463
+ * `retrieve_context` (memory retrieval), the skills tools, and the tools
464
+ * of `registerTaskTools`, `registerDelegationTools`,
465
+ * `registerBackgroundCommandTools` and `registerGitTools`. Name them in
466
+ * `discoveryAlwaysVisible` to keep them in full.
467
+ * Per-call tools, per-call whitelists, forced `toolChoice` tools and
468
+ * already-discovered tools stay hot in both scopes. Ignored unless
469
+ * `discovery` is true.
470
+ * Default: "external".
471
+ */
472
+ discoveryScope?: "external" | "all";
473
+ /**
474
+ * Tool-name patterns (exact names or `*` globs) that `discovery` never
475
+ * defers, in either scope. E.g. `["utility-server_*"]` keeps every tool
476
+ * whose name starts with `utility-server_` in full on every request.
477
+ * Ignored unless `discovery` is true.
478
+ */
479
+ discoveryAlwaysVisible?: string[];
449
480
  /** Whether built-in tools should be disabled (equivalent to excluding all direct tools) */
450
481
  disableBuiltinTools?: boolean;
451
482
  /** Whether custom tools are allowed */
@@ -1839,7 +1839,15 @@ export type NativeGenerateLoopArgs = {
1839
1839
  /** Tool declarations in the v3 shape doGenerate already knows how to convert. */
1840
1840
  tools?: Array<Record<string, unknown>>;
1841
1841
  /** Registered tools, used to execute a call the model asks for. */
1842
- toolsRecord: Record<string, unknown>;
1842
+ toolsRecord: Record<string, Tool>;
1843
+ /**
1844
+ * Builds the v3 declaration for one tool. When set, every step declares any
1845
+ * tool that joined `toolsRecord` mid-turn (discovery's search_tools loads
1846
+ * tools into it), so the model can call what it just loaded. Only the
1847
+ * OpenAI-compatible `generate()` path supplies it; the other providers that
1848
+ * run this loop do not declare a tool `search_tools` loaded mid-turn.
1849
+ */
1850
+ declareTool?: (name: string, tool: Tool) => Record<string, unknown>;
1843
1851
  /** The turn's resolved tool choice, applied per step via `resolveStepToolChoice`. */
1844
1852
  toolChoice?: unknown;
1845
1853
  /** Leading steps a forced `toolChoice` stays in force (default 1). */
@@ -35,8 +35,12 @@ export type ResolvedToolPolicy = {
35
35
  includeBound?: string[];
36
36
  /** Denylist of tool-name patterns (exact names or `*` globs), applied after include. */
37
37
  exclude: string[];
38
- /** Defer external MCP tool schemas behind the search_tools meta-tool. */
38
+ /** Defer tool schemas behind the search_tools meta-tool. */
39
39
  discovery: boolean;
40
+ /** Which tools discovery may defer. Absent means "external". */
41
+ discoveryScope?: "external" | "all";
42
+ /** Tool-name patterns (exact names or `*` globs) discovery never defers. Absent means none. */
43
+ discoveryAlwaysVisible?: string[];
40
44
  /** Which option/config sources contributed to this policy (telemetry/debugging). */
41
45
  sources: string[];
42
46
  };
@@ -0,0 +1,7 @@
1
+ /**
2
+ * @description Builds the id that pairs one tool call's `tool:start` and `tool:end` events.
3
+ * @param toolName {string} - Name of the tool being run.
4
+ * @param startTime {number} - When the call started, in epoch milliseconds.
5
+ * @returns {string} `<toolName>-<startTime>-<nine random base-36 characters>`.
6
+ */
7
+ export declare function createToolExecutionId(toolName: string, startTime?: number): string;
@@ -0,0 +1,9 @@
1
+ /**
2
+ * @description Builds the id that pairs one tool call's `tool:start` and `tool:end` events.
3
+ * @param toolName {string} - Name of the tool being run.
4
+ * @param startTime {number} - When the call started, in epoch milliseconds.
5
+ * @returns {string} `<toolName>-<startTime>-<nine random base-36 characters>`.
6
+ */
7
+ export function createToolExecutionId(toolName, startTime = Date.now()) {
8
+ return `${toolName}-${startTime}-${Math.random().toString(36).slice(2, 11)}`;
9
+ }
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@juspay/neurolink",
3
- "version": "13.5.1",
3
+ "version": "13.6.1",
4
4
  "packageManager": "pnpm@10.15.1",
5
5
  "description": "The pipe layer of an AI nervous system: one interface connecting provider neurons to your application, across three inference types — generate, stream and decide. `decide` returns typed, calibrated judgements from a non-generative model (~400ms, ~$0.00002/call) for routing, tool selection and context budgeting. MCP-native (4 transports), voice TTS/STT/realtime, RAG, agents, memory, compaction, 9 observability exporters. OpenAI · Anthropic · Gemini · Bedrock · Azure · Ollama · TypeSafe Jev and more.",
6
6
  "author": {