@juspay/neurolink 12.12.7 → 12.12.8

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.
@@ -9,7 +9,7 @@
9
9
  * params, timing, and error status. Memory is bounded: serialized results are
10
10
  * capped at `maxResultChars` and the record list at `maxRecords`.
11
11
  */
12
- import type { Tool, ToolExecutionCaptureOptions, ToolExecutionRecord } from "../types/index.js";
12
+ import type { Tool, ToolExecutionCaptureOptions, ToolExecutionRecord, StandardRecord, ToolExecutionSummaryInternal } from "../types/index.js";
13
13
  /** Default cap on serialized result characters kept per record (~8KB). */
14
14
  export declare const DEFAULT_TOOL_RESULT_CAPTURE_CHARS = 8192;
15
15
  /** Default cap on records kept per turn. */
@@ -73,4 +73,19 @@ export declare function toToolExecutionRecords(legacyExecutions: unknown[] | und
73
73
  * carried a recorder that saw executions, else a conversion of the loop's
74
74
  * legacy accumulator entries.
75
75
  */
76
+ /**
77
+ * The public `toolCalls` view of a native turn's execution summaries.
78
+ *
79
+ * The native generate loops record every executed call in
80
+ * `ToolExecutionSummaryInternal[]` and surface it as `toolExecutions`, but
81
+ * never mapped it onto `EnhancedGenerateResult.toolCalls` — the field the type
82
+ * has always declared and the ai-package formatter used to fill. A caller
83
+ * reading `result.toolCalls` after a tool ran saw nothing. This is the one
84
+ * place that mapping lives, so the three native paths cannot drift apart.
85
+ */
86
+ export declare function toolCallsFromSummaries(summaries: ReadonlyArray<ToolExecutionSummaryInternal>): Array<{
87
+ toolCallId: string;
88
+ toolName: string;
89
+ args: StandardRecord;
90
+ }>;
76
91
  export declare function resolveToolExecutionRecords(options: unknown, legacyExecutions?: unknown[]): ToolExecutionRecord[];
@@ -247,6 +247,27 @@ export function toToolExecutionRecords(legacyExecutions, capture) {
247
247
  * carried a recorder that saw executions, else a conversion of the loop's
248
248
  * legacy accumulator entries.
249
249
  */
250
+ /**
251
+ * The public `toolCalls` view of a native turn's execution summaries.
252
+ *
253
+ * The native generate loops record every executed call in
254
+ * `ToolExecutionSummaryInternal[]` and surface it as `toolExecutions`, but
255
+ * never mapped it onto `EnhancedGenerateResult.toolCalls` — the field the type
256
+ * has always declared and the ai-package formatter used to fill. A caller
257
+ * reading `result.toolCalls` after a tool ran saw nothing. This is the one
258
+ * place that mapping lives, so the three native paths cannot drift apart.
259
+ */
260
+ export function toolCallsFromSummaries(summaries) {
261
+ return summaries.map((summary) => ({
262
+ toolCallId: summary.toolCallId,
263
+ toolName: summary.toolName,
264
+ args: summary.input !== null &&
265
+ typeof summary.input === "object" &&
266
+ !Array.isArray(summary.input)
267
+ ? summary.input
268
+ : {},
269
+ }));
270
+ }
250
271
  export function resolveToolExecutionRecords(options, legacyExecutions) {
251
272
  const recorder = ToolExecutionRecorder.from(options);
252
273
  if (recorder?.hasRecords()) {
@@ -1,6 +1,45 @@
1
1
  import { createBlockedResponse, createBlockedStream, applyContentFiltering, handlePrecallGuardrails, } from "../utils/guardrailsUtils.js";
2
2
  import { logger } from "../../utils/logger.js";
3
3
  import { generateOnceNative } from "../../utils/nativeSingleShot.js";
4
+ /**
5
+ * Filter each contiguous run of text parts as one string.
6
+ *
7
+ * A prohibited term that straddles two adjacent text parts is invisible to a
8
+ * per-part filter, and the parts are concatenated downstream (`lifecycle.ts`
9
+ * joins adjacent text), so the term reached the caller whole. Anthropic's
10
+ * native path emits one text part per content block, so adjacent parts are a
11
+ * real shape, not a theoretical one. Non-text parts keep their position; a run
12
+ * is rebuilt as a single text part only when the filter changed it.
13
+ */
14
+ const filterTextRuns = (content, badWords, context) => {
15
+ const out = [];
16
+ let run = [];
17
+ const flushRun = () => {
18
+ if (run.length === 0) {
19
+ return;
20
+ }
21
+ const merged = run.map((part) => part.text).join("");
22
+ const filtered = applyContentFiltering(merged, badWords, context);
23
+ if (filtered.hasChanges) {
24
+ out.push({ ...run[0], text: filtered.filteredText });
25
+ }
26
+ else {
27
+ out.push(...run);
28
+ }
29
+ run = [];
30
+ };
31
+ for (const part of content) {
32
+ if (part.type === "text") {
33
+ run.push(part);
34
+ }
35
+ else {
36
+ flushRun();
37
+ out.push(part);
38
+ }
39
+ }
40
+ flushRun();
41
+ return out;
42
+ };
4
43
  /**
5
44
  * Create Guardrails AI middleware for content filtering and policy enforcement
6
45
  * @param config Configuration for the guardrails middleware
@@ -61,12 +100,7 @@ export function createGuardrailsMiddleware(config = {}) {
61
100
  let result = await doGenerate();
62
101
  result = {
63
102
  ...result,
64
- content: result.content.map((part) => part.type === "text"
65
- ? {
66
- ...part,
67
- text: applyContentFiltering(part.text, config.badWords, "generate").filteredText,
68
- }
69
- : part),
103
+ content: filterTextRuns(result.content, config.badWords, "generate"),
70
104
  };
71
105
  if (config.modelFilter?.enabled && config.modelFilter.filterModel) {
72
106
  logger.debug(`[GuardrailsMiddleware] Invoking model-based filter.`);
@@ -113,22 +147,38 @@ export function createGuardrailsMiddleware(config = {}) {
113
147
  }
114
148
  const { stream, ...rest } = await doStream();
115
149
  let hasYieldedChunks = false;
150
+ // With bad-word filtering on, a text run is buffered and filtered as one
151
+ // string: a term split across deltas is invisible per delta and every
152
+ // consumer reassembles it. The run is released when a non-text part
153
+ // arrives or the stream ends, so the guardrail trades incremental
154
+ // delivery of that run for not being bypassable by chunking. With
155
+ // filtering off, deltas pass through untouched and unbuffered.
156
+ const bufferTextRuns = config.badWords?.enabled === true;
157
+ let pendingText;
158
+ const releaseText = (controller) => {
159
+ if (!pendingText) {
160
+ return;
161
+ }
162
+ const filtered = applyContentFiltering(pendingText.delta, config.badWords, "stream");
163
+ controller.enqueue(filtered.hasChanges
164
+ ? { ...pendingText, delta: filtered.filteredText }
165
+ : pendingText);
166
+ pendingText = undefined;
167
+ };
116
168
  const transformStream = new TransformStream({
117
169
  transform(chunk, controller) {
118
170
  hasYieldedChunks = true;
119
- let filteredChunk = chunk;
120
- if (filteredChunk.type === "text-delta") {
121
- const filterResult = applyContentFiltering(filteredChunk.delta, config.badWords, "stream");
122
- if (filterResult.hasChanges) {
123
- filteredChunk = {
124
- ...filteredChunk,
125
- delta: filterResult.filteredText,
126
- };
127
- }
171
+ if (chunk.type === "text-delta" && bufferTextRuns) {
172
+ pendingText = pendingText
173
+ ? { ...pendingText, delta: pendingText.delta + chunk.delta }
174
+ : chunk;
175
+ return;
128
176
  }
129
- controller.enqueue(filteredChunk);
177
+ releaseText(controller);
178
+ controller.enqueue(chunk);
130
179
  },
131
- flush() {
180
+ flush(controller) {
181
+ releaseText(controller);
132
182
  if (!hasYieldedChunks) {
133
183
  logger.warn(`[GuardrailsMiddleware] Stream ended without yielding any chunks`);
134
184
  }
package/dist/neurolink.js CHANGED
@@ -4359,6 +4359,7 @@ Current user's request: ${currentInput}`;
4359
4359
  : undefined,
4360
4360
  responseTime: textResult.responseTime,
4361
4361
  toolsUsed: textResult.toolsUsed,
4362
+ toolCalls: textResult.toolCalls ?? [],
4362
4363
  toolExecutions: toToolExecutionRecords(textResult.toolExecutions),
4363
4364
  enhancedWithTools: textResult.enhancedWithTools,
4364
4365
  availableTools: transformAvailableTools(textResult.availableTools),
@@ -5877,6 +5878,7 @@ Current user's request: ${currentInput}`;
5877
5878
  rawFinishReason: result.rawFinishReason,
5878
5879
  stepsUsed: result.stepsUsed,
5879
5880
  toolsUsed: result.toolsUsed || [],
5881
+ toolCalls: result.toolCalls ?? [],
5880
5882
  toolExecutions: transformedToolExecutions,
5881
5883
  enhancedWithTools: Boolean(hasToolExecutions),
5882
5884
  availableTools: transformToolsForMCP(transformToolsToExpectedFormat(availableTools)),
@@ -6028,6 +6030,7 @@ Current user's request: ${currentInput}`;
6028
6030
  rawFinishReason: poolResult.rawFinishReason,
6029
6031
  stepsUsed: poolResult.stepsUsed,
6030
6032
  toolsUsed: poolResult.toolsUsed || [],
6033
+ toolCalls: poolResult.toolCalls ?? [],
6031
6034
  // Lossless pass-through: keep the full ToolExecutionRecord
6032
6035
  // fields (params/resultText/isError/timing) alongside the
6033
6036
  // legacy {toolName,executionTime,success} shape this internal
@@ -6398,6 +6401,9 @@ Current user's request: ${currentInput}`;
6398
6401
  rawFinishReason: result.rawFinishReason,
6399
6402
  stepsUsed: result.stepsUsed,
6400
6403
  toolsUsed: result.toolsUsed || [],
6404
+ // The providers record executed calls; without this line the public
6405
+ // result never carried them, whatever the provider returned.
6406
+ toolCalls: result.toolCalls ?? [],
6401
6407
  // Lossless pass-through: keep the full ToolExecutionRecord fields
6402
6408
  // alongside the legacy {toolName,executionTime,success} shape this
6403
6409
  // internal result declares, so the final GenerateResult mapping
@@ -2,7 +2,7 @@ import { BaseProvider } from "../core/baseProvider.js";
2
2
  import { createStreamChannel } from "../core/streamChannel.js";
3
3
  import { logger } from "../utils/logger.js";
4
4
  import { resolveRequestKind } from "../core/resolveRequestKind.js";
5
- import { resolveToolExecutionRecords } from "../core/toolExecutionRecorder.js";
5
+ import { resolveToolExecutionRecords, toolCallsFromSummaries, } from "../core/toolExecutionRecorder.js";
6
6
  import { transformToolExecutions } from "../utils/transformationUtils.js";
7
7
  import { convertZodToJsonSchema } from "../utils/schemaConversion.js";
8
8
  import { withProviderRetry } from "../utils/providerRetry.js";
@@ -198,6 +198,7 @@ export class AmazonSageMakerProvider extends BaseProvider {
198
198
  },
199
199
  responseTime: Date.now() - startTime,
200
200
  toolsUsed: loop.toolsUsed,
201
+ toolCalls: toolCallsFromSummaries(toolExecutionSummaries),
201
202
  toolExecutions: resolveToolExecutionRecords(options, transformToolExecutions(toolExecutionSummaries)),
202
203
  enhancedWithTools: loop.toolsUsed.length > 0,
203
204
  };
@@ -28,7 +28,7 @@ import { runAgenticLoop } from "../../core/loopEngine.js";
28
28
  import { hasNativeDoGenerate, runNativeGenerateLoop, } from "../../core/nativeGenerateLoop.js";
29
29
  import { withProviderRetry } from "../../utils/providerRetry.js";
30
30
  import { resolveRequestKind } from "../../core/resolveRequestKind.js";
31
- import { resolveToolExecutionRecords } from "../../core/toolExecutionRecorder.js";
31
+ import { resolveToolExecutionRecords, toolCallsFromSummaries, } from "../../core/toolExecutionRecorder.js";
32
32
  import { transformToolExecutions } from "../../utils/transformationUtils.js";
33
33
  import { createAnthropicConfig, getProviderModel, validateApiKey, } from "../../utils/providerConfig.js";
34
34
  import { composeAbortSignals, createTimeoutController, mergeAbortSignals, TimeoutError, } from "../../utils/timeout.js";
@@ -1479,6 +1479,7 @@ export class AnthropicProvider extends BaseProvider {
1479
1479
  },
1480
1480
  responseTime: Date.now() - startTime,
1481
1481
  toolsUsed: loop.toolsUsed,
1482
+ toolCalls: toolCallsFromSummaries(toolExecutionSummaries),
1482
1483
  toolExecutions: resolveToolExecutionRecords(options, transformToolExecutions(toolExecutionSummaries)),
1483
1484
  enhancedWithTools: loop.toolsUsed.length > 0,
1484
1485
  };
@@ -34,7 +34,7 @@ import { composeAbortSignalsScoped, createTimeoutController, mergeAbortSignals,
34
34
  import { emitToolEndFromStepFinish } from "../utils/toolEndEmitter.js";
35
35
  import { resolveRequestKind } from "../core/resolveRequestKind.js";
36
36
  import { appendJsonSchemaInstruction, hasNativeDoGenerate, runNativeGenerateLoop, } from "../core/nativeGenerateLoop.js";
37
- import { resolveToolExecutionRecords } from "../core/toolExecutionRecorder.js";
37
+ import { resolveToolExecutionRecords, toolCallsFromSummaries, } from "../core/toolExecutionRecorder.js";
38
38
  import { convertZodToJsonSchema } from "../utils/schemaConversion.js";
39
39
  import { coerceJsonToSchema, schemaAccepts } from "../utils/json/coerce.js";
40
40
  import { resolveToolChoice } from "../utils/toolChoice.js";
@@ -989,6 +989,7 @@ export class OpenAIChatCompletionsProvider extends BaseProvider {
989
989
  },
990
990
  responseTime: Date.now() - startTime,
991
991
  toolsUsed,
992
+ toolCalls: toolCallsFromSummaries(toolExecutionSummaries),
992
993
  toolExecutions: resolveToolExecutionRecords(options, transformToolExecutions(toolExecutionSummaries)),
993
994
  enhancedWithTools: toolsUsed.length > 0,
994
995
  };
@@ -1490,6 +1490,12 @@ export type TextGenerationResult = {
1490
1490
  model?: string;
1491
1491
  usage?: TokenUsage;
1492
1492
  responseTime?: number;
1493
+ /** The executed tool calls of a native turn — the same shape `GenerateResult` exposes. */
1494
+ toolCalls?: Array<{
1495
+ toolCallId: string;
1496
+ toolName: string;
1497
+ args: StandardRecord;
1498
+ }>;
1493
1499
  toolsUsed?: string[];
1494
1500
  toolExecutions?: Array<{
1495
1501
  toolName: string;
@@ -3,7 +3,7 @@ import type { EvaluationData, GetPromptFunction } from "./evaluation.js";
3
3
  import type { AuthenticatedUser, RouteDefinition, ServerContext } from "./server.js";
4
4
  import type { LanguageModelMiddleware as BaseLanguageModelMiddleware } from "./aiCompat.js";
5
5
  export type { LanguageModelMiddleware } from "./aiCompat.js";
6
- export type { LanguageModelV3, LanguageModelV3CallOptions, LanguageModelV3Message, LanguageModelV3Prompt, LanguageModelV3StreamPart, LanguageModelV3ToolCall, LanguageModelV3ToolChoice, LanguageModelV3Source, LanguageModelV3Middleware, LanguageModelV3GenerateResult, LanguageModelV3StreamResult, JSONSchema7, } from "./aiCompat.js";
6
+ export type { LanguageModelV3, LanguageModelV3CallOptions, LanguageModelV3Message, LanguageModelV3Prompt, LanguageModelV3StreamPart, LanguageModelV3Content, LanguageModelV3ToolCall, LanguageModelV3ToolChoice, LanguageModelV3Source, LanguageModelV3Middleware, LanguageModelV3GenerateResult, LanguageModelV3StreamResult, JSONSchema7, } from "./aiCompat.js";
7
7
  /**
8
8
  * Metadata type for NeuroLink middleware
9
9
  * Provides additional information about middleware without affecting execution
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@juspay/neurolink",
3
- "version": "12.12.7",
3
+ "version": "12.12.8",
4
4
  "packageManager": "pnpm@10.15.1",
5
5
  "description": "TypeScript AI SDK with 24+ LLM providers behind one consistent API. MCP-native (connect any MCP server), voice TTS/STT/realtime, RAG, agents, memory, context compaction. OpenAI · Anthropic · Gemini · Bedrock · Azure · Ollama · DeepSeek · NVIDIA NIM and more.",
6
6
  "author": {
@@ -112,6 +112,7 @@
112
112
  "test:mcp:spans": "pnpm exec tsx test/continuous-test-suite-mcp-spans.ts",
113
113
  "test:mcp:infra": "pnpm exec tsx test/continuous-test-suite-mcp-infra.ts",
114
114
  "test:vendor-recovery": "pnpm exec tsx test/continuous-test-suite-native-vendor-recovery.ts",
115
+ "test:browser-bundle": "pnpm exec tsx test/continuous-test-suite-browser-bundle.ts",
115
116
  "test:providers-mocked": "pnpm exec tsx test/continuous-test-suite-providers-mocked.ts",
116
117
  "scaffold:provider": "pnpm exec tsx tools/scaffold-provider.ts",
117
118
  "verify:provider-onboarding": "pnpm exec tsx tools/verify-provider-onboarding.ts",