@juspay/neurolink 12.47.14 → 12.48.0

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.
Files changed (94) hide show
  1. package/CHANGELOG.md +3 -3
  2. package/dist/auth/middleware/AuthMiddleware.d.ts +1 -1
  3. package/dist/browser/neurolink.min.js +578 -565
  4. package/dist/cli/factories/commandFactory.d.ts +7 -0
  5. package/dist/cli/factories/commandFactory.js +26 -0
  6. package/dist/cli/loop/optionsSchema.js +9 -0
  7. package/dist/context/budgetChecker.js +54 -2
  8. package/dist/context/contextDecision.js +2 -0
  9. package/dist/context/toolPairRepair.d.ts +12 -0
  10. package/dist/context/toolPairRepair.js +29 -3
  11. package/dist/core/baseProvider.d.ts +92 -3
  12. package/dist/core/baseProvider.js +254 -10
  13. package/dist/core/conversationMemoryManager.js +17 -2
  14. package/dist/core/loopEngine.js +1 -1
  15. package/dist/core/modules/MessageBuilder.js +11 -3
  16. package/dist/core/modules/TelemetryHandler.d.ts +2 -2
  17. package/dist/core/modules/ToolsManager.d.ts +35 -0
  18. package/dist/core/modules/ToolsManager.js +159 -55
  19. package/dist/core/nativeGenerateLoop.d.ts +58 -1
  20. package/dist/core/nativeGenerateLoop.js +277 -3
  21. package/dist/core/redisConversationMemoryManager.js +10 -3
  22. package/dist/core/resolveRequestKind.d.ts +24 -4
  23. package/dist/core/resolveRequestKind.js +31 -4
  24. package/dist/core/toolExecutionRecorder.d.ts +6 -0
  25. package/dist/core/toolExecutionRecorder.js +24 -2
  26. package/dist/core/toolRoutingDecision.js +1 -0
  27. package/dist/factories/mediaHandlerCatalog.js +5 -0
  28. package/dist/factories/providerDescriptors.js +15 -0
  29. package/dist/files/fileTools.d.ts +1 -1
  30. package/dist/index.d.ts +5 -2
  31. package/dist/index.js +9 -2
  32. package/dist/memory/condensationPrompt.d.ts +8 -0
  33. package/dist/memory/condensationPrompt.js +29 -0
  34. package/dist/memory/hippocampusInitializer.d.ts +14 -2
  35. package/dist/memory/hippocampusInitializer.js +64 -3
  36. package/dist/neurolink.d.ts +252 -3
  37. package/dist/neurolink.js +898 -43
  38. package/dist/providers/amazonBedrock/client.js +6 -0
  39. package/dist/providers/amazonSagemaker.js +14 -1
  40. package/dist/providers/anthropic/client.js +148 -19
  41. package/dist/providers/anthropic/loopAdapter.js +2 -2
  42. package/dist/providers/googleAiStudio/client.js +2 -1
  43. package/dist/providers/openaiChatCompletionsBase.js +103 -23
  44. package/dist/providers/systemOneDecision.d.ts +6 -0
  45. package/dist/providers/systemOneDecision.js +14 -63
  46. package/dist/providers/typesafe.d.ts +6 -0
  47. package/dist/providers/typesafe.js +6 -0
  48. package/dist/rag/retrieval/searchDecision.js +1 -0
  49. package/dist/routing/classifierRouter.js +6 -5
  50. package/dist/routing/classifierStrategies.js +8 -1
  51. package/dist/types/classifierRouter.d.ts +16 -3
  52. package/dist/types/common.d.ts +2 -0
  53. package/dist/types/config.d.ts +9 -0
  54. package/dist/types/context.d.ts +8 -1
  55. package/dist/types/conversation.d.ts +87 -0
  56. package/dist/types/decision.d.ts +210 -1
  57. package/dist/types/decision.js +9 -1
  58. package/dist/types/dispatch.d.ts +11 -4
  59. package/dist/types/generate.d.ts +118 -54
  60. package/dist/types/loopEngine.d.ts +7 -2
  61. package/dist/types/memory.d.ts +177 -9
  62. package/dist/types/openaiCompatible.d.ts +11 -3
  63. package/dist/types/stream.d.ts +13 -16
  64. package/dist/types/stt.d.ts +58 -1
  65. package/dist/types/tools.d.ts +14 -0
  66. package/dist/types/tools.js +8 -0
  67. package/dist/types/tts.d.ts +108 -0
  68. package/dist/types/voice.d.ts +41 -4
  69. package/dist/utils/decisionAnswers.d.ts +31 -1
  70. package/dist/utils/decisionAnswers.js +114 -0
  71. package/dist/utils/decisionLimits.d.ts +39 -0
  72. package/dist/utils/decisionLimits.js +116 -0
  73. package/dist/utils/logger.d.ts +7 -0
  74. package/dist/utils/logger.js +9 -0
  75. package/dist/utils/messageBuilder.d.ts +45 -1
  76. package/dist/utils/messageBuilder.js +393 -16
  77. package/dist/utils/multimodalOptionsBuilder.d.ts +1 -0
  78. package/dist/utils/multimodalOptionsBuilder.js +1 -0
  79. package/dist/utils/speechText.d.ts +41 -0
  80. package/dist/utils/speechText.js +364 -0
  81. package/dist/utils/toolResultStatus.d.ts +26 -0
  82. package/dist/utils/toolResultStatus.js +73 -0
  83. package/dist/utils/ttsProcessor.d.ts +29 -0
  84. package/dist/utils/ttsProcessor.js +84 -2
  85. package/dist/voice/index.d.ts +1 -0
  86. package/dist/voice/index.js +3 -0
  87. package/dist/voice/providers/ElevenLabsSTT.d.ts +42 -0
  88. package/dist/voice/providers/ElevenLabsSTT.js +351 -0
  89. package/dist/voice/providers/ElevenLabsTTS.d.ts +81 -1
  90. package/dist/voice/providers/ElevenLabsTTS.js +415 -74
  91. package/docs-site/static/search-index.json +48 -33
  92. package/package.json +5 -2
  93. package/dist/utils/toolEndEmitter.d.ts +0 -25
  94. package/dist/utils/toolEndEmitter.js +0 -64
