@juspay/neurolink 9.25.2 → 9.26.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.
- package/CHANGELOG.md +6 -0
- package/dist/adapters/providerImageAdapter.d.ts +3 -27
- package/dist/adapters/providerImageAdapter.js +9 -199
- package/dist/agent/directTools.d.ts +35 -3
- package/dist/agent/directTools.js +122 -0
- package/dist/cli/commands/config.d.ts +6 -6
- package/dist/context/contextCompactor.d.ts +1 -2
- package/dist/context/contextCompactor.js +7 -1
- package/dist/context/prompts/summarizationPrompt.d.ts +3 -3
- package/dist/context/prompts/summarizationPrompt.js +16 -9
- package/dist/context/stages/structuredSummarizer.d.ts +2 -2
- package/dist/context/stages/structuredSummarizer.js +80 -30
- package/dist/lib/adapters/providerImageAdapter.d.ts +3 -27
- package/dist/lib/adapters/providerImageAdapter.js +9 -199
- package/dist/lib/agent/directTools.d.ts +33 -1
- package/dist/lib/agent/directTools.js +122 -0
- package/dist/lib/context/contextCompactor.d.ts +1 -2
- package/dist/lib/context/contextCompactor.js +7 -1
- package/dist/lib/context/prompts/summarizationPrompt.d.ts +3 -3
- package/dist/lib/context/prompts/summarizationPrompt.js +16 -9
- package/dist/lib/context/stages/structuredSummarizer.d.ts +2 -2
- package/dist/lib/context/stages/structuredSummarizer.js +80 -30
- package/dist/lib/mcp/servers/agent/directToolsServer.js +2 -0
- package/dist/lib/mcp/toolRegistry.d.ts +8 -0
- package/dist/lib/mcp/toolRegistry.js +20 -0
- package/dist/lib/neurolink.d.ts +10 -0
- package/dist/lib/neurolink.js +281 -17
- package/dist/lib/providers/googleAiStudio.js +13 -7
- package/dist/lib/types/configTypes.d.ts +3 -0
- package/dist/lib/types/contextTypes.d.ts +5 -2
- package/dist/lib/types/contextTypes.js +8 -8
- package/dist/lib/types/generateTypes.d.ts +25 -0
- package/dist/lib/types/modelTypes.d.ts +2 -2
- package/dist/lib/utils/messageBuilder.js +2 -0
- package/dist/lib/utils/modelAliasResolver.d.ts +17 -0
- package/dist/lib/utils/modelAliasResolver.js +55 -0
- package/dist/lib/utils/pdfProcessor.d.ts +1 -1
- package/dist/lib/utils/pdfProcessor.js +7 -7
- package/dist/lib/utils/toolUtils.d.ts +8 -0
- package/dist/lib/utils/toolUtils.js +15 -0
- package/dist/lib/workflow/config.d.ts +24 -24
- package/dist/mcp/servers/agent/directToolsServer.js +2 -0
- package/dist/mcp/toolRegistry.d.ts +8 -0
- package/dist/mcp/toolRegistry.js +20 -0
- package/dist/neurolink.d.ts +10 -0
- package/dist/neurolink.js +281 -17
- package/dist/providers/googleAiStudio.js +13 -7
- package/dist/server/utils/validation.d.ts +2 -2
- package/dist/types/configTypes.d.ts +3 -0
- package/dist/types/contextTypes.d.ts +5 -2
- package/dist/types/contextTypes.js +8 -8
- package/dist/types/generateTypes.d.ts +25 -0
- package/dist/utils/messageBuilder.js +2 -0
- package/dist/utils/modelAliasResolver.d.ts +17 -0
- package/dist/utils/modelAliasResolver.js +54 -0
- package/dist/utils/pdfProcessor.d.ts +1 -1
- package/dist/utils/pdfProcessor.js +7 -7
- package/dist/utils/toolUtils.d.ts +8 -0
- package/dist/utils/toolUtils.js +15 -0
- package/dist/workflow/config.d.ts +82 -82
- package/package.json +1 -1
package/dist/lib/neurolink.js
CHANGED
|
@@ -64,8 +64,48 @@ import { BinaryTaskClassifier } from "./utils/taskClassifier.js";
|
|
|
64
64
|
// Transformation utilities
|
|
65
65
|
import { extractToolNames, optimizeToolForCollection, transformAvailableTools, transformParamsForLogging, transformToolExecutions, transformToolExecutionsForMCP, transformToolsForMCP, transformToolsToDescriptions, transformToolsToExpectedFormat, } from "./utils/transformationUtils.js";
|
|
66
66
|
import { isNonNullObject } from "./utils/typeUtils.js";
|
|
67
|
+
import { resolveModel } from "./utils/modelAliasResolver.js";
|
|
67
68
|
import { getWorkflow } from "./workflow/core/workflowRegistry.js";
|
|
68
69
|
import { runWorkflow } from "./workflow/core/workflowRunner.js";
|
|
70
|
+
/**
|
|
71
|
+
* NL-002: Classify MCP error messages into categories for AI disambiguation.
|
|
72
|
+
* Returns a human-readable error category based on error message content.
|
|
73
|
+
*/
|
|
74
|
+
function classifyMcpErrorMessage(text) {
|
|
75
|
+
const lower = text.toLowerCase();
|
|
76
|
+
if (lower.includes("not found") ||
|
|
77
|
+
lower.includes("404") ||
|
|
78
|
+
lower.includes("does not exist") ||
|
|
79
|
+
lower.includes("no such")) {
|
|
80
|
+
return "not_found";
|
|
81
|
+
}
|
|
82
|
+
if (lower.includes("permission") ||
|
|
83
|
+
lower.includes("forbidden") ||
|
|
84
|
+
lower.includes("403") ||
|
|
85
|
+
lower.includes("unauthorized") ||
|
|
86
|
+
lower.includes("401") ||
|
|
87
|
+
lower.includes("access denied")) {
|
|
88
|
+
return "permission_denied";
|
|
89
|
+
}
|
|
90
|
+
if (lower.includes("timeout") ||
|
|
91
|
+
lower.includes("timed out") ||
|
|
92
|
+
lower.includes("deadline exceeded")) {
|
|
93
|
+
return "timeout";
|
|
94
|
+
}
|
|
95
|
+
if (lower.includes("rate limit") ||
|
|
96
|
+
lower.includes("429") ||
|
|
97
|
+
lower.includes("too many requests") ||
|
|
98
|
+
lower.includes("throttl")) {
|
|
99
|
+
return "rate_limited";
|
|
100
|
+
}
|
|
101
|
+
if (lower.includes("invalid") ||
|
|
102
|
+
lower.includes("validation") ||
|
|
103
|
+
lower.includes("bad request") ||
|
|
104
|
+
lower.includes("400")) {
|
|
105
|
+
return "validation_error";
|
|
106
|
+
}
|
|
107
|
+
return "unknown";
|
|
108
|
+
}
|
|
69
109
|
/**
|
|
70
110
|
* Check if an error is a non-retryable provider error that should immediately
|
|
71
111
|
* stop the retry/fallback chain. These errors represent permanent failures
|
|
@@ -129,6 +169,16 @@ export class NeuroLink {
|
|
|
129
169
|
// Cache for available tools to improve performance
|
|
130
170
|
toolCache = null;
|
|
131
171
|
toolCacheDuration;
|
|
172
|
+
// NL-004: Model alias/deprecation configuration
|
|
173
|
+
modelAliasConfig;
|
|
174
|
+
// Compaction watermark: prevents re-triggering compaction on already-compacted messages
|
|
175
|
+
// Per-session map to avoid cross-session pollution in server mode
|
|
176
|
+
lastCompactionMessageCount = new Map();
|
|
177
|
+
/** Extract sessionId from options context for compaction watermark keying */
|
|
178
|
+
getCompactionSessionId(options) {
|
|
179
|
+
return (options.context
|
|
180
|
+
?.sessionId || "__default__");
|
|
181
|
+
}
|
|
132
182
|
// Enhanced error handling support
|
|
133
183
|
toolCircuitBreakers = new Map();
|
|
134
184
|
toolExecutionMetrics = new Map();
|
|
@@ -375,6 +425,10 @@ export class NeuroLink {
|
|
|
375
425
|
this.observabilityConfig = config?.observability;
|
|
376
426
|
// Initialize orchestration setting
|
|
377
427
|
this.enableOrchestration = config?.enableOrchestration ?? false;
|
|
428
|
+
// NL-004: Initialize model alias configuration
|
|
429
|
+
if (config?.modelAliasConfig) {
|
|
430
|
+
this.modelAliasConfig = config.modelAliasConfig;
|
|
431
|
+
}
|
|
378
432
|
logger.setEventEmitter(this.emitter);
|
|
379
433
|
// Read tool cache duration from environment variables, with a default
|
|
380
434
|
const cacheDurationEnv = process.env.NEUROLINK_TOOL_CACHE_DURATION;
|
|
@@ -2053,9 +2107,12 @@ Current user's request: ${currentInput}`;
|
|
|
2053
2107
|
try {
|
|
2054
2108
|
const originalPrompt = this._extractOriginalPrompt(optionsOrPrompt);
|
|
2055
2109
|
// Convert string prompt to full options
|
|
2110
|
+
// Shallow-copy caller's object to avoid mutating their original reference
|
|
2056
2111
|
const options = typeof optionsOrPrompt === "string"
|
|
2057
2112
|
? { input: { text: optionsOrPrompt } }
|
|
2058
|
-
: optionsOrPrompt;
|
|
2113
|
+
: { ...optionsOrPrompt };
|
|
2114
|
+
// NL-004: Resolve model aliases/deprecations before processing
|
|
2115
|
+
options.model = resolveModel(options.model, this.modelAliasConfig);
|
|
2059
2116
|
// Set span attributes for observability
|
|
2060
2117
|
generateSpan.setAttribute("neurolink.provider", options.provider || "default");
|
|
2061
2118
|
generateSpan.setAttribute("neurolink.model", options.model || "default");
|
|
@@ -2141,6 +2198,10 @@ Current user's request: ${currentInput}`;
|
|
|
2141
2198
|
});
|
|
2142
2199
|
// Use orchestrated options
|
|
2143
2200
|
Object.assign(options, orchestratedOptions);
|
|
2201
|
+
// Re-resolve model alias in case orchestration returned an alias
|
|
2202
|
+
if (orchestratedOptions.model) {
|
|
2203
|
+
options.model = resolveModel(options.model, this.modelAliasConfig);
|
|
2204
|
+
}
|
|
2144
2205
|
}
|
|
2145
2206
|
catch (error) {
|
|
2146
2207
|
logger.warn("Orchestration failed, continuing with original options", {
|
|
@@ -2328,6 +2389,8 @@ Current user's request: ${currentInput}`;
|
|
|
2328
2389
|
audio: textResult.audio,
|
|
2329
2390
|
video: textResult.video,
|
|
2330
2391
|
ppt: textResult.ppt,
|
|
2392
|
+
// NL-007: Copy retry metadata from MCP generation path
|
|
2393
|
+
...(textResult.retries && { retries: textResult.retries }),
|
|
2331
2394
|
};
|
|
2332
2395
|
// Accumulate session cost for budget tracking
|
|
2333
2396
|
if (generateResult.analytics?.cost &&
|
|
@@ -2342,6 +2405,8 @@ Current user's request: ${currentInput}`;
|
|
|
2342
2405
|
generateSpan.setAttribute("neurolink.finish_reason", generateResult.finishReason || "unknown");
|
|
2343
2406
|
generateSpan.setAttribute("neurolink.result_provider", generateResult.provider || "unknown");
|
|
2344
2407
|
generateSpan.setAttribute("neurolink.result_model", generateResult.model || "unknown");
|
|
2408
|
+
// NL-007: Expose retry count in OTel span
|
|
2409
|
+
generateSpan.setAttribute("generate.retry_count", generateResult.retries?.count || 0);
|
|
2345
2410
|
generateSpan.setStatus({ code: SpanStatusCode.OK });
|
|
2346
2411
|
return generateResult;
|
|
2347
2412
|
});
|
|
@@ -2694,6 +2759,8 @@ Current user's request: ${currentInput}`;
|
|
|
2694
2759
|
options.prompt.trim() === "") {
|
|
2695
2760
|
throw new Error("GenerateText options must include prompt as a non-empty string");
|
|
2696
2761
|
}
|
|
2762
|
+
// NL-004: Resolve model aliases/deprecations before processing
|
|
2763
|
+
options.model = resolveModel(options.model, this.modelAliasConfig);
|
|
2697
2764
|
// Use internal generation method directly
|
|
2698
2765
|
return await this.generateTextInternal(options);
|
|
2699
2766
|
}
|
|
@@ -3038,6 +3105,9 @@ Current user's request: ${currentInput}`;
|
|
|
3038
3105
|
*/
|
|
3039
3106
|
async performMCPGenerationRetries(options, generateInternalId, generateInternalStartTime, generateInternalHrTimeStart, functionTag) {
|
|
3040
3107
|
const maxMcpRetries = RETRY_ATTEMPTS.QUICK;
|
|
3108
|
+
// NL-007: Track retry metadata for observability
|
|
3109
|
+
const retryErrors = [];
|
|
3110
|
+
let retryCount = 0;
|
|
3041
3111
|
const maxAttempts = maxMcpRetries + 1;
|
|
3042
3112
|
for (let attempt = 1; attempt <= maxAttempts; attempt++) {
|
|
3043
3113
|
if (options.abortSignal?.aborted) {
|
|
@@ -3054,7 +3124,12 @@ Current user's request: ${currentInput}`;
|
|
|
3054
3124
|
contentLength: mcpResult.content?.length || 0,
|
|
3055
3125
|
toolsUsed: mcpResult.toolsUsed?.length || 0,
|
|
3056
3126
|
toolExecutions: mcpResult.toolExecutions?.length || 0,
|
|
3127
|
+
retryCount,
|
|
3057
3128
|
});
|
|
3129
|
+
// NL-007: Attach retry metadata to result
|
|
3130
|
+
if (retryCount > 0) {
|
|
3131
|
+
mcpResult.retries = { count: retryCount, errors: retryErrors };
|
|
3132
|
+
}
|
|
3058
3133
|
return mcpResult;
|
|
3059
3134
|
}
|
|
3060
3135
|
else {
|
|
@@ -3072,9 +3147,19 @@ Current user's request: ${currentInput}`;
|
|
|
3072
3147
|
logger.debug(`[${functionTag}] AbortError detected on attempt ${attempt}, stopping retries`);
|
|
3073
3148
|
throw error;
|
|
3074
3149
|
}
|
|
3150
|
+
// NL-007: Record retry error for observability
|
|
3151
|
+
retryCount++;
|
|
3152
|
+
const errMsg = error instanceof Error ? error.message : String(error);
|
|
3153
|
+
const errCode = error instanceof NeuroLinkError
|
|
3154
|
+
? error.code
|
|
3155
|
+
: error instanceof Error
|
|
3156
|
+
? error.name
|
|
3157
|
+
: "UNKNOWN";
|
|
3158
|
+
retryErrors.push({ code: errCode, message: errMsg.substring(0, 500) });
|
|
3075
3159
|
logger.debug(`[${functionTag}] MCP generation failed on attempt ${attempt}/${maxAttempts}`, {
|
|
3076
|
-
error:
|
|
3160
|
+
error: errMsg,
|
|
3077
3161
|
willRetry: attempt < maxAttempts,
|
|
3162
|
+
retryCount,
|
|
3078
3163
|
});
|
|
3079
3164
|
// Check for non-retryable errors — skip remaining retries immediately
|
|
3080
3165
|
// NoSuchToolError / InvalidToolArgumentsError from Vercel AI SDK are never
|
|
@@ -3162,6 +3247,11 @@ Current user's request: ${currentInput}`;
|
|
|
3162
3247
|
: options.provider;
|
|
3163
3248
|
// Get available tools
|
|
3164
3249
|
let availableTools = await this.getAllAvailableTools();
|
|
3250
|
+
// NL-001: Filter out tools with OPEN circuit breakers
|
|
3251
|
+
const { tools: circuitBreakerFilteredTools, unavailableTools } = this.toolRegistry.getAvailableTools(this.toolCircuitBreakers);
|
|
3252
|
+
// Intersect: keep only tools that pass both getAllAvailableTools and circuit breaker filtering
|
|
3253
|
+
const cbFilteredNames = new Set(circuitBreakerFilteredTools.map((t) => t.name));
|
|
3254
|
+
availableTools = availableTools.filter((t) => cbFilteredNames.has(t.name));
|
|
3165
3255
|
// Apply per-call tool filtering for system prompt tool descriptions
|
|
3166
3256
|
availableTools = this.applyToolInfoFiltering(availableTools, options);
|
|
3167
3257
|
const targetTool = availableTools.find((t) => t.name.includes("SuccessRateSRByTime") ||
|
|
@@ -3169,6 +3259,8 @@ Current user's request: ${currentInput}`;
|
|
|
3169
3259
|
logger.debug("Available tools for AI prompt generation", {
|
|
3170
3260
|
toolsCount: availableTools.length,
|
|
3171
3261
|
toolNames: availableTools.map((t) => t.name),
|
|
3262
|
+
unavailableToolsCount: unavailableTools.length,
|
|
3263
|
+
unavailableTools: unavailableTools,
|
|
3172
3264
|
hasTargetTool: !!targetTool,
|
|
3173
3265
|
targetToolDetails: targetTool
|
|
3174
3266
|
? {
|
|
@@ -3178,10 +3270,15 @@ Current user's request: ${currentInput}`;
|
|
|
3178
3270
|
}
|
|
3179
3271
|
: null,
|
|
3180
3272
|
});
|
|
3273
|
+
// NL-001: Inject system note about unavailable tools
|
|
3274
|
+
let circuitBreakerNote = "";
|
|
3275
|
+
if (unavailableTools.length > 0) {
|
|
3276
|
+
circuitBreakerNote = `\n\nNOTE: The following tools are temporarily unavailable due to repeated failures: ${unavailableTools.join(", ")}. Do not attempt to call these tools.`;
|
|
3277
|
+
}
|
|
3181
3278
|
// Create tool-aware system prompt (skip if skipToolPromptInjection is true)
|
|
3182
3279
|
const enhancedSystemPrompt = options.skipToolPromptInjection
|
|
3183
|
-
? options.systemPrompt || ""
|
|
3184
|
-
: this.createToolAwareSystemPrompt(options.systemPrompt, availableTools);
|
|
3280
|
+
? (options.systemPrompt || "") + circuitBreakerNote
|
|
3281
|
+
: this.createToolAwareSystemPrompt(options.systemPrompt, availableTools) + circuitBreakerNote;
|
|
3185
3282
|
logger.debug("Tool-aware system prompt created", {
|
|
3186
3283
|
requestId,
|
|
3187
3284
|
originalPromptLength: options.systemPrompt?.length || 0,
|
|
@@ -3260,7 +3357,12 @@ Current user's request: ${currentInput}`;
|
|
|
3260
3357
|
conversationMessageCount: conversationMessages?.length || 0,
|
|
3261
3358
|
shouldCompact: budgetResult.shouldCompact,
|
|
3262
3359
|
});
|
|
3263
|
-
|
|
3360
|
+
const messageCount = conversationMessages?.length || 0;
|
|
3361
|
+
const compactionSessionId = this.getCompactionSessionId(options);
|
|
3362
|
+
if (budgetResult.shouldCompact &&
|
|
3363
|
+
this.conversationMemory &&
|
|
3364
|
+
messageCount >
|
|
3365
|
+
(this.lastCompactionMessageCount.get(compactionSessionId) ?? 0)) {
|
|
3264
3366
|
logger.info("[NeuroLink] Context budget exceeded, triggering auto-compaction", {
|
|
3265
3367
|
usageRatio: budgetResult.usageRatio,
|
|
3266
3368
|
estimatedTokens: budgetResult.estimatedInputTokens,
|
|
@@ -3277,6 +3379,7 @@ Current user's request: ${currentInput}`;
|
|
|
3277
3379
|
if (compactionResult.compacted) {
|
|
3278
3380
|
const repairedResult = repairToolPairs(compactionResult.messages);
|
|
3279
3381
|
conversationMessages = repairedResult.messages;
|
|
3382
|
+
this.lastCompactionMessageCount.set(compactionSessionId, conversationMessages.length);
|
|
3280
3383
|
logger.info("[NeuroLink] Context compacted successfully", {
|
|
3281
3384
|
stagesUsed: compactionResult.stagesUsed,
|
|
3282
3385
|
tokensSaved: compactionResult.tokensSaved,
|
|
@@ -3495,12 +3598,24 @@ Current user's request: ${currentInput}`;
|
|
|
3495
3598
|
? Object.values(options.tools)
|
|
3496
3599
|
: undefined,
|
|
3497
3600
|
});
|
|
3498
|
-
|
|
3499
|
-
|
|
3500
|
-
|
|
3601
|
+
const dpgMessageCount = conversationMessages?.length || 0;
|
|
3602
|
+
const dpgCompactionSessionId = this.getCompactionSessionId(options);
|
|
3603
|
+
if (budgetCheck.shouldCompact &&
|
|
3604
|
+
this.conversationMemory &&
|
|
3605
|
+
dpgMessageCount >
|
|
3606
|
+
(this.lastCompactionMessageCount.get(dpgCompactionSessionId) ?? 0)) {
|
|
3607
|
+
const compactor = new ContextCompactor({
|
|
3608
|
+
provider: providerName,
|
|
3609
|
+
summarizationProvider: this.conversationMemoryConfig?.conversationMemory
|
|
3610
|
+
?.summarizationProvider,
|
|
3611
|
+
summarizationModel: this.conversationMemoryConfig?.conversationMemory
|
|
3612
|
+
?.summarizationModel,
|
|
3613
|
+
});
|
|
3614
|
+
const compactionResult = await compactor.compact(conversationMessages, budgetCheck.availableInputTokens, this.conversationMemoryConfig?.conversationMemory, options.context?.requestId);
|
|
3501
3615
|
if (compactionResult.compacted) {
|
|
3502
3616
|
const repairedResult = repairToolPairs(compactionResult.messages);
|
|
3503
3617
|
conversationMessages = repairedResult.messages;
|
|
3618
|
+
this.lastCompactionMessageCount.set(dpgCompactionSessionId, conversationMessages.length);
|
|
3504
3619
|
}
|
|
3505
3620
|
// POST-COMPACTION BUDGET RE-CHECK (BUG-003 fix)
|
|
3506
3621
|
const postCompactBudget = checkContextBudget({
|
|
@@ -3782,6 +3897,8 @@ Current user's request: ${currentInput}`;
|
|
|
3782
3897
|
* @throws {Error} When conversation memory operations fail (if enabled)
|
|
3783
3898
|
*/
|
|
3784
3899
|
async stream(options) {
|
|
3900
|
+
// Shallow-copy caller's object to avoid mutating their original reference
|
|
3901
|
+
options = { ...options };
|
|
3785
3902
|
// Set metrics trace context for parent-child span linking
|
|
3786
3903
|
const metricsTraceId = crypto.randomUUID().replace(/-/g, "");
|
|
3787
3904
|
const metricsParentSpanId = crypto
|
|
@@ -3806,6 +3923,8 @@ Current user's request: ${currentInput}`;
|
|
|
3806
3923
|
});
|
|
3807
3924
|
const spanStartTime = Date.now();
|
|
3808
3925
|
try {
|
|
3926
|
+
// NL-004: Resolve model aliases/deprecations before processing
|
|
3927
|
+
options.model = resolveModel(options.model, this.modelAliasConfig);
|
|
3809
3928
|
const startTime = Date.now();
|
|
3810
3929
|
const hrTimeStart = process.hrtime.bigint();
|
|
3811
3930
|
const streamId = `neurolink-${Date.now()}-${Math.random().toString(36).substr(2, 9)}`;
|
|
@@ -4110,6 +4229,10 @@ Current user's request: ${currentInput}`;
|
|
|
4110
4229
|
});
|
|
4111
4230
|
// Use orchestrated options
|
|
4112
4231
|
Object.assign(options, orchestratedOptions);
|
|
4232
|
+
// Re-resolve model alias in case orchestration returned an alias
|
|
4233
|
+
if (orchestratedOptions.model) {
|
|
4234
|
+
options.model = resolveModel(options.model, this.modelAliasConfig);
|
|
4235
|
+
}
|
|
4113
4236
|
}
|
|
4114
4237
|
catch (error) {
|
|
4115
4238
|
logger.warn("Stream orchestration failed, continuing with original options", {
|
|
@@ -4488,12 +4611,60 @@ Current user's request: ${currentInput}`;
|
|
|
4488
4611
|
currentPrompt: options.input.text,
|
|
4489
4612
|
toolDefinitions: availableTools,
|
|
4490
4613
|
});
|
|
4491
|
-
|
|
4492
|
-
|
|
4493
|
-
|
|
4614
|
+
const streamMessageCount = conversationMessages?.length || 0;
|
|
4615
|
+
const streamCompactionSessionId = this.getCompactionSessionId(options);
|
|
4616
|
+
if (streamBudget.shouldCompact &&
|
|
4617
|
+
this.conversationMemory &&
|
|
4618
|
+
streamMessageCount >
|
|
4619
|
+
(this.lastCompactionMessageCount.get(streamCompactionSessionId) ?? 0)) {
|
|
4620
|
+
const compactor = new ContextCompactor({
|
|
4621
|
+
provider: providerName,
|
|
4622
|
+
summarizationProvider: this.conversationMemoryConfig?.conversationMemory
|
|
4623
|
+
?.summarizationProvider,
|
|
4624
|
+
summarizationModel: this.conversationMemoryConfig?.conversationMemory?.summarizationModel,
|
|
4625
|
+
});
|
|
4626
|
+
const compactionResult = await compactor.compact(conversationMessages, streamBudget.availableInputTokens, this.conversationMemoryConfig?.conversationMemory, options.context?.requestId);
|
|
4494
4627
|
if (compactionResult.compacted) {
|
|
4495
4628
|
const repairedResult = repairToolPairs(compactionResult.messages);
|
|
4496
4629
|
conversationMessages = repairedResult.messages;
|
|
4630
|
+
this.lastCompactionMessageCount.set(streamCompactionSessionId, conversationMessages.length);
|
|
4631
|
+
}
|
|
4632
|
+
// POST-COMPACTION BUDGET RE-CHECK (mirrors tryMCPGeneration / directProviderGeneration)
|
|
4633
|
+
const postCompactBudget = checkContextBudget({
|
|
4634
|
+
provider: providerName,
|
|
4635
|
+
model: options.model,
|
|
4636
|
+
maxTokens: options.maxTokens,
|
|
4637
|
+
systemPrompt: enhancedSystemPrompt,
|
|
4638
|
+
conversationMessages: conversationMessages,
|
|
4639
|
+
currentPrompt: options.input.text,
|
|
4640
|
+
toolDefinitions: availableTools,
|
|
4641
|
+
});
|
|
4642
|
+
if (!postCompactBudget.withinBudget) {
|
|
4643
|
+
logger.warn("[NeuroLink] Stream: post-compaction still over budget, emergency truncation", {
|
|
4644
|
+
estimatedTokens: postCompactBudget.estimatedInputTokens,
|
|
4645
|
+
availableTokens: postCompactBudget.availableInputTokens,
|
|
4646
|
+
overagePercent: Math.round((postCompactBudget.usageRatio - 1.0) * 100),
|
|
4647
|
+
});
|
|
4648
|
+
conversationMessages = emergencyContentTruncation(conversationMessages, postCompactBudget.availableInputTokens, postCompactBudget.breakdown, providerName);
|
|
4649
|
+
const finalBudget = checkContextBudget({
|
|
4650
|
+
provider: providerName,
|
|
4651
|
+
model: options.model,
|
|
4652
|
+
maxTokens: options.maxTokens,
|
|
4653
|
+
systemPrompt: enhancedSystemPrompt,
|
|
4654
|
+
conversationMessages: conversationMessages,
|
|
4655
|
+
currentPrompt: options.input.text,
|
|
4656
|
+
toolDefinitions: availableTools,
|
|
4657
|
+
});
|
|
4658
|
+
if (!finalBudget.withinBudget) {
|
|
4659
|
+
throw new ContextBudgetExceededError(`Stream context exceeds model budget after all compaction stages. ` +
|
|
4660
|
+
`Estimated: ${finalBudget.estimatedInputTokens} tokens, ` +
|
|
4661
|
+
`Budget: ${finalBudget.availableInputTokens} tokens.`, {
|
|
4662
|
+
estimatedTokens: finalBudget.estimatedInputTokens,
|
|
4663
|
+
availableTokens: finalBudget.availableInputTokens,
|
|
4664
|
+
stagesUsed: compactionResult.stagesUsed,
|
|
4665
|
+
breakdown: finalBudget.breakdown,
|
|
4666
|
+
});
|
|
4667
|
+
}
|
|
4497
4668
|
}
|
|
4498
4669
|
}
|
|
4499
4670
|
// 🔧 FIX: Pass enhanced system prompt to real streaming
|
|
@@ -5416,11 +5587,15 @@ Current user's request: ${currentInput}`;
|
|
|
5416
5587
|
// Track memory usage for tool execution
|
|
5417
5588
|
const { MemoryManager } = await import("./utils/performance.js");
|
|
5418
5589
|
const startMemory = MemoryManager.getMemoryUsageMB();
|
|
5590
|
+
// NL-004: Use composite key (serverId.toolName) to avoid cross-server collisions
|
|
5591
|
+
const toolInfo = this.toolRegistry.getToolInfo(toolName);
|
|
5592
|
+
const breakerServerId = externalTool?.serverId || toolInfo?.tool?.serverId || "unknown";
|
|
5593
|
+
const breakerKey = `${breakerServerId}.${toolName}`;
|
|
5419
5594
|
// Get or create circuit breaker for this tool
|
|
5420
|
-
if (!this.toolCircuitBreakers.has(
|
|
5421
|
-
this.toolCircuitBreakers.set(
|
|
5595
|
+
if (!this.toolCircuitBreakers.has(breakerKey)) {
|
|
5596
|
+
this.toolCircuitBreakers.set(breakerKey, new CircuitBreaker(CIRCUIT_BREAKER.FAILURE_THRESHOLD, CIRCUIT_BREAKER_RESET_MS));
|
|
5422
5597
|
}
|
|
5423
|
-
const circuitBreaker = this.toolCircuitBreakers.get(
|
|
5598
|
+
const circuitBreaker = this.toolCircuitBreakers.get(breakerKey);
|
|
5424
5599
|
// Initialize metrics for this tool if not exists
|
|
5425
5600
|
if (!this.toolExecutionMetrics.has(toolName)) {
|
|
5426
5601
|
this.toolExecutionMetrics.set(toolName, {
|
|
@@ -5497,6 +5672,61 @@ Current user's request: ${currentInput}`;
|
|
|
5497
5672
|
typeof result === "object" &&
|
|
5498
5673
|
"isError" in result &&
|
|
5499
5674
|
result.isError === true;
|
|
5675
|
+
// NL-001: Count isError:true results as circuit breaker failures
|
|
5676
|
+
// This ensures tools that return error results (not just thrown errors) are tracked
|
|
5677
|
+
// TODO(NL-009): This records a failure AFTER the circuit breaker already recorded
|
|
5678
|
+
// success inside `circuitBreaker.execute()`. The correct fix is to check `isToolError`
|
|
5679
|
+
// inside the execute callback and throw before returning, so the breaker never sees
|
|
5680
|
+
// success. Deferred because moving the check inside the callback requires restructuring
|
|
5681
|
+
// the retry/timeout wrapper chain and is high-risk for a hot-path change.
|
|
5682
|
+
if (isToolError && circuitBreaker) {
|
|
5683
|
+
// Record a failure by executing a rejected promise through the breaker
|
|
5684
|
+
try {
|
|
5685
|
+
await circuitBreaker.execute(async () => {
|
|
5686
|
+
throw new Error(`Tool ${toolName} returned isError:true`);
|
|
5687
|
+
});
|
|
5688
|
+
}
|
|
5689
|
+
catch {
|
|
5690
|
+
// Expected — we intentionally triggered the failure recording
|
|
5691
|
+
}
|
|
5692
|
+
mcpLogger.debug(`[${functionTag}] Circuit breaker failure recorded for isError result`, {
|
|
5693
|
+
toolName,
|
|
5694
|
+
circuitBreakerState: circuitBreaker.getState(),
|
|
5695
|
+
circuitBreakerFailures: circuitBreaker.getFailureCount(),
|
|
5696
|
+
});
|
|
5697
|
+
}
|
|
5698
|
+
// NL-002 + NL-003: Format and capture MCP error results
|
|
5699
|
+
if (isToolError) {
|
|
5700
|
+
const resultObj = result;
|
|
5701
|
+
const contentArr = resultObj.content;
|
|
5702
|
+
const errorText = contentArr
|
|
5703
|
+
?.filter((c) => c.type === "text" && c.text)
|
|
5704
|
+
.map((c) => c.text)
|
|
5705
|
+
.join(" ") ||
|
|
5706
|
+
(typeof resultObj.error === "string"
|
|
5707
|
+
? resultObj.error
|
|
5708
|
+
: "Unknown error");
|
|
5709
|
+
const errorCategory = classifyMcpErrorMessage(errorText);
|
|
5710
|
+
const prefix = `[TOOL_ERROR: ${toolName} failed (${errorCategory})] `;
|
|
5711
|
+
// NL-002: Clone content array to avoid mutating shared objects, then prefix error
|
|
5712
|
+
if (contentArr && Array.isArray(contentArr)) {
|
|
5713
|
+
const clonedContent = contentArr.map((c) => ({ ...c }));
|
|
5714
|
+
for (const content of clonedContent) {
|
|
5715
|
+
if (content.type === "text" && content.text) {
|
|
5716
|
+
content.text = prefix + content.text;
|
|
5717
|
+
break; // Only prefix the first text content
|
|
5718
|
+
}
|
|
5719
|
+
}
|
|
5720
|
+
resultObj.content = clonedContent;
|
|
5721
|
+
}
|
|
5722
|
+
// NL-003: Capture error details in span attributes for telemetry
|
|
5723
|
+
toolSpan.setAttribute("tool.error.message", errorText.substring(0, 500));
|
|
5724
|
+
toolSpan.setAttribute("tool.error.category", errorCategory);
|
|
5725
|
+
toolSpan.setStatus({
|
|
5726
|
+
code: SpanStatusCode.ERROR,
|
|
5727
|
+
message: `MCP tool returned isError: ${errorText.substring(0, 200)}`,
|
|
5728
|
+
});
|
|
5729
|
+
}
|
|
5500
5730
|
toolSpan.setAttribute("tool.result.status", isToolError ? "error" : "success");
|
|
5501
5731
|
toolSpan.setAttribute("tool.duration_ms", executionTime);
|
|
5502
5732
|
return result;
|
|
@@ -6214,6 +6444,15 @@ Current user's request: ${currentInput}`;
|
|
|
6214
6444
|
}
|
|
6215
6445
|
return metrics;
|
|
6216
6446
|
}
|
|
6447
|
+
/**
|
|
6448
|
+
* NL-004: Set model alias/deprecation configuration.
|
|
6449
|
+
* Models in the alias map will be warned, redirected, or blocked based on their action.
|
|
6450
|
+
* @param config - Model alias configuration with aliases map
|
|
6451
|
+
*/
|
|
6452
|
+
setModelAliasConfig(config) {
|
|
6453
|
+
this.modelAliasConfig = config;
|
|
6454
|
+
logger.info(`[ModelAlias] Configured ${Object.keys(config.aliases).length} model aliases`);
|
|
6455
|
+
}
|
|
6217
6456
|
/**
|
|
6218
6457
|
* Get circuit breaker status for all tools
|
|
6219
6458
|
* @returns Object with circuit breaker status for each tool
|
|
@@ -6257,9 +6496,17 @@ Current user's request: ${currentInput}`;
|
|
|
6257
6496
|
// Get all tool names from toolRegistry
|
|
6258
6497
|
const allTools = await this.toolRegistry.listTools();
|
|
6259
6498
|
const allToolNames = new Set(allTools.map((tool) => tool.name));
|
|
6499
|
+
// Build a lookup from tool name to serverId for composite breaker keys
|
|
6500
|
+
const toolServerIdMap = new Map();
|
|
6501
|
+
for (const tool of allTools) {
|
|
6502
|
+
if (!toolServerIdMap.has(tool.name)) {
|
|
6503
|
+
toolServerIdMap.set(tool.name, tool.serverId || "unknown");
|
|
6504
|
+
}
|
|
6505
|
+
}
|
|
6260
6506
|
for (const toolName of allToolNames) {
|
|
6261
6507
|
const metrics = this.toolExecutionMetrics.get(toolName);
|
|
6262
|
-
const
|
|
6508
|
+
const breakerKey = `${toolServerIdMap.get(toolName) || "unknown"}.${toolName}`;
|
|
6509
|
+
const circuitBreaker = this.toolCircuitBreakers.get(breakerKey);
|
|
6263
6510
|
const successRate = metrics
|
|
6264
6511
|
? metrics.totalExecutions > 0
|
|
6265
6512
|
? metrics.successfulExecutions / metrics.totalExecutions
|
|
@@ -6410,6 +6657,7 @@ Current user's request: ${currentInput}`;
|
|
|
6410
6657
|
retriable: false,
|
|
6411
6658
|
});
|
|
6412
6659
|
}
|
|
6660
|
+
this.lastCompactionMessageCount.delete(sessionId);
|
|
6413
6661
|
return await this.conversationMemory.clearSession(sessionId);
|
|
6414
6662
|
}
|
|
6415
6663
|
/**
|
|
@@ -6428,6 +6676,7 @@ Current user's request: ${currentInput}`;
|
|
|
6428
6676
|
retriable: false,
|
|
6429
6677
|
});
|
|
6430
6678
|
}
|
|
6679
|
+
this.lastCompactionMessageCount.clear();
|
|
6431
6680
|
await this.conversationMemory.clearAllSessions();
|
|
6432
6681
|
}
|
|
6433
6682
|
/**
|
|
@@ -6870,6 +7119,8 @@ Current user's request: ${currentInput}`;
|
|
|
6870
7119
|
*/
|
|
6871
7120
|
async dispose() {
|
|
6872
7121
|
logger.debug("[NeuroLink] Starting disposal of resources...");
|
|
7122
|
+
// Clear per-session compaction watermarks
|
|
7123
|
+
this.lastCompactionMessageCount.clear();
|
|
6873
7124
|
const cleanupErrors = [];
|
|
6874
7125
|
try {
|
|
6875
7126
|
// 1. Flush and shutdown OpenTelemetry
|
|
@@ -7014,8 +7265,21 @@ Current user's request: ${currentInput}`;
|
|
|
7014
7265
|
if (!messages || messages.length === 0) {
|
|
7015
7266
|
return null;
|
|
7016
7267
|
}
|
|
7017
|
-
const compactor = new ContextCompactor(
|
|
7018
|
-
|
|
7268
|
+
const compactor = new ContextCompactor({
|
|
7269
|
+
...config,
|
|
7270
|
+
summarizationProvider: config?.summarizationProvider ??
|
|
7271
|
+
this.conversationMemoryConfig?.conversationMemory
|
|
7272
|
+
?.summarizationProvider,
|
|
7273
|
+
summarizationModel: config?.summarizationModel ??
|
|
7274
|
+
this.conversationMemoryConfig?.conversationMemory?.summarizationModel,
|
|
7275
|
+
});
|
|
7276
|
+
// Use actual context window to determine target, not arbitrary heuristic
|
|
7277
|
+
const budgetInfo = checkContextBudget({
|
|
7278
|
+
provider: config?.provider || "openai",
|
|
7279
|
+
conversationMessages: messages,
|
|
7280
|
+
});
|
|
7281
|
+
// Target 60% of available input tokens — leave room for new messages
|
|
7282
|
+
const targetTokens = Math.floor(budgetInfo.availableInputTokens * 0.6);
|
|
7019
7283
|
const result = await compactor.compact(messages, targetTokens, this.conversationMemoryConfig?.conversationMemory);
|
|
7020
7284
|
if (result.compacted) {
|
|
7021
7285
|
repairToolPairs(result.messages);
|
|
@@ -1,17 +1,17 @@
|
|
|
1
1
|
import { createGoogleGenerativeAI } from "@ai-sdk/google";
|
|
2
2
|
import { embed, embedMany, streamText, } from "ai";
|
|
3
3
|
import { ErrorCategory, ErrorSeverity, GoogleAIModels, } from "../constants/enums.js";
|
|
4
|
-
import { estimateTokens } from "../utils/tokenEstimation.js";
|
|
5
4
|
import { BaseProvider } from "../core/baseProvider.js";
|
|
6
5
|
import { DEFAULT_MAX_STEPS } from "../core/constants.js";
|
|
7
6
|
import { streamAnalyticsCollector } from "../core/streamAnalytics.js";
|
|
7
|
+
import { ATTR, tracers, withClientSpan } from "../telemetry/index.js";
|
|
8
8
|
import { AuthenticationError, NetworkError, ProviderError, RateLimitError, } from "../types/errors.js";
|
|
9
9
|
import { ERROR_CODES, NeuroLinkError } from "../utils/errorHandling.js";
|
|
10
10
|
import { logger } from "../utils/logger.js";
|
|
11
11
|
import { isGemini3Model } from "../utils/modelDetection.js";
|
|
12
|
-
import { tracers, ATTR, withClientSpan } from "../telemetry/index.js";
|
|
13
12
|
import { composeAbortSignals, createTimeoutController, TimeoutError, } from "../utils/timeout.js";
|
|
14
|
-
import {
|
|
13
|
+
import { estimateTokens } from "../utils/tokenEstimation.js";
|
|
14
|
+
import { buildNativeConfig, buildNativeToolDeclarations, collectStreamChunks, computeMaxSteps, executeNativeToolCalls, extractTextFromParts, handleMaxStepsTermination, pushModelResponseToHistory, sanitizeToolsForGemini, } from "./googleNativeGemini3.js";
|
|
15
15
|
// Google AI Live API types now imported from ../types/providerSpecific.js
|
|
16
16
|
// Import proper types for multimodal message handling
|
|
17
17
|
// Create Google GenAI client
|
|
@@ -626,9 +626,12 @@ export class GoogleAIStudioProvider extends BaseProvider {
|
|
|
626
626
|
// Add model response with ALL parts (including thoughtSignature) to history
|
|
627
627
|
pushModelResponseToHistory(currentContents, chunkResult.rawResponseParts, chunkResult.stepFunctionCalls);
|
|
628
628
|
const functionResponses = await executeNativeToolCalls("[GoogleAIStudio]", chunkResult.stepFunctionCalls, executeMap, failedTools, allToolCalls, { abortSignal: composedSignal });
|
|
629
|
-
// Add function responses to history
|
|
629
|
+
// Add function responses to history — the @google/genai SDK
|
|
630
|
+
// only accepts "user" and "model" as valid roles in contents.
|
|
631
|
+
// Function/tool responses must use role: "user" (matching the
|
|
632
|
+
// SDK's own automaticFunctionCalling implementation).
|
|
630
633
|
currentContents.push({
|
|
631
|
-
role: "
|
|
634
|
+
role: "user",
|
|
632
635
|
parts: functionResponses,
|
|
633
636
|
});
|
|
634
637
|
}
|
|
@@ -776,9 +779,12 @@ export class GoogleAIStudioProvider extends BaseProvider {
|
|
|
776
779
|
// This is critical for Gemini 3 - it requires thought signatures in subsequent turns
|
|
777
780
|
pushModelResponseToHistory(currentContents, chunkResult.rawResponseParts, chunkResult.stepFunctionCalls);
|
|
778
781
|
const functionResponses = await executeNativeToolCalls("[GoogleAIStudio]", chunkResult.stepFunctionCalls, executeMap, failedTools, allToolCalls, { toolExecutions, abortSignal: composedSignal });
|
|
779
|
-
// Add function responses to history
|
|
782
|
+
// Add function responses to history — the @google/genai SDK
|
|
783
|
+
// only accepts "user" and "model" as valid roles in contents.
|
|
784
|
+
// Function/tool responses must use role: "user" (matching the
|
|
785
|
+
// SDK's own automaticFunctionCalling implementation).
|
|
780
786
|
currentContents.push({
|
|
781
|
-
role: "
|
|
787
|
+
role: "user",
|
|
782
788
|
parts: functionResponses,
|
|
783
789
|
});
|
|
784
790
|
}
|
|
@@ -27,6 +27,7 @@ export type NeurolinkConstructorConfig = {
|
|
|
27
27
|
hitl?: HITLConfig;
|
|
28
28
|
toolRegistry?: MCPToolRegistry;
|
|
29
29
|
observability?: ObservabilityConfig;
|
|
30
|
+
modelAliasConfig?: import("./generateTypes.js").ModelAliasConfig;
|
|
30
31
|
};
|
|
31
32
|
/**
|
|
32
33
|
* Provider-specific configuration
|
|
@@ -117,6 +118,8 @@ export type ToolConfig = {
|
|
|
117
118
|
maxToolsPerProvider?: number;
|
|
118
119
|
/** Whether MCP tools should be enabled */
|
|
119
120
|
enableMCPTools?: boolean;
|
|
121
|
+
/** Whether the bash command execution tool should be enabled (opt-in, defaults to false) */
|
|
122
|
+
enableBashTool?: boolean;
|
|
120
123
|
};
|
|
121
124
|
/**
|
|
122
125
|
* Backup metadata information
|
|
@@ -2,10 +2,9 @@
|
|
|
2
2
|
* Context Types for NeuroLink - Factory Pattern Implementation
|
|
3
3
|
* Provides type-safe context integration for AI generation
|
|
4
4
|
*/
|
|
5
|
-
import type { JsonObject } from "./common.js";
|
|
6
5
|
import type { ExecutionContext } from "../types/tools.js";
|
|
6
|
+
import type { JsonObject } from "./common.js";
|
|
7
7
|
import type { ChatMessage, ConversationMemoryConfig } from "./conversation.js";
|
|
8
|
-
import type { CompactionStage } from "../context/contextCompactor.js";
|
|
9
8
|
/**
|
|
10
9
|
* Base context type for all AI operations
|
|
11
10
|
*/
|
|
@@ -173,6 +172,8 @@ export declare class ContextConverter {
|
|
|
173
172
|
private static inferProvider;
|
|
174
173
|
private static extractCustomData;
|
|
175
174
|
}
|
|
175
|
+
/** Stages available in the compaction pipeline. */
|
|
176
|
+
export type CompactionStage = "prune" | "deduplicate" | "summarize" | "truncate";
|
|
176
177
|
/** Result of multi-stage context compaction. */
|
|
177
178
|
export type CompactionResult = {
|
|
178
179
|
compacted: boolean;
|
|
@@ -447,6 +448,8 @@ export type SummarizeConfig = {
|
|
|
447
448
|
model?: string;
|
|
448
449
|
keepRecentRatio?: number;
|
|
449
450
|
memoryConfig?: Partial<ConversationMemoryConfig>;
|
|
451
|
+
/** Target token budget — when set, split uses token counting instead of message count */
|
|
452
|
+
targetTokens?: number;
|
|
450
453
|
};
|
|
451
454
|
/** Result of structured LLM summarization (Stage 3). */
|
|
452
455
|
export type SummarizeResult = {
|