@juspay/neurolink 10.5.3 → 10.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.
- package/CHANGELOG.md +12 -0
- package/dist/agent/agent.js +39 -8
- package/dist/agent/agentNetwork.js +62 -20
- package/dist/agent/agentToolRegistrar.d.ts +48 -0
- package/dist/agent/agentToolRegistrar.js +336 -0
- package/dist/agent/index.d.ts +3 -0
- package/dist/agent/index.js +4 -0
- package/dist/agent/isolatedAgentRunner.d.ts +73 -0
- package/dist/agent/isolatedAgentRunner.js +946 -0
- package/dist/agent/structuredRecovery.d.ts +30 -0
- package/dist/agent/structuredRecovery.js +104 -0
- package/dist/browser/neurolink.min.js +414 -398
- package/dist/cli/loop/optionsSchema.d.ts +1 -1
- package/dist/core/baseProvider.d.ts +8 -0
- package/dist/core/baseProvider.js +40 -19
- package/dist/core/modules/GenerationHandler.d.ts +2 -6
- package/dist/core/modules/GenerationHandler.js +10 -1
- package/dist/core/toolExecutionRecorder.d.ts +76 -0
- package/dist/core/toolExecutionRecorder.js +261 -0
- package/dist/evaluation/contextBuilder.js +6 -6
- package/dist/lib/agent/agent.js +39 -8
- package/dist/lib/agent/agentNetwork.js +62 -20
- package/dist/lib/agent/agentToolRegistrar.d.ts +48 -0
- package/dist/lib/agent/agentToolRegistrar.js +337 -0
- package/dist/lib/agent/index.d.ts +3 -0
- package/dist/lib/agent/index.js +4 -0
- package/dist/lib/agent/isolatedAgentRunner.d.ts +73 -0
- package/dist/lib/agent/isolatedAgentRunner.js +947 -0
- package/dist/lib/agent/structuredRecovery.d.ts +30 -0
- package/dist/lib/agent/structuredRecovery.js +105 -0
- package/dist/lib/core/baseProvider.d.ts +8 -0
- package/dist/lib/core/baseProvider.js +40 -19
- package/dist/lib/core/modules/GenerationHandler.d.ts +2 -6
- package/dist/lib/core/modules/GenerationHandler.js +10 -1
- package/dist/lib/core/toolExecutionRecorder.d.ts +76 -0
- package/dist/lib/core/toolExecutionRecorder.js +262 -0
- package/dist/lib/evaluation/contextBuilder.js +6 -6
- package/dist/lib/models/modelRegistry.d.ts +23 -0
- package/dist/lib/models/modelRegistry.js +62 -1
- package/dist/lib/models/modelResolver.js +1 -0
- package/dist/lib/neurolink.d.ts +80 -1
- package/dist/lib/neurolink.js +199 -37
- package/dist/lib/processors/media/index.js +0 -10
- package/dist/lib/providers/amazonBedrock.js +15 -2
- package/dist/lib/providers/anthropic.js +23 -11
- package/dist/lib/providers/googleAiStudio.js +8 -1
- package/dist/lib/providers/googleNativeGemini3.d.ts +2 -0
- package/dist/lib/providers/googleNativeGemini3.js +8 -1
- package/dist/lib/providers/googleVertex.js +56 -16
- package/dist/lib/providers/openaiChatCompletionsClient.js +18 -5
- package/dist/lib/tasks/autoresearchTaskExecutor.js +7 -6
- package/dist/lib/tasks/taskExecutor.js +2 -5
- package/dist/lib/types/agentNetwork.d.ts +23 -8
- package/dist/lib/types/generate.d.ts +62 -5
- package/dist/lib/types/index.d.ts +1 -0
- package/dist/lib/types/index.js +1 -0
- package/dist/lib/types/isolatedAgent.d.ts +365 -0
- package/dist/lib/types/isolatedAgent.js +12 -0
- package/dist/lib/types/model.d.ts +8 -0
- package/dist/lib/types/stream.d.ts +3 -1
- package/dist/lib/types/task.d.ts +3 -3
- package/dist/lib/utils/logger.d.ts +12 -3
- package/dist/lib/utils/logger.js +11 -3
- package/dist/lib/utils/transformationUtils.d.ts +14 -0
- package/dist/lib/utils/transformationUtils.js +33 -10
- package/dist/lib/voice/livekit/realtimeVoiceAgent.js +6 -3
- package/dist/models/modelRegistry.d.ts +23 -0
- package/dist/models/modelRegistry.js +62 -1
- package/dist/models/modelResolver.js +1 -0
- package/dist/neurolink.d.ts +80 -1
- package/dist/neurolink.js +199 -37
- package/dist/processors/media/index.js +0 -10
- package/dist/providers/amazonBedrock.js +15 -2
- package/dist/providers/anthropic.js +23 -11
- package/dist/providers/googleAiStudio.js +8 -1
- package/dist/providers/googleNativeGemini3.d.ts +2 -0
- package/dist/providers/googleNativeGemini3.js +8 -1
- package/dist/providers/googleVertex.js +56 -16
- package/dist/providers/openaiChatCompletionsClient.js +18 -5
- package/dist/tasks/autoresearchTaskExecutor.js +7 -6
- package/dist/tasks/taskExecutor.js +2 -5
- package/dist/types/agentNetwork.d.ts +23 -8
- package/dist/types/generate.d.ts +62 -5
- package/dist/types/index.d.ts +1 -0
- package/dist/types/index.js +1 -0
- package/dist/types/isolatedAgent.d.ts +365 -0
- package/dist/types/isolatedAgent.js +11 -0
- package/dist/types/model.d.ts +8 -0
- package/dist/types/stream.d.ts +3 -1
- package/dist/types/task.d.ts +3 -3
- package/dist/utils/logger.d.ts +12 -3
- package/dist/utils/logger.js +11 -3
- package/dist/utils/transformationUtils.d.ts +14 -0
- package/dist/utils/transformationUtils.js +33 -10
- package/dist/voice/livekit/realtimeVoiceAgent.js +6 -3
- package/package.json +3 -2
|
@@ -4,4 +4,4 @@ import type { OptionSchema, TextGenerationOptions } from "../../lib/types/index.
|
|
|
4
4
|
* This object provides metadata for validation and help text in the CLI loop.
|
|
5
5
|
* It is derived from the main TextGenerationOptions interface to ensure consistency.
|
|
6
6
|
*/
|
|
7
|
-
export declare const textGenerationOptionsSchema: Record<keyof Omit<TextGenerationOptions, "prompt" | "input" | "schema" | "tools" | "context" | "conversationHistory" | "conversationMessages" | "conversationMemoryConfig" | "originalPrompt" | "middleware" | "expectedOutcome" | "evaluationCriteria" | "region" | "csvOptions" | "pdfOptions" | "tts" | "stt" | "thinkingConfig" | "requestId" | "fileRegistry" | "abortSignal" | "toolFilter" | "excludeTools" | "toolChoice" | "prepareStep" | "credentials" | "onFinish" | "onError" | "processors" | "piiDetection" | "responseValidation" | "inputValidation">, OptionSchema>;
|
|
7
|
+
export declare const textGenerationOptionsSchema: Record<keyof Omit<TextGenerationOptions, "prompt" | "input" | "schema" | "tools" | "context" | "conversationHistory" | "conversationMessages" | "conversationMemoryConfig" | "originalPrompt" | "middleware" | "expectedOutcome" | "evaluationCriteria" | "region" | "csvOptions" | "pdfOptions" | "tts" | "stt" | "thinkingConfig" | "requestId" | "fileRegistry" | "abortSignal" | "toolFilter" | "excludeTools" | "toolChoice" | "prepareStep" | "credentials" | "onFinish" | "onError" | "processors" | "piiDetection" | "responseValidation" | "inputValidation" | "toolExecutionCapture" | "toolExecutionRecorder">, OptionSchema>;
|
|
@@ -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
|
-
|
|
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
|
|
1164
|
-
//
|
|
1165
|
-
toolExecutions: result.toolExecutions?.map((te) => {
|
|
1166
|
-
|
|
1167
|
-
|
|
1168
|
-
|
|
1169
|
-
|
|
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:
|
|
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:
|
|
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,261 @@
|
|
|
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
|
+
}
|
|
@@ -116,15 +116,15 @@ export class ContextBuilder {
|
|
|
116
116
|
}
|
|
117
117
|
return result.toolExecutions.map((exec) => {
|
|
118
118
|
const toolResult = {
|
|
119
|
-
success:
|
|
120
|
-
data: exec.
|
|
119
|
+
success: !exec.isError,
|
|
120
|
+
data: exec.resultText,
|
|
121
121
|
};
|
|
122
122
|
return {
|
|
123
|
-
toolName: exec.
|
|
124
|
-
params: exec.
|
|
123
|
+
toolName: exec.toolName,
|
|
124
|
+
params: exec.params,
|
|
125
125
|
result: toolResult,
|
|
126
|
-
executionTime:
|
|
127
|
-
timestamp: Date.now(),
|
|
126
|
+
executionTime: exec.durationMs,
|
|
127
|
+
timestamp: exec.startedAt || Date.now(),
|
|
128
128
|
};
|
|
129
129
|
});
|
|
130
130
|
}
|
package/dist/lib/agent/agent.js
CHANGED
|
@@ -108,6 +108,12 @@ export class Agent {
|
|
|
108
108
|
timestamp: startTime,
|
|
109
109
|
});
|
|
110
110
|
try {
|
|
111
|
+
// No ghost runs: an already-aborted parent stops the agent before any
|
|
112
|
+
// model call is made. Typed so callers can branch on the ABORT
|
|
113
|
+
// category instead of parsing free text.
|
|
114
|
+
if (options?.abortSignal?.aborted) {
|
|
115
|
+
throw ErrorFactory.aborted(new Error("Agent execution aborted before start"));
|
|
116
|
+
}
|
|
111
117
|
// Validate input if schema provided
|
|
112
118
|
if (this.inputSchema && typeof input !== "string") {
|
|
113
119
|
const validation = this.inputSchema.safeParse(input);
|
|
@@ -149,19 +155,14 @@ export class Agent {
|
|
|
149
155
|
contentLength: result.content?.length || 0,
|
|
150
156
|
toolsUsed: result.toolsUsed?.length || 0,
|
|
151
157
|
});
|
|
152
|
-
// Pass through toolExecutions, adding duration (not provided by generate())
|
|
153
|
-
const toolExecutions = result.toolExecutions?.map((te) => ({
|
|
154
|
-
name: te.name,
|
|
155
|
-
input: te.input,
|
|
156
|
-
output: te.output,
|
|
157
|
-
duration: 0,
|
|
158
|
-
}));
|
|
159
158
|
const agentResult = {
|
|
160
159
|
content: result.content || "",
|
|
161
160
|
object: parsedOutput,
|
|
162
161
|
usage: result.usage,
|
|
163
162
|
toolsUsed: result.toolsUsed,
|
|
164
|
-
|
|
163
|
+
// Real per-call records straight from the tool loop (N2)
|
|
164
|
+
toolExecutions: result.toolExecutions,
|
|
165
|
+
stopReason: result.stopReason,
|
|
165
166
|
duration,
|
|
166
167
|
status: "success",
|
|
167
168
|
agentId: this.id,
|
|
@@ -219,6 +220,10 @@ export class Agent {
|
|
|
219
220
|
traceId,
|
|
220
221
|
};
|
|
221
222
|
try {
|
|
223
|
+
// Mirror execute(): an already-aborted parent never starts the stream.
|
|
224
|
+
if (options?.abortSignal?.aborted) {
|
|
225
|
+
throw ErrorFactory.aborted(new Error("Agent stream aborted before start"));
|
|
226
|
+
}
|
|
222
227
|
// Validate input if schema provided
|
|
223
228
|
if (this.inputSchema && typeof input !== "string") {
|
|
224
229
|
const validation = this.inputSchema.safeParse(input);
|
|
@@ -367,6 +372,20 @@ export class Agent {
|
|
|
367
372
|
...(this.tools && this.tools.length > 0 && { toolFilter: this.tools }),
|
|
368
373
|
maxSteps: options?.maxSteps ?? this.maxSteps,
|
|
369
374
|
requestId: traceId,
|
|
375
|
+
// Turn budget + cancellation, forwarded verbatim to generate() — the
|
|
376
|
+
// machinery (wrap-up nudge, stall watchdog, honest stopReason) lives
|
|
377
|
+
// in the tool loop; the agent layer just passes it through.
|
|
378
|
+
...(options?.abortSignal && { abortSignal: options.abortSignal }),
|
|
379
|
+
...(options?.timeout !== undefined && { timeout: options.timeout }),
|
|
380
|
+
...(options?.turnTimeoutMs !== undefined && {
|
|
381
|
+
turnTimeoutMs: options.turnTimeoutMs,
|
|
382
|
+
}),
|
|
383
|
+
...(options?.wrapupTimeLeadMs !== undefined && {
|
|
384
|
+
wrapupTimeLeadMs: options.wrapupTimeLeadMs,
|
|
385
|
+
}),
|
|
386
|
+
...(options?.stallTimeoutMs !== undefined && {
|
|
387
|
+
stallTimeoutMs: options.stallTimeoutMs,
|
|
388
|
+
}),
|
|
370
389
|
context: {
|
|
371
390
|
agentId: this.id,
|
|
372
391
|
agentName: this.name,
|
|
@@ -390,6 +409,18 @@ export class Agent {
|
|
|
390
409
|
// toolFilter delegates to BaseProvider.applyToolFiltering() natively
|
|
391
410
|
...(this.tools && this.tools.length > 0 && { toolFilter: this.tools }),
|
|
392
411
|
maxSteps: options?.maxSteps ?? this.maxSteps,
|
|
412
|
+
// Turn budget + cancellation, forwarded verbatim to stream()
|
|
413
|
+
...(options?.abortSignal && { abortSignal: options.abortSignal }),
|
|
414
|
+
...(options?.timeout !== undefined && { timeout: options.timeout }),
|
|
415
|
+
...(options?.turnTimeoutMs !== undefined && {
|
|
416
|
+
turnTimeoutMs: options.turnTimeoutMs,
|
|
417
|
+
}),
|
|
418
|
+
...(options?.wrapupTimeLeadMs !== undefined && {
|
|
419
|
+
wrapupTimeLeadMs: options.wrapupTimeLeadMs,
|
|
420
|
+
}),
|
|
421
|
+
...(options?.stallTimeoutMs !== undefined && {
|
|
422
|
+
stallTimeoutMs: options.stallTimeoutMs,
|
|
423
|
+
}),
|
|
393
424
|
context: {
|
|
394
425
|
agentId: this.id,
|
|
395
426
|
agentName: this.name,
|