@juspay/neurolink 12.0.3 → 12.0.5

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/dist/neurolink.js CHANGED
@@ -5548,7 +5548,15 @@ Current user's request: ${currentInput}`;
5548
5548
  : "empty",
5549
5549
  hasCustomSystemPrompt: !!options.systemPrompt,
5550
5550
  });
5551
- const conversationMessages = (await getConversationMessages(this.conversationMemory, options));
5551
+ // Caller-supplied conversationMessages win mirroring
5552
+ // directProviderGeneration. getConversationMessages() returns [] whenever
5553
+ // no memory manager / session context is configured, which silently
5554
+ // dropped an inline history on the MCP-first path (the default path for
5555
+ // any bare `new NeuroLink()` with tools enabled) while the direct path
5556
+ // honored it.
5557
+ const conversationMessages = (options.conversationMessages?.length
5558
+ ? options.conversationMessages
5559
+ : await getConversationMessages(this.conversationMemory, options));
5552
5560
  this.logMCPConversationSummary(requestId, conversationMessages);
5553
5561
  logger.debug("[Observability] Available tools for LLM", {
5554
5562
  requestId,
@@ -6062,32 +6070,118 @@ Current user's request: ${currentInput}`;
6062
6070
  // oversized case. When the budget check shows the request is
6063
6071
  // over budget but there's nothing to compact (no memory + no
6064
6072
  // inline messages — e.g. a huge prompt or huge tool definitions
6065
- // alone), throw before dispatch instead of wasting a roundtrip.
6073
+ // alone), recover by WINDOWING the prompt when the prompt is what
6074
+ // blew the budget: keep its head and tail around an elision marker
6075
+ // and dispatch. Agentic callers (Yama's session loop) carry their
6076
+ // whole tool transcript in the prompt; the previous
6077
+ // unconditional throw dead-ended every such turn — observed live
6078
+ // as an unbounded retry loop (93K→299K tokens, no verdict, ever).
6079
+ // The throw remains for the truly unrecoverable case: system
6080
+ // prompt + tool definitions alone exceed the budget.
6081
+ let promptWindowRecovered = false;
6066
6082
  if (!budgetCheck.withinBudget && !dpgHasCompactableMessages) {
6067
- try {
6068
- this.emitter.emit("compaction.insufficient", {
6069
- stagesAttempted: ["pre-dispatch hard cap"],
6070
- finalTokens: budgetCheck.estimatedInputTokens,
6071
- budget: budgetCheck.availableInputTokens,
6072
- provider: providerName,
6073
- model: options.model,
6074
- phase: "pre-dispatch-no-recovery",
6075
- timestamp: Date.now(),
6076
- });
6083
+ const fixedOverhead = (budgetCheck.breakdown?.systemPrompt ?? 0) +
6084
+ (budgetCheck.breakdown?.toolDefinitions ?? 0) +
6085
+ (budgetCheck.breakdown?.fileAttachments ?? 0);
6086
+ // 3% margin against estimator drift; 1024-token floor — below
6087
+ // that, a windowed prompt carries too little to answer from.
6088
+ const promptBudget = Math.floor((budgetCheck.availableInputTokens - fixedOverhead) * 0.97);
6089
+ const promptText = typeof options.prompt === "string" ? options.prompt : undefined;
6090
+ if (promptText &&
6091
+ promptBudget >= 1024 &&
6092
+ (budgetCheck.breakdown?.currentPrompt ?? 0) > promptBudget) {
6093
+ const marker = "\n\n[... middle of this prompt elided by NeuroLink to fit the model's context window ...]\n\n";
6094
+ // Proportional char budget from the observed chars-per-token of
6095
+ // THIS text, re-checked and shrunk until the estimator agrees.
6096
+ let charBudget = Math.floor(promptText.length *
6097
+ (promptBudget /
6098
+ Math.max(budgetCheck.breakdown?.currentPrompt ?? 1, 1)));
6099
+ let windowed = promptText;
6100
+ for (let attempt = 0; attempt < 4; attempt++) {
6101
+ const headChars = Math.floor(charBudget * 0.6);
6102
+ const tailChars = Math.max(charBudget - headChars - marker.length, 0);
6103
+ windowed =
6104
+ promptText.slice(0, headChars) +
6105
+ marker +
6106
+ (tailChars > 0 ? promptText.slice(-tailChars) : "");
6107
+ const recheck = checkContextBudget({
6108
+ provider: providerName,
6109
+ model: options.model,
6110
+ maxTokens: options.maxTokens,
6111
+ systemPrompt: options.systemPrompt,
6112
+ conversationMessages: [],
6113
+ currentPrompt: windowed,
6114
+ toolDefinitions: options.tools
6115
+ ? Object.values(options.tools)
6116
+ : undefined,
6117
+ });
6118
+ if (recheck.withinBudget) {
6119
+ break;
6120
+ }
6121
+ charBudget = Math.floor(charBudget * 0.8);
6122
+ if (attempt === 3) {
6123
+ windowed = promptText; // give up — fall through to the throw
6124
+ }
6125
+ }
6126
+ if (windowed !== promptText) {
6127
+ logger.warn("[NeuroLink] Prompt exceeded the model's context budget with nothing to compact — " +
6128
+ "windowed the prompt (head+tail kept, middle elided) to fit.", {
6129
+ provider: providerName,
6130
+ model: options.model,
6131
+ estimatedTokens: budgetCheck.estimatedInputTokens,
6132
+ budget: budgetCheck.availableInputTokens,
6133
+ originalPromptChars: promptText.length,
6134
+ windowedPromptChars: windowed.length,
6135
+ });
6136
+ try {
6137
+ this.emitter.emit("compaction.applied", {
6138
+ stagesAttempted: ["pre-dispatch prompt window"],
6139
+ finalTokens: budgetCheck.availableInputTokens,
6140
+ budget: budgetCheck.availableInputTokens,
6141
+ provider: providerName,
6142
+ model: options.model,
6143
+ phase: "pre-dispatch-prompt-window",
6144
+ timestamp: Date.now(),
6145
+ });
6146
+ }
6147
+ catch {
6148
+ /* listener errors are non-fatal */
6149
+ }
6150
+ options.prompt = windowed;
6151
+ const inputHolder = options
6152
+ .input;
6153
+ if (inputHolder && typeof inputHolder.text === "string") {
6154
+ inputHolder.text = windowed;
6155
+ }
6156
+ promptWindowRecovered = true;
6157
+ }
6077
6158
  }
6078
- catch {
6079
- /* listener errors are non-fatal */
6159
+ if (!promptWindowRecovered) {
6160
+ try {
6161
+ this.emitter.emit("compaction.insufficient", {
6162
+ stagesAttempted: ["pre-dispatch hard cap"],
6163
+ finalTokens: budgetCheck.estimatedInputTokens,
6164
+ budget: budgetCheck.availableInputTokens,
6165
+ provider: providerName,
6166
+ model: options.model,
6167
+ phase: "pre-dispatch-no-recovery",
6168
+ timestamp: Date.now(),
6169
+ });
6170
+ }
6171
+ catch {
6172
+ /* listener errors are non-fatal */
6173
+ }
6174
+ throw new ContextBudgetExceededError(`Context exceeds model budget and no compaction is possible ` +
6175
+ `(no conversationMemory, no inline conversationMessages — only ` +
6176
+ `prompt + tools). Estimated: ${budgetCheck.estimatedInputTokens} ` +
6177
+ `tokens, budget: ${budgetCheck.availableInputTokens} tokens. ` +
6178
+ `Reduce prompt or tool-definition size, or trim the request.`, {
6179
+ estimatedTokens: budgetCheck.estimatedInputTokens,
6180
+ availableTokens: budgetCheck.availableInputTokens,
6181
+ stagesUsed: [],
6182
+ breakdown: budgetCheck.breakdown,
6183
+ });
6080
6184
  }
6081
- throw new ContextBudgetExceededError(`Context exceeds model budget and no compaction is possible ` +
6082
- `(no conversationMemory, no inline conversationMessages — only ` +
6083
- `prompt + tools). Estimated: ${budgetCheck.estimatedInputTokens} ` +
6084
- `tokens, budget: ${budgetCheck.availableInputTokens} tokens. ` +
6085
- `Reduce prompt or tool-definition size, or trim the request.`, {
6086
- estimatedTokens: budgetCheck.estimatedInputTokens,
6087
- availableTokens: budgetCheck.availableInputTokens,
6088
- stagesUsed: [],
6089
- breakdown: budgetCheck.breakdown,
6090
- });
6091
6185
  }
6092
6186
  if (budgetCheck.shouldCompact &&
6093
6187
  (this.conversationMemory || dpgHasInlineMessages) &&
@@ -235,9 +235,47 @@ async function _prepareRAGToolInner(ragConfig, fallbackProvider) {
235
235
  const EMBEDDING_DIMENSION = 128;
236
236
  const vectorStore = new InMemoryVectorStore();
237
237
  const indexName = "rag-index";
238
+ // When the caller configured an embedding provider/model, embed BOTH the
239
+ // index chunks and (below) the queries through that provider — previously
240
+ // those config fields had no runtime effect and retrieval always used the
241
+ // deterministic hash embedding, which is a lexical fingerprint rather than
242
+ // a semantic space. Index and query must share one embedding space, so the
243
+ // provider path replaces the hash path wholesale; any provider failure
244
+ // falls back to the hash for both sides.
245
+ const wantProviderEmbeddings = Boolean(embeddingProvider || embeddingModel);
246
+ const embedProviderName = embeddingProvider || fallbackProvider || "vertex";
247
+ const embedModelName = embeddingModel || "gemini-2.5-flash";
248
+ let embedFn = (text) => Promise.resolve(generateSimpleEmbedding(text, EMBEDDING_DIMENSION));
249
+ if (wantProviderEmbeddings) {
250
+ try {
251
+ const { AIProviderFactory } = await import("../core/factory.js");
252
+ const embedderProvider = (await AIProviderFactory.createProvider(embedProviderName, embedModelName));
253
+ if (typeof embedderProvider.embed === "function") {
254
+ const providerEmbed = embedderProvider.embed.bind(embedderProvider);
255
+ embedFn = (text) => providerEmbed(text, embedModelName);
256
+ }
257
+ else {
258
+ logger.warn(`[RAG] Embedding provider '${embedProviderName}' has no embed(); falling back to hash embeddings`);
259
+ }
260
+ }
261
+ catch (error) {
262
+ logger.warn("[RAG] Failed to create embedding provider; falling back to hash embeddings", { error: error instanceof Error ? error.message : String(error) });
263
+ }
264
+ }
265
+ let chunkVectors;
266
+ try {
267
+ chunkVectors = await Promise.all(allChunks.map((chunk) => embedFn(chunk.text)));
268
+ }
269
+ catch (error) {
270
+ // One failed chunk must not leave a mixed-space index — flip the whole
271
+ // index AND all queries back to the hash space together.
272
+ logger.warn("[RAG] Provider embedding failed mid-index; falling back to hash embeddings for index and queries", { error: error instanceof Error ? error.message : String(error) });
273
+ embedFn = (text) => Promise.resolve(generateSimpleEmbedding(text, EMBEDDING_DIMENSION));
274
+ chunkVectors = allChunks.map((chunk) => generateSimpleEmbedding(chunk.text, EMBEDDING_DIMENSION));
275
+ }
238
276
  const items = allChunks.map((chunk, i) => ({
239
277
  id: `rag-chunk-${i}`,
240
- vector: generateSimpleEmbedding(chunk.text, EMBEDDING_DIMENSION),
278
+ vector: chunkVectors[i],
241
279
  metadata: {
242
280
  text: chunk.text,
243
281
  ...chunk.metadata,
@@ -274,9 +312,9 @@ async function _prepareRAGToolInner(ragConfig, fallbackProvider) {
274
312
  "rag.top_k": topK ?? 5,
275
313
  },
276
314
  }, async (span) => {
277
- // For the in-memory store with simple embeddings,
278
- // generate a query embedding using the same method
279
- const queryEmbedding = generateSimpleEmbedding(query, EMBEDDING_DIMENSION);
315
+ // Query through the same embedding space the index was built in —
316
+ // provider embeddings when configured (and healthy), else the hash.
317
+ const queryEmbedding = await embedFn(query).catch(() => generateSimpleEmbedding(query, EMBEDDING_DIMENSION));
280
318
  // Fetch more candidates than needed so diversity can select across files
281
319
  const fetchK = fileContents.length > 1 ? topK * 3 : topK;
282
320
  const rawResults = await vectorStore.query({
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@juspay/neurolink",
3
- "version": "12.0.3",
3
+ "version": "12.0.5",
4
4
  "packageManager": "pnpm@10.15.1",
5
5
  "description": "TypeScript AI SDK with 24+ LLM providers behind one consistent API. MCP-native (connect any MCP server), voice TTS/STT/realtime, RAG, agents, memory, context compaction. OpenAI · Anthropic · Gemini · Bedrock · Azure · Ollama · DeepSeek · NVIDIA NIM and more.",
6
6
  "author": {