@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
@@ -51,10 +51,25 @@ export type STTOptions = {
51
51
  /**
52
52
  * STT result from transcription
53
53
  */
54
+ /**
55
+ * Where a provider's `STTResult.confidence` came from, when it has to say —
56
+ * Scribe reports a per-word `logprob` on some responses and only a
57
+ * language-detection probability on others, and a caller reading the number
58
+ * should know which it is getting. `"none"` means the provider reported no
59
+ * transcript-level signal at all and the field is 0.
60
+ */
61
+ export type STTConfidenceSource = "word_logprobs" | "language_probability" | "none";
54
62
  export type STTResult = {
55
63
  /** Full transcribed text */
56
64
  text: string;
57
- /** Confidence score (0-1) */
65
+ /**
66
+ * Transcript confidence (0-1) — Deepgram's alternative confidence,
67
+ * Whisper's `exp(avg_logprob)`, Scribe's mean per-word `exp(logprob)`.
68
+ * Always present: a strict-TS consumer reads it without a guard. A provider
69
+ * with no transcript-level signal reports 0 and, where it can, says so in
70
+ * `metadata.confidenceSource` rather than passing off a different number
71
+ * (a language-detection probability, say) as this one silently.
72
+ */
58
73
  confidence: number;
59
74
  /** Detected language code */
60
75
  language?: string;
@@ -74,6 +89,8 @@ export type STTResult = {
74
89
  provider?: string;
75
90
  /** Model used */
76
91
  model?: string;
92
+ /** What `confidence` was derived from, for providers that must choose. */
93
+ confidenceSource?: STTConfidenceSource;
77
94
  /** Additional provider-specific metadata */
78
95
  [key: string]: unknown;
79
96
  };
@@ -435,6 +452,46 @@ export type ElevenLabsVoice = {
435
452
  export type ElevenLabsVoicesResponse = {
436
453
  voices: ElevenLabsVoice[];
437
454
  };
455
+ /**
456
+ * Scribe model ids the batch `speech-to-text` endpoint accepts. `scribe_v2`
457
+ * is the current model and the default; `scribe_v2_medical` is its clinical
458
+ * fine-tune; `scribe_v1` (and the experimental variant) are deprecated. The
459
+ * `(string & {})` member keeps the union open so a newly released id can be
460
+ * used without a cast.
461
+ */
462
+ export type ElevenLabsSTTModel = "scribe_v2" | "scribe_v2_medical" | "scribe_v1" | "scribe_v1_experimental" | (string & {});
463
+ export type ElevenLabsSTTOptions = STTOptions & {
464
+ /** Scribe model id. Default `scribe_v2`. */
465
+ model?: ElevenLabsSTTModel;
466
+ /**
467
+ * Annotate non-speech audio events ("(laughter)", "(music)") in the
468
+ * transcript. ElevenLabs defaults this to true; NeuroLink sends `false` so
469
+ * the transcript reads as plain speech unless asked otherwise.
470
+ * @default false
471
+ */
472
+ tagAudioEvents?: boolean;
473
+ /** Per-request timeout in milliseconds. Default 60_000. */
474
+ timeoutMs?: number;
475
+ /**
476
+ * API base URL including the `/v1` prefix. Default
477
+ * `ELEVENLABS_BASE_URL` or `https://api.elevenlabs.io/v1`.
478
+ */
479
+ baseUrl?: string;
480
+ };
481
+ export type ElevenLabsSTTWord = {
482
+ text: string;
483
+ start?: number;
484
+ end?: number;
485
+ type?: "word" | "spacing" | "audio_event" | string;
486
+ speaker_id?: string;
487
+ logprob?: number;
488
+ };
489
+ export type ElevenLabsSTTResponse = {
490
+ language_code?: string;
491
+ language_probability?: number;
492
+ text: string;
493
+ words?: ElevenLabsSTTWord[];
494
+ };
438
495
  export type AzureVoiceInfo = {
439
496
  Name: string;
440
497
  DisplayName: string;
@@ -358,6 +358,8 @@ export type ToolEventPayload = {
358
358
  timestamp?: number;
359
359
  duration?: number;
360
360
  executionId?: string;
361
+ /** The model's tool-call id when the loop supplied one, for pairing start/end. */
362
+ toolCallId?: string;
361
363
  };
362
364
  /**
363
365
  * Tool execution summary for completed executions
@@ -570,3 +572,15 @@ export type BashToolResult = {
570
572
  stderr: string;
571
573
  error?: string;
572
574
  };
575
+ /**
576
+ * Own property stamped on a tool object whose `execute` already emits one
577
+ * `tool:start` / `tool:end` pair per execution. It lives on the OBJECT, not
578
+ * the function: later layers (the execution recorder, discovery) replace
579
+ * `execute` via `{ ...tool, execute }`, which drops a function-identity check
580
+ * but carries a symbol-keyed own property along.
581
+ */
582
+ export declare const TOOL_EVENTS_WRAPPED: unique symbol;
583
+ /** A `Tool` that may carry the {@link TOOL_EVENTS_WRAPPED} marker. */
584
+ export type EventWrappedTool = Tool & {
585
+ [TOOL_EVENTS_WRAPPED]?: true;
586
+ };
@@ -22,3 +22,11 @@ export function isToolDefinition(value) {
22
22
  typeof value.description === "string" &&
23
23
  typeof value.execute === "function");
24
24
  }
25
+ /**
26
+ * Own property stamped on a tool object whose `execute` already emits one
27
+ * `tool:start` / `tool:end` pair per execution. It lives on the OBJECT, not
28
+ * the function: later layers (the execution recorder, discovery) replace
29
+ * `execute` via `{ ...tool, execute }`, which drops a function-identity check
30
+ * but carries a symbol-keyed own property along.
31
+ */
32
+ export const TOOL_EVENTS_WRAPPED = Symbol.for("neurolink.toolEventsWrapped");
@@ -25,12 +25,80 @@ export type TTSQuality = "standard" | "hd";
25
25
  * still surfacing the built-in choices in editor autocomplete.
26
26
  */
27
27
  export type TTSProviderName = "google-ai" | "vertex" | "openai-tts" | "elevenlabs" | "elevenlabs-tts" | "azure-tts" | "fish-audio" | "sixtydb" | "cartesia" | (string & {});
28
+ /**
29
+ * What `generate({ tts })` synthesizes.
30
+ *
31
+ * - `"direct"` — the input text / prompt itself, with no LLM call. This is
32
+ * the default, and what every documented "speak this text" caller relies
33
+ * on.
34
+ * - `"response"` — the model's generated reply, after generation completes.
35
+ *
36
+ * `stream()` always synthesizes the streamed response and ignores this.
37
+ */
38
+ export type TTSSynthesisMode = "direct" | "response";
39
+ /**
40
+ * Options for {@link prepareTextForSpeech} — the optional cleanup pass that
41
+ * turns model output (markdown, URLs, emoji) into text a TTS voice can read
42
+ * aloud. Every default below is what `tts: { sanitize: true }` applies.
43
+ */
44
+ export type SpeechSanitizeOptions = {
45
+ /**
46
+ * Strip markdown syntax: headings, emphasis, inline code, list bullets,
47
+ * blockquotes, tables (kept as comma-separated rows), links and images
48
+ * (kept as their text). Fenced code blocks follow `codeBlocks`.
49
+ * @default true
50
+ */
51
+ markdown?: boolean;
52
+ /**
53
+ * How fenced code blocks are spoken. `"drop"` removes them entirely,
54
+ * `"phrase"` replaces each block with `codeBlockPhrase`, `"keep"` strips
55
+ * only the fence lines and reads the contents. Incremental `stream()`
56
+ * synthesis always uses `"keep"`: a segment may hold an unterminated fence,
57
+ * so the block's extent is unknowable per segment.
58
+ * @default "phrase"
59
+ */
60
+ codeBlocks?: "drop" | "phrase" | "keep";
61
+ /**
62
+ * Spoken stand-in for a fenced code block when `codeBlocks` is `"phrase"`.
63
+ * @default "Code block omitted."
64
+ */
65
+ codeBlockPhrase?: string;
66
+ /**
67
+ * How URLs are spoken. `"hostname"` reduces `https://docs.example.com/a/b`
68
+ * to `docs.example.com`; `"remove"` drops the URL altogether.
69
+ * @default "hostname"
70
+ */
71
+ urls?: "hostname" | "remove";
72
+ /**
73
+ * Remove emoji and pictographs (`\p{Extended_Pictographic}`,
74
+ * `\p{Emoji_Presentation}`, plus modifiers and joiners).
75
+ * @default true
76
+ */
77
+ emoji?: boolean;
78
+ };
79
+ /** {@link SpeechSanitizeOptions} with every default applied. */
80
+ export type ResolvedSpeechSanitizeOptions = Required<SpeechSanitizeOptions>;
28
81
  /**
29
82
  * TTS configuration options
30
83
  */
31
84
  export type TTSOptions = {
32
85
  /** Enable TTS output */
33
86
  enabled?: boolean;
87
+ /**
88
+ * What to synthesize: the input text (`"direct"`, the default) or the
89
+ * model's reply (`"response"`). An explicit `mode` wins over the legacy
90
+ * `useAiResponse` flag. Ignored by `stream()`, which always synthesizes the
91
+ * streamed response.
92
+ *
93
+ * @example Speak the model's reply
94
+ * ```typescript
95
+ * const result = await neurolink.generate({
96
+ * input: { text: "Tell me a joke" },
97
+ * tts: { enabled: true, mode: "response" },
98
+ * });
99
+ * ```
100
+ */
101
+ mode?: TTSSynthesisMode;
34
102
  /**
35
103
  * Use the AI-generated response for TTS instead of the input text
36
104
  *
@@ -41,6 +109,8 @@ export type TTSOptions = {
41
109
  * When true: TTS will synthesize the AI-generated response after generation completes
42
110
  *
43
111
  * @default false
112
+ * @deprecated Use `mode: "response"` / `mode: "direct"` instead. Still
113
+ * honoured when `mode` is not set.
44
114
  *
45
115
  * @example Using input text directly (default)
46
116
  * ```typescript
@@ -63,12 +133,37 @@ export type TTSOptions = {
63
133
  * ```
64
134
  */
65
135
  useAiResponse?: boolean;
136
+ /**
137
+ * Clean the text before it reaches the voice: strip markdown, reduce URLs
138
+ * to their hostname, drop emoji. `true` applies the defaults of
139
+ * {@link SpeechSanitizeOptions}; an object tunes them. Off by default so
140
+ * existing callers get byte-identical synthesis input. In `stream()` the
141
+ * pass runs per segment with fenced code kept (fence lines stripped).
142
+ * @default false
143
+ */
144
+ sanitize?: boolean | SpeechSanitizeOptions;
145
+ /**
146
+ * Cancels synthesis. A handler that supports it (ElevenLabs) aborts the
147
+ * request in flight and makes no further retry attempt once this fires.
148
+ * `generate()` derives one from its own synthesis timeout and the caller's
149
+ * `abortSignal`, so a handler's retry loop cannot outlive the call that
150
+ * started it and issue a billable request nobody is waiting for.
151
+ */
152
+ signal?: AbortSignal;
66
153
  /** Voice identifier (e.g., "en-US-Neural2-C") */
67
154
  voice?: string;
68
155
  /** Audio format (default: mp3) */
69
156
  format?: TTSAudioFormat;
70
157
  /** Speaking rate 0.25-4.0 (default: 1.0) */
71
158
  speed?: number;
159
+ /**
160
+ * BCP-47 language tag (e.g. "en-US", "hi") for providers that accept one.
161
+ * Provider-specific: ElevenLabs reduces it to the ISO 639-1 primary subtag
162
+ * (`en-US` → `en`) and forwards it as `language_code` on every model except
163
+ * `eleven_multilingual_v2`, which rejects the field; Cartesia maps it to
164
+ * `language`.
165
+ */
166
+ language?: string;
72
167
  /** Voice pitch adjustment -20.0 to 20.0 semitones (default: 0.0) */
73
168
  pitch?: number;
74
169
  /** Volume gain in dB -96.0 to 16.0 (default: 0.0) */
@@ -88,6 +183,19 @@ export type TTSOptions = {
88
183
  */
89
184
  streamingBufferSize?: number;
90
185
  };
186
+ /**
187
+ * Handler-level wall-clock knobs a provider option type may carry (today:
188
+ * `ElevenLabsTTSOptions`). `generate()`'s outer synthesis timeout is raised to
189
+ * at least `timeoutMs × (1 + retries)` plus backoff slack — with the handler
190
+ * defaults (30 s, one retry) standing in for a knob the caller left unset —
191
+ * so a handler's own retry loop is never cut short from outside.
192
+ */
193
+ export type TTSHandlerTimeBudget = {
194
+ /** Per-attempt request timeout in milliseconds (default 30 000; capped at 2³¹−1). */
195
+ timeoutMs?: number;
196
+ /** Retries after the first attempt (total attempts = 1 + retries). */
197
+ retries?: number;
198
+ };
91
199
  /**
92
200
  * TTS audio result returned from generation
93
201
  */
@@ -13,7 +13,7 @@
13
13
  export * from "./tts.js";
14
14
  export * from "./stt.js";
15
15
  export * from "./realtime.js";
16
- import type { TTSAudioFormat, TTSOptions, TTSResult, TTSVoice } from "./tts.js";
16
+ import type { TTSAudioFormat, TTSHandlerTimeBudget, TTSOptions, TTSResult, TTSVoice } from "./tts.js";
17
17
  import type { TTSHandler } from "./common.js";
18
18
  import type { STTResult, STTHandler } from "./stt.js";
19
19
  import type { RealtimeHandler } from "./realtime.js";
@@ -28,7 +28,7 @@ export type VoiceProviderType = "tts" | "stt" | "realtime";
28
28
  /**
29
29
  * Voice provider name union type
30
30
  */
31
- export type VoiceProviderName = "google-tts" | "elevenlabs" | "openai-tts" | "sixtydb" | "azure-tts" | "sarvam" | "murf" | "playai" | "speechify" | "cartesia" | "deepgram" | "gladia" | "whisper" | "assemblyai" | "google-stt" | "azure-stt" | "openai-realtime" | "gemini-live";
31
+ export type VoiceProviderName = "google-tts" | "elevenlabs" | "openai-tts" | "sixtydb" | "azure-tts" | "sarvam" | "murf" | "playai" | "speechify" | "cartesia" | "deepgram" | "gladia" | "whisper" | "assemblyai" | "google-stt" | "azure-stt" | "elevenlabs-stt" | "openai-realtime" | "gemini-live";
32
32
  /**
33
33
  * Base voice provider configuration
34
34
  */
@@ -264,13 +264,50 @@ export type AzureTTSOptions = TTSOptions & {
264
264
  */
265
265
  allowRawSSML?: boolean;
266
266
  };
267
- export type ElevenLabsModel = "eleven_multilingual_v2" | "eleven_turbo_v2_5" | "eleven_turbo_v2" | "eleven_monolingual_v1";
268
- export type ElevenLabsTTSOptions = TTSOptions & {
267
+ export type ElevenLabsModel = "eleven_multilingual_v2" | "eleven_turbo_v2_5" | "eleven_turbo_v2" | "eleven_flash_v2_5" | "eleven_flash_v2" | "eleven_v3" | "eleven_monolingual_v1";
268
+ /** Opus bitrates (kbps) ElevenLabs accepts on `opus_48000_*`. */
269
+ export type ElevenLabsOpusBitrate = 32 | 64 | 96 | 128 | 192;
270
+ /** MP3 bitrates (kbps) ElevenLabs accepts on `mp3_44100_*`. */
271
+ export type ElevenLabsMp3Bitrate = 32 | 64 | 96 | 128 | 192;
272
+ /**
273
+ * Every `output_format` value the ElevenLabs text-to-speech endpoint accepts.
274
+ * There is no `ogg_*` family — Ogg/Opus is `opus_48000_<kbps>`.
275
+ */
276
+ export type ElevenLabsOutputFormat = "mp3_22050_32" | "mp3_44100_32" | "mp3_44100_64" | "mp3_44100_96" | "mp3_44100_128" | "mp3_44100_192" | "opus_48000_32" | "opus_48000_64" | "opus_48000_96" | "opus_48000_128" | "opus_48000_192" | "pcm_8000" | "pcm_16000" | "pcm_22050" | "pcm_24000" | "pcm_44100" | "pcm_48000" | "ulaw_8000" | "alaw_8000";
277
+ /** The `voice_settings` object on an ElevenLabs synthesis request. */
278
+ export type ElevenLabsVoiceSettings = {
279
+ stability?: number;
280
+ similarity_boost?: number;
281
+ style?: number;
282
+ use_speaker_boost?: boolean;
283
+ /** 0.7–1.2; `TTSOptions.speed` is clamped into this range. */
284
+ speed?: number;
285
+ };
286
+ export type ElevenLabsTTSOptions = TTSOptions & TTSHandlerTimeBudget & {
269
287
  model?: ElevenLabsModel;
270
288
  stability?: number;
271
289
  similarityBoost?: number;
272
290
  style?: number;
273
291
  useSpeakerBoost?: boolean;
292
+ /** Bitrate for `format: "ogg" | "opus"` (→ `opus_48000_<kbps>`). Default 64; an unlisted value falls back to it with a warning. */
293
+ opusBitrate?: ElevenLabsOpusBitrate;
294
+ /** Bitrate for `format: "mp3"` (→ `mp3_44100_<kbps>`). Default 128; an unlisted value falls back to it with a warning. */
295
+ mp3Bitrate?: ElevenLabsMp3Bitrate;
296
+ /**
297
+ * Language sent as `language_code`, an ISO 639-1 code; a BCP-47 tag is
298
+ * reduced to its primary subtag (`en-US` → `en`). Sent on every model
299
+ * except `eleven_multilingual_v2`, which does not accept the field (the
300
+ * API ignores the code on any other model that cannot enforce it).
301
+ * Falls back to the generic `TTSOptions.language`.
302
+ */
303
+ languageCode?: string;
304
+ /** Raw `voice_settings` overrides; explicit fields win over the camelCase ones. */
305
+ voiceSettings?: ElevenLabsVoiceSettings;
306
+ /**
307
+ * API base URL including the `/v1` prefix. Default
308
+ * `ELEVENLABS_BASE_URL` or `https://api.elevenlabs.io/v1`.
309
+ */
310
+ baseUrl?: string;
274
311
  };
275
312
  export type GoogleVoiceType = "Standard" | "WaveNet" | "Neural2" | "Studio" | "Polyglot";
276
313
  export type GoogleTTSOptions = TTSOptions & {
@@ -7,7 +7,7 @@
7
7
  *
8
8
  * @module utils/decisionAnswers
9
9
  */
10
- import type { DecisionAnswerMap, DecisionBooleanGate, DecisionChoiceReading, DecisionScoreReading, ProviderDescriptor } from "../types/index.js";
10
+ import type { DecisionAnswerMap, DecisionBooleanGate, DecisionChoiceReading, DecisionQuestion, DecisionResult, DecisionScoreReading, ProviderDescriptor } from "../types/index.js";
11
11
  /**
12
12
  * Read a yes/no answer as a probability.
13
13
  *
@@ -54,3 +54,33 @@ export declare function gateDecisionBoolean(answers: DecisionAnswerMap, id: stri
54
54
  * after any normalisation the wire might apply.
55
55
  */
56
56
  export declare function decisionKey(namespace: string, index: number): string;
57
+ /** Namespace for questions a host adds through `decisionHooks.extendQuestions`. */
58
+ export declare const HOST_DECISION_NAMESPACE = "host";
59
+ /** Whether a wire id is one the funnel gave a host's question (`host__N`). */
60
+ export declare function isHostDecisionKey(id: string): boolean;
61
+ /**
62
+ * A validated copy of a question a host handed back from `extendQuestions`,
63
+ * or undefined when it is malformed or cannot be copied. The wire carries
64
+ * the copy, never the host's object: a getter, a proxy or a later edit on
65
+ * the host's side can then reach neither the request nor the answers keyed
66
+ * on it. A value `structuredClone` refuses (a proxy, a function-valued
67
+ * field, a throwing getter) is treated as malformed.
68
+ */
69
+ export declare function snapshotDecisionQuestion(value: unknown): DecisionQuestion | undefined;
70
+ /**
71
+ * `result` without the answers to a host's namespaced questions — what a
72
+ * consumer is handed when the funnel's host-facing steps failed after the
73
+ * wire call had already returned. A null-prototype map, so an own id such
74
+ * as `__proto__` survives the copy.
75
+ */
76
+ export declare function stripHostAnswers(result: DecisionResult, hostIdByWireId: ReadonlyMap<string, string>): DecisionResult;
77
+ /**
78
+ * Runtime check that a value from outside the type system — a host's
79
+ * `extendQuestions` hook — is a well-formed question, so a malformed one
80
+ * is dropped on its own rather than failing the whole request that
81
+ * NeuroLink's own routing depends on.
82
+ *
83
+ * A `choice` needs at least two options and a `score` at least two levels;
84
+ * fewer carries no information, and the servers refuse it anyway.
85
+ */
86
+ export declare function isDecisionQuestion(value: unknown): value is DecisionQuestion;
@@ -93,3 +93,117 @@ export function gateDecisionBoolean(answers, id, gate) {
93
93
  export function decisionKey(namespace, index) {
94
94
  return `${namespace}__${index}`;
95
95
  }
96
+ /** Namespace for questions a host adds through `decisionHooks.extendQuestions`. */
97
+ export const HOST_DECISION_NAMESPACE = "host";
98
+ /** Whether a wire id is one the funnel gave a host's question (`host__N`). */
99
+ export function isHostDecisionKey(id) {
100
+ return id.startsWith(`${HOST_DECISION_NAMESPACE}__`);
101
+ }
102
+ /**
103
+ * A validated copy of a question a host handed back from `extendQuestions`,
104
+ * or undefined when it is malformed or cannot be copied. The wire carries
105
+ * the copy, never the host's object: a getter, a proxy or a later edit on
106
+ * the host's side can then reach neither the request nor the answers keyed
107
+ * on it. A value `structuredClone` refuses (a proxy, a function-valued
108
+ * field, a throwing getter) is treated as malformed.
109
+ */
110
+ export function snapshotDecisionQuestion(value) {
111
+ let copy;
112
+ try {
113
+ copy = structuredClone(value);
114
+ }
115
+ catch {
116
+ return undefined;
117
+ }
118
+ return isDecisionQuestion(copy) ? copy : undefined;
119
+ }
120
+ /**
121
+ * `result` without the answers to a host's namespaced questions — what a
122
+ * consumer is handed when the funnel's host-facing steps failed after the
123
+ * wire call had already returned. A null-prototype map, so an own id such
124
+ * as `__proto__` survives the copy.
125
+ */
126
+ export function stripHostAnswers(result, hostIdByWireId) {
127
+ if (hostIdByWireId.size === 0) {
128
+ return result;
129
+ }
130
+ const answers = Object.create(null);
131
+ for (const [id, answer] of Object.entries(result.answers)) {
132
+ if (!hostIdByWireId.has(id)) {
133
+ answers[id] = answer;
134
+ }
135
+ }
136
+ return { ...result, answers };
137
+ }
138
+ const isRecordValue = (value) => typeof value === "object" && value !== null && !Array.isArray(value);
139
+ /**
140
+ * Whether `value` is something a decision model accepts as content: a
141
+ * string, number or boolean, or an array or plain object of the same.
142
+ * `null` and `undefined` are not — a question with no instructions is
143
+ * refused by every provider.
144
+ */
145
+ function isDecisionInput(value) {
146
+ switch (typeof value) {
147
+ case "string":
148
+ case "boolean":
149
+ return true;
150
+ case "number":
151
+ // NaN and ±Infinity serialise to null and would make the provider
152
+ // reject the whole request, not just this question.
153
+ return Number.isFinite(value);
154
+ case "object":
155
+ if (value === null) {
156
+ return false;
157
+ }
158
+ return Array.isArray(value)
159
+ ? value.every(isDecisionInput)
160
+ : Object.values(value).every(isDecisionInput);
161
+ default:
162
+ return false;
163
+ }
164
+ }
165
+ /**
166
+ * Runtime check that a value from outside the type system — a host's
167
+ * `extendQuestions` hook — is a well-formed question, so a malformed one
168
+ * is dropped on its own rather than failing the whole request that
169
+ * NeuroLink's own routing depends on.
170
+ *
171
+ * A `choice` needs at least two options and a `score` at least two levels;
172
+ * fewer carries no information, and the servers refuse it anyway.
173
+ */
174
+ export function isDecisionQuestion(value) {
175
+ try {
176
+ return isWellFormedDecisionQuestion(value);
177
+ }
178
+ catch {
179
+ // Cyclic or otherwise exotic host input: the walk below can overflow the
180
+ // stack. That drops this question alone rather than failing the request.
181
+ return false;
182
+ }
183
+ }
184
+ function isWellFormedDecisionQuestion(value) {
185
+ if (!isRecordValue(value) || !isDecisionInput(value.instructions)) {
186
+ return false;
187
+ }
188
+ switch (value.type) {
189
+ case "boolean": {
190
+ if (value.criteria === undefined) {
191
+ return true;
192
+ }
193
+ if (!isRecordValue(value.criteria)) {
194
+ return false;
195
+ }
196
+ return Object.values(value.criteria).every((side) => side === null || side === undefined || isDecisionInput(side));
197
+ }
198
+ case "choice":
199
+ return (isRecordValue(value.criteria) &&
200
+ Object.keys(value.criteria).length >= 2 &&
201
+ Object.values(value.criteria).every((rubric) => rubric === null || isDecisionInput(rubric)));
202
+ case "score":
203
+ return (Array.isArray(value.criteria) &&
204
+ value.criteria.length >= 2 &&
205
+ value.criteria.every((level) => level === null || isDecisionInput(level)));
206
+ default:
207
+ return false;
208
+ }
209
+ }
@@ -0,0 +1,39 @@
1
+ /**
2
+ * Decision limits — the one place a provider's `decisionLimits` are read and
3
+ * a state's size is estimated against them.
4
+ *
5
+ * `SystemOneDecisionProvider` refuses an over-limit request through these
6
+ * functions, and `NeuroLink.decisionLimits()` / `estimateDecisionStateTokens`
7
+ * expose the same functions to hosts, so what a host measures before the call
8
+ * is exactly what the pre-flight check measures inside it. Two estimators that
9
+ * drift apart would let a host size a state as fitting and then watch it be
10
+ * refused.
11
+ *
12
+ * @module utils/decisionLimits
13
+ */
14
+ import type { DecisionLimits, DecisionLimitsReading, DecisionState, ProviderDescriptor } from "../types/index.js";
15
+ /**
16
+ * Estimate how many tokens a decision model will read from `state`.
17
+ *
18
+ * `estimateTokens` assumes ~4 characters per token. That holds for English
19
+ * and is several times too generous for other scripts on an English
20
+ * tokenizer, so when the limits declare a `nonAsciiTokensPerChar` rate,
21
+ * non-ASCII characters are counted at it instead. Digits, ASCII punctuation
22
+ * and astral characters (emoji) are each counted separately only when the
23
+ * limits declare a rate for that class — a tokenizer that reads each digit,
24
+ * and most punctuation, on its own makes a number-heavy or JSON-heavy state
25
+ * several times longer than the ~4-characters-per-token estimate suggests. A
26
+ * non-string state is serialized first, as it is on the wire.
27
+ *
28
+ * This is the estimator the pre-flight refusal uses: a state this reports at
29
+ * or under `maxStateTokens` is sent, one over it is refused before any
30
+ * network call.
31
+ */
32
+ export declare function estimateDecisionStateTokens(state: DecisionState, limits?: Pick<DecisionLimits, "nonAsciiTokensPerChar" | "digitTokensPerChar" | "symbolTokensPerChar" | "astralTokensPerChar">): number;
33
+ /**
34
+ * Flatten a descriptor's `decisionLimits` for one model: the per-model entry
35
+ * overrides the base, field by field, and `advisory` becomes
36
+ * `enforcedLocally: false`. Returns null for a descriptor that declares no
37
+ * limits.
38
+ */
39
+ export declare function resolveDecisionLimitsReading(descriptor: Pick<ProviderDescriptor, "name" | "decisionLimits">, model: string): DecisionLimitsReading | null;
@@ -0,0 +1,116 @@
1
+ /**
2
+ * Decision limits — the one place a provider's `decisionLimits` are read and
3
+ * a state's size is estimated against them.
4
+ *
5
+ * `SystemOneDecisionProvider` refuses an over-limit request through these
6
+ * functions, and `NeuroLink.decisionLimits()` / `estimateDecisionStateTokens`
7
+ * expose the same functions to hosts, so what a host measures before the call
8
+ * is exactly what the pre-flight check measures inside it. Two estimators that
9
+ * drift apart would let a host size a state as fitting and then watch it be
10
+ * refused.
11
+ *
12
+ * @module utils/decisionLimits
13
+ */
14
+ import { CHARS_PER_TOKEN, estimateTokens, serializeForEstimate, } from "./tokenEstimation.js";
15
+ /**
16
+ * Estimate how many tokens a decision model will read from `state`.
17
+ *
18
+ * `estimateTokens` assumes ~4 characters per token. That holds for English
19
+ * and is several times too generous for other scripts on an English
20
+ * tokenizer, so when the limits declare a `nonAsciiTokensPerChar` rate,
21
+ * non-ASCII characters are counted at it instead. Digits, ASCII punctuation
22
+ * and astral characters (emoji) are each counted separately only when the
23
+ * limits declare a rate for that class — a tokenizer that reads each digit,
24
+ * and most punctuation, on its own makes a number-heavy or JSON-heavy state
25
+ * several times longer than the ~4-characters-per-token estimate suggests. A
26
+ * non-string state is serialized first, as it is on the wire.
27
+ *
28
+ * This is the estimator the pre-flight refusal uses: a state this reports at
29
+ * or under `maxStateTokens` is sent, one over it is refused before any
30
+ * network call.
31
+ */
32
+ export function estimateDecisionStateTokens(state, limits) {
33
+ const text = serializeForEstimate(state);
34
+ const nonAsciiTokensPerChar = limits?.nonAsciiTokensPerChar;
35
+ const digit = limits?.digitTokensPerChar;
36
+ const symbol = limits?.symbolTokensPerChar;
37
+ const astral = limits?.astralTokensPerChar;
38
+ if (nonAsciiTokensPerChar === undefined &&
39
+ digit === undefined &&
40
+ symbol === undefined &&
41
+ astral === undefined) {
42
+ return estimateTokens(text);
43
+ }
44
+ // Each class of character is counted once, at its own rate. A class with no
45
+ // declared rate stays in the ordinary four-characters-a-token estimate (or,
46
+ // for non-ASCII, at that estimate's per-character rate), so a provider that
47
+ // declares none of the extra rates is estimated exactly as before.
48
+ const isDigit = (c) => c >= "0" && c <= "9";
49
+ const isSymbol = (c) => /[!-/:-@[-`{-~]/.test(c);
50
+ let digits = 0;
51
+ let symbols = 0;
52
+ let astrals = 0;
53
+ let nonAscii = 0;
54
+ const rest = [];
55
+ for (const c of text) {
56
+ const code = c.codePointAt(0) ?? 0;
57
+ if (code > 0xffff && astral !== undefined) {
58
+ astrals += 1;
59
+ }
60
+ else if (code > 0x7f) {
61
+ nonAscii += 1;
62
+ }
63
+ else if (digit !== undefined && isDigit(c)) {
64
+ digits += 1;
65
+ }
66
+ else if (symbol !== undefined && isSymbol(c)) {
67
+ symbols += 1;
68
+ }
69
+ else {
70
+ rest.push(c);
71
+ }
72
+ }
73
+ return (estimateTokens(rest.join("")) +
74
+ Math.ceil(digits * (digit ?? 0)) +
75
+ Math.ceil(symbols * (symbol ?? 0)) +
76
+ Math.ceil(astrals * (astral ?? 0)) +
77
+ Math.ceil(nonAscii * (nonAsciiTokensPerChar ?? 1 / CHARS_PER_TOKEN)));
78
+ }
79
+ /**
80
+ * Flatten a descriptor's `decisionLimits` for one model: the per-model entry
81
+ * overrides the base, field by field, and `advisory` becomes
82
+ * `enforcedLocally: false`. Returns null for a descriptor that declares no
83
+ * limits.
84
+ */
85
+ export function resolveDecisionLimitsReading(descriptor, model) {
86
+ const limits = descriptor.decisionLimits;
87
+ if (!limits) {
88
+ return null;
89
+ }
90
+ const modelLimits = limits.models?.[model];
91
+ const nonAsciiTokensPerChar = modelLimits?.nonAsciiTokensPerChar ?? limits.nonAsciiTokensPerChar;
92
+ return {
93
+ provider: descriptor.name,
94
+ model,
95
+ maxStateTokens: modelLimits?.maxStateTokens ?? limits.maxStateTokens,
96
+ // Absent for a provider that caps by tokens or bytes rather than by
97
+ // question count (TypeSafe, XOR).
98
+ ...(limits.maxQuestions !== undefined
99
+ ? { maxQuestions: limits.maxQuestions }
100
+ : {}),
101
+ ...(nonAsciiTokensPerChar !== undefined ? { nonAsciiTokensPerChar } : {}),
102
+ // Not model-overridable — `models` only carries `maxStateTokens` and
103
+ // `nonAsciiTokensPerChar` — so these three come from the base limits only.
104
+ ...(limits.digitTokensPerChar !== undefined
105
+ ? { digitTokensPerChar: limits.digitTokensPerChar }
106
+ : {}),
107
+ ...(limits.symbolTokensPerChar !== undefined
108
+ ? { symbolTokensPerChar: limits.symbolTokensPerChar }
109
+ : {}),
110
+ ...(limits.astralTokensPerChar !== undefined
111
+ ? { astralTokensPerChar: limits.astralTokensPerChar }
112
+ : {}),
113
+ ...(limits.media !== undefined ? { media: limits.media } : {}),
114
+ enforcedLocally: limits.advisory !== true,
115
+ };
116
+ }