@@ -210,6 +210,10 @@ export class AmazonBedrockProvider extends BaseProvider {
210
210
  });
211
211
  // Cast options to StreamOptions for multimodal processing
212
212
  const streamOptions = options;
213
+ // This provider bypasses BaseProvider's message builders, so the
214
+ // instance-level `conversationMemory.replayToolSteps` has to be folded
215
+ // in here or only the per-request value would ever apply.
216
+ this.applyToolReplayDefault(streamOptions);
213
217
  const multimodalOptions = buildMultimodalOptions(streamOptions, this.providerName, this.modelName);
214
218
  const multimodalMessages = await buildMultimodalMessagesArray(multimodalOptions, this.providerName, this.modelName);
215
219
  // Convert to Bedrock format
@@ -923,6 +927,8 @@ export class AmazonBedrockProvider extends BaseProvider {
923
927
  hasPDFFiles: !!options.input?.pdfFiles?.length,
924
928
  pdfFileCount: options.input?.pdfFiles?.length || 0,
925
929
  });
930
+ // Same instance-default fold as the generate() path above.
931
+ this.applyToolReplayDefault(options);
926
932
  const multimodalOptions = buildMultimodalOptions(options, this.providerName, this.modelName);
927
933
  const multimodalMessages = await buildMultimodalMessagesArray(multimodalOptions, this.providerName, this.modelName);
928
934
  // Convert to Bedrock format
@@ -6,6 +6,7 @@ import { resolveToolExecutionRecords, toolCallsFromSummaries, } from "../core/to
6
6
  import { transformToolExecutions } from "../utils/transformationUtils.js";
7
7
  import { convertZodToJsonSchema } from "../utils/schemaConversion.js";
8
8
  import { withProviderRetry } from "../utils/providerRetry.js";
9
+ import { resolveToolChoice } from "../utils/toolChoice.js";
9
10
  import { DEFAULT_MAX_STEPS } from "../core/constants.js";
10
11
  import { hasNativeDoGenerate, runNativeGenerateLoop, } from "../core/nativeGenerateLoop.js";
11
12
  import { withSpan } from "../telemetry/withSpan.js";
