@juspay/neurolink 10.5.2 → 10.6.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 +12 -0
  2. package/dist/agent/agent.js +39 -8
  3. package/dist/agent/agentNetwork.js +62 -20
  4. package/dist/agent/agentToolRegistrar.d.ts +48 -0
  5. package/dist/agent/agentToolRegistrar.js +336 -0
  6. package/dist/agent/index.d.ts +3 -0
  7. package/dist/agent/index.js +4 -0
  8. package/dist/agent/isolatedAgentRunner.d.ts +73 -0
  9. package/dist/agent/isolatedAgentRunner.js +946 -0
  10. package/dist/agent/structuredRecovery.d.ts +30 -0
  11. package/dist/agent/structuredRecovery.js +104 -0
  12. package/dist/browser/neurolink.min.js +414 -398
  13. package/dist/cli/loop/optionsSchema.d.ts +1 -1
  14. package/dist/core/baseProvider.d.ts +8 -0
  15. package/dist/core/baseProvider.js +40 -19
  16. package/dist/core/modules/GenerationHandler.d.ts +2 -6
  17. package/dist/core/modules/GenerationHandler.js +10 -1
  18. package/dist/core/toolExecutionRecorder.d.ts +76 -0
  19. package/dist/core/toolExecutionRecorder.js +261 -0
  20. package/dist/evaluation/contextBuilder.js +6 -6
  21. package/dist/lib/agent/agent.js +39 -8
  22. package/dist/lib/agent/agentNetwork.js +62 -20
  23. package/dist/lib/agent/agentToolRegistrar.d.ts +48 -0
  24. package/dist/lib/agent/agentToolRegistrar.js +337 -0
  25. package/dist/lib/agent/index.d.ts +3 -0
  26. package/dist/lib/agent/index.js +4 -0
  27. package/dist/lib/agent/isolatedAgentRunner.d.ts +73 -0
  28. package/dist/lib/agent/isolatedAgentRunner.js +947 -0
  29. package/dist/lib/agent/structuredRecovery.d.ts +30 -0
  30. package/dist/lib/agent/structuredRecovery.js +105 -0
  31. package/dist/lib/core/baseProvider.d.ts +8 -0
  32. package/dist/lib/core/baseProvider.js +40 -19
  33. package/dist/lib/core/modules/GenerationHandler.d.ts +2 -6
  34. package/dist/lib/core/modules/GenerationHandler.js +10 -1
  35. package/dist/lib/core/toolExecutionRecorder.d.ts +76 -0
  36. package/dist/lib/core/toolExecutionRecorder.js +262 -0
  37. package/dist/lib/evaluation/contextBuilder.js +6 -6
  38. package/dist/lib/models/modelRegistry.d.ts +23 -0
  39. package/dist/lib/models/modelRegistry.js +62 -1
  40. package/dist/lib/models/modelResolver.js +1 -0
  41. package/dist/lib/neurolink.d.ts +80 -1
  42. package/dist/lib/neurolink.js +199 -37
  43. package/dist/lib/providers/amazonBedrock.js +15 -2
  44. package/dist/lib/providers/anthropic.js +23 -11
  45. package/dist/lib/providers/googleAiStudio.js +8 -1
  46. package/dist/lib/providers/googleNativeGemini3.d.ts +2 -0
  47. package/dist/lib/providers/googleNativeGemini3.js +8 -1
  48. package/dist/lib/providers/googleVertex.js +56 -16
  49. package/dist/lib/providers/openaiChatCompletionsClient.js +18 -5
  50. package/dist/lib/tasks/autoresearchTaskExecutor.js +7 -6
  51. package/dist/lib/tasks/taskExecutor.js +2 -5
  52. package/dist/lib/types/agentNetwork.d.ts +23 -8
  53. package/dist/lib/types/generate.d.ts +62 -5
  54. package/dist/lib/types/index.d.ts +1 -0
  55. package/dist/lib/types/index.js +1 -0
  56. package/dist/lib/types/isolatedAgent.d.ts +365 -0
  57. package/dist/lib/types/isolatedAgent.js +12 -0
  58. package/dist/lib/types/model.d.ts +8 -0
  59. package/dist/lib/types/stream.d.ts +3 -1
  60. package/dist/lib/types/task.d.ts +3 -3
  61. package/dist/lib/utils/logger.d.ts +12 -3
  62. package/dist/lib/utils/logger.js +11 -3
  63. package/dist/lib/utils/transformationUtils.d.ts +14 -0
  64. package/dist/lib/utils/transformationUtils.js +33 -10
  65. package/dist/lib/voice/livekit/realtimeVoiceAgent.js +6 -3
  66. package/dist/models/modelRegistry.d.ts +23 -0
  67. package/dist/models/modelRegistry.js +62 -1
  68. package/dist/models/modelResolver.js +1 -0
  69. package/dist/neurolink.d.ts +80 -1
  70. package/dist/neurolink.js +199 -37
  71. package/dist/providers/amazonBedrock.js +15 -2
  72. package/dist/providers/anthropic.js +23 -11
  73. package/dist/providers/googleAiStudio.js +8 -1
  74. package/dist/providers/googleNativeGemini3.d.ts +2 -0
  75. package/dist/providers/googleNativeGemini3.js +8 -1
  76. package/dist/providers/googleVertex.js +56 -16
  77. package/dist/providers/openaiChatCompletionsClient.js +18 -5
  78. package/dist/tasks/autoresearchTaskExecutor.js +7 -6
  79. package/dist/tasks/taskExecutor.js +2 -5
  80. package/dist/types/agentNetwork.d.ts +23 -8
  81. package/dist/types/generate.d.ts +62 -5
  82. package/dist/types/index.d.ts +1 -0
  83. package/dist/types/index.js +1 -0
  84. package/dist/types/isolatedAgent.d.ts +365 -0
  85. package/dist/types/isolatedAgent.js +11 -0
  86. package/dist/types/model.d.ts +8 -0
  87. package/dist/types/stream.d.ts +3 -1
  88. package/dist/types/task.d.ts +3 -3
  89. package/dist/utils/logger.d.ts +12 -3
  90. package/dist/utils/logger.js +11 -3
  91. package/dist/utils/transformationUtils.d.ts +14 -0
  92. package/dist/utils/transformationUtils.js +33 -10
  93. package/dist/voice/livekit/realtimeVoiceAgent.js +6 -3
  94. package/package.json +3 -2
