@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
|
@@ -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
|
-
|
|
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,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:
|
|
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
|
}
|