@juspay/neurolink 12.47.14 → 12.48.1

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 +2 -2
  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
@@ -0,0 +1,364 @@
1
+ /**
2
+ * Text preparation for speech synthesis.
3
+ *
4
+ * Model output is written for a screen: markdown structure, URLs, emoji.
5
+ * Read aloud, every one of those is noise — "asterisk asterisk", a 90-character
6
+ * URL spelled letter by letter, "grinning face with smiling eyes". This module
7
+ * rewrites such text into what a voice should actually say.
8
+ *
9
+ * Every regular expression here is linear-time by construction: no nested
10
+ * quantifiers, no lazy scan to a closing token, and no unbounded greedy class
11
+ * followed by a required literal (the `[ \t]+\n` shape that goes quadratic on
12
+ * a long run of spaces). Where a closing token is required, the scan between
13
+ * the two is bounded AND excludes the opener's own character, so an unclosed
14
+ * opener fails at the next opener rather than walking the bound — a run of
15
+ * `[` costs one step per character, not one thousand. Block-level structure
16
+ * (fenced code, lists, tables, headings, blockquotes) is a line pass that
17
+ * toggles state, so an unterminated fence costs one scan rather than a
18
+ * backtracking search.
19
+ *
20
+ * @module utils/speechText
21
+ */
22
+ // A namespace import, not a named one: the browser bundle replaces every
23
+ // `node:` module with a fixed stub that has no `domainToUnicode`, and a named
24
+ // import of a missing export fails that build. `hostnameOf` checks for the
25
+ // function at runtime and falls back to the punycode form.
26
+ import * as nodeUrl from "node:url";
27
+ const DEFAULT_CODE_BLOCK_PHRASE = "Code block omitted.";
28
+ /** A fence opener/closer: up to three spaces of indent, then ``` or ~~~. */
29
+ const FENCE_LINE = /^[ \t]{0,3}(?:`{3,}|~{3,})/;
30
+ /** ATX heading marker: `#` to `######` followed by whitespace. */
31
+ const HEADING_MARKER = /^[ \t]{0,3}#{1,6}[ \t]+/;
32
+ /** A line made only of `=`, `-`, `*`, `_` and spaces (setext underline or thematic break). */
33
+ const RULE_LINE = /^[ \t]{0,3}[=\-*_][=\-*_ \t]*$/;
34
+ /** A line made only of pipes, dashes, colons and spaces (table separator row). */
35
+ const TABLE_SEPARATOR_CHARS = /^[|:\- \t]+$/;
36
+ /** Unordered list bullet. */
37
+ const BULLET_MARKER = /^([ \t]*)[-*+][ \t]+/;
38
+ /** Ordered list marker (`1.` / `1)`), bounded to nine digits. */
39
+ const ORDERED_MARKER = /^([ \t]*)\d{1,9}[.)][ \t]+/;
40
+ /** Task-list checkbox left after a bullet is stripped. */
41
+ const TASK_CHECKBOX = /^\[[ xX]\][ \t]+/;
42
+ /** One level of blockquote. */
43
+ const BLOCKQUOTE_MARKER = /^[ \t]{0,3}>[ \t]?/;
44
+ // Inline constructs need a closing token. The span between opener and closer
45
+ // is bounded so an unclosed opener never scans to the end of the document.
46
+ // The bound alone is not enough: a label class that admits the opener's own
47
+ // character makes every `[` in a run of `[` walk the full bound before it
48
+ // fails, which is linear with a constant of LINK_TEXT_MAX — 2.6 s per
49
+ // megabyte of `[`. Excluding the opener from the class makes the failure
50
+ // immediate, so a run of openers costs one step each.
51
+ const LINK_TEXT_MAX = 1000;
52
+ const LINK_TARGET_MAX = 2048;
53
+ const CODE_SPAN_MAX = 1000;
54
+ const TAG_BODY_MAX = 500;
55
+ const EMPHASIS_SPAN_MAX = 1000;
56
+ /** Inline image: `![alt](src)` → alt. */
57
+ const INLINE_IMAGE = new RegExp(`!\\[([^\\][\\n]{0,${LINK_TEXT_MAX}})\\]\\([^)[\\n]{0,${LINK_TARGET_MAX}}\\)`, "g");
58
+ /** Inline link: `[text](href)` → text. */
59
+ const INLINE_LINK = new RegExp(`\\[([^\\][\\n]{0,${LINK_TEXT_MAX}})\\]\\([^)[\\n]{0,${LINK_TARGET_MAX}}\\)`, "g");
60
+ /** Reference link: `[text][ref]` → text. */
61
+ const REFERENCE_LINK = new RegExp(`\\[([^\\][\\n]{0,${LINK_TEXT_MAX}})\\]\\[[^\\][\\n]{0,${LINK_TEXT_MAX}}\\]`, "g");
62
+ /**
63
+ * Autolink: `<https://…>` → the bare URL, so the URL pass sees it. The target
64
+ * class excludes `<` for the same reason the link classes exclude `[`: an
65
+ * unterminated `<http://` used to walk the full target bound before failing,
66
+ * which made 1 MB of them cost ~380 ms instead of under 1 ms.
67
+ */
68
+ const AUTOLINK = new RegExp(`<(https?://[^<>\\s]{1,${LINK_TARGET_MAX}})>`, "g");
69
+ /** Inline code span: the backticks go, the contents stay. */
70
+ const INLINE_CODE = new RegExp("`{1,3}([^`\\n]{0," + CODE_SPAN_MAX + "})`{1,3}", "g");
71
+ /**
72
+ * An HTML tag, opening or closing — but only a tag whose name is HTML. Model
73
+ * output uses angle brackets for placeholders (`<Enter>`, `<order-id>`),
74
+ * generics (`Map<string, T>`) and comparisons (`x<y and y>z`), and none of
75
+ * those may be swallowed: the word inside is usually the instruction.
76
+ */
77
+ const HTML_TAG_NAMES = "p|br|b|i|u|em|strong|a|span|div|code|pre|ul|ol|li|h[1-6]|img|table|thead|tbody|tr|td|th|blockquote|hr|sup|sub|small|mark|del|ins|s";
78
+ const HTML_TAG = new RegExp(`</?(?:${HTML_TAG_NAMES})(?=[\\s/>])[^<>\\n]{0,${TAG_BODY_MAX}}>`, "gi");
79
+ /** Strikethrough markers abutting text. */
80
+ const STRIKETHROUGH = /~~(?=\S)|(?<=\S)~~/g;
81
+ // Emphasis is a PAIR: an opening run preceded by the start of the text,
82
+ // whitespace, punctuation or a symbol and followed by a non-space, then a
83
+ // closing run preceded by a non-space and followed by the end, whitespace,
84
+ // punctuation or a symbol. A `*` between two digits (`2*3*4`, `*123*1#`)
85
+ // satisfies neither side and is left alone — the old "any run touching a
86
+ // non-space" rule spoke `2*3*4 = 24` as "234 = 24". The span between the
87
+ // runs excludes the delimiter, so an unpaired opener fails at the next
88
+ // delimiter instead of scanning to the bound.
89
+ //
90
+ // The boundary classes are Unicode: `\p{P}` and `\p{S}` rather than a list
91
+ // of ASCII brackets and sentence marks. An ASCII list left `**` in Hindi
92
+ // (`**भुगतान सफल रहा**।` — the closer sits before a danda), Chinese
93
+ // (`**总结**:` — a full-width colon), curly quotes (`“**Quoted**”`) and
94
+ // before an em dash (`**Fast**—and`); for a voice assistant those are the
95
+ // common cases, not the edge ones. The delimiter is itself punctuation, so
96
+ // it is excluded from its own boundary: without that, `x**2 + y**2` read the
97
+ // second `*` of each run as an opener/closer and became `x*2 + y*2`.
98
+ //
99
+ // Han, Hiragana, Katakana and Hangul also count as a boundary: those scripts
100
+ // have no spaces between words, so `**` glued straight to surrounding CJK
101
+ // text (`这是**重点**内容`, not `这是 **重点** 内容`) is the normal way it is
102
+ // written, not an edge case — without this, that `**` had neither side
103
+ // satisfy the boundary and was spoken aloud as asterisks.
104
+ const EMPHASIS_BOUNDARY = "\\s\\p{White_Space}\\p{P}\\p{S}\\p{Script=Han}\\p{Script=Hiragana}\\p{Script=Katakana}\\p{Script=Hangul}";
105
+ function emphasisPattern(delimiter) {
106
+ const openBefore = `(?<![^${EMPHASIS_BOUNDARY}]|${delimiter})`;
107
+ const closeAfter = `(?![^${EMPHASIS_BOUNDARY}]|${delimiter})`;
108
+ const run = `${delimiter}{1,3}`;
109
+ const span = `([^${delimiter}\\n]{1,${EMPHASIS_SPAN_MAX}}?)`;
110
+ return new RegExp(`${openBefore}${run}(?=[^\\s${delimiter}])${span}(?<=\\S)${run}${closeAfter}`, "gu");
111
+ }
112
+ const EMPHASIS_ASTERISK = emphasisPattern("\\*");
113
+ const EMPHASIS_UNDERSCORE = emphasisPattern("_");
114
+ /** `**bold *and italic* inside**`: each pass peels one level, so nesting needs more than one. */
115
+ const EMPHASIS_PASSES = 3;
116
+ // Backslash escapes must be resolved BEFORE the emphasis and link passes —
117
+ // `\*not bold\*` is literal asterisks, not emphasis — and the resolved
118
+ // character must then survive those passes. Each escapable character is
119
+ // swapped for a sentinel first and swapped back last. The sentinels are
120
+ // Unicode noncharacters (U+FDD0–U+FDEF), which are reserved for exactly this
121
+ // process-internal use and never occur in interchanged text; any that do
122
+ // arrive are dropped so they cannot masquerade as an escape.
123
+ const ESCAPABLE_PUNCTUATION = "\\`*_{}[]()#+-.!|>~<";
124
+ const ESCAPED_PUNCTUATION = /\\([\\`*_{}[\]()#+\-.!|>~<])/g;
125
+ const ESCAPE_SENTINEL_BASE = 0xfdd0;
126
+ const ESCAPE_SENTINELS = /[﷐-﷯]/g;
127
+ /** An absolute http(s) URL, as one unbroken token. */
128
+ const HTTP_URL = /https?:\/\/[^\s<>()[\]"']+/gi;
129
+ /** A bare `www.` host with optional path. */
130
+ const WWW_URL = /\bwww\.[^\s<>()[\]"']+/gi;
131
+ const TRAILING_PUNCTUATION = new Set([".", ",", ";", ":", "!", "?"]);
132
+ // One pictograph with the modifiers, variation selectors and keycap combiner
133
+ // that may trail it, then any number of further pictographs joined to it by
134
+ // U+200D. The whole sequence goes as one unit so a joined family or a
135
+ // skin-toned hand leaves nothing behind. The joiner is consumed ONLY here,
136
+ // between or beside pictographs: U+200D is also a letter-shaping control in
137
+ // Devanagari, Malayalam, Arabic and others, and stripping it everywhere
138
+ // changed the shape and pronunciation of ordinary Indic text.
139
+ const PICTOGRAPH = "(?:\\p{Extended_Pictographic}|\\p{Emoji_Presentation})(?:\\p{Emoji_Modifier}|\\uFE0E|\\uFE0F|\\u20E3)*";
140
+ const EMOJI = new RegExp(`\\u200D?${PICTOGRAPH}(?:\\u200D${PICTOGRAPH})*\\u200D?`, "gu");
141
+ /** Skin-tone modifiers, variation selectors and the keycap combiner left over after a bare digit or symbol. */
142
+ const EMOJI_JOINERS = /\p{Emoji_Modifier}|\uFE0E|\uFE0F|\u20E3/gu;
143
+ /** An `http`/`https` scheme, matched case-insensitively (RFC 3986 \u00A73.1). */
144
+ const HTTP_SCHEME = /^https?:\/\//i;
145
+ const LINE_BREAKS = /\r\n?/g;
146
+ /** Runs of two or more horizontal spaces; nothing is required after the run. */
147
+ const HORIZONTAL_RUNS = /[ \t]{2,}/g;
148
+ /** Applied only after HORIZONTAL_RUNS, when no run is longer than one character. */
149
+ const SPACE_BEFORE_NEWLINE = /[ \t]\n/g;
150
+ const SPACE_AFTER_NEWLINE = /\n[ \t]/g;
151
+ const BLANK_LINE_RUNS = /\n{3,}/g;
152
+ function resolveOptions(opts) {
153
+ return {
154
+ markdown: opts?.markdown ?? true,
155
+ codeBlocks: opts?.codeBlocks ?? "phrase",
156
+ codeBlockPhrase: opts?.codeBlockPhrase ?? DEFAULT_CODE_BLOCK_PHRASE,
157
+ urls: opts?.urls ?? "hostname",
158
+ emoji: opts?.emoji ?? true,
159
+ };
160
+ }
161
+ /**
162
+ * Resolve a `TTSOptions.sanitize` value into options, or `undefined` when the
163
+ * pass is off. `true` means the defaults; an object tunes them.
164
+ */
165
+ export function resolveSpeechSanitizeOptions(sanitize) {
166
+ if (sanitize === undefined || sanitize === false) {
167
+ return undefined;
168
+ }
169
+ return sanitize === true ? {} : sanitize;
170
+ }
171
+ function isRuleLine(line) {
172
+ if (!RULE_LINE.test(line)) {
173
+ return false;
174
+ }
175
+ let markers = 0;
176
+ for (const ch of line) {
177
+ if (ch === "=" || ch === "-" || ch === "*" || ch === "_") {
178
+ markers += 1;
179
+ }
180
+ }
181
+ return markers >= 3;
182
+ }
183
+ function isTableSeparator(line) {
184
+ return line.includes("---") && TABLE_SEPARATOR_CHARS.test(line);
185
+ }
186
+ function isTableRow(line) {
187
+ const trimmed = line.trim();
188
+ return trimmed.startsWith("|") || trimmed.endsWith("|");
189
+ }
190
+ /** `| a | b |` → `a, b`. Split on the pipe and trim each cell — no regex over the padding. */
191
+ function flattenTableRow(line) {
192
+ const cells = [];
193
+ for (const cell of line.split("|")) {
194
+ const trimmed = cell.trim();
195
+ if (trimmed.length > 0) {
196
+ cells.push(trimmed);
197
+ }
198
+ }
199
+ return cells.join(", ");
200
+ }
201
+ function stripBlockquote(line) {
202
+ let current = line;
203
+ for (;;) {
204
+ const next = current.replace(BLOCKQUOTE_MARKER, "");
205
+ if (next === current) {
206
+ return current;
207
+ }
208
+ current = next;
209
+ }
210
+ }
211
+ /**
212
+ * One pass over the lines: fenced code per `codeBlocks`, then the per-line
213
+ * block markers (headings, rules, bullets, blockquotes, tables).
214
+ */
215
+ function stripBlockMarkdown(text, options) {
216
+ const out = [];
217
+ let inFence = false;
218
+ for (const line of text.split("\n")) {
219
+ if (FENCE_LINE.test(line)) {
220
+ if (options.codeBlocks === "keep") {
221
+ // Streaming: the block's extent is unknowable per segment, so only
222
+ // the fence line itself is dropped and the contents are spoken.
223
+ continue;
224
+ }
225
+ if (!inFence) {
226
+ inFence = true;
227
+ if (options.codeBlocks === "phrase") {
228
+ out.push(options.codeBlockPhrase);
229
+ }
230
+ }
231
+ else {
232
+ inFence = false;
233
+ }
234
+ continue;
235
+ }
236
+ if (inFence) {
237
+ continue;
238
+ }
239
+ if (isRuleLine(line) || isTableSeparator(line)) {
240
+ continue;
241
+ }
242
+ let current = stripBlockquote(line);
243
+ current = current.replace(HEADING_MARKER, "");
244
+ current = current.replace(BULLET_MARKER, "$1");
245
+ current = current.replace(ORDERED_MARKER, "$1");
246
+ current = current.replace(TASK_CHECKBOX, "");
247
+ if (isTableRow(current)) {
248
+ current = flattenTableRow(current);
249
+ }
250
+ out.push(current);
251
+ }
252
+ return out.join("\n");
253
+ }
254
+ /** `\*` → sentinel, so the character it protects is invisible to the passes that follow. */
255
+ function protectEscapes(text) {
256
+ return text
257
+ .replace(ESCAPE_SENTINELS, "")
258
+ .replace(ESCAPED_PUNCTUATION, (_match, ch) => String.fromCharCode(ESCAPE_SENTINEL_BASE + ESCAPABLE_PUNCTUATION.indexOf(ch)));
259
+ }
260
+ /** Sentinel → the literal character `\*` stood for. */
261
+ function restoreEscapes(text) {
262
+ return text.replace(ESCAPE_SENTINELS, (sentinel) => ESCAPABLE_PUNCTUATION[sentinel.charCodeAt(0) - ESCAPE_SENTINEL_BASE] ??
263
+ "");
264
+ }
265
+ function stripEmphasis(text) {
266
+ let current = text;
267
+ for (let pass = 0; pass < EMPHASIS_PASSES; pass++) {
268
+ const next = current
269
+ .replace(EMPHASIS_ASTERISK, "$1")
270
+ .replace(EMPHASIS_UNDERSCORE, "$1");
271
+ if (next === current) {
272
+ break;
273
+ }
274
+ current = next;
275
+ }
276
+ return current;
277
+ }
278
+ function stripInlineMarkdown(text) {
279
+ const stripped = protectEscapes(text)
280
+ .replace(INLINE_IMAGE, "$1")
281
+ .replace(INLINE_LINK, "$1")
282
+ .replace(REFERENCE_LINK, "$1")
283
+ .replace(AUTOLINK, "$1")
284
+ .replace(INLINE_CODE, "$1")
285
+ .replace(HTML_TAG, " ")
286
+ .replace(STRIKETHROUGH, "");
287
+ return restoreEscapes(stripEmphasis(stripped));
288
+ }
289
+ /** Split a URL token into the URL proper and the sentence punctuation glued to its end. */
290
+ function splitTrailingPunctuation(token) {
291
+ let end = token.length;
292
+ while (end > 0 && TRAILING_PUNCTUATION.has(token[end - 1] ?? "")) {
293
+ end -= 1;
294
+ }
295
+ return { url: token.slice(0, end), trailing: token.slice(end) };
296
+ }
297
+ /**
298
+ * The host a URL should be spoken as. `URL.hostname` is the punycode form
299
+ * (`xn--mnchen-3ya.example`), which a voice spells letter by letter; Node's
300
+ * `domainToUnicode` gives back `münchen.example`. The scheme test is
301
+ * case-insensitive because `HTTP_URL` is: `HTTPS://Example.COM/x` used to
302
+ * fail a case-sensitive `startsWith("http")`, get a second scheme prepended,
303
+ * and be spoken as "https".
304
+ */
305
+ function hostnameOf(url) {
306
+ const withScheme = HTTP_SCHEME.test(url) ? url : `https://${url}`;
307
+ try {
308
+ const host = new URL(withScheme).hostname;
309
+ return typeof nodeUrl.domainToUnicode === "function"
310
+ ? nodeUrl.domainToUnicode(host)
311
+ : host;
312
+ }
313
+ catch {
314
+ return "";
315
+ }
316
+ }
317
+ function rewriteUrls(text, mode) {
318
+ const replacer = (match) => {
319
+ const { url, trailing } = splitTrailingPunctuation(match);
320
+ if (mode === "remove") {
321
+ return trailing;
322
+ }
323
+ const host = hostnameOf(url);
324
+ return host ? `${host}${trailing}` : trailing;
325
+ };
326
+ return text.replace(HTTP_URL, replacer).replace(WWW_URL, replacer);
327
+ }
328
+ function removeEmoji(text) {
329
+ return text.replace(EMOJI, "").replace(EMOJI_JOINERS, "");
330
+ }
331
+ function collapseWhitespace(text) {
332
+ return text
333
+ .replace(HORIZONTAL_RUNS, " ")
334
+ .replace(SPACE_BEFORE_NEWLINE, "\n")
335
+ .replace(SPACE_AFTER_NEWLINE, "\n")
336
+ .replace(BLANK_LINE_RUNS, "\n\n")
337
+ .trim();
338
+ }
339
+ /**
340
+ * Rewrite `text` into what a TTS voice should say.
341
+ *
342
+ * Pure and deterministic: the same input and options always yield the same
343
+ * output, and nothing here reads the environment. Runs in time linear in the
344
+ * length of `text`.
345
+ *
346
+ * @example
347
+ * ```typescript
348
+ * prepareTextForSpeech("## Title\n\nSee **this** at https://docs.example.com/x 🎉");
349
+ * // → "Title\n\nSee this at docs.example.com"
350
+ * ```
351
+ */
352
+ export function prepareTextForSpeech(text, opts) {
353
+ const options = resolveOptions(opts);
354
+ let out = text.replace(LINE_BREAKS, "\n");
355
+ if (options.markdown) {
356
+ out = stripBlockMarkdown(out, options);
357
+ out = stripInlineMarkdown(out);
358
+ }
359
+ out = rewriteUrls(out, options.urls);
360
+ if (options.emoji) {
361
+ out = removeEmoji(out);
362
+ }
363
+ return collapseWhitespace(out);
364
+ }
@@ -0,0 +1,26 @@
1
+ /**
2
+ * Classification of a tool result that the loop received as a normal return
3
+ * but that reports a failure in-band.
4
+ *
5
+ * `neurolink.executeTool` converts a thrown error and a circuit-breaker
6
+ * refusal into `{ isError: true, content: [...] }`, MCP servers return the
7
+ * same shape for a failed call, and the documented custom-tool pattern is
8
+ * `return { error: "User not found" }`. Every consumer that decides "did this
9
+ * tool succeed" — the `tool:end` event, the stored `tool_result` row, the
10
+ * replayed marker, and `toolExecutions[].isError` — has to read those shapes
11
+ * the same way, so the rule lives in ONE predicate, `isErrorShapedToolResult`,
12
+ * and this module only extracts the text to report for it.
13
+ */
14
+ /**
15
+ * The error text carried by an error-shaped result (`isError: true`, an
16
+ * `error` field, or a failing `status`), or undefined when the value is not
17
+ * one. Prefers an `error` string, then a serialized non-string `error`, then
18
+ * the joined text parts of an MCP `content` array, then a fixed fallback.
19
+ */
20
+ export declare function extractIsErrorText(output: unknown): string | undefined;
21
+ /**
22
+ * Same classification for a serialized result: the memory stores keep
23
+ * `tool_result.content` as a JSON string, so a row written before the store
24
+ * learned to flag error shapes can still be recognised at replay.
25
+ */
26
+ export declare function extractIsErrorTextFromContent(content: unknown): string | undefined;
@@ -0,0 +1,73 @@
1
+ /**
2
+ * Classification of a tool result that the loop received as a normal return
3
+ * but that reports a failure in-band.
4
+ *
5
+ * `neurolink.executeTool` converts a thrown error and a circuit-breaker
6
+ * refusal into `{ isError: true, content: [...] }`, MCP servers return the
7
+ * same shape for a failed call, and the documented custom-tool pattern is
8
+ * `return { error: "User not found" }`. Every consumer that decides "did this
9
+ * tool succeed" — the `tool:end` event, the stored `tool_result` row, the
10
+ * replayed marker, and `toolExecutions[].isError` — has to read those shapes
11
+ * the same way, so the rule lives in ONE predicate, `isErrorShapedToolResult`,
12
+ * and this module only extracts the text to report for it.
13
+ */
14
+ import { isErrorShapedToolResult, isMeaningfulError, } from "../core/toolExecutionRecorder.js";
15
+ const ISERROR_FALLBACK_TEXT = "Tool returned isError: true";
16
+ const ERROR_SHAPE_FALLBACK_TEXT = "Tool reported an error";
17
+ /**
18
+ * The error text carried by an error-shaped result (`isError: true`, an
19
+ * `error` field, or a failing `status`), or undefined when the value is not
20
+ * one. Prefers an `error` string, then a serialized non-string `error`, then
21
+ * the joined text parts of an MCP `content` array, then a fixed fallback.
22
+ */
23
+ export function extractIsErrorText(output) {
24
+ if (!isErrorShapedToolResult(output)) {
25
+ return undefined;
26
+ }
27
+ const record = output;
28
+ if (typeof record.error === "string" && record.error.length > 0) {
29
+ return record.error;
30
+ }
31
+ if (record.error !== true && isMeaningfulError(record.error)) {
32
+ try {
33
+ return JSON.stringify(record.error) ?? String(record.error);
34
+ }
35
+ catch {
36
+ return String(record.error);
37
+ }
38
+ }
39
+ if (Array.isArray(record.content)) {
40
+ const texts = record.content
41
+ .filter((c) => c && c.type === "text" && typeof c.text === "string")
42
+ .map((c) => c.text);
43
+ if (texts.length > 0) {
44
+ return texts.join(" ");
45
+ }
46
+ }
47
+ return record.isError === true
48
+ ? ISERROR_FALLBACK_TEXT
49
+ : ERROR_SHAPE_FALLBACK_TEXT;
50
+ }
51
+ /**
52
+ * Same classification for a serialized result: the memory stores keep
53
+ * `tool_result.content` as a JSON string, so a row written before the store
54
+ * learned to flag error shapes can still be recognised at replay.
55
+ */
56
+ export function extractIsErrorTextFromContent(content) {
57
+ if (typeof content !== "string") {
58
+ return extractIsErrorText(content);
59
+ }
60
+ const trimmed = content.trim();
61
+ if (!trimmed.startsWith("{") ||
62
+ !(trimmed.includes('"isError"') ||
63
+ trimmed.includes('"error"') ||
64
+ trimmed.includes('"status"'))) {
65
+ return undefined;
66
+ }
67
+ try {
68
+ return extractIsErrorText(JSON.parse(trimmed));
69
+ }
70
+ catch {
71
+ return undefined;
72
+ }
73
+ }
@@ -20,6 +20,28 @@ export declare const TTS_ERROR_CODES: {
20
20
  readonly SYNTHESIS_FAILED: "TTS_SYNTHESIS_FAILED";
21
21
  readonly INVALID_INPUT: "TTS_INVALID_INPUT";
22
22
  };
23
+ /**
24
+ * The wall-clock a handler may legitimately spend on one `synthesize()` call:
25
+ * `timeoutMs × (1 + retries)` plus pause slack (see `TTSHandlerTimeBudget`).
26
+ * A knob the caller did not set takes the handler default — 30 s per attempt
27
+ * and one retry — so the budget is always a number. It used to be `undefined`
28
+ * when neither knob was set, which left the outer synthesis timeout at the
29
+ * provider's own value: with the 60 s provider default, a first attempt that
30
+ * hung to its 30 s abort left no room for the documented default retry, and
31
+ * the caller got a timeout error instead of the retried audio.
32
+ *
33
+ * Used by `BaseProvider`'s generate() TTS paths so the outer `withTimeoutFn`
34
+ * never undercuts the handler's own retry loop.
35
+ *
36
+ * `provider` is the name the synthesis will actually be dispatched to —
37
+ * `tts.provider`, falling back to the request's or the instance's provider —
38
+ * not just `options.provider`: a caller that reaches ElevenLabs through the
39
+ * outer provider and leaves `tts.provider` unset got a budget of 0, and the
40
+ * outer timeout cut the handler's documented default retry. The registry
41
+ * resolves names case-insensitively, so this check does too; `"ElevenLabs"`
42
+ * found the handler but not the budget.
43
+ */
44
+ export declare function resolveTTSHandlerBudgetMs(options: TTSOptions, provider?: string): number;
23
45
  /** Internal signal raised after all buffered segments have been attempted. */
24
46
  export declare class IncrementalTTSSynthesisError extends Error {
25
47
  readonly firstError: unknown;
@@ -74,6 +96,13 @@ export declare class TTSProcessor {
74
96
  * @private
75
97
  */
76
98
  private static readonly DEFAULT_MAX_TEXT_LENGTH;
99
+ /**
100
+ * The text cap `synthesize()` enforces for `providerName`: the handler's
101
+ * `maxTextLength`, or the default when the handler declares none or is not
102
+ * registered. Lets a caller bound work it does BEFORE synthesis — the
103
+ * sanitize pass in `BaseProvider` — to text that could ever be accepted.
104
+ */
105
+ static maxTextLengthFor(providerName: string): number;
77
106
  /**
78
107
  * Register a TTS handler for a specific provider
79
108
  *
@@ -8,6 +8,7 @@
8
8
  */
9
9
  import { logger } from "./logger.js";
10
10
  import { VALID_AUDIO_FORMATS } from "../types/index.js";
11
+ import { prepareTextForSpeech, resolveSpeechSanitizeOptions, } from "./speechText.js";
11
12
  import { ErrorCategory, ErrorSeverity } from "../constants/enums.js";
12
13
  import { NeuroLinkError } from "./errorHandling.js";
13
14
  import { HandlerRegistry } from "../core/handlerRegistry.js";
@@ -26,6 +27,62 @@ export const TTS_ERROR_CODES = {
26
27
  };
27
28
  const DEFAULT_STREAMING_BUFFER_SIZE = 120;
28
29
  const SENTENCE_BOUNDARY = /[.!?]+(?:["')\]]+)?(?=\s|$)/g;
30
+ /**
31
+ * Slack added to a handler's declared time budget for the pauses its retry
32
+ * loop takes between attempts (Retry-After is capped at 10 s per pause).
33
+ */
34
+ const HANDLER_RETRY_PAUSE_SLACK_MS = 10_000;
35
+ /** The handler defaults the budget assumes when the caller set no knobs (ElevenLabs: 30 s, one retry). */
36
+ const HANDLER_DEFAULT_TIMEOUT_MS = 30_000;
37
+ const HANDLER_DEFAULT_RETRIES = 1;
38
+ /** Handlers with a built-in retry loop, i.e. the ones the defaults describe. */
39
+ const RETRYING_TTS_PROVIDERS = new Set([
40
+ "elevenlabs",
41
+ "elevenlabs-tts",
42
+ ]);
43
+ /** Largest delay `setTimeout` honours; anything above fires at once. */
44
+ const MAX_TIMER_MS = 2_147_483_647;
45
+ /**
46
+ * The wall-clock a handler may legitimately spend on one `synthesize()` call:
47
+ * `timeoutMs × (1 + retries)` plus pause slack (see `TTSHandlerTimeBudget`).
48
+ * A knob the caller did not set takes the handler default — 30 s per attempt
49
+ * and one retry — so the budget is always a number. It used to be `undefined`
50
+ * when neither knob was set, which left the outer synthesis timeout at the
51
+ * provider's own value: with the 60 s provider default, a first attempt that
52
+ * hung to its 30 s abort left no room for the documented default retry, and
53
+ * the caller got a timeout error instead of the retried audio.
54
+ *
55
+ * Used by `BaseProvider`'s generate() TTS paths so the outer `withTimeoutFn`
56
+ * never undercuts the handler's own retry loop.
57
+ *
58
+ * `provider` is the name the synthesis will actually be dispatched to —
59
+ * `tts.provider`, falling back to the request's or the instance's provider —
60
+ * not just `options.provider`: a caller that reaches ElevenLabs through the
61
+ * outer provider and leaves `tts.provider` unset got a budget of 0, and the
62
+ * outer timeout cut the handler's documented default retry. The registry
63
+ * resolves names case-insensitively, so this check does too; `"ElevenLabs"`
64
+ * found the handler but not the budget.
65
+ */
66
+ export function resolveTTSHandlerBudgetMs(options, provider) {
67
+ const { timeoutMs, retries } = options;
68
+ const hasTimeout = typeof timeoutMs === "number" &&
69
+ Number.isFinite(timeoutMs) &&
70
+ timeoutMs > 0;
71
+ const hasRetries = typeof retries === "number" && Number.isInteger(retries) && retries >= 0;
72
+ const resolvedProvider = String(provider ?? options.provider ?? "").toLowerCase();
73
+ // The defaults describe a retry loop. Only a handler that has one gets a
74
+ // default budget; for every other provider (Google, OpenAI, Azure,
75
+ // Cartesia — one attempt, no knobs) the budget is 0 so the caller's own
76
+ // `timeout` stays the bound instead of being raised to ~70 s.
77
+ if (!hasTimeout &&
78
+ !hasRetries &&
79
+ !RETRYING_TTS_PROVIDERS.has(resolvedProvider)) {
80
+ return 0;
81
+ }
82
+ const perAttempt = Math.min(hasTimeout ? timeoutMs : HANDLER_DEFAULT_TIMEOUT_MS, MAX_TIMER_MS);
83
+ const attempts = 1 + (hasRetries ? retries : HANDLER_DEFAULT_RETRIES);
84
+ return Math.min(perAttempt * attempts + (attempts - 1) * HANDLER_RETRY_PAUSE_SLACK_MS, MAX_TIMER_MS);
85
+ }
29
86
  /** Internal signal raised after all buffered segments have been attempted. */
30
87
  export class IncrementalTTSSynthesisError extends Error {
31
88
  firstError;
@@ -247,6 +304,16 @@ export class TTSProcessor {
247
304
  * @private
248
305
  */
249
306
  static DEFAULT_MAX_TEXT_LENGTH = 3000;
307
+ /**
308
+ * The text cap `synthesize()` enforces for `providerName`: the handler's
309
+ * `maxTextLength`, or the default when the handler declares none or is not
310
+ * registered. Lets a caller bound work it does BEFORE synthesis — the
311
+ * sanitize pass in `BaseProvider` — to text that could ever be accepted.
312
+ */
313
+ static maxTextLengthFor(providerName) {
314
+ return (this.getHandler(providerName)?.maxTextLength ??
315
+ this.DEFAULT_MAX_TEXT_LENGTH);
316
+ }
250
317
  /**
251
318
  * Register a TTS handler for a specific provider
252
319
  *
@@ -779,6 +846,13 @@ export class TTSProcessor {
779
846
  const maxTextLength = Math.max(1, handler?.maxTextLength ?? processor.DEFAULT_MAX_TEXT_LENGTH);
780
847
  const requestedBoundary = options.streamingBufferSize ?? DEFAULT_STREAMING_BUFFER_SIZE;
781
848
  const flushBoundary = Math.min(Math.max(1, Math.trunc(requestedBoundary)), maxTextLength);
849
+ // Per-segment cleanup when `tts.sanitize` is on. A segment may hold an
850
+ // unterminated fence, so code blocks are always kept (fence lines
851
+ // stripped, contents spoken) regardless of the caller's `codeBlocks`.
852
+ const sanitizeOptions = resolveSpeechSanitizeOptions(options.sanitize);
853
+ const segmentSanitize = sanitizeOptions
854
+ ? { ...sanitizeOptions, codeBlocks: "keep" }
855
+ : undefined;
782
856
  let buffer = "";
783
857
  let chunkIndex = 0;
784
858
  let cumulativeSize = 0;
@@ -920,8 +994,8 @@ export class TTSProcessor {
920
994
  };
921
995
  try {
922
996
  while (!stopped()) {
923
- const segment = takeSegment();
924
- if (segment === undefined) {
997
+ const rawSegment = takeSegment();
998
+ if (rawSegment === undefined) {
925
999
  if (inputComplete) {
926
1000
  break;
927
1001
  }
@@ -935,6 +1009,14 @@ export class TTSProcessor {
935
1009
  }
936
1010
  continue;
937
1011
  }
1012
+ const segment = segmentSanitize
1013
+ ? prepareTextForSpeech(rawSegment, segmentSanitize)
1014
+ : rawSegment;
1015
+ // A segment that was only markup (a fence line, a table separator,
1016
+ // a bare heading marker) has nothing to say; it is not a failure.
1017
+ if (segment.length === 0) {
1018
+ continue;
1019
+ }
938
1020
  const currentSegment = ++segmentNumber;
939
1021
  // NOTHING BELOW MAY `yield` INSIDE A `try` THAT HAS A `catch`.
940
1022
  // A consumer can resume this generator with `AsyncGenerator.throw()`
@@ -32,6 +32,7 @@ export { FishAudioTTS, FishAudioTTS as FishAudioTTSHandler, } from "./providers/
32
32
  export { OpenAITTS, OpenAITTS as OpenAITTSHandler, } from "./providers/OpenAITTS.js";
33
33
  export { AzureSTT, AzureSTT as AzureSTTHandler } from "./providers/AzureSTT.js";
34
34
  export { DeepgramSTT, DeepgramSTT as DeepgramSTTHandler, } from "./providers/DeepgramSTT.js";
35
+ export { ElevenLabsSTT, ElevenLabsSTT as ElevenLabsSTTHandler, } from "./providers/ElevenLabsSTT.js";
35
36
  export { GoogleSTT, GoogleSTT as GoogleSTTHandler, } from "./providers/GoogleSTT.js";
36
37
  export { OpenAISTT, OpenAISTTHandler, WhisperSTT, WhisperSTTHandler, } from "./providers/OpenAISTT.js";
37
38
  export { GeminiLive, GeminiLive as GeminiLiveHandler, } from "./providers/GeminiLive.js";
@@ -61,6 +61,7 @@ export { OpenAITTS, OpenAITTS as OpenAITTSHandler, } from "./providers/OpenAITTS
61
61
  // ============================================================================
62
62
  export { AzureSTT, AzureSTT as AzureSTTHandler } from "./providers/AzureSTT.js";
63
63
  export { DeepgramSTT, DeepgramSTT as DeepgramSTTHandler, } from "./providers/DeepgramSTT.js";
64
+ export { ElevenLabsSTT, ElevenLabsSTT as ElevenLabsSTTHandler, } from "./providers/ElevenLabsSTT.js";
64
65
  export { GoogleSTT, GoogleSTT as GoogleSTTHandler, } from "./providers/GoogleSTT.js";
65
66
  export { OpenAISTT, OpenAISTTHandler, WhisperSTT, WhisperSTTHandler, } from "./providers/OpenAISTT.js";
66
67
  // ============================================================================
@@ -79,6 +80,7 @@ import { SixtyDBTTS } from "./providers/SixtyDBTTS.js";
79
80
  import { OpenAITTS } from "./providers/OpenAITTS.js";
80
81
  import { AzureSTT } from "./providers/AzureSTT.js";
81
82
  import { DeepgramSTT } from "./providers/DeepgramSTT.js";
83
+ import { ElevenLabsSTT } from "./providers/ElevenLabsSTT.js";
82
84
  import { GoogleSTT } from "./providers/GoogleSTT.js";
83
85
  import { OpenAISTT } from "./providers/OpenAISTT.js";
84
86
  import { GeminiLive } from "./providers/GeminiLive.js";
@@ -107,6 +109,7 @@ const STT_HANDLER_FACTORIES = {
107
109
  deepgram: () => new DeepgramSTT(),
108
110
  "google-stt": () => new GoogleSTT(),
109
111
  "azure-stt": () => new AzureSTT(),
112
+ "elevenlabs-stt": () => new ElevenLabsSTT(),
110
113
  };
111
114
  const STT_HANDLER_CANDIDATES = MEDIA_HANDLER_CATALOG.filter((entry) => entry.kind === "stt").map((entry) => {
112
115
  const factory = STT_HANDLER_FACTORIES[entry.name];