@@ -0,0 +1,30 @@
1
+ /**
2
+ * Structured-output recovery ladder for the isolated-agent runner.
3
+ *
4
+ * Ports the production-hardened pattern the framework's consumers built by
5
+ * hand: candidates are tried in a fixed order — provider `structuredData` →
6
+ * raw JSON object → ```json fence → first/last-brace span → top-level array —
7
+ * each passed through the caller's `coerce` normalizer and validated against
8
+ * the (lenient) local schema. The first schema-valid candidate wins;
9
+ * validation errors are collected for corrective re-asks.
10
+ */
11
+ import type { z } from "zod";
12
+ import type { StructuredRecoveryCandidate, StructuredRecoveryResult } from "../types/index.js";
13
+ /**
14
+ * Collect parse candidates from model text (and provider structuredData) in
15
+ * ladder order. Never throws.
16
+ */
17
+ export declare function collectStructuredCandidates(content: string | undefined, structuredData?: unknown): StructuredRecoveryCandidate[];
18
+ /**
19
+ * Run the full ladder: candidates (through `coerce`) validated against
20
+ * `schema`; first success wins. Never throws — coerce/validation failures
21
+ * become entries in `errors`.
22
+ */
23
+ export declare function recoverStructuredData(args: {
24
+ content: string | undefined;
25
+ structuredData?: unknown;
26
+ schema: z.ZodSchema;
27
+ coerce?: (candidate: unknown) => unknown;
28
+ }): StructuredRecoveryResult;
29
+ /** Bounded, prompt-friendly summary of a Zod validation error. */
30
+ export declare function summarizeZodError(error: z.ZodError): string;
@@ -0,0 +1,105 @@
1
+ /**
2
+ * Structured-output recovery ladder for the isolated-agent runner.
3
+ *
4
+ * Ports the production-hardened pattern the framework's consumers built by
5
+ * hand: candidates are tried in a fixed order — provider `structuredData` →
6
+ * raw JSON object → ```json fence → first/last-brace span → top-level array —
7
+ * each passed through the caller's `coerce` normalizer and validated against
8
+ * the (lenient) local schema. The first schema-valid candidate wins;
9
+ * validation errors are collected for corrective re-asks.
10
+ */
11
+ function tryParse(text) {
12
+ try {
13
+ return JSON.parse(text);
14
+ }
15
+ catch {
16
+ return undefined;
17
+ }
18
+ }
19
+ /**
20
+ * Collect parse candidates from model text (and provider structuredData) in
21
+ * ladder order. Never throws.
22
+ */
23
+ export function collectStructuredCandidates(content, structuredData) {
24
+ const candidates = [];
25
+ if (structuredData !== undefined && structuredData !== null) {
26
+ candidates.push({ source: "structured-data", value: structuredData });
27
+ }
28
+ const text = (content ?? "").trim();
29
+ if (!text) {
30
+ return candidates;
31
+ }
32
+ // Raw JSON object/array/scalar
33
+ const raw = tryParse(text);
34
+ if (raw !== undefined) {
35
+ candidates.push({ source: "raw-json", value: raw });
36
+ }
37
+ // ```json fenced block (first fence wins)
38
+ const fenceMatch = text.match(/```(?:json)?\s*\n?([\s\S]*?)\n?```/i);
39
+ if (fenceMatch?.[1]) {
40
+ const fenced = tryParse(fenceMatch[1].trim());
41
+ if (fenced !== undefined) {
42
+ candidates.push({ source: "json-fence", value: fenced });
43
+ }
44
+ }
45
+ // First-brace .. last-brace span
46
+ const firstBrace = text.indexOf("{");
47
+ const lastBrace = text.lastIndexOf("}");
48
+ if (firstBrace !== -1 && lastBrace > firstBrace) {
49
+ const span = tryParse(text.slice(firstBrace, lastBrace + 1));
50
+ if (span !== undefined) {
51
+ candidates.push({ source: "brace-span", value: span });
52
+ }
53
+ }
54
+ // Top-level array span
55
+ const firstBracket = text.indexOf("[");
56
+ const lastBracket = text.lastIndexOf("]");
57
+ if (firstBracket !== -1 && lastBracket > firstBracket) {
58
+ const arraySpan = tryParse(text.slice(firstBracket, lastBracket + 1));
59
+ if (arraySpan !== undefined && Array.isArray(arraySpan)) {
60
+ candidates.push({ source: "top-level-array", value: arraySpan });
61
+ }
62
+ }
63
+ return candidates;
64
+ }
65
+ /**
66
+ * Run the full ladder: candidates (through `coerce`) validated against
67
+ * `schema`; first success wins. Never throws — coerce/validation failures
68
+ * become entries in `errors`.
69
+ */
70
+ export function recoverStructuredData(args) {
71
+ const errors = [];
72
+ const candidates = collectStructuredCandidates(args.content, args.structuredData);
73
+ for (const candidate of candidates) {
74
+ let value = candidate.value;
75
+ if (args.coerce) {
76
+ try {
77
+ value = args.coerce(value);
78
+ }
79
+ catch (coerceError) {
80
+ errors.push(`${candidate.source}: coerce failed — ${coerceError instanceof Error
81
+ ? coerceError.message
82
+ : String(coerceError)}`);
83
+ continue;
84
+ }
85
+ }
86
+ const parsed = args.schema.safeParse(value);
87
+ if (parsed.success) {
88
+ return { data: parsed.data, source: candidate.source, errors };
89
+ }
90
+ errors.push(`${candidate.source}: ${summarizeZodError(parsed.error)}`);
91
+ }
92
+ if (candidates.length === 0) {
93
+ errors.push("no JSON-shaped candidates found in output");
94
+ }
95
+ return { errors };
96
+ }
97
+ /** Bounded, prompt-friendly summary of a Zod validation error. */
98
+ export function summarizeZodError(error) {
99
+ const issues = error.issues
100
+ .slice(0, 5)
101
+ .map((issue) => `${issue.path.join(".") || "(root)"}: ${issue.message}`);
102
+ const extra = error.issues.length > 5 ? ` (+${error.issues.length - 5})` : "";
103
+ return issues.join("; ") + extra;
104
+ }
105
+ //# sourceMappingURL=structuredRecovery.js.map
@@ -132,6 +132,14 @@ export declare abstract class BaseProvider implements AIProvider {
132
132
  * All providers should call this instead of getAllTools() directly.
133
133
  */
134
134
  protected getToolsForStream(options: StreamOptions | TextGenerationOptions): Promise<Record<string, Tool>>;
135
+ /**
136
+ * Create (or reuse) the per-call ToolExecutionRecorder and wrap the final
137
+ * tool record with it. The recorder rides on the options object so provider
138
+ * loops and result assembly observe the same capture state; wrapping is
139
+ * idempotent, so paths that re-enter (stream→generate fallback) never
140
+ * double-record.
141
+ */
142
+ protected wrapToolsForExecutionCapture(tools: Record<string, Tool>, options: StreamOptions | TextGenerationOptions): Record<string, Tool>;
135
143
  /**
136
144
  * Build messages array for generation - delegated to MessageBuilder
137
145
  */
@@ -12,6 +12,7 @@ import { withTimeoutFn } from "../utils/async/withTimeout.js";
12
12
  import { composeAbortSignals, createTimeoutController, TimeoutError, } from "../utils/timeout.js";
13
13
  import { shouldDisableBuiltinTools } from "../utils/toolUtils.js";
14
14
  import { getKeyCount, getKeysAsString } from "../utils/transformationUtils.js";
15
+ import { ToolExecutionRecorder, resolveToolExecutionRecords, } from "./toolExecutionRecorder.js";
15
16
  import { TTSProcessor } from "../utils/ttsProcessor.js";
16
17
  import { executeVideoAnalysis, hasVideoFrames, } from "../utils/videoAnalysisProcessor.js";
17
18
  import { dedupeTools } from "./toolDedup.js";
@@ -759,6 +760,11 @@ export class BaseProvider {
759
760
  : {};
760
761
  // Apply per-call tool filtering (whitelist/blacklist)
761
762
  tools = this.applyToolFiltering(tools, options);
763
+ // Per-call execution capture: wrap every executable tool so real
764
+ // params/results/timing surface on result.toolExecutions. Must run
765
+ // BEFORE discovery — search_tools hydration mutates the discovery
766
+ // record in place, so tools hydrated mid-turn stay wrapped.
767
+ tools = this.wrapToolsForExecutionCapture(tools, options);
762
768
  // On-demand discovery: defer external MCP schemas behind search_tools
763
769
  tools = await this.applyToolDiscovery(tools, options);
764
770
  logger.debug(`Final tools prepared for AI`, {
@@ -792,6 +798,10 @@ export class BaseProvider {
792
798
  let merged = { ...baseTools, ...externalTools };
793
799
  // Apply per-call tool filtering (whitelist/blacklist)
794
800
  merged = this.applyToolFiltering(merged, options);
801
+ // Per-call execution capture (native loops obtain their tools here, so
802
+ // this single wrap covers the Gemini/Anthropic native paths too). Must
803
+ // run BEFORE discovery so tools hydrated mid-turn stay wrapped.
804
+ merged = this.wrapToolsForExecutionCapture(merged, options);
795
805
  // On-demand discovery: defer external MCP schemas behind search_tools
796
806
  merged = await this.applyToolDiscovery(merged, options);
797
807
  logger.debug(`Tools prepared for streaming`, {
@@ -802,6 +812,24 @@ export class BaseProvider {
802
812
  });
803
813
  return merged;
804
814
  }
815
+ /**
816
+ * Create (or reuse) the per-call ToolExecutionRecorder and wrap the final
817
+ * tool record with it. The recorder rides on the options object so provider
818
+ * loops and result assembly observe the same capture state; wrapping is
819
+ * idempotent, so paths that re-enter (stream→generate fallback) never
820
+ * double-record.
821
+ */
822
+ wrapToolsForExecutionCapture(tools, options) {
823
+ if (Object.keys(tools).length === 0) {
824
+ return tools;
825
+ }
826
+ let recorder = ToolExecutionRecorder.from(options);
827
+ if (!recorder) {
828
+ recorder = new ToolExecutionRecorder(options.toolExecutionCapture);
829
+ recorder.attachTo(options);
830
+ }
831
+ return recorder.wrapTools(tools);
832
+ }
805
833
  /**
806
834
  * Build messages array for generation - delegated to MessageBuilder
807
835
  */
@@ -1091,7 +1119,11 @@ export class BaseProvider {
1091
1119
  const responseTime = Date.now() - startTime;
1092
1120
  await this.recordPerformanceMetrics(generateResult.usage, responseTime);
1093
1121
  const { toolsUsed, toolExecutions } = this.extractToolInformation(generateResult);
1094
- let enhancedResult = this.formatEnhancedResult(generateResult, tools, toolsUsed, toolExecutions, options);
1122
+ // Prefer the per-call recorder's real records (params/result/timing per
1123
+ // execution); fall back to a conversion of the step-extraction entries
1124
+ // for tools the recorder could not wrap (provider-executed tools).
1125
+ const toolExecutionRecords = resolveToolExecutionRecords(options, toolExecutions);
1126
+ let enhancedResult = this.formatEnhancedResult(generateResult, tools, toolsUsed, toolExecutionRecords, options);
1095
1127
  enhancedResult = await this.synthesizeAIResponseIfNeeded(enhancedResult, options);
1096
1128
  const finalResult = await this.enhanceResult(enhancedResult, options, startTime);
1097
1129
  return finalResult;
@@ -1160,24 +1192,13 @@ export class BaseProvider {
1160
1192
  },
1161
1193
  responseTime: 0, // BaseProvider doesn't track response time directly
1162
1194
  toolsUsed: result.toolsUsed || [],
1163
- // Map toolExecutions from EnhancedGenerateResult shape to TextGenerationResult shape
1164
- // Preserve original timing/status fields when present, fall back to safe defaults
1165
- toolExecutions: result.toolExecutions?.map((te) => {
1166
- const t = te;
1167
- return {
1168
- // Spread original fields first so normalized fields take precedence
1169
- ...te,
1170
- toolName: te.name,
1171
- executionTime: typeof t.executionTime === "number"
1172
- ? t.executionTime
1173
- : typeof t.duration === "number"
1174
- ? t.duration
1175
- : 0,
1176
- success: typeof t.success === "boolean"
1177
- ? t.success
1178
- : t.status === undefined || t.status === "success",
1179
- };
1180
- }),
1195
+ // Map ToolExecutionRecord entries to the legacy TextGenerationResult
1196
+ // shape: real timing and error status now come from the records.
1197
+ toolExecutions: result.toolExecutions?.map((te) => ({
1198
+ toolName: te.toolName,
1199
+ executionTime: te.durationMs,
1200
+ success: !te.isError,
1201
+ })),
1181
1202
  enhancedWithTools: !!(result.toolsUsed && result.toolsUsed.length > 0),
1182
1203
  analytics: result.analytics,
1183
1204
  evaluation: result.evaluation,
@@ -12,7 +12,7 @@
12
12
  *
13
13
  * @module core/modules/GenerationHandler
14
14
  */
15
- import type { AIProviderName, EnhancedGenerateResult, NeuroLinkEvents, StandardRecord, TextGenerationOptions, TypedEventEmitter } from "../../types/index.js";
15
+ import type { AIProviderName, EnhancedGenerateResult, NeuroLinkEvents, StandardRecord, TextGenerationOptions, ToolExecutionRecord, TypedEventEmitter } from "../../types/index.js";
16
16
  import type { LanguageModel, ModelMessage, Tool } from "../../types/index.js";
17
17
  import { generateText } from "../../utils/generation.js";
18
18
  /**
@@ -87,11 +87,7 @@ export declare class GenerationHandler {
87
87
  /**
88
88
  * Format the enhanced result
89
89
  */
90
- formatEnhancedResult(generateResult: Awaited<ReturnType<typeof generateText>>, tools: Record<string, Tool>, toolsUsed: string[], toolExecutions: Array<{
91
- name: string;
92
- input: StandardRecord;
93
- output: unknown;
94
- }>, options: TextGenerationOptions): EnhancedGenerateResult;
90
+ formatEnhancedResult(generateResult: Awaited<ReturnType<typeof generateText>>, tools: Record<string, Tool>, toolsUsed: string[], toolExecutions: ToolExecutionRecord[], options: TextGenerationOptions): EnhancedGenerateResult;
95
91
  /**
96
92
  * Analyze AI response structure and log detailed debugging information
97
93
  */
@@ -14,6 +14,7 @@
14
14
  */
15
15
  import { SpanKind, SpanStatusCode } from "@opentelemetry/api";
16
16
  import { getModelId } from "../../providers/providerTypeUtils.js";
17
+ import { resolveSamplingParams } from "../../models/modelRegistry.js";
17
18
  import { tracers } from "../../telemetry/tracers.js";
18
19
  import { logger } from "../../utils/logger.js";
19
20
  import { emitToolEndFromStepFinish } from "../../utils/toolEndEmitter.js";
@@ -256,6 +257,14 @@ export class GenerationHandler {
256
257
  return cachedOverhead.tokens;
257
258
  },
258
259
  });
260
+ // Registry-driven strip: models that reject sampling params (Sonnet 5 /
261
+ // Opus 4.7+ / Fable 5 families — e.g. Claude on Bedrock or behind any
262
+ // AI-SDK provider) must not receive temperature. Applies uniformly to
263
+ // every provider on this loop path; the reactive
264
+ // isTemperatureDeprecatedError retry below remains the safety net.
265
+ const samplingParams = resolveSamplingParams(this.providerName, getModelId(model, this.modelName || ""), options.temperature !== undefined
266
+ ? { temperature: options.temperature }
267
+ : {}, "aiSdk.generateText");
259
268
  const result = await generateText({
260
269
  model,
261
270
  ...(system && { system }),
@@ -308,7 +317,7 @@ export class GenerationHandler {
308
317
  }
309
318
  return prepared;
310
319
  }),
311
- temperature: options.temperature,
320
+ temperature: samplingParams.temperature,
312
321
  maxOutputTokens: options.maxTokens,
313
322
  maxRetries: 0, // NL11: Disable AI SDK's invisible internal retries; we handle retries with OTel instrumentation
314
323
  abortSignal: options.abortSignal,
@@ -0,0 +1,76 @@
1
+ /**
2
+ * ToolExecutionRecorder — real per-call tool execution capture.
3
+ *
4
+ * One recorder is created per generate()/stream() call and attached to the
5
+ * request options (surviving option spreads). Tools are wrapped exactly once
6
+ * at the central tool-assembly points (BaseProvider.prepareGenerationContext /
7
+ * getToolsForStream), so every loop — the AI-SDK loop and the native
8
+ * Gemini/Anthropic loops alike — records through the same wrapper with real
9
+ * params, timing, and error status. Memory is bounded: serialized results are
10
+ * capped at `maxResultChars` and the record list at `maxRecords`.
11
+ */
12
+ import type { Tool, ToolExecutionCaptureOptions, ToolExecutionRecord } from "../types/index.js";
13
+ /** Default cap on serialized result characters kept per record (~8KB). */
14
+ export declare const DEFAULT_TOOL_RESULT_CAPTURE_CHARS = 8192;
15
+ /** Default cap on records kept per turn. */
16
+ export declare const DEFAULT_TOOL_EXECUTION_MAX_RECORDS = 500;
17
+ /**
18
+ * Serialize a tool result to bounded text. JSON when serializable, else
19
+ * String(); truncation is explicit so consumers know the text is partial.
20
+ */
21
+ export declare function serializeToolResult(result: unknown, maxChars: number): string;
22
+ /**
23
+ * Error-shaped tool results (MCP `isError`, `{error}` payloads) count as
24
+ * failures even though the loop received them as normal returns.
25
+ */
26
+ export declare function isErrorShapedToolResult(result: unknown): boolean;
27
+ /**
28
+ * Per-call recorder for tool executions. See module doc.
29
+ */
30
+ export declare class ToolExecutionRecorder {
31
+ #private;
32
+ constructor(capture?: ToolExecutionCaptureOptions);
33
+ /** Record one completed (or failed) execution. Never throws. */
34
+ record(entry: {
35
+ toolName: string;
36
+ params: unknown;
37
+ result?: unknown;
38
+ error?: unknown;
39
+ startedAt: number;
40
+ durationMs: number;
41
+ }): void;
42
+ /** All records captured so far (oldest first, bounded by maxRecords). */
43
+ getRecords(): ToolExecutionRecord[];
44
+ /** True when at least one execution was recorded. */
45
+ hasRecords(): boolean;
46
+ /** Number of records evicted by the maxRecords cap. */
47
+ getDroppedCount(): number;
48
+ /**
49
+ * Wrap every executable tool so its invocations are recorded. Wrapping is
50
+ * idempotent — already-wrapped executes are left untouched, so a generate
51
+ * that falls back to another path never double-records.
52
+ */
53
+ wrapTools(tools: Record<string, Tool>): Record<string, Tool>;
54
+ /**
55
+ * Attach this recorder to a request options object. The property is
56
+ * enumerable on purpose: downstream stages spread options (`{...options}`)
57
+ * and the recorder must survive into the provider loops.
58
+ */
59
+ attachTo(options: Record<string, unknown>): void;
60
+ /** Retrieve the recorder a request is carrying, if any. */
61
+ static from(options: unknown): ToolExecutionRecorder | undefined;
62
+ }
63
+ /**
64
+ * Convert legacy loop-local `{name, input, output}` entries (plus optional
65
+ * timing/error fields) into `ToolExecutionRecord`s. Used as the fallback when
66
+ * a path produced executions the recorder did not see (e.g. results captured
67
+ * before the recorder existed, or breaker/not-found entries that never
68
+ * reached a real execute()).
69
+ */
70
+ export declare function toToolExecutionRecords(legacyExecutions: unknown[] | undefined, capture?: ToolExecutionCaptureOptions): ToolExecutionRecord[];
71
+ /**
72
+ * Resolve the toolExecutions for a result: recorder records when the request
73
+ * carried a recorder that saw executions, else a conversion of the loop's
74
+ * legacy accumulator entries.
75
+ */
76
+ export declare function resolveToolExecutionRecords(options: unknown, legacyExecutions?: unknown[]): ToolExecutionRecord[];
@@ -0,0 +1,262 @@
1
+ /**
2
+ * ToolExecutionRecorder — real per-call tool execution capture.
3
+ *
4
+ * One recorder is created per generate()/stream() call and attached to the
5
+ * request options (surviving option spreads). Tools are wrapped exactly once
6
+ * at the central tool-assembly points (BaseProvider.prepareGenerationContext /
7
+ * getToolsForStream), so every loop — the AI-SDK loop and the native
8
+ * Gemini/Anthropic loops alike — records through the same wrapper with real
9
+ * params, timing, and error status. Memory is bounded: serialized results are
10
+ * capped at `maxResultChars` and the record list at `maxRecords`.
11
+ */
12
+ import { logger } from "../utils/logger.js";
13
+ /** Default cap on serialized result characters kept per record (~8KB). */
14
+ export const DEFAULT_TOOL_RESULT_CAPTURE_CHARS = 8192;
15
+ /** Default cap on records kept per turn. */
16
+ export const DEFAULT_TOOL_EXECUTION_MAX_RECORDS = 500;
17
+ /** Marker set on execute functions the recorder has already wrapped. */
18
+ const RECORDER_WRAPPED = Symbol.for("neurolink.toolExecutionRecorder.wrapped");
19
+ /** Internal options key the per-call recorder rides on (enumerable so it survives option spreads). */
20
+ const RECORDER_OPTIONS_KEY = "toolExecutionRecorder";
21
+ /**
22
+ * Serialize a tool result to bounded text. JSON when serializable, else
23
+ * String(); truncation is explicit so consumers know the text is partial.
24
+ */
25
+ export function serializeToolResult(result, maxChars) {
26
+ let text;
27
+ if (typeof result === "string") {
28
+ text = result;
29
+ }
30
+ else {
31
+ try {
32
+ text = JSON.stringify(result) ?? String(result);
33
+ }
34
+ catch {
35
+ text = String(result);
36
+ }
37
+ }
38
+ if (text.length > maxChars) {
39
+ const dropped = text.length - maxChars;
40
+ text = `${text.slice(0, maxChars)}…[truncated ${dropped} chars]`;
41
+ }
42
+ return text;
43
+ }
44
+ /**
45
+ * Error-shaped tool results (MCP `isError`, `{error}` payloads) count as
46
+ * failures even though the loop received them as normal returns.
47
+ */
48
+ export function isErrorShapedToolResult(result) {
49
+ if (!result || typeof result !== "object") {
50
+ return false;
51
+ }
52
+ const record = result;
53
+ if (record.isError === true) {
54
+ return true;
55
+ }
56
+ if (record.error !== undefined && record.error !== null) {
57
+ return true;
58
+ }
59
+ if (typeof record.status === "string") {
60
+ return ["error", "failed", "failure", "fail"].includes(record.status.toLowerCase().trim());
61
+ }
62
+ return false;
63
+ }
64
+ /**
65
+ * Per-call recorder for tool executions. See module doc.
66
+ */
67
+ export class ToolExecutionRecorder {
68
+ // ES #private fields on purpose: the recorder rides on request options as
69
+ // an ENUMERABLE property (spread survival), so TS-only `private` fields
70
+ // would still be dragged into any JSON.stringify(options) — up to
71
+ // maxRecords × maxResultChars of tool results in a debug dump. #fields are
72
+ // invisible to serialization: the recorder stringifies as `{}`.
73
+ #maxResultChars;
74
+ #maxRecords;
75
+ #onRecord;
76
+ #records = [];
77
+ #droppedRecords = 0;
78
+ constructor(capture) {
79
+ this.#maxResultChars =
80
+ capture?.maxResultChars ?? DEFAULT_TOOL_RESULT_CAPTURE_CHARS;
81
+ this.#maxRecords =
82
+ capture?.maxRecords ?? DEFAULT_TOOL_EXECUTION_MAX_RECORDS;
83
+ this.#onRecord = capture?.onRecord;
84
+ }
85
+ /** Record one completed (or failed) execution. Never throws. */
86
+ record(entry) {
87
+ try {
88
+ const failed = entry.error !== undefined;
89
+ const record = {
90
+ toolName: entry.toolName,
91
+ params: entry.params,
92
+ resultText: serializeToolResult(failed
93
+ ? entry.error instanceof Error
94
+ ? entry.error.message
95
+ : entry.error
96
+ : entry.result, this.#maxResultChars),
97
+ isError: failed || isErrorShapedToolResult(entry.result),
98
+ startedAt: entry.startedAt,
99
+ durationMs: entry.durationMs,
100
+ };
101
+ this.#records.push(record);
102
+ if (this.#records.length > this.#maxRecords) {
103
+ this.#records.splice(0, this.#records.length - this.#maxRecords);
104
+ this.#droppedRecords++;
105
+ }
106
+ if (this.#onRecord) {
107
+ try {
108
+ // Swallow async rejections too — the contract is that listener
109
+ // errors (sync OR async) never disrupt the turn.
110
+ void Promise.resolve(this.#onRecord(record)).catch(() => {
111
+ // Listener errors never break the run.
112
+ });
113
+ }
114
+ catch {
115
+ // Listener errors never break the run.
116
+ }
117
+ }
118
+ }
119
+ catch (recordError) {
120
+ logger.debug("[ToolExecutionRecorder] Failed to record execution", {
121
+ error: recordError instanceof Error
122
+ ? recordError.message
123
+ : String(recordError),
124
+ });
125
+ }
126
+ }
127
+ /** All records captured so far (oldest first, bounded by maxRecords). */
128
+ getRecords() {
129
+ return [...this.#records];
130
+ }
131
+ /** True when at least one execution was recorded. */
132
+ hasRecords() {
133
+ return this.#records.length > 0;
134
+ }
135
+ /** Number of records evicted by the maxRecords cap. */
136
+ getDroppedCount() {
137
+ return this.#droppedRecords;
138
+ }
139
+ /**
140
+ * Wrap every executable tool so its invocations are recorded. Wrapping is
141
+ * idempotent — already-wrapped executes are left untouched, so a generate
142
+ * that falls back to another path never double-records.
143
+ */
144
+ wrapTools(tools) {
145
+ // Null prototype: a tool named "__proto__" must stay an own entry
146
+ // (mirrors BaseProvider.sortToolRecord / toolDiscovery partitioning).
147
+ const wrapped = Object.create(null);
148
+ for (const [name, tool] of Object.entries(tools)) {
149
+ const execute = tool.execute;
150
+ if (typeof execute !== "function" ||
151
+ execute[RECORDER_WRAPPED]) {
152
+ wrapped[name] = tool;
153
+ continue;
154
+ }
155
+ const recorder = this;
156
+ const recordingExecute = async (params, execOptions) => {
157
+ const startedAt = Date.now();
158
+ try {
159
+ const result = await execute(params, execOptions);
160
+ recorder.record({
161
+ toolName: name,
162
+ params,
163
+ result,
164
+ startedAt,
165
+ durationMs: Date.now() - startedAt,
166
+ });
167
+ return result;
168
+ }
169
+ catch (error) {
170
+ recorder.record({
171
+ toolName: name,
172
+ params,
173
+ error,
174
+ startedAt,
175
+ durationMs: Date.now() - startedAt,
176
+ });
177
+ throw error;
178
+ }
179
+ };
180
+ recordingExecute[RECORDER_WRAPPED] = true;
181
+ wrapped[name] = { ...tool, execute: recordingExecute };
182
+ }
183
+ return wrapped;
184
+ }
185
+ /**
186
+ * Attach this recorder to a request options object. The property is
187
+ * enumerable on purpose: downstream stages spread options (`{...options}`)
188
+ * and the recorder must survive into the provider loops.
189
+ */
190
+ attachTo(options) {
191
+ options[RECORDER_OPTIONS_KEY] = this;
192
+ }
193
+ /** Retrieve the recorder a request is carrying, if any. */
194
+ static from(options) {
195
+ if (!options || typeof options !== "object") {
196
+ return undefined;
197
+ }
198
+ const candidate = options[RECORDER_OPTIONS_KEY];
199
+ return candidate instanceof ToolExecutionRecorder ? candidate : undefined;
200
+ }
201
+ }
202
+ /**
203
+ * Convert legacy loop-local `{name, input, output}` entries (plus optional
204
+ * timing/error fields) into `ToolExecutionRecord`s. Used as the fallback when
205
+ * a path produced executions the recorder did not see (e.g. results captured
206
+ * before the recorder existed, or breaker/not-found entries that never
207
+ * reached a real execute()).
208
+ */
209
+ export function toToolExecutionRecords(legacyExecutions, capture) {
210
+ if (!legacyExecutions || !Array.isArray(legacyExecutions)) {
211
+ return [];
212
+ }
213
+ const maxChars = capture?.maxResultChars ?? DEFAULT_TOOL_RESULT_CAPTURE_CHARS;
214
+ const maxRecords = capture?.maxRecords ?? DEFAULT_TOOL_EXECUTION_MAX_RECORDS;
215
+ return legacyExecutions.slice(-maxRecords).map((entry, index) => {
216
+ const record = (entry ?? {});
217
+ // A canonical record round-tripping through this converter keeps its
218
+ // already-serialized resultText verbatim instead of re-serializing an
219
+ // absent `output` into the string "undefined".
220
+ const output = record.output ??
221
+ record.result ??
222
+ record.response ??
223
+ (typeof record.resultText === "string" ? record.resultText : undefined);
224
+ const durationMs = typeof record.durationMs === "number"
225
+ ? record.durationMs
226
+ : typeof record.duration === "number"
227
+ ? record.duration
228
+ : typeof record.executionTime === "number"
229
+ ? record.executionTime
230
+ : 0;
231
+ return {
232
+ toolName: record.name ||
233
+ record.toolName ||
234
+ `tool_execution_${index}`,
235
+ params: record.input ?? record.params ?? record.args ?? {},
236
+ resultText: serializeToolResult(output, maxChars),
237
+ isError: record.isError === true ||
238
+ record.success === false ||
239
+ record.error !== undefined ||
240
+ isErrorShapedToolResult(output),
241
+ startedAt: typeof record.startedAt === "number" ? record.startedAt : 0,
242
+ durationMs,
243
+ };
244
+ });
245
+ }
246
+ /**
247
+ * Resolve the toolExecutions for a result: recorder records when the request
248
+ * carried a recorder that saw executions, else a conversion of the loop's
249
+ * legacy accumulator entries.
250
+ */
251
+ export function resolveToolExecutionRecords(options, legacyExecutions) {
252
+ const recorder = ToolExecutionRecorder.from(options);
253
+ if (recorder?.hasRecords()) {
254
+ return recorder.getRecords();
255
+ }
256
+ const capture = options && typeof options === "object"
257
+ ? options
258
+ .toolExecutionCapture
259
+ : undefined;
260
+ return toToolExecutionRecords(legacyExecutions, capture);
261
+ }
262
+ //# sourceMappingURL=toolExecutionRecorder.js.map
@@ -116,15 +116,15 @@ export class ContextBuilder {
116
116
  }
117
117
  return result.toolExecutions.map((exec) => {
118
118
  const toolResult = {
119
- success: true,
120
- data: exec.output,
119
+ success: !exec.isError,
120
+ data: exec.resultText,
121
121
  };
122
122
  return {
123
- toolName: exec.name,
124
- params: exec.input,
123
+ toolName: exec.toolName,
124
+ params: exec.params,
125
125
  result: toolResult,
126
- executionTime: 0,
127
- timestamp: Date.now(),
126
+ executionTime: exec.durationMs,
127
+ timestamp: exec.startedAt || Date.now(),
128
128
  };
129
129
  });
130
130
  }