@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.
Files changed (61) hide show
  1. package/CHANGELOG.md +6 -0
  2. package/dist/adapters/providerImageAdapter.d.ts +3 -27
  3. package/dist/adapters/providerImageAdapter.js +9 -199
  4. package/dist/agent/directTools.d.ts +35 -3
  5. package/dist/agent/directTools.js +122 -0
  6. package/dist/cli/commands/config.d.ts +6 -6
  7. package/dist/context/contextCompactor.d.ts +1 -2
  8. package/dist/context/contextCompactor.js +7 -1
  9. package/dist/context/prompts/summarizationPrompt.d.ts +3 -3
  10. package/dist/context/prompts/summarizationPrompt.js +16 -9
  11. package/dist/context/stages/structuredSummarizer.d.ts +2 -2
  12. package/dist/context/stages/structuredSummarizer.js +80 -30
  13. package/dist/lib/adapters/providerImageAdapter.d.ts +3 -27
  14. package/dist/lib/adapters/providerImageAdapter.js +9 -199
  15. package/dist/lib/agent/directTools.d.ts +33 -1
  16. package/dist/lib/agent/directTools.js +122 -0
  17. package/dist/lib/context/contextCompactor.d.ts +1 -2
  18. package/dist/lib/context/contextCompactor.js +7 -1
  19. package/dist/lib/context/prompts/summarizationPrompt.d.ts +3 -3
  20. package/dist/lib/context/prompts/summarizationPrompt.js +16 -9
  21. package/dist/lib/context/stages/structuredSummarizer.d.ts +2 -2
  22. package/dist/lib/context/stages/structuredSummarizer.js +80 -30
  23. package/dist/lib/mcp/servers/agent/directToolsServer.js +2 -0
  24. package/dist/lib/mcp/toolRegistry.d.ts +8 -0
  25. package/dist/lib/mcp/toolRegistry.js +20 -0
  26. package/dist/lib/neurolink.d.ts +10 -0
  27. package/dist/lib/neurolink.js +281 -17
  28. package/dist/lib/providers/googleAiStudio.js +13 -7
  29. package/dist/lib/types/configTypes.d.ts +3 -0
  30. package/dist/lib/types/contextTypes.d.ts +5 -2
  31. package/dist/lib/types/contextTypes.js +8 -8
  32. package/dist/lib/types/generateTypes.d.ts +25 -0
  33. package/dist/lib/types/modelTypes.d.ts +2 -2
  34. package/dist/lib/utils/messageBuilder.js +2 -0
  35. package/dist/lib/utils/modelAliasResolver.d.ts +17 -0
  36. package/dist/lib/utils/modelAliasResolver.js +55 -0
  37. package/dist/lib/utils/pdfProcessor.d.ts +1 -1
  38. package/dist/lib/utils/pdfProcessor.js +7 -7
  39. package/dist/lib/utils/toolUtils.d.ts +8 -0
  40. package/dist/lib/utils/toolUtils.js +15 -0
  41. package/dist/lib/workflow/config.d.ts +24 -24
  42. package/dist/mcp/servers/agent/directToolsServer.js +2 -0
  43. package/dist/mcp/toolRegistry.d.ts +8 -0
  44. package/dist/mcp/toolRegistry.js +20 -0
  45. package/dist/neurolink.d.ts +10 -0
  46. package/dist/neurolink.js +281 -17
  47. package/dist/providers/googleAiStudio.js +13 -7
  48. package/dist/server/utils/validation.d.ts +2 -2
  49. package/dist/types/configTypes.d.ts +3 -0
  50. package/dist/types/contextTypes.d.ts +5 -2
  51. package/dist/types/contextTypes.js +8 -8
  52. package/dist/types/generateTypes.d.ts +25 -0
  53. package/dist/utils/messageBuilder.js +2 -0
  54. package/dist/utils/modelAliasResolver.d.ts +17 -0
  55. package/dist/utils/modelAliasResolver.js +54 -0
  56. package/dist/utils/pdfProcessor.d.ts +1 -1
  57. package/dist/utils/pdfProcessor.js +7 -7
  58. package/dist/utils/toolUtils.d.ts +8 -0
  59. package/dist/utils/toolUtils.js +15 -0
  60. package/dist/workflow/config.d.ts +82 -82
  61. package/package.json +1 -1
@@ -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: error instanceof Error ? error.message : String(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
- if (budgetResult.shouldCompact && this.conversationMemory) {
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
- if (budgetCheck.shouldCompact && this.conversationMemory) {
3499
- const compactor = new ContextCompactor({ provider: providerName });
3500
- const compactionResult = await compactor.compact(conversationMessages, budgetCheck.availableInputTokens, undefined, options.context?.requestId);
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
- if (streamBudget.shouldCompact && this.conversationMemory) {
4492
- const compactor = new ContextCompactor({ provider: providerName });
4493
- const compactionResult = await compactor.compact(conversationMessages, streamBudget.availableInputTokens, undefined, options.context?.requestId);
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(toolName)) {
5421
- this.toolCircuitBreakers.set(toolName, new CircuitBreaker(CIRCUIT_BREAKER.FAILURE_THRESHOLD, CIRCUIT_BREAKER_RESET_MS));
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(toolName);
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 circuitBreaker = this.toolCircuitBreakers.get(toolName);
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(config);
7018
- const targetTokens = Math.floor(messages.length * 100); // Rough target
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 { buildNativeToolDeclarations, buildNativeConfig, computeMaxSteps, collectStreamChunks, extractTextFromParts, executeNativeToolCalls, handleMaxStepsTermination, pushModelResponseToHistory, sanitizeToolsForGemini, } from "./googleNativeGemini3.js";
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: "function",
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: "function",
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 = {