@@ -233,6 +234,17 @@ export class AmazonSageMakerProvider extends BaseProvider {
233
234
  ...(responseFormat ? { responseFormat } : {}),
234
235
  ...(v3Tools.length > 0 ? { tools: v3Tools } : {}),
235
236
  toolsRecord,
237
+ // Was never forwarded here, so a caller's toolChoice degraded to the
238
+ // endpoint's default on SageMaker alone; the language model already
239
+ // maps it onto `tool_choice`.
240
+ ...(v3Tools.length > 0 && options.toolChoice
241
+ ? { toolChoice: resolveToolChoice(options, toolsRecord, true) }
242
+ : {}),
243
+ ...(options.toolChoiceSteps !== undefined
244
+ ? { toolChoiceSteps: options.toolChoiceSteps }
245
+ : {}),
246
+ ...(options.prepareStep ? { prepareStep: options.prepareStep } : {}),
247
+ modelId: this.modelName,
236
248
  maxSteps: options.maxSteps || DEFAULT_MAX_STEPS,
237
249
  ...(options.maxTokens ? { maxOutputTokens: options.maxTokens } : {}),
238
250
  ...(options.temperature !== undefined
@@ -245,6 +257,7 @@ export class AmazonSageMakerProvider extends BaseProvider {
245
257
  runStep: (call) => withProviderRetry(call, undefined, "sagemaker generate", undefined, options.abortSignal).catch((err) => {
246
258
  throw this.handleProviderError(err);
247
259
  }),
260
+ onRejectedToolCall: (name, error, id) => this.emitRejectedToolCall(name, error, id),
248
261
  }, toolExecutionSummaries);
249
262
  const enhanced = {
250
263
  content: loop.text,
@@ -263,7 +276,7 @@ export class AmazonSageMakerProvider extends BaseProvider {
263
276
  toolExecutions: resolveToolExecutionRecords(options, transformToolExecutions(toolExecutionSummaries)),
264
277
  enhancedWithTools: loop.toolsUsed.length > 0,
265
278
  };
266
- return this.finalizeNativeGenerate(enhanced, options, startTime);
279
+ return this.finalizeNativeGenerate(enhanced, options, startTime, toolExecutionSummaries);
267
280
  }
268
281
  /**
269
282
  * Streaming was previously an `executeStream` override that unconditionally
@@ -26,7 +26,7 @@ import { stringifyAnthropicToolOutput } from "./toolOutput.js";
26
26
  import { createAnthropicLoopAdapter } from "./loopAdapter.js";
27
27
  import { DEFAULT_BEFORE_STEP_TIMEOUT_MS } from "../../utils/parameterValidation.js";
28
28
  import { runAgenticLoop } from "../../core/loopEngine.js";
29
- import { hasNativeDoGenerate, runNativeGenerateLoop, } from "../../core/nativeGenerateLoop.js";
29
+ import { hasNativeDoGenerate, resolveStepToolChoice, runNativeGenerateLoop, toPrepareStepRecord, } from "../../core/nativeGenerateLoop.js";
30
30
  import { withProviderRetry } from "../../utils/providerRetry.js";
31
31
  import { resolveRequestKind } from "../../core/resolveRequestKind.js";
32
32
  import { resolveToolExecutionRecords, toolCallsFromSummaries, } from "../../core/toolExecutionRecorder.js";
@@ -35,7 +35,6 @@ import { createAnthropicConfig, getProviderModel, validateApiKey, } from "../../
35
35
  import { composeAbortSignals, composeAbortSignalsScoped, createTimeoutController, mergeAbortSignals, TimeoutError, } from "../../utils/timeout.js";
36
36
  import { raceWithAbort } from "../../utils/async/index.js";
37
37
  import { normalizeResolvedToolChoice, resolveToolChoice, } from "../../utils/toolChoice.js";
38
- import { emitToolEndFromStepFinish } from "../../utils/toolEndEmitter.js";
39
38
  import { NoOutputGeneratedError } from "../../utils/generationErrors.js";
40
39
  import { buildNoOutputSentinel, stampNoOutputSpan, } from "../../utils/noOutputSentinel.js";
41
40
  import { convertZodToJsonSchema } from "../../utils/schemaConversion.js";
@@ -1613,6 +1612,11 @@ export class AnthropicProvider extends BaseProvider {
1613
1612
  ...(hasTools && options.toolChoice
1614
1613
  ? { toolChoice: resolveToolChoice(options, toolsRecord, true) }
1615
1614
  : {}),
1615
+ ...(options.toolChoiceSteps !== undefined
1616
+ ? { toolChoiceSteps: options.toolChoiceSteps }
1617
+ : {}),
1618
+ ...(options.prepareStep ? { prepareStep: options.prepareStep } : {}),
1619
+ modelId,
1616
1620
  ...(responseFormat ? { responseFormat } : {}),
1617
1621
  ...(providerOptions ? { providerOptions } : {}),
1618
1622
  maxSteps: options.maxSteps || DEFAULT_MAX_STEPS,
@@ -1627,6 +1631,7 @@ export class AnthropicProvider extends BaseProvider {
1627
1631
  runStep: (call) => withProviderRetry(call, trace.getActiveSpan() ?? undefined, "anthropic generate", undefined, options.abortSignal).catch((err) => {
1628
1632
  throw this.handleProviderError(err);
1629
1633
  }),
1634
+ onRejectedToolCall: (name, error, id) => this.emitRejectedToolCall(name, error, id),
1630
1635
  }, toolExecutionSummaries);
1631
1636
  const enhanced = {
1632
1637
  content: loop.text,
@@ -1660,7 +1665,7 @@ export class AnthropicProvider extends BaseProvider {
1660
1665
  toolExecutions: resolveToolExecutionRecords(options, transformToolExecutions(toolExecutionSummaries)),
1661
1666
  enhancedWithTools: loop.toolsUsed.length > 0,
1662
1667
  };
1663
- return this.finalizeNativeGenerate(enhanced, options, startTime);
1668
+ return this.finalizeNativeGenerate(enhanced, options, startTime, toolExecutionSummaries);
1664
1669
  }
1665
1670
  /**
1666
1671
  * Fold a captured snapshot into the provider's usage bookkeeping and log it.
@@ -1748,6 +1753,11 @@ export class AnthropicProvider extends BaseProvider {
1748
1753
  // options.tools is pre-merged by BaseProvider.stream() with base tools
1749
1754
  // (MCP/built-in) + user-provided tools (RAG, etc.)
1750
1755
  shouldUseTools = !options.disableTools && this.supportsTools();
1756
+ // Used as is: BaseProvider.stream() already event-wrapped, recorder-
1757
+ // wrapped and discovery-partitioned this record, and search_tools
1758
+ // hydrates new tools into THIS object mid-turn. A copy would lose the
1759
+ // null prototype, the non-enumerable resolver and every later
1760
+ // hydration, so a discovered tool the model then called was "not found".
1751
1761
  toolsRecord = shouldUseTools
1752
1762
  ? options.tools || (await this.getAllTools())
1753
1763
  : {};
@@ -1776,9 +1786,17 @@ export class AnthropicProvider extends BaseProvider {
1776
1786
  throw this.handleProviderError(setupErr);
1777
1787
  }
1778
1788
  const modelId = this.modelName || getDefaultAnthropicModel();
1779
- const anthropicToolChoice = shouldUseTools && anthropicTools && anthropicTools.length > 0
1780
- ? relaxForcedToolChoice(modelId, toolChoiceToAnthropic(resolveToolChoice(options, toolsRecord, shouldUseTools)))
1789
+ // The turn's choice in NeuroLink shape. Translated to the wire PER STEP
1790
+ // inside buildParams, because a forced choice applies only to the first
1791
+ // `toolChoiceSteps` steps — baked in once, it compelled a tool call on
1792
+ // every step and the turn ran until maxSteps.
1793
+ const turnToolChoice = shouldUseTools && anthropicTools && anthropicTools.length > 0
1794
+ ? resolveToolChoice(options, toolsRecord, shouldUseTools)
1781
1795
  : undefined;
1796
+ // Completed-step records handed to a caller's prepareStep hook. Filled by
1797
+ // the buildToolResultMessages wrapper below, which is the only per-step
1798
+ // hook that sees a step's calls and results together.
1799
+ const stepRecords = [];
1782
1800
  // Extended thinking: enabled when the caller supplies an explicit token
1783
1801
  // budget (mirrors the previous experimental_thinking gating). Thinking
1784
1802
  // deltas stream out on the `reasoning` chunk channel.
@@ -1803,7 +1821,6 @@ export class AnthropicProvider extends BaseProvider {
1803
1821
  },
1804
1822
  });
1805
1823
  const maxSteps = options.maxSteps || DEFAULT_MAX_STEPS;
1806
- const emitter = this.neurolink?.getEventEmitter();
1807
1824
  const channel = createStreamChannel();
1808
1825
  const { push: pushChunk } = channel;
1809
1826
  const { usagePromise, finishPromise, resolveUsage, resolveFinish } = createDeferredAnalytics();
@@ -1842,7 +1859,15 @@ export class AnthropicProvider extends BaseProvider {
1842
1859
  });
1843
1860
  let capturedProviderError;
1844
1861
  const client = this.client;
1862
+ // Every attempted call, for the no-output sentinel below; the result
1863
+ // reports only calls that returned a value, the rule the generate loop
1864
+ // and Bedrock already follow.
1845
1865
  const toolsUsed = [];
1866
+ const toolsSucceeded = [];
1867
+ // Every tool this turn ran, in the loop's own summary shape. Backs the
1868
+ // result's toolCalls / toolResults / toolExecutions getters, which read it
1869
+ // live so a consumer that drains the stream first sees the full turn.
1870
+ const streamToolSummaries = [];
1846
1871
  const streamStartTime = Date.now();
1847
1872
  // Mutable-reference contract from StreamResult.metadata: created before
1848
1873
  // the loop and filled once the turn drains, because wrapper spreads
@@ -1965,7 +1990,27 @@ export class AnthropicProvider extends BaseProvider {
1965
1990
  }
1966
1991
  return { conversation: rebuilt };
1967
1992
  };
1968
- const buildParams = (conversation) => {
1993
+ const buildParams = async (conversation, step) => {
1994
+ // Consulted whenever there is a base choice OR a hook: a hook with
1995
+ // no base choice still gets to force a tool on a given step.
1996
+ // No declared tools → no tool_choice and no hook call; Anthropic
1997
+ // rejects a tool_choice on a request without tools.
1998
+ const anthropicToolChoice = (turnToolChoice !== undefined || options.prepareStep) &&
1999
+ anthropicTools &&
2000
+ anthropicTools.length > 0
2001
+ ? // Per step, so a prepareStep-forced choice gets the same
2002
+ // Claude 5.5/5.1 relaxation as the turn's base choice.
2003
+ relaxForcedToolChoice(modelId, toolChoiceToAnthropic(await resolveStepToolChoice({
2004
+ base: turnToolChoice,
2005
+ step,
2006
+ toolChoiceSteps: options.toolChoiceSteps,
2007
+ prepareStep: options.prepareStep,
2008
+ steps: stepRecords,
2009
+ maxSteps,
2010
+ model: modelId,
2011
+ ...(abortSignal ? { abortSignal } : {}),
2012
+ })))
2013
+ : undefined;
1969
2014
  // Mid-turn discovery sync: search_tools (tools.discovery) hydrates
1970
2015
  // new tools into toolsRecord between steps; Claude only calls tools
1971
2016
  // declared in the request, so advertise them now.
@@ -2055,33 +2100,77 @@ export class AnthropicProvider extends BaseProvider {
2055
2100
  const adapter = {
2056
2101
  ...baseAdapter,
2057
2102
  buildToolResultMessages: (conversation, stepResult, toolResults, engineStep) => {
2103
+ const settledAt = new Date();
2058
2104
  for (const result of toolResults) {
2059
2105
  toolsUsed.push(result.name);
2106
+ if (!result.error) {
2107
+ toolsSucceeded.push(result.name);
2108
+ }
2109
+ streamToolSummaries.push({
2110
+ toolCallId: result.id,
2111
+ toolName: result.name,
2112
+ input: result.args,
2113
+ ...(result.error
2114
+ ? { error: result.error }
2115
+ : { output: result.output }),
2116
+ // The engine reports a step's results together, after the
2117
+ // batch has settled, so per-tool timing is not available here;
2118
+ // `toolExecutions` prefers the execution recorder's own records,
2119
+ // which do carry it.
2120
+ startTime: settledAt,
2121
+ endTime: settledAt,
2122
+ stepIndex: engineStep,
2123
+ });
2060
2124
  }
2125
+ const rawBlocks = stepResult.raw;
2126
+ stepRecords.push(toPrepareStepRecord({
2127
+ stepNumber: engineStep,
2128
+ content: rawBlocks.map((block) => block),
2129
+ text: stepResult.text,
2130
+ toolCalls: stepResult.toolCalls.map((call) => ({
2131
+ toolName: call.name,
2132
+ toolCallId: call.id,
2133
+ input: call.args,
2134
+ })),
2135
+ toolResults: toolResults.map((result) => ({
2136
+ toolName: result.name,
2137
+ toolCallId: result.id,
2138
+ output: result.error ? { error: result.error } : result.output,
2139
+ })),
2140
+ finishReason: "tool-calls",
2141
+ inputTokens: stepResult.usage.inputTokens,
2142
+ outputTokens: stepResult.usage.outputTokens,
2143
+ ...(stepResult.usage.cacheReadTokens !== undefined ||
2144
+ stepResult.usage.cacheWriteTokens !== undefined
2145
+ ? {
2146
+ cacheReadTokens: stepResult.usage.cacheReadTokens ?? 0,
2147
+ cacheWriteTokens: stepResult.usage.cacheWriteTokens ?? 0,
2148
+ }
2149
+ : {}),
2150
+ }));
2151
+ // `tool:start` / `tool:end` are emitted by the executor itself
2152
+ // (ToolsManager, external MCP, neurolink.executeTool), so nothing is
2153
+ // emitted here: a second `tool:end` per result — with a zeroed
2154
+ // responseTime — used to be, and doubled every consumer's count.
2061
2155
  const toolCallsForStorage = toolResults.map((result) => ({
2062
- type: "tool-call",
2063
2156
  toolCallId: result.id,
2064
2157
  toolName: result.name,
2065
2158
  args: result.args,
2159
+ stepIndex: engineStep,
2066
2160
  }));
2067
2161
  const toolResultsForStorage = toolResults.map((result) => result.error
2068
2162
  ? {
2069
- type: "tool-result",
2070
2163
  toolCallId: result.id,
2071
2164
  toolName: result.name,
2072
2165
  error: result.error,
2166
+ stepIndex: engineStep,
2073
2167
  }
2074
2168
  : {
2075
- type: "tool-result",
2076
2169
  toolCallId: result.id,
2077
2170
  toolName: result.name,
2078
- result: result.output,
2171
+ output: result.output,
2172
+ stepIndex: engineStep,
2079
2173
  });
2080
- emitToolEndFromStepFinish(emitter, toolResultsForStorage.map((tr) => ({
2081
- toolName: tr.toolName,
2082
- result: "result" in tr ? tr.result : undefined,
2083
- error: "error" in tr ? tr.error : undefined,
2084
- })));
2085
2174
  this.handleToolExecutionStorage(toolCallsForStorage, toolResultsForStorage, options, new Date()).catch((storageErr) => {
2086
2175
  logger.warn("[AnthropicProvider] Failed to store tool executions", {
2087
2176
  provider: this.providerName,
@@ -2344,7 +2433,7 @@ export class AnthropicProvider extends BaseProvider {
2344
2433
  }
2345
2434
  }
2346
2435
  };
2347
- return {
2436
+ const result = {
2348
2437
  stream: transformedStream(),
2349
2438
  provider: this.providerName,
2350
2439
  // Cell-3 identity fix: `this.modelName` is the raw, possibly-unset
@@ -2355,8 +2444,14 @@ export class AnthropicProvider extends BaseProvider {
2355
2444
  // build the request. Reporting the raw field previously let a
2356
2445
  // default-model turn's StreamResult claim an undefined/empty model.
2357
2446
  model: modelId,
2358
- toolCalls: [],
2359
- toolResults: [],
2447
+ // `toolCalls` / `toolResults` / `toolExecutions` are defined as live
2448
+ // getters below, over the per-turn summaries.
2449
+ // The live array: pushed as steps settle, so a consumer reading after
2450
+ // the drain sees every tool that ran and returned a value (a thrown or
2451
+ // not-found call is not "used"). This was never returned before, so
2452
+ // `toolsUsed` came back empty for every tool on this path and
2453
+ // `enhancedWithTools` was always false.
2454
+ toolsUsed: toolsSucceeded,
2360
2455
  metadata: turnMetadata,
2361
2456
  // Wire the deferred usage/finish promises into the analytics collector
2362
2457
  // (mirrors openaiChatCompletionsBase). Without this the loop computed a
@@ -2384,6 +2479,40 @@ export class AnthropicProvider extends BaseProvider {
2384
2479
  return analytics;
2385
2480
  }),
2386
2481
  };
2482
+ // Live getters, mirroring openaiChatCompletionsBase: the wrapper layers
2483
+ // (BaseProvider.stream, NeuroLink.stream) re-apply accessor descriptors
2484
+ // rather than spreading values, so these resolve when read, after the
2485
+ // background loop has run the tools — a plain value here would be the
2486
+ // empty snapshot taken before the first chunk.
2487
+ Object.defineProperty(result, "toolCalls", {
2488
+ enumerable: true,
2489
+ configurable: true,
2490
+ get: () => toolCallsFromSummaries(streamToolSummaries),
2491
+ });
2492
+ Object.defineProperty(result, "toolResults", {
2493
+ enumerable: true,
2494
+ configurable: true,
2495
+ get: () => streamToolSummaries.map((s) => ({
2496
+ toolName: s.toolName,
2497
+ id: s.toolCallId,
2498
+ status: s.error ? "failure" : "success",
2499
+ ...(s.error ? { error: s.error } : {}),
2500
+ ...(s.output !== undefined
2501
+ ? { output: s.output }
2502
+ : {}),
2503
+ })),
2504
+ });
2505
+ Object.defineProperty(result, "toolExecutions", {
2506
+ enumerable: true,
2507
+ configurable: true,
2508
+ get: () => resolveToolExecutionRecords(options, transformToolExecutions(streamToolSummaries.map((s) => ({
2509
+ toolName: s.toolName,
2510
+ input: s.input,
2511
+ output: s.error ? { error: s.error } : s.output,
2512
+ duration: s.endTime.getTime() - s.startTime.getTime(),
2513
+ })))),
2514
+ });
2515
+ return result;
2387
2516
  }
2388
2517
  async isAvailable() {
2389
2518
  try {
@@ -382,8 +382,8 @@ export function createAnthropicLoopAdapter(config) {
382
382
  execute: async (args, opts) => execute(args, opts),
383
383
  };
384
384
  },
385
- buildStepRequest(conversation, step) {
386
- return { raw: config.buildParams(conversation, step) };
385
+ async buildStepRequest(conversation, step) {
386
+ return { raw: await config.buildParams(conversation, step) };
387
387
  },
388
388
  async executeStep(request, channel, signal) {
389
389
  const params = request.raw;
@@ -8,6 +8,7 @@ import { ERROR_CODES, NeuroLinkError } from "../../utils/errorHandling.js";
8
8
  import { logger } from "../../utils/logger.js";
9
9
  import { drainDetachedPump } from "../../utils/drainDetachedPump.js";
10
10
  import { createGeminiLoopAdapter } from "../../core/geminiLoopAdapter.js";
11
+ import { isDirectTTSRequest } from "../../core/resolveRequestKind.js";
11
12
  import { runAgenticLoop } from "../../core/loopEngine.js";
12
13
  import { DEFAULT_TOOL_MAX_RETRIES, resolveToolTimeoutMs, } from "../../core/constants.js";
13
14
  import { isToolsSchemaExclusionInForce } from "../../core/modules/structuredOutputPolicy.js";
@@ -2043,7 +2044,7 @@ export class GoogleAIStudioProvider extends BaseProvider {
2043
2044
  // TTS direct-synthesis mode: synthesise the input text directly (no LLM
2044
2045
  // call). BaseProvider.runGenerateInActiveContext does the same dispatch
2045
2046
  // — replicated here because AI Studio's override bypasses that path.
2046
- if (options.tts?.enabled && !options.tts?.useAiResponse) {
2047
+ if (isDirectTTSRequest(options.tts)) {
2047
2048
  logger.info("[GoogleAIStudio] Routing TTS direct-synthesis to handleDirectTTSSynthesis", { model: modelName });
2048
2049
  return this.handleDirectTTSSynthesis(options, Date.now());
2049
2050
  }
@@ -33,9 +33,8 @@ import { redactUrlCredentials } from "../utils/logSanitize.js";
33
33
  import { NoOutputGeneratedError } from "../utils/generationErrors.js";
34
34
  import { buildNoOutputSentinel, stampNoOutputSpan, } from "../utils/noOutputSentinel.js";
35
35
  import { composeAbortSignalsScoped, createTimeoutController, mergeAbortSignals, TimeoutError, } from "../utils/timeout.js";
36
- import { emitToolEndFromStepFinish } from "../utils/toolEndEmitter.js";
37
36
  import { resolveRequestKind } from "../core/resolveRequestKind.js";
38
- import { appendJsonSchemaInstruction, hasNativeDoGenerate, runNativeGenerateLoop, } from "../core/nativeGenerateLoop.js";
37
+ import { appendJsonSchemaInstruction, hasNativeDoGenerate, resolveStepToolChoice, runNativeGenerateLoop, toPrepareStepRecord, } from "../core/nativeGenerateLoop.js";
39
38
  import { resolveToolExecutionRecords, toolCallsFromSummaries, } from "../core/toolExecutionRecorder.js";
40
39
  import { convertZodToJsonSchema } from "../utils/schemaConversion.js";
41
40
  import { coerceJsonToSchema, recoverScalarRoot, schemaAccepts, } from "../utils/json/coerce.js";
@@ -908,6 +907,11 @@ export class OpenAIChatCompletionsProvider extends BaseProvider {
908
907
  }
909
908
  const doGenerate = model.doGenerate.bind(model);
910
909
  const shouldUseTools = !options.disableTools && this.supportsTools();
910
+ // `options.tools` arrives from BaseProvider's merge (getToolsForStream)
911
+ // already event-wrapped, recorder-wrapped and discovery-partitioned. It
912
+ // must be used AS IS: copying it would drop the discovery record's null
913
+ // prototype and non-enumerable resolver, and tools search_tools hydrates
914
+ // later land only in the original object.
911
915
  const toolsRecord = shouldUseTools
912
916
  ? options.tools || (await this.getAllTools())
913
917
  : {};
@@ -979,6 +983,11 @@ export class OpenAIChatCompletionsProvider extends BaseProvider {
979
983
  ...(withTools && hasTools && options.toolChoice
980
984
  ? { toolChoice: resolveToolChoice(options, toolsRecord, true) }
981
985
  : {}),
986
+ ...(options.toolChoiceSteps !== undefined
987
+ ? { toolChoiceSteps: options.toolChoiceSteps }
988
+ : {}),
989
+ ...(options.prepareStep ? { prepareStep: options.prepareStep } : {}),
990
+ modelId,
982
991
  // The per-call `timeout` keeps its per-MODEL-CALL meaning once
983
992
  // `turnTimeoutMs` owns the whole-turn deadline, and it reaches the
984
993
  // model layer only through this channel. Without it each step fell
@@ -1008,6 +1017,7 @@ export class OpenAIChatCompletionsProvider extends BaseProvider {
1008
1017
  runStep: (call) => withProviderRetry(call, trace.getActiveSpan() ?? undefined, `${this.providerName} generate`, undefined, options.abortSignal).catch((err) => {
1009
1018
  throw this.handleProviderError(err);
1010
1019
  }),
1020
+ onRejectedToolCall: (name, error, id) => this.emitRejectedToolCall(name, error, id),
1011
1021
  }, toolExecutionSummaries);
1012
1022
  };
1013
1023
  // Structured output rides `response_format` first. When a vendor rejects
@@ -1138,7 +1148,7 @@ export class OpenAIChatCompletionsProvider extends BaseProvider {
1138
1148
  toolExecutions: resolveToolExecutionRecords(options, transformToolExecutions(toolExecutionSummaries)),
1139
1149
  enhancedWithTools: toolsUsed.length > 0,
1140
1150
  };
1141
- return this.finalizeNativeGenerate(enhanced, options, startTime);
1151
+ return this.finalizeNativeGenerate(enhanced, options, startTime, toolExecutionSummaries);
1142
1152
  }
1143
1153
  /**
1144
1154
  * Recover structured output on a turn where tools forced `response_format`
@@ -1454,6 +1464,7 @@ export class OpenAIChatCompletionsProvider extends BaseProvider {
1454
1464
  doGenerate,
1455
1465
  conversation: conv,
1456
1466
  toolsRecord: {},
1467
+ modelId,
1457
1468
  ...(format ? { responseFormat: format } : {}),
1458
1469
  maxSteps: 1,
1459
1470
  ...(options.maxTokens
@@ -1575,7 +1586,10 @@ export class OpenAIChatCompletionsProvider extends BaseProvider {
1575
1586
  let toolsRecord;
1576
1587
  let wireNameMaps;
1577
1588
  let openAITools;
1578
- let openAIToolChoice;
1589
+ // The turn's tool choice in NeuroLink shape; translated to the wire per
1590
+ // step (see stepToolChoice below) so a forced choice can lapse after
1591
+ // `toolChoiceSteps` instead of compelling a tool call on every step.
1592
+ let turnToolChoice;
1579
1593
  // The prompt is kept in its pre-wire shape. Model middleware transforms
1580
1594
  // `params.prompt`, and the conversion to the chat-completions wire format
1581
1595
  // has to happen AFTER that or the transform would be discarded.
@@ -1583,6 +1597,8 @@ export class OpenAIChatCompletionsProvider extends BaseProvider {
1583
1597
  try {
1584
1598
  modelId = await this.resolveModelName();
1585
1599
  const shouldUseTools = !options.disableTools && this.supportsTools();
1600
+ // Same identity rule as executeNativeGenerate: the merged record is
1601
+ // the discovery hot record and is never copied here.
1586
1602
  toolsRecord = shouldUseTools
1587
1603
  ? options.tools || (await this.getAllTools())
1588
1604
  : {};
@@ -1594,7 +1610,7 @@ export class OpenAIChatCompletionsProvider extends BaseProvider {
1594
1610
  openAITools = shouldUseTools
1595
1611
  ? buildToolsForOpenAI(toolsRecord, wireNameMaps?.toWire)
1596
1612
  : undefined;
1597
- openAIToolChoice = mapNeuroLinkToolChoice(resolveToolChoice(options, toolsRecord, shouldUseTools), wireNameMaps?.toWire);
1613
+ turnToolChoice = resolveToolChoice(options, toolsRecord, shouldUseTools);
1598
1614
  promptMessages = (await this.buildMessagesForStream(options));
1599
1615
  }
1600
1616
  catch (setupErr) {
@@ -1621,7 +1637,6 @@ export class OpenAIChatCompletionsProvider extends BaseProvider {
1621
1637
  const url = this.getChatCompletionsURL(modelId);
1622
1638
  const fetchImpl = createProxyFetch();
1623
1639
  const maxSteps = options.maxSteps || DEFAULT_MAX_STEPS;
1624
- const emitter = this.neurolink?.getEventEmitter();
1625
1640
  const toolsUsed = [];
1626
1641
  const toolExecutionSummaries = [];
1627
1642
  const { usagePromise, finishPromise, resolveUsage, resolveFinish } = createDeferredAnalytics();
@@ -1715,10 +1730,22 @@ export class OpenAIChatCompletionsProvider extends BaseProvider {
1715
1730
  options: sampled,
1716
1731
  conversation,
1717
1732
  openAITools,
1718
- openAIToolChoice,
1733
+ // No declared tools → no tool_choice, and the hook is not consulted:
1734
+ // a forced choice on a tool-less request is a 400.
1735
+ stepToolChoice: async (step, steps) => openAITools && openAITools.length > 0
1736
+ ? mapNeuroLinkToolChoice(await resolveStepToolChoice({
1737
+ base: turnToolChoice,
1738
+ step,
1739
+ toolChoiceSteps: options.toolChoiceSteps,
1740
+ prepareStep: options.prepareStep,
1741
+ steps,
1742
+ maxSteps,
1743
+ model: modelId,
1744
+ ...(abortSignal ? { abortSignal } : {}),
1745
+ }), wireNameMaps?.toWire)
1746
+ : undefined,
1719
1747
  toolsRecord,
1720
1748
  toolNameFromWire: wireNameMaps?.fromWire,
1721
- emitter,
1722
1749
  toolsUsed,
1723
1750
  toolExecutionSummaries,
1724
1751
  pushChunk: channel.push,
@@ -2012,7 +2039,7 @@ export class OpenAIChatCompletionsProvider extends BaseProvider {
2012
2039
  return result;
2013
2040
  }
2014
2041
  async runStreamLoop(args) {
2015
- const { maxSteps, modelId, url, fetchImpl, abortSignal, options, conversation, openAITools, openAIToolChoice, toolsRecord, toolNameFromWire, emitter, toolsUsed, toolExecutionSummaries, pushChunk, closeChannel, resolveUsage, resolveFinish, responseFormat, onModelObserved, } = args;
2042
+ const { maxSteps, modelId, url, fetchImpl, abortSignal, options, conversation, openAITools, stepToolChoice, toolsRecord, toolNameFromWire, toolsUsed, toolExecutionSummaries, pushChunk, closeChannel, resolveUsage, resolveFinish, responseFormat, onModelObserved, } = args;
2016
2043
  // Hoisted above the try so the catch can resolve the usage accumulated
2017
2044
  // by steps that completed BEFORE the failure — those steps were billed.
2018
2045
  let stepFinish = null;
@@ -2049,7 +2076,10 @@ export class OpenAIChatCompletionsProvider extends BaseProvider {
2049
2076
  // two different payloads would be meaningless.
2050
2077
  let lastObservedPromptTokens;
2051
2078
  let lastSentEstimate;
2079
+ // Completed-step records handed to a caller's prepareStep hook.
2080
+ const stepRecords = [];
2052
2081
  for (let step = 0; step < maxSteps; step++) {
2082
+ const openAIToolChoice = await stepToolChoice(step, stepRecords);
2053
2083
  // Mid-turn discovery sync: search_tools (tools.discovery) hydrates
2054
2084
  // new tools into toolsRecord between steps. Dispatch already re-reads
2055
2085
  // the record — this block makes the request DECLARE them so the model
@@ -2137,11 +2167,54 @@ export class OpenAIChatCompletionsProvider extends BaseProvider {
2137
2167
  conversation,
2138
2168
  toolsRecord,
2139
2169
  toolNameFromWire: effectiveToolNameFromWire,
2140
- emitter,
2141
2170
  toolsUsed,
2142
2171
  toolExecutionSummaries,
2143
2172
  options,
2173
+ stepIndex: step,
2144
2174
  });
2175
+ const executed = toolExecutionSummaries.slice(-stepResult.toolCalls.size);
2176
+ stepRecords.push(toPrepareStepRecord({
2177
+ stepNumber: step,
2178
+ content: [
2179
+ ...(stepResult.text
2180
+ ? [{ type: "text", text: stepResult.text }]
2181
+ : []),
2182
+ ...executed.map((s) => ({
2183
+ type: "tool-call",
2184
+ toolCallId: s.toolCallId,
2185
+ toolName: s.toolName,
2186
+ input: s.input,
2187
+ })),
2188
+ ],
2189
+ text: stepResult.text,
2190
+ toolCalls: executed.map((s) => ({
2191
+ toolName: s.toolName,
2192
+ toolCallId: s.toolCallId,
2193
+ input: s.input,
2194
+ })),
2195
+ toolResults: executed.map((s) => ({
2196
+ toolName: s.toolName,
2197
+ toolCallId: s.toolCallId,
2198
+ output: s.output,
2199
+ })),
2200
+ finishReason: "tool-calls",
2201
+ // prompt_tokens already includes cached_tokens; subtract it so
2202
+ // this agrees with the generate loop's toDeferredUsage, which
2203
+ // reports the disjoint noCache figure for the same field —
2204
+ // otherwise a cached step reports cached tokens twice (once
2205
+ // here, once in cacheReadTokens) and a prepareStep hook that
2206
+ // budgets on steps[].usage sees different numbers from
2207
+ // stream() and generate() for the same step.
2208
+ inputTokens: Math.max(0, (stepResult.usage?.prompt_tokens ?? 0) -
2209
+ (stepResult.usage?.prompt_tokens_details?.cached_tokens ?? 0)),
2210
+ outputTokens: stepResult.usage?.completion_tokens ?? 0,
2211
+ ...(stepResult.usage?.prompt_tokens_details?.cached_tokens !==
2212
+ undefined
2213
+ ? {
2214
+ cacheReadTokens: Math.min(stepResult.usage.prompt_tokens_details.cached_tokens, stepResult.usage.prompt_tokens ?? 0),
2215
+ }
2216
+ : {}),
2217
+ }));
2145
2218
  }
2146
2219
  resolveUsage(toDeferredUsage());
2147
2220
  resolveFinish(stepFinish ?? "stop");
@@ -2258,7 +2331,7 @@ export class OpenAIChatCompletionsProvider extends BaseProvider {
2258
2331
  });
2259
2332
  }
2260
2333
  async executeToolBatch(args) {
2261
- const { stepResult, conversation, toolsRecord, toolNameFromWire, emitter, toolsUsed, toolExecutionSummaries, options, } = args;
2334
+ const { stepResult, conversation, toolsRecord, toolNameFromWire, toolsUsed, toolExecutionSummaries, options, stepIndex, } = args;
2262
2335
  const toolCallsForMessage = [];
2263
2336
  for (const [, t] of stepResult.toolCalls) {
2264
2337
  toolCallsForMessage.push({
@@ -2295,14 +2368,14 @@ export class OpenAIChatCompletionsProvider extends BaseProvider {
2295
2368
  // loaded via search_tools — that's a real tool, not a hallucination.
2296
2369
  const toolDef = toolsRecord[registryName] ??
2297
2370
  resolveDeferredTool(toolsRecord, registryName);
2298
- emitter?.emit("tool:start", {
2299
- toolName: registryName,
2300
- toolCallId: t.id,
2301
- input,
2302
- });
2371
+ // No `tool:start` here either: registered and built-in tools emit their
2372
+ // own start/end pair from the executor that runs them, and this raw
2373
+ // emission made every one of them count twice.
2303
2374
  if (!toolDef || typeof toolDef.execute !== "function") {
2304
2375
  errorMsg = `Tool '${registryName}' is not registered.`;
2305
2376
  output = { error: errorMsg };
2377
+ // Rejected before any execute ran, so no executor will report it.
2378
+ this.emitRejectedToolCall(registryName, errorMsg, t.id);
2306
2379
  }
2307
2380
  else {
2308
2381
  try {
@@ -2323,6 +2396,7 @@ export class OpenAIChatCompletionsProvider extends BaseProvider {
2323
2396
  ...(errorMsg ? { error: errorMsg } : {}),
2324
2397
  startTime: startedAt,
2325
2398
  endTime: endedAt,
2399
+ stepIndex,
2326
2400
  });
2327
2401
  conversation.push({
2328
2402
  role: "tool",
@@ -2331,21 +2405,27 @@ export class OpenAIChatCompletionsProvider extends BaseProvider {
2331
2405
  });
2332
2406
  }
2333
2407
  const justExecuted = toolExecutionSummaries.slice(-stepResult.toolCalls.size);
2334
- emitToolEndFromStepFinish(emitter, justExecuted.map((s) => ({
2335
- toolName: s.toolName,
2336
- output: s.output,
2337
- ...(s.error ? { error: s.error } : {}),
2338
- })));
2408
+ // No `tool:end` here: the executor already emitted start/end with the
2409
+ // real duration, and a second emission per result doubled every
2410
+ // consumer's count.
2339
2411
  try {
2412
+ // `args`, not `input`: both stores read `args`, so the previous
2413
+ // `input:` key persisted every tool call with empty arguments.
2340
2414
  await this.handleToolExecutionStorage(justExecuted.map((s) => ({
2341
2415
  toolCallId: s.toolCallId,
2342
2416
  toolName: s.toolName,
2343
- input: s.input,
2344
- output: s.output,
2417
+ args: s.input !== null &&
2418
+ typeof s.input === "object" &&
2419
+ !Array.isArray(s.input)
2420
+ ? s.input
2421
+ : {},
2422
+ ...(s.stepIndex !== undefined ? { stepIndex: s.stepIndex } : {}),
2345
2423
  })), justExecuted.map((s) => ({
2346
2424
  toolCallId: s.toolCallId,
2347
2425
  toolName: s.toolName,
2348
2426
  output: s.output,
2427
+ ...(s.error ? { error: s.error } : {}),
2428
+ ...(s.stepIndex !== undefined ? { stepIndex: s.stepIndex } : {}),
2349
2429
  })), options, new Date());
2350
2430
  }
2351
2431
  catch (err) {