@juspay/neurolink 12.47.14 → 12.48.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 (94) hide show
  1. package/CHANGELOG.md +3 -3
  2. package/dist/auth/middleware/AuthMiddleware.d.ts +1 -1
  3. package/dist/browser/neurolink.min.js +578 -565
  4. package/dist/cli/factories/commandFactory.d.ts +7 -0
  5. package/dist/cli/factories/commandFactory.js +26 -0
  6. package/dist/cli/loop/optionsSchema.js +9 -0
  7. package/dist/context/budgetChecker.js +54 -2
  8. package/dist/context/contextDecision.js +2 -0
  9. package/dist/context/toolPairRepair.d.ts +12 -0
  10. package/dist/context/toolPairRepair.js +29 -3
  11. package/dist/core/baseProvider.d.ts +92 -3
  12. package/dist/core/baseProvider.js +254 -10
  13. package/dist/core/conversationMemoryManager.js +17 -2
  14. package/dist/core/loopEngine.js +1 -1
  15. package/dist/core/modules/MessageBuilder.js +11 -3
  16. package/dist/core/modules/TelemetryHandler.d.ts +2 -2
  17. package/dist/core/modules/ToolsManager.d.ts +35 -0
  18. package/dist/core/modules/ToolsManager.js +159 -55
  19. package/dist/core/nativeGenerateLoop.d.ts +58 -1
  20. package/dist/core/nativeGenerateLoop.js +277 -3
  21. package/dist/core/redisConversationMemoryManager.js +10 -3
  22. package/dist/core/resolveRequestKind.d.ts +24 -4
  23. package/dist/core/resolveRequestKind.js +31 -4
  24. package/dist/core/toolExecutionRecorder.d.ts +6 -0
  25. package/dist/core/toolExecutionRecorder.js +24 -2
  26. package/dist/core/toolRoutingDecision.js +1 -0
  27. package/dist/factories/mediaHandlerCatalog.js +5 -0
  28. package/dist/factories/providerDescriptors.js +15 -0
  29. package/dist/files/fileTools.d.ts +1 -1
  30. package/dist/index.d.ts +5 -2
  31. package/dist/index.js +9 -2
  32. package/dist/memory/condensationPrompt.d.ts +8 -0
  33. package/dist/memory/condensationPrompt.js +29 -0
  34. package/dist/memory/hippocampusInitializer.d.ts +14 -2
  35. package/dist/memory/hippocampusInitializer.js +64 -3
  36. package/dist/neurolink.d.ts +252 -3
  37. package/dist/neurolink.js +898 -43
  38. package/dist/providers/amazonBedrock/client.js +6 -0
  39. package/dist/providers/amazonSagemaker.js +14 -1
  40. package/dist/providers/anthropic/client.js +148 -19
  41. package/dist/providers/anthropic/loopAdapter.js +2 -2
  42. package/dist/providers/googleAiStudio/client.js +2 -1
  43. package/dist/providers/openaiChatCompletionsBase.js +103 -23
  44. package/dist/providers/systemOneDecision.d.ts +6 -0
  45. package/dist/providers/systemOneDecision.js +14 -63
  46. package/dist/providers/typesafe.d.ts +6 -0
  47. package/dist/providers/typesafe.js +6 -0
  48. package/dist/rag/retrieval/searchDecision.js +1 -0
  49. package/dist/routing/classifierRouter.js +6 -5
  50. package/dist/routing/classifierStrategies.js +8 -1
  51. package/dist/types/classifierRouter.d.ts +16 -3
  52. package/dist/types/common.d.ts +2 -0
  53. package/dist/types/config.d.ts +9 -0
  54. package/dist/types/context.d.ts +8 -1
  55. package/dist/types/conversation.d.ts +87 -0
  56. package/dist/types/decision.d.ts +210 -1
  57. package/dist/types/decision.js +9 -1
  58. package/dist/types/dispatch.d.ts +11 -4
  59. package/dist/types/generate.d.ts +118 -54
  60. package/dist/types/loopEngine.d.ts +7 -2
  61. package/dist/types/memory.d.ts +177 -9
  62. package/dist/types/openaiCompatible.d.ts +11 -3
  63. package/dist/types/stream.d.ts +13 -16
  64. package/dist/types/stt.d.ts +58 -1
  65. package/dist/types/tools.d.ts +14 -0
  66. package/dist/types/tools.js +8 -0
  67. package/dist/types/tts.d.ts +108 -0
  68. package/dist/types/voice.d.ts +41 -4
  69. package/dist/utils/decisionAnswers.d.ts +31 -1
  70. package/dist/utils/decisionAnswers.js +114 -0
  71. package/dist/utils/decisionLimits.d.ts +39 -0
  72. package/dist/utils/decisionLimits.js +116 -0
  73. package/dist/utils/logger.d.ts +7 -0
  74. package/dist/utils/logger.js +9 -0
  75. package/dist/utils/messageBuilder.d.ts +45 -1
  76. package/dist/utils/messageBuilder.js +393 -16
  77. package/dist/utils/multimodalOptionsBuilder.d.ts +1 -0
  78. package/dist/utils/multimodalOptionsBuilder.js +1 -0
  79. package/dist/utils/speechText.d.ts +41 -0
  80. package/dist/utils/speechText.js +364 -0
  81. package/dist/utils/toolResultStatus.d.ts +26 -0
  82. package/dist/utils/toolResultStatus.js +73 -0
  83. package/dist/utils/ttsProcessor.d.ts +29 -0
  84. package/dist/utils/ttsProcessor.js +84 -2
  85. package/dist/voice/index.d.ts +1 -0
  86. package/dist/voice/index.js +3 -0
  87. package/dist/voice/providers/ElevenLabsSTT.d.ts +42 -0
  88. package/dist/voice/providers/ElevenLabsSTT.js +351 -0
  89. package/dist/voice/providers/ElevenLabsTTS.d.ts +81 -1
  90. package/dist/voice/providers/ElevenLabsTTS.js +415 -74
  91. package/docs-site/static/search-index.json +48 -33
  92. package/package.json +5 -2
  93. package/dist/utils/toolEndEmitter.d.ts +0 -25
  94. package/dist/utils/toolEndEmitter.js +0 -64
@@ -133,6 +133,12 @@ export declare abstract class SystemOneDecisionProvider extends BaseProvider {
133
133
  * Refuse a request the model cannot read in full. An encoder cuts the state
134
134
  * off past its window without saying so, so without this a decision would
135
135
  * be made on input the model never saw — and reported as if it had.
136
+ *
137
+ * Advisory limits (`decisionLimits.advisory`) are skipped: they publish a
138
+ * server's own ceiling for `NeuroLink.decisionLimits()` to report, and the
139
+ * server enforces them itself. The estimator and the flattening are the
140
+ * same functions that method uses, so a host that sizes a state against the
141
+ * reading measures exactly what is checked here.
136
142
  */
137
143
  private assertWithinDecisionLimits;
138
144
  /**
@@ -6,7 +6,7 @@ import { ProviderError } from "../types/index.js";
6
6
  import { prepareDecisionMedia } from "../utils/decisionMedia.js";
7
7
  import { logger } from "../utils/logger.js";
8
8
  import { redactUrlsInText } from "../utils/logSanitize.js";
9
- import { CHARS_PER_TOKEN, estimateTokens, serializeForEstimate, } from "../utils/tokenEstimation.js";
9
+ import { estimateDecisionStateTokens, resolveDecisionLimitsReading, } from "../utils/decisionLimits.js";
10
10
  /**
11
11
  * Generous enough for a cold start (measured at 2.0–2.7s after idle on Jev)
12
12
  * while still bounded. Every internal caller is fail-open, so this only bites
@@ -171,59 +171,6 @@ const sleep = (ms, signal) => new Promise((resolve, reject) => {
171
171
  }
172
172
  signal?.addEventListener("abort", onAbort, { once: true });
173
173
  });
174
- /**
175
- * `estimateTokens` assumes ~4 characters per token. That holds for English
176
- * and is several times too generous for other scripts on an English
177
- * tokenizer, so when a provider declares a rate, non-ASCII characters are
178
- * counted at it instead. ASCII text is estimated exactly as before.
179
- */
180
- function estimateDecisionStateTokens(text, nonAsciiTokensPerChar, rates = {}) {
181
- const { digit, symbol, astral } = rates;
182
- if (nonAsciiTokensPerChar === undefined &&
183
- digit === undefined &&
184
- symbol === undefined &&
185
- astral === undefined) {
186
- return estimateTokens(text);
187
- }
188
- // Each class of character is counted once, at its own rate. A class with no
189
- // declared rate stays in the ordinary four-characters-a-token estimate (or,
190
- // for non-ASCII, at that estimate's per-character rate), so a provider that
191
- // declares none of the extra rates is estimated exactly as before.
192
- const isDigit = (c) => c >= "0" && c <= "9";
193
- const isSymbol = (c) => /[!-/:-@[-`{-~]/.test(c);
194
- let digits = 0;
195
- let symbols = 0;
196
- let astrals = 0;
197
- let nonAscii = 0;
198
- const rest = [];
199
- for (const c of text) {
200
- const code = c.codePointAt(0) ?? 0;
201
- if (code > 0xffff && astral !== undefined) {
202
- astrals += 1;
203
- }
204
- else if (code > 0x7f) {
205
- nonAscii += 1;
206
- }
207
- else if (digit !== undefined && isDigit(c)) {
208
- digits += 1;
209
- }
210
- else if (symbol !== undefined && isSymbol(c)) {
211
- symbols += 1;
212
- }
213
- else {
214
- rest.push(c);
215
- }
216
- }
217
- // Digits, punctuation and astral characters (emoji) are charged separately
218
- // only when the provider declares a rate for them: a tokenizer that reads each
219
- // digit, and most punctuation, on its own makes a number-heavy or JSON-heavy
220
- // state several times longer than four characters a token suggests.
221
- return (estimateTokens(rest.join("")) +
222
- Math.ceil(digits * (digit ?? 0)) +
223
- Math.ceil(symbols * (symbol ?? 0)) +
224
- Math.ceil(astrals * (astral ?? 0)) +
225
- Math.ceil(nonAscii * (nonAsciiTokensPerChar ?? 1 / CHARS_PER_TOKEN)));
226
- }
227
174
  /**
228
175
  * The shared half of every "System One" decision provider — a model that takes
229
176
  * one `state` plus typed questions and returns one typed answer each, with no
@@ -551,10 +498,19 @@ export class SystemOneDecisionProvider extends BaseProvider {
551
498
  * Refuse a request the model cannot read in full. An encoder cuts the state
552
499
  * off past its window without saying so, so without this a decision would
553
500
  * be made on input the model never saw — and reported as if it had.
501
+ *
502
+ * Advisory limits (`decisionLimits.advisory`) are skipped: they publish a
503
+ * server's own ceiling for `NeuroLink.decisionLimits()` to report, and the
504
+ * server enforces them itself. The estimator and the flattening are the
505
+ * same functions that method uses, so a host that sizes a state against the
506
+ * reading measures exactly what is checked here.
554
507
  */
555
508
  assertWithinDecisionLimits(request, model, questionCount) {
556
- const limits = PROVIDER_DESCRIPTORS_BY_NAME.get(this.providerName)?.decisionLimits;
557
- if (!limits) {
509
+ const descriptor = PROVIDER_DESCRIPTORS_BY_NAME.get(this.providerName);
510
+ const limits = descriptor
511
+ ? resolveDecisionLimitsReading(descriptor, model)
512
+ : null;
513
+ if (!limits || !limits.enforcedLocally) {
558
514
  return;
559
515
  }
560
516
  const label = this.vendorLabel();
@@ -566,13 +522,8 @@ export class SystemOneDecisionProvider extends BaseProvider {
566
522
  retryable: false,
567
523
  });
568
524
  }
569
- const modelLimits = limits.models?.[model];
570
- const maxStateTokens = modelLimits?.maxStateTokens ?? limits.maxStateTokens;
571
- const stateTokens = estimateDecisionStateTokens(serializeForEstimate(request.state), modelLimits?.nonAsciiTokensPerChar ?? limits.nonAsciiTokensPerChar, {
572
- digit: limits.digitTokensPerChar,
573
- symbol: limits.symbolTokensPerChar,
574
- astral: limits.astralTokensPerChar,
575
- });
525
+ const { maxStateTokens } = limits;
526
+ const stateTokens = estimateDecisionStateTokens(request.state, limits);
576
527
  if (stateTokens > maxStateTokens) {
577
528
  throw this.decisionError({
578
529
  kind: "max_tokens_exceeded",
@@ -4,6 +4,12 @@ import { SystemOneDecisionProvider } from "./systemOneDecision.js";
4
4
  * The binding limit in practice: `state` plus the SINGLE LONGEST question.
5
5
  * Measured by bisection at single-character resolution — 33,002 reported
6
6
  * input tokens accepted, 33,003 not.
7
+ *
8
+ * The same figure sits on the TypeSafe descriptor as an ADVISORY
9
+ * `decisionLimits.maxStateTokens`, which is what `NeuroLink.decisionLimits()`
10
+ * reads; the server enforces it, NeuroLink only reports it. The descriptor
11
+ * cannot import this constant without a cycle, so the decide suite pins them
12
+ * equal instead.
7
13
  */
8
14
  export declare const TYPESAFE_MAX_STATE_TOKENS = 33000;
9
15
  /**
@@ -27,6 +27,12 @@ const GATEWAY_SPEC_VERSION = "4";
27
27
  * The binding limit in practice: `state` plus the SINGLE LONGEST question.
28
28
  * Measured by bisection at single-character resolution — 33,002 reported
29
29
  * input tokens accepted, 33,003 not.
30
+ *
31
+ * The same figure sits on the TypeSafe descriptor as an ADVISORY
32
+ * `decisionLimits.maxStateTokens`, which is what `NeuroLink.decisionLimits()`
33
+ * reads; the server enforces it, NeuroLink only reports it. The descriptor
34
+ * cannot import this constant without a cycle, so the decide suite pins them
35
+ * equal instead.
30
36
  */
31
37
  export const TYPESAFE_MAX_STATE_TOKENS = 33_000;
32
38
  /**
@@ -75,6 +75,7 @@ export async function decideSearchPlan(query, decide, capabilities) {
75
75
  };
76
76
  }
77
77
  const result = await decide({
78
+ site: "ragPlan",
78
79
  state: { query: query.slice(0, 4000) },
79
80
  // The map is assembled dynamically above; its members are all valid
80
81
  // question shapes, which the cast asserts once here rather than at four
@@ -98,7 +98,7 @@ export class ClassifierRouter {
98
98
  try {
99
99
  // Both the LLM and Jev strategies can pick a model straight out of the
100
100
  // pool — the generic path for custom/registry-less models.
101
- const strategy = this.resolveStrategy();
101
+ const strategy = this.resolveStrategy(input);
102
102
  const usesCandidates = (strategy === "jev" && !!this.deps.decide) ||
103
103
  (strategy === "llm" && !!this.deps.generate);
104
104
  const built = usesCandidates ? this.buildCandidates() : undefined;
@@ -138,7 +138,7 @@ export class ClassifierRouter {
138
138
  * moment a decision provider is configured; without one the behaviour is
139
139
  * exactly what it was before Jev existed.
140
140
  */
141
- resolveStrategy() {
141
+ resolveStrategy(input) {
142
142
  const configured = this.config.classifier ?? "auto";
143
143
  if (configured !== "auto") {
144
144
  return configured;
@@ -146,10 +146,11 @@ export class ClassifierRouter {
146
146
  // A decision provider that is both registered AND configured is the whole
147
147
  // activation condition — this is where "if somebody sets it we start
148
148
  // using it" lives for routing. The caller's check also counts the
149
- // credentials it was constructed with.
149
+ // credentials it was constructed with and the request's own per-call
150
+ // credentials, which the decision call itself will use.
150
151
  const hasProvider = this.deps.hasDecisionProvider
151
- ? this.deps.hasDecisionProvider()
152
- : resolveDefaultDecisionProvider() !== undefined;
152
+ ? this.deps.hasDecisionProvider(input.credentials)
153
+ : resolveDefaultDecisionProvider(input.credentials) !== undefined;
153
154
  return this.deps.decide && hasProvider ? "jev" : "heuristic";
154
155
  }
155
156
  /** Run the chosen strategy; every one falls back to heuristic on failure. */
@@ -297,8 +297,15 @@ export async function classifyJev(input, decide, timeoutMs, candidates, threshol
297
297
  // short conversational one.
298
298
  // Continuity is sent as `session_bound` / `prior_messages`, never as the
299
299
  // raw sessionId: the id is an opaque identifier that tells a third-party
300
- // model nothing, so it stays in-process.
300
+ // model nothing, so it stays in-process — as do the site stamp, the
301
+ // per-call credentials and the ids below, which the caller reads and the
302
+ // wire never sees.
301
303
  const result = await decide({
304
+ site: "routing",
305
+ credentials: input.credentials,
306
+ signal: input.signal,
307
+ sessionId: input.sessionId,
308
+ requestId: input.requestId,
302
309
  state: {
303
310
  request: (input.prompt ?? "").slice(0, 8000),
304
311
  estimated_input_tokens: input.estimatedInputTokens ?? 0,
@@ -15,6 +15,7 @@
15
15
  */
16
16
  import type { ValidationSchema } from "./aliases.js";
17
17
  import type { DecisionCallerFn } from "./decision.js";
18
+ import type { NeurolinkCredentials } from "./providers.js";
18
19
  /** Coarse difficulty buckets the classifier maps a request into. */
19
20
  export type ClassifierDifficulty = "trivial" | "simple" | "moderate" | "hard" | "expert";
20
21
  /**
@@ -307,6 +308,16 @@ export type ClassifierRouterInput = {
307
308
  sessionBound?: boolean;
308
309
  /** Number of prior conversation messages the caller supplied, when known. */
309
310
  priorMessageCount?: number;
311
+ /**
312
+ * The outer request's per-call credentials, forwarded to the decision call
313
+ * so it reaches the caller's provider account rather than the instance's.
314
+ * Never part of the state sent to the model.
315
+ */
316
+ credentials?: NeurolinkCredentials;
317
+ /** The outer request's abort signal; an abandoned turn abandons its routing call. */
318
+ signal?: AbortSignal;
319
+ /** The outer request's id, for hook and event correlation. Never sent to the model. */
320
+ requestId?: string;
310
321
  };
311
322
  /** Enriched per-model metadata used while ranking pool members. */
312
323
  export type ClassifierModelMeta = {
@@ -359,9 +370,11 @@ export type ClassifierRouterDeps = {
359
370
  decide?: ClassifierDecideFn;
360
371
  /**
361
372
  * Whether a decision provider is configured for this caller, counting the
362
- * credentials it was given as well as the environment. Omit to check the
363
- * environment alone.
373
+ * credentials it was given as well as the environment — and the request's
374
+ * own per-call `credentials`, passed in from `ClassifierRouterInput`, so a
375
+ * caller that holds no decision key itself can still route a request that
376
+ * carries one. Omit to check the environment alone.
364
377
  */
365
- hasDecisionProvider?: () => boolean;
378
+ hasDecisionProvider?: (callCredentials?: NeurolinkCredentials) => boolean;
366
379
  logger?: ClassifierLogger;
367
380
  };
@@ -102,6 +102,8 @@ export type NeuroLinkEvents = {
102
102
  "stream:error": unknown;
103
103
  "generation:start": unknown;
104
104
  "generation:end": unknown;
105
+ "decision:before": unknown;
106
+ "decision:after": unknown;
105
107
  "response:start": unknown;
106
108
  "response:end": unknown;
107
109
  "externalMCP:serverConnected": unknown;
@@ -14,6 +14,7 @@ import type { ModelPoolConfig } from "./modelPool.js";
14
14
  import type { RequestRouter } from "./requestRouter.js";
15
15
  import type { ClassifierRouterConfig } from "./classifierRouter.js";
16
16
  import type { ContextRelevanceOptions } from "./context.js";
17
+ import type { DecisionHooks } from "./decision.js";
17
18
  import type { SkillsConfig } from "./skills.js";
18
19
  import type { KnowledgeGroundingConfig } from "./knowledge.js";
19
20
  /**
@@ -155,6 +156,14 @@ export type NeurolinkConstructorConfig = {
155
156
  * which is the same activation rule every other decision consumer uses.
156
157
  */
157
158
  contextRelevance?: ContextRelevanceOptions;
159
+ /**
160
+ * Ride along on the decision calls NeuroLink already makes for routing,
161
+ * tool routing, compaction and RAG planning: add your own questions to the
162
+ * same round trip and read the answers back. Observe-only with respect to
163
+ * NeuroLink's behaviour, fail-open, and inert until a decision provider is
164
+ * configured. See {@link DecisionHooks}.
165
+ */
166
+ decisionHooks?: DecisionHooks;
158
167
  /**
159
168
  * Native skills: versioned, discoverable instruction packs (SOPs,
160
169
  * playbooks) with progressive disclosure. When enabled, each
@@ -4,7 +4,7 @@
4
4
  */
5
5
  import type { ExecutionContext } from "../types/tools.js";
6
6
  import type { JsonObject } from "./common.js";
7
- import type { ChatMessage, ConversationMemoryConfig } from "./conversation.js";
7
+ import type { ChatMessage, ConversationMemoryConfig, ToolReplayMode } from "./conversation.js";
8
8
  import type { DecisionCallerFn } from "./decision.js";
9
9
  /**
10
10
  * Base context type for all AI operations
@@ -237,6 +237,13 @@ export type BudgetCheckParams = {
237
237
  }>;
238
238
  /** Compaction trigger threshold (0.0-1.0). Default: 0.80 */
239
239
  compactionThreshold?: number;
240
+ /**
241
+ * How this request replays stored `tool_call` / `tool_result` rows
242
+ * (`replayToolSteps`). When set, tool rows are estimated at the size they
243
+ * reach the model in that mode rather than at stored size; when omitted
244
+ * they count at stored size.
245
+ */
246
+ toolReplayMode?: ToolReplayMode;
240
247
  };
241
248
  /** A file prepared for potential summarization. */
242
249
  export type FileForSummarization = {
@@ -44,12 +44,99 @@ export type { ModelMessage, SystemModelMessage, UserModelMessage, AssistantModel
44
44
  * uniqueness checks see.
45
45
  */
46
46
  export type StorageConfig = HippocampusStorageConfig;
47
+ /**
48
+ * How stored `tool_call` / `tool_result` rows are replayed into the prompt
49
+ * on later turns of a session (see `ConversationMemoryConfig.replayToolSteps`).
50
+ *
51
+ * - `"full"`: replayed as real tool-call / tool-result turns, so the model
52
+ * sees the exact call and its output.
53
+ * - `"marker"`: a compact text line per tool call
54
+ * (`[called <tool> → ok]` / `[called <tool> → error]`) folded into the
55
+ * assistant turn, so the model knows what it did without the payload.
56
+ * - `"off"`: tool rows are dropped from the prompt, as before this option.
57
+ */
58
+ export type ToolReplayMode = "full" | "marker" | "off";
59
+ /**
60
+ * The fields the tool-step replay reads off a stored history row. Structural
61
+ * rather than `ChatMessage` because the multimodal builder's history is typed
62
+ * as `{ role: string; content: string }` while carrying the same rows.
63
+ */
64
+ export type ToolHistoryRow = {
65
+ /** Row id; a `repair-*` prefix marks a placeholder `repairToolPairs` invented. */
66
+ id?: string;
67
+ role: string;
68
+ content?: unknown;
69
+ tool?: string;
70
+ toolCallId?: string;
71
+ args?: Record<string, unknown>;
72
+ result?: {
73
+ success?: boolean;
74
+ error?: string;
75
+ };
76
+ metadata?: {
77
+ stepIndex?: number;
78
+ isSummary?: boolean;
79
+ isSkill?: boolean;
80
+ };
81
+ };
82
+ /**
83
+ * What a replayed step tells the model about the call's outcome: `"ok"`,
84
+ * `"error"` (the result was a failure — thrown, rejected, or an `isError`
85
+ * payload), or `"unknown"` when the real result was lost to compaction and
86
+ * `repairToolPairs` filled the gap with a placeholder.
87
+ */
88
+ export type ToolReplayOutcome = "ok" | "error" | "unknown";
89
+ /** One paired call/result from a session's history, ready to replay. */
90
+ export type ToolReplayStep = {
91
+ callId: string;
92
+ toolName: string;
93
+ args: Record<string, unknown>;
94
+ output: string;
95
+ outcome: ToolReplayOutcome;
96
+ };
97
+ /**
98
+ * One tool call as handed to conversation-memory storage after a loop step.
99
+ * The stores read `args` (Redis also `arguments` / `parameters`), so the
100
+ * field is named for what they read — an `input` key here is silently
101
+ * persisted as `{}`, which is exactly the bug this type exists to prevent.
102
+ */
103
+ export type MemoryToolCallRecord = {
104
+ toolCallId?: string;
105
+ toolName?: string;
106
+ args?: Record<string, unknown>;
107
+ /** Zero-based loop step, persisted as `metadata.stepIndex` by stores that keep it. */
108
+ stepIndex?: number;
109
+ /** Gemini 3 thought signature riding on the step's first call. */
110
+ thoughtSignature?: string;
111
+ timestamp?: Date;
112
+ };
113
+ /** One tool result as handed to conversation-memory storage after a loop step. */
114
+ export type MemoryToolResultRecord = {
115
+ toolCallId?: string;
116
+ toolName?: string;
117
+ output?: unknown;
118
+ /** Legacy alias of `output`; stores read whichever is present. */
119
+ result?: unknown;
120
+ /** Set when the execution failed; persisted as `result.error` and `success: false`. */
121
+ error?: string;
122
+ stepIndex?: number;
123
+ timestamp?: Date;
124
+ };
47
125
  /**
48
126
  * Configuration for conversation memory feature
49
127
  */
50
128
  export type ConversationMemoryConfig = {
51
129
  /** Enable conversation memory feature */
52
130
  enabled: boolean;
131
+ /**
132
+ * How a session's stored tool steps are replayed into later prompts.
133
+ * Default `"marker"`. `"full"` replays real tool-call / tool-result turns
134
+ * (costs tokens per replayed step and, on Anthropic, changes the prompt
135
+ * prefix ahead of the cached system breakpoint); `"off"` restores the
136
+ * previous behaviour of dropping them. Overridable per request via
137
+ * `replayToolSteps` on the generate / stream options.
138
+ */
139
+ replayToolSteps?: ToolReplayMode;
53
140
  /** Maximum number of sessions to keep in memory (default: 50) */
54
141
  maxSessions?: number;
55
142
  /** Enable automatic summarization */
@@ -182,6 +182,56 @@ export type DecisionLimits = {
182
182
  }>>;
183
183
  /** What the provider accepts besides text. Absent means text only. */
184
184
  media?: DecisionMediaLimits;
185
+ /**
186
+ * True when these figures describe the provider's own server-side ceiling,
187
+ * published so callers can plan against it, and NeuroLink does NOT refuse
188
+ * locally — the server does, with its own `max_tokens_exceeded`. Absent or
189
+ * false means NeuroLink refuses an over-limit request before any network
190
+ * call. `NeuroLink.decisionLimits()` reports the distinction as
191
+ * `enforcedLocally`.
192
+ */
193
+ advisory?: boolean;
194
+ };
195
+ /**
196
+ * A provider's decision limits resolved for one model — the figure the
197
+ * pre-flight check compares against, flattened from a descriptor's
198
+ * `decisionLimits` with the per-model override applied.
199
+ *
200
+ * Returned by `NeuroLink.decisionLimits()` so a host extending NeuroLink's
201
+ * questions (see {@link DecisionHooks}) can size its additions before the
202
+ * call, instead of discovering the cap when the whole request is refused.
203
+ */
204
+ export type DecisionLimitsReading = {
205
+ provider: string;
206
+ model: string;
207
+ maxStateTokens: number;
208
+ /**
209
+ * Absent when the provider caps a request by tokens (or bytes) rather
210
+ * than by question count — TypeSafe and XOR; `Infinity` is never used.
211
+ */
212
+ maxQuestions?: number;
213
+ nonAsciiTokensPerChar?: number;
214
+ /** Tokens charged per ASCII digit; see {@link DecisionLimits.digitTokensPerChar}. */
215
+ digitTokensPerChar?: number;
216
+ /** Tokens charged per ASCII punctuation/symbol; see {@link DecisionLimits.symbolTokensPerChar}. */
217
+ symbolTokensPerChar?: number;
218
+ /** Tokens charged per astral character; see {@link DecisionLimits.astralTokensPerChar}. */
219
+ astralTokensPerChar?: number;
220
+ /** What the provider accepts besides text; absent means text only. */
221
+ media?: DecisionMediaLimits;
222
+ /**
223
+ * Whether NeuroLink itself refuses an over-limit request before any network
224
+ * call (Laya), or merely reports the server's ceiling (TypeSafe).
225
+ */
226
+ enforcedLocally: boolean;
227
+ };
228
+ /** What `NeuroLink.decisionLimits()` accepts: the same selection `decide()` takes. */
229
+ export type DecisionLimitsQuery = {
230
+ /** Provider name or alias; defaults to the configured decision provider. */
231
+ provider?: string;
232
+ /** Defaults to the provider's configured model, then its default model. */
233
+ model?: string;
234
+ credentials?: NeurolinkCredentials;
185
235
  };
186
236
  export type DecisionMediaLimits = {
187
237
  /** Most images one request may carry. */
@@ -287,6 +337,162 @@ export type DecisionOptions = DecisionRequest & {
287
337
  /** Per-call credential overrides, as `generate()` takes. */
288
338
  credentials?: NeurolinkCredentials;
289
339
  };
340
+ /**
341
+ * The built-in consumers of the `decide` inference type — one name per place
342
+ * NeuroLink asks a decision model something on its own behalf. Each consumer
343
+ * stamps its request with its site, which is what lets a host's
344
+ * {@link DecisionHooks} tell the calls apart and lets telemetry attribute them.
345
+ *
346
+ * - `routing` — the classifier router (difficulty, capabilities, risk, model, context scope)
347
+ * - `toolRouting` — one yes/no per MCP server
348
+ * - `contextRelevance` — one yes/no per earlier message, compaction stage 0
349
+ * - `summaryGate` — accept or reject a generated summary, compaction stage 3
350
+ * - `ragPlan` — per-query retrieval plan (`RAGPipeline` only)
351
+ */
352
+ export type DecisionSite = "routing" | "toolRouting" | "contextRelevance" | "summaryGate" | "ragPlan";
353
+ /**
354
+ * Options an internal decision site hands its caller: the public
355
+ * {@link DecisionOptions} plus in-process context that never reaches the
356
+ * wire. A bound `NeuroLink.tryDecide` accepts it unchanged; a host wrapping
357
+ * one can read `site` to see which consumer is asking.
358
+ */
359
+ export type DecisionCallerOptions = DecisionOptions & {
360
+ /** Which built-in consumer is asking. Unset on a host's own `tryDecide` call. */
361
+ site?: DecisionSite;
362
+ /** The conversation the outer request belongs to. Never sent to the model. */
363
+ sessionId?: string;
364
+ /** The outer request's id. Never sent to the model. */
365
+ requestId?: string;
366
+ };
367
+ /**
368
+ * What an internal decision site inherits from the `generate()` / `stream()`
369
+ * call it runs inside: the per-call credentials (so the decision goes to the
370
+ * caller's provider account, not the instance's), the abort signal, and the
371
+ * ids a host's {@link DecisionHooks} may want to correlate on.
372
+ */
373
+ export type DecisionSiteContext = {
374
+ credentials?: NeurolinkCredentials;
375
+ signal?: AbortSignal;
376
+ sessionId?: string;
377
+ requestId?: string;
378
+ };
379
+ /** What a site call adds to its decision span beyond the request itself. */
380
+ export type DecisionSiteAttributes = {
381
+ site: DecisionSite;
382
+ /** Questions a host's `extendQuestions` hook added; 0 without hooks. */
383
+ hostQuestionCount: number;
384
+ };
385
+ /**
386
+ * What a {@link DecisionHooks} callback sees before the call: the site, the
387
+ * state NeuroLink is about to send, and NeuroLink's own questions — as
388
+ * copies, so a hook that edits them cannot reach the wire request.
389
+ */
390
+ export type DecisionHookContext = {
391
+ site: DecisionSite;
392
+ state: DecisionState;
393
+ /** NeuroLink's own questions for this site, keyed by its own ids. */
394
+ questions: DecisionQuestionMap;
395
+ sessionId?: string;
396
+ requestId?: string;
397
+ };
398
+ /** What `onAnswers` receives once the decision has returned. */
399
+ export type DecisionHookAnswersContext = DecisionHookContext & {
400
+ /**
401
+ * The host's answers, keyed by the ids the host used in `extendQuestions`
402
+ * — the namespacing applied on the wire is undone here. Empty when the
403
+ * host added no questions.
404
+ */
405
+ answers: DecisionAnswerMap;
406
+ /** The whole result, including NeuroLink's own answers under its own ids. */
407
+ result: DecisionResult;
408
+ };
409
+ /**
410
+ * Ride along on the decision calls NeuroLink already makes.
411
+ *
412
+ * Latency on a decision model is flat in question count, so a host with its
413
+ * own yes/no or choice questions about the same request pays nothing to ask
414
+ * them in the round trip NeuroLink is making anyway — one request per
415
+ * {@link DecisionSite}, never a second. The host's questions are namespaced
416
+ * on the wire so they can never collide with NeuroLink's, capped so they can
417
+ * never push the request past the provider's question limit (which would
418
+ * refuse the whole call and degrade NeuroLink's own routing), and answered
419
+ * back under the host's original ids.
420
+ *
421
+ * Both hooks are fail-open: a hook that throws is logged and the decision
422
+ * proceeds with NeuroLink's own questions, and neither can change what
423
+ * NeuroLink does with its own answers — every value a hook or listener is
424
+ * handed is a copy. Without hooks, the decision payload (state and
425
+ * questions) is exactly what the consumer built; the outer request's
426
+ * per-call `credentials` are still forwarded, so the account and base URL
427
+ * on the wire can differ from the instance's. Hooks are inert without a
428
+ * decision provider: neither runs, and no event fires.
429
+ */
430
+ export type DecisionHooks = {
431
+ /**
432
+ * Return extra questions to send with this site's call, or `undefined` to
433
+ * add none. An invalid question is dropped with a warning; questions past
434
+ * the provider's cap are dropped from the end, also with a warning.
435
+ */
436
+ extendQuestions?: (context: DecisionHookContext) => DecisionQuestionMap | undefined | Promise<DecisionQuestionMap | undefined>;
437
+ /**
438
+ * Called with the host's answers (under the host's ids) and the full result
439
+ * whenever a site call returns one. Observe-only: it receives a copy, so
440
+ * mutating it cannot reach the answers NeuroLink's own consumer reads.
441
+ */
442
+ onAnswers?: (context: DecisionHookAnswersContext) => void | Promise<void>;
443
+ /**
444
+ * Upper bound, in milliseconds, on each hook call. Both hooks sit on the
445
+ * request path (routing, tool routing, compaction, RAG planning), so a
446
+ * hook that hangs would stall the turn; past this bound the call proceeds
447
+ * as if the hook had returned nothing, with a warning. Default 2000. Must
448
+ * be a finite number from 1 to {@link MAX_DECISION_HOOK_TIMEOUT_MS};
449
+ * anything else (including `Infinity`) falls back to the default with one
450
+ * warning, since a timer given such a delay fires immediately.
451
+ */
452
+ hookTimeoutMs?: number;
453
+ };
454
+ /** Default bound on a `DecisionHooks` callback, in milliseconds. */
455
+ export declare const DEFAULT_DECISION_HOOK_TIMEOUT_MS = 2000;
456
+ /**
457
+ * Largest `hookTimeoutMs` a timer honours (2^31 − 1 ms, about 24.8 days).
458
+ * A delay past it — or `Infinity`, `NaN`, 0, a negative — fires at once,
459
+ * so such a value falls back to the default instead of timing every hook
460
+ * out immediately.
461
+ */
462
+ export declare const MAX_DECISION_HOOK_TIMEOUT_MS = 2147483647;
463
+ /**
464
+ * Payload of the `decision:before` event, emitted once per site call. `state`
465
+ * and `questions` are copies: a listener that edits them changes neither
466
+ * the request on the wire nor the consumer's own question objects.
467
+ */
468
+ export type DecisionBeforeEvent = {
469
+ site: DecisionSite;
470
+ state: DecisionState;
471
+ /** Everything about to be sent, NeuroLink's own questions plus the host's (namespaced). */
472
+ questions: DecisionQuestionMap;
473
+ hostQuestionCount: number;
474
+ sessionId?: string;
475
+ requestId?: string;
476
+ };
477
+ /**
478
+ * Payload of the `decision:after` event, emitted once per site call whether
479
+ * or not it produced a result — a decision path that stopped working must
480
+ * stay observable. `result` is null when an emitted call failed; with no
481
+ * decision provider configured, no event fires at all.
482
+ */
483
+ export type DecisionAfterEvent = {
484
+ site: DecisionSite;
485
+ /** NeuroLink's own answers, under its own ids. Empty when `result` is null. */
486
+ answers: DecisionAnswerMap;
487
+ /** The host's answers, under the host's original ids. Empty when `result` is null. */
488
+ hostAnswers: DecisionAnswerMap;
489
+ latencyMs: number;
490
+ provider?: string;
491
+ model?: string;
492
+ result: DecisionResult | null;
493
+ sessionId?: string;
494
+ requestId?: string;
495
+ };
290
496
  /**
291
497
  * Injected fail-open decision caller — typically a bound `NeuroLink.tryDecide`,
292
498
  * which returns `null` on any failure rather than throwing.
@@ -297,8 +503,11 @@ export type DecisionOptions = DecisionRequest & {
297
503
  * makes "no decision provider configured" indistinguishable from "the call
298
504
  * failed" at every call site: both are `null`, and both mean *carry on as
299
505
  * before*.
506
+ *
507
+ * The options carry a {@link DecisionSite} stamp; a bound `tryDecide` routes
508
+ * a stamped request through the host's {@link DecisionHooks}.
300
509
  */
301
- export type DecisionCallerFn = (options: DecisionOptions) => Promise<DecisionResult | null>;
510
+ export type DecisionCallerFn = (options: DecisionCallerOptions) => Promise<DecisionResult | null>;
302
511
  /**
303
512
  * Shared gate for acting on a yes/no answer.
304
513
  *
@@ -18,4 +18,12 @@
18
18
  * `src/lib/types/` (CLAUDE.md rule 9). Note `evaluate`/`Evaluation*` is a
19
19
  * different, unrelated feature — scoring a generated response with RAGAS.
20
20
  */
21
- export {};
21
+ /** Default bound on a `DecisionHooks` callback, in milliseconds. */
22
+ export const DEFAULT_DECISION_HOOK_TIMEOUT_MS = 2000;
23
+ /**
24
+ * Largest `hookTimeoutMs` a timer honours (2^31 − 1 ms, about 24.8 days).
25
+ * A delay past it — or `Infinity`, `NaN`, 0, a negative — fires at once,
26
+ * so such a value falls back to the default instead of timing every hook
27
+ * out immediately.
28
+ */
29
+ export const MAX_DECISION_HOOK_TIMEOUT_MS = 2_147_483_647;