agents 0.22.0 → 0.23.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 (186) hide show
  1. package/README.md +20 -1
  2. package/dist/{agent-routing-CnkaHb-v.d.ts → agent-routing-DE5zmCQ8.d.ts} +1038 -394
  3. package/dist/agent-routing.d.ts +1 -1
  4. package/dist/agent-tool-types.d.ts +26 -26
  5. package/dist/{agent-tools-C0XJqWzB.d.ts → agent-tools-DtXMTDGM.d.ts} +7 -7
  6. package/dist/agent-tools.d.ts +21 -21
  7. package/dist/browser/ai.js +4 -1
  8. package/dist/browser/ai.js.map +1 -1
  9. package/dist/browser/tanstack-ai.js.map +1 -1
  10. package/dist/callable-decorator-DP__HhBA.d.ts +72 -0
  11. package/dist/callable-decorator.d.ts +18 -0
  12. package/dist/callable-decorator.js +71 -0
  13. package/dist/callable-decorator.js.map +1 -0
  14. package/dist/capability-BjSKYpzg.js.map +1 -1
  15. package/dist/capability-runner-Be_-PLR1.d.ts +459 -0
  16. package/dist/channel-Bnm4S7T2.d.ts +491 -0
  17. package/dist/channels/ai-sdk.d.ts +46 -0
  18. package/dist/channels/ai-sdk.js +120 -0
  19. package/dist/channels/ai-sdk.js.map +1 -0
  20. package/dist/channels/email.d.ts +95 -0
  21. package/dist/channels/email.js +323 -0
  22. package/dist/channels/email.js.map +1 -0
  23. package/dist/channels/index.d.ts +233 -0
  24. package/dist/channels/index.js +608 -0
  25. package/dist/channels/index.js.map +1 -0
  26. package/dist/channels/slack.d.ts +140 -0
  27. package/dist/channels/slack.js +614 -0
  28. package/dist/channels/slack.js.map +1 -0
  29. package/dist/channels/tanstack-ai.d.ts +39 -0
  30. package/dist/channels/tanstack-ai.js +17 -0
  31. package/dist/channels/tanstack-ai.js.map +1 -0
  32. package/dist/channels/telegram.d.ts +106 -0
  33. package/dist/channels/telegram.js +427 -0
  34. package/dist/channels/telegram.js.map +1 -0
  35. package/dist/channels/voice.d.ts +45 -0
  36. package/dist/channels/voice.js +122 -0
  37. package/dist/channels/voice.js.map +1 -0
  38. package/dist/chat/index.d.ts +2325 -2019
  39. package/dist/chat/index.js +888 -518
  40. package/dist/chat/index.js.map +1 -1
  41. package/dist/chat-sdk/index.d.ts +7 -7
  42. package/dist/chat-sdk/index.js +1 -1
  43. package/dist/client.d.ts +1 -1
  44. package/dist/context/index.d.ts +216 -0
  45. package/dist/context/index.js +454 -0
  46. package/dist/context/index.js.map +1 -0
  47. package/dist/{current-agent-CuMErtly.d.ts → current-agent-Da_C9a3b.d.ts} +90 -107
  48. package/dist/current-agent-DhoDkSnH.js.map +1 -1
  49. package/dist/{diagnostics-CaBjfz4J.js → diagnostics-BzvaX2UT.js} +5 -1
  50. package/dist/diagnostics-BzvaX2UT.js.map +1 -0
  51. package/dist/diagnostics-C4jcz3VK.js +360 -0
  52. package/dist/diagnostics-C4jcz3VK.js.map +1 -0
  53. package/dist/index-BB0kqhIz.d.ts +101 -0
  54. package/dist/index-BVVgDSdq.d.ts +1 -0
  55. package/dist/index-XDkuQ7zm.d.ts +89 -0
  56. package/dist/{index-DcSAZKsB.d.ts → index-YSKgfgg9.d.ts} +3 -1
  57. package/dist/index.d.ts +91 -82
  58. package/dist/index.js +3 -2
  59. package/dist/ingress-BfetZbMO.js +83 -0
  60. package/dist/ingress-BfetZbMO.js.map +1 -0
  61. package/dist/internal-CYlgHl1l.js +59 -0
  62. package/dist/internal-CYlgHl1l.js.map +1 -0
  63. package/dist/internal_context.d.ts +1 -1
  64. package/dist/lifecycle/index.d.ts +35 -17
  65. package/dist/lifecycle/index.js +1 -1
  66. package/dist/lifecycle-CMRGjZdw.js +1299 -0
  67. package/dist/lifecycle-CMRGjZdw.js.map +1 -0
  68. package/dist/mcp/client/index.d.ts +20 -20
  69. package/dist/mcp/index.d.ts +35 -35
  70. package/dist/mcp/index.js +1 -1
  71. package/dist/observability/index.d.ts +1 -1
  72. package/dist/observability/index.js +1 -1
  73. package/dist/react.d.ts +4 -4
  74. package/dist/{retries-CAvxtG9d.d.ts → retries-D9Ds-1lz.d.ts} +17 -6
  75. package/dist/retries.d.ts +8 -6
  76. package/dist/retries.js +13 -1
  77. package/dist/retries.js.map +1 -1
  78. package/dist/routing/index.d.ts +137 -0
  79. package/dist/routing/index.js +244 -0
  80. package/dist/routing/index.js.map +1 -0
  81. package/dist/sanitize-D9TujEK8.js +79 -0
  82. package/dist/sanitize-D9TujEK8.js.map +1 -0
  83. package/dist/scheduler-DD9NdYbF.js +665 -0
  84. package/dist/scheduler-DD9NdYbF.js.map +1 -0
  85. package/dist/{scheduler-DQoTGoAW.d.ts → scheduler-Dwh85ZGl.d.ts} +21 -22
  86. package/dist/schedules/index.d.ts +1 -1
  87. package/dist/schedules/index.js +1 -1
  88. package/dist/sentence-chunker-BAidJ4DA.d.ts +68 -0
  89. package/dist/serializable.d.ts +1 -1
  90. package/dist/sessions/index.d.ts +441 -0
  91. package/dist/sessions/index.js +2063 -0
  92. package/dist/sessions/index.js.map +1 -0
  93. package/dist/skills/index.d.ts +99 -0
  94. package/dist/skills/index.js +254 -5
  95. package/dist/skills/index.js.map +1 -1
  96. package/dist/{src-5W6JNKVb.js → src-DlSHshb2.js} +1460 -1110
  97. package/dist/src-DlSHshb2.js.map +1 -0
  98. package/dist/streams/index.d.ts +120 -0
  99. package/dist/streams/index.js +107 -0
  100. package/dist/streams/index.js.map +1 -0
  101. package/dist/streams-D6tJ0NN9.d.ts +370 -0
  102. package/dist/streams-DZKgAj9b.js +709 -0
  103. package/dist/streams-DZKgAj9b.js.map +1 -0
  104. package/dist/sub-routing.d.ts +12 -12
  105. package/dist/surface-bZZJqBka.js +17 -0
  106. package/dist/surface-bZZJqBka.js.map +1 -0
  107. package/dist/tasks/index.d.ts +64 -0
  108. package/dist/tasks/index.js +2 -0
  109. package/dist/tasks-BRJ5zgya.d.ts +517 -0
  110. package/dist/tasks-ylZgBjhj.js +1656 -0
  111. package/dist/tasks-ylZgBjhj.js.map +1 -0
  112. package/dist/text-segment-joiner-BtAFQSA_.js +57 -0
  113. package/dist/text-segment-joiner-BtAFQSA_.js.map +1 -0
  114. package/dist/text-stream-CpdiKrJB.js +272 -0
  115. package/dist/text-stream-CpdiKrJB.js.map +1 -0
  116. package/dist/tokens-nHAKcN6M.js +52 -0
  117. package/dist/tokens-nHAKcN6M.js.map +1 -0
  118. package/dist/tool-schema-CBjGPrsQ.js +31 -0
  119. package/dist/tool-schema-CBjGPrsQ.js.map +1 -0
  120. package/dist/types-B7LojTe4.d.ts +202 -0
  121. package/dist/types-_Faxb570.d.ts +439 -0
  122. package/dist/voice/client.d.ts +226 -0
  123. package/dist/voice/client.js +932 -0
  124. package/dist/voice/client.js.map +1 -0
  125. package/dist/voice/errors.d.ts +43 -0
  126. package/dist/voice/errors.js +41 -0
  127. package/dist/voice/errors.js.map +1 -0
  128. package/dist/voice/index.d.ts +271 -0
  129. package/dist/voice/index.js +1812 -0
  130. package/dist/voice/index.js.map +1 -0
  131. package/dist/voice/react.d.ts +167 -0
  132. package/dist/voice/react.js +234 -0
  133. package/dist/voice/react.js.map +1 -0
  134. package/dist/voice/sfu.d.ts +71 -0
  135. package/dist/voice/sfu.js +157 -0
  136. package/dist/voice/sfu.js.map +1 -0
  137. package/dist/voice/text.d.ts +6 -0
  138. package/dist/voice/text.js +2 -0
  139. package/dist/voice/types.d.ts +58 -0
  140. package/dist/voice/types.js +18 -0
  141. package/dist/voice/types.js.map +1 -0
  142. package/dist/voice/workers-ai.d.ts +136 -0
  143. package/dist/voice/workers-ai.js +568 -0
  144. package/dist/voice/workers-ai.js.map +1 -0
  145. package/dist/websockets/index.d.ts +192 -0
  146. package/dist/websockets/index.js +2 -0
  147. package/dist/websockets-DUfRHPRq.js +502 -0
  148. package/dist/websockets-DUfRHPRq.js.map +1 -0
  149. package/dist/workflow-types.d.ts +25 -25
  150. package/dist/workflows.d.ts +21 -21
  151. package/dist/workflows.js +1 -1
  152. package/docs/agent-class.md +2 -2
  153. package/docs/agent-tools.md +2 -1
  154. package/docs/channels.md +323 -0
  155. package/docs/chat-agents.md +6 -13
  156. package/docs/context.md +131 -0
  157. package/docs/index.md +15 -12
  158. package/docs/lifecycle.md +102 -55
  159. package/docs/long-running-agents.md +2 -2
  160. package/docs/mcp-servers.md +5 -1
  161. package/docs/resumable-streaming.md +1 -1
  162. package/docs/routing.md +105 -0
  163. package/docs/sessions.md +237 -871
  164. package/docs/streams.md +213 -0
  165. package/docs/sub-agents.md +184 -124
  166. package/docs/tasks.md +246 -0
  167. package/docs/voice.md +745 -0
  168. package/package.json +115 -13
  169. package/dist/capability-runner-CvHGZqUu.d.ts +0 -150
  170. package/dist/compaction-helpers-iiKMr2TQ.js +0 -340
  171. package/dist/compaction-helpers-iiKMr2TQ.js.map +0 -1
  172. package/dist/compaction-helpers-wUz6M3us.d.ts +0 -621
  173. package/dist/diagnostics-CaBjfz4J.js.map +0 -1
  174. package/dist/durable-object-lifecycle-D6nNQJJd.js +0 -862
  175. package/dist/durable-object-lifecycle-D6nNQJJd.js.map +0 -1
  176. package/dist/experimental/memory/session/index.d.ts +0 -671
  177. package/dist/experimental/memory/session/index.js +0 -2379
  178. package/dist/experimental/memory/session/index.js.map +0 -1
  179. package/dist/experimental/memory/utils/index.d.ts +0 -96
  180. package/dist/experimental/memory/utils/index.js +0 -79
  181. package/dist/experimental/memory/utils/index.js.map +0 -1
  182. package/dist/scheduler-CR9RHGos.js +0 -857
  183. package/dist/scheduler-CR9RHGos.js.map +0 -1
  184. package/dist/src-5W6JNKVb.js.map +0 -1
  185. package/dist/tool-output-truncation-CNnnGZQ3.js +0 -98
  186. package/dist/tool-output-truncation-CNnnGZQ3.js.map +0 -1
@@ -0,0 +1,272 @@
1
+ import { i as _classPrivateFieldInitSpec, n as _classPrivateFieldSet2, r as _assertClassBrand, t as _classPrivateFieldGet2 } from "./classPrivateFieldGet2-DZBYAB34.js";
2
+ import { t as _classPrivateMethodInitSpec } from "./classPrivateMethodInitSpec-qMjJ6sHQ.js";
3
+ import { t as TextSegmentJoiner } from "./text-segment-joiner-BtAFQSA_.js";
4
+ //#region src/voice/sentence-chunker.ts
5
+ /**
6
+ * Sentence chunker — accumulates streaming text and yields complete sentences.
7
+ *
8
+ * Isolated and testable: no dependencies on the voice pipeline, Agent, or AI APIs.
9
+ * Feed it tokens via `add()`, get back sentences via the return value.
10
+ * Call `flush()` at end-of-stream to get any remaining text.
11
+ *
12
+ * Current implementation: splits on sentence-ending punctuation (. ! ?) followed
13
+ * by a space or end-of-input. This is intentionally simple — optimize later with
14
+ * better heuristics (abbreviations, decimal numbers, quoted speech, etc.).
15
+ */
16
+ /**
17
+ * Punctuation characters that can end a sentence.
18
+ */
19
+ const SENTENCE_TERMINATORS = /* @__PURE__ */ new Set([
20
+ ".",
21
+ "!",
22
+ "?"
23
+ ]);
24
+ /**
25
+ * Minimum character count before we'll emit a sentence.
26
+ * Prevents emitting fragments like "Dr." or "U.S." as standalone sentences,
27
+ * while still allowing short responses like "Sure thing!" to stream quickly.
28
+ */
29
+ const MIN_SENTENCE_LENGTH = 10;
30
+ var _buffer = /* @__PURE__ */ new WeakMap();
31
+ var _SentenceChunker_brand = /* @__PURE__ */ new WeakSet();
32
+ var SentenceChunker = class {
33
+ constructor() {
34
+ _classPrivateMethodInitSpec(this, _SentenceChunker_brand);
35
+ _classPrivateFieldInitSpec(this, _buffer, "");
36
+ }
37
+ /**
38
+ * Add a chunk of text (e.g. a streamed LLM token).
39
+ * Returns an array of complete sentences extracted from the buffer.
40
+ * May return 0, 1, or multiple sentences depending on the input.
41
+ */
42
+ add(text) {
43
+ _classPrivateFieldSet2(_buffer, this, _classPrivateFieldGet2(_buffer, this) + text);
44
+ return _assertClassBrand(_SentenceChunker_brand, this, _extractSentences).call(this);
45
+ }
46
+ /**
47
+ * Flush any remaining text in the buffer as a final sentence.
48
+ * Call this when the LLM stream ends.
49
+ * Returns the remaining text (trimmed), or an empty array if nothing is left.
50
+ */
51
+ flush() {
52
+ const remaining = _classPrivateFieldGet2(_buffer, this).trim();
53
+ _classPrivateFieldSet2(_buffer, this, "");
54
+ if (remaining.length > 0) return [remaining];
55
+ return [];
56
+ }
57
+ /**
58
+ * Reset the chunker, discarding any buffered text.
59
+ */
60
+ reset() {
61
+ _classPrivateFieldSet2(_buffer, this, "");
62
+ }
63
+ };
64
+ /**
65
+ * Extract complete sentences from the buffer.
66
+ * A sentence boundary is a terminator (. ! ?) followed by:
67
+ * - a space and an uppercase letter (start of next sentence)
68
+ * - a space and end of current buffer (likely a boundary)
69
+ * - end of buffer after the terminator
70
+ *
71
+ * We leave ambiguous cases in the buffer until more text arrives.
72
+ */
73
+ function _extractSentences() {
74
+ const sentences = [];
75
+ while (true) {
76
+ const boundary = _assertClassBrand(_SentenceChunker_brand, this, _findSentenceBoundary).call(this);
77
+ if (boundary === -1) break;
78
+ const sentence = _classPrivateFieldGet2(_buffer, this).slice(0, boundary + 1).trim();
79
+ _classPrivateFieldSet2(_buffer, this, _classPrivateFieldGet2(_buffer, this).slice(boundary + 1).trimStart());
80
+ if (sentence.length > 0) sentences.push(sentence);
81
+ }
82
+ return sentences;
83
+ }
84
+ /**
85
+ * Find the index of the end of the first complete sentence in the buffer.
86
+ * Returns -1 if no complete sentence boundary is found.
87
+ */
88
+ function _findSentenceBoundary() {
89
+ for (let i = 0; i < _classPrivateFieldGet2(_buffer, this).length; i++) {
90
+ const char = _classPrivateFieldGet2(_buffer, this)[i];
91
+ if (!SENTENCE_TERMINATORS.has(char)) continue;
92
+ const nextChar = _classPrivateFieldGet2(_buffer, this)[i + 1];
93
+ if (nextChar === void 0) continue;
94
+ if (nextChar === " " || nextChar === "\n") {
95
+ if (_classPrivateFieldGet2(_buffer, this).slice(0, i + 1).trim().length >= MIN_SENTENCE_LENGTH) return i;
96
+ }
97
+ }
98
+ return -1;
99
+ }
100
+ //#endregion
101
+ //#region src/voice/text-stream.ts
102
+ const warnedTextStreamSources = /* @__PURE__ */ new WeakSet();
103
+ /**
104
+ * Turn any {@link TextSource} into a lazy async generator of string chunks.
105
+ *
106
+ * - `string` → yields the string once (if non-empty).
107
+ * - `ReadableStream<string>` → yields each chunk directly.
108
+ * - `ReadableStream<Uint8Array>` → decodes and parses as newline-delimited
109
+ * JSON (NDJSON) / SSE (`data: …` lines), extracting text from common AI
110
+ * response formats.
111
+ * - `AsyncIterable<string>` → re-yields each chunk.
112
+ */
113
+ async function* iterateText(source) {
114
+ for await (const event of iterateTextEvents(source)) if (event.type === "text") yield event.text;
115
+ else if (event.type === "error") throw toError(event.error);
116
+ }
117
+ async function* iterateTextEvents(source) {
118
+ if (typeof source === "string") {
119
+ if (source) yield textEvent(source);
120
+ return;
121
+ }
122
+ if (hasCustomAsyncIterator(source)) {
123
+ for await (const event of iterateAsyncTextEvents(source)) yield event;
124
+ return;
125
+ }
126
+ if (source instanceof ReadableStream) {
127
+ const reader = source.getReader();
128
+ const first = await reader.read();
129
+ if (first.done || first.value === void 0) return;
130
+ if (first.value instanceof Uint8Array) {
131
+ const peeked = first.value;
132
+ const combined = new ReadableStream({ async start(controller) {
133
+ controller.enqueue(peeked);
134
+ while (true) {
135
+ const { done, value } = await reader.read();
136
+ if (done) break;
137
+ controller.enqueue(value);
138
+ }
139
+ controller.close();
140
+ } });
141
+ for await (const chunk of parseNDJSON(combined.getReader())) {
142
+ const ai = chunk;
143
+ if (ai.response) yield textEvent(ai.response);
144
+ else if (ai.choices && ai.choices.length > 0) {
145
+ const choice = ai.choices[0];
146
+ if (choice.delta?.content && choice.delta?.role === "assistant") yield textEvent(choice.delta.content);
147
+ }
148
+ }
149
+ } else for await (const event of iterateAsyncTextEvents(readWithFirst(first.value, reader))) yield event;
150
+ return;
151
+ }
152
+ if (Symbol.asyncIterator in source) for await (const event of iterateAsyncTextEvents(source)) yield event;
153
+ }
154
+ async function* readWithFirst(first, reader) {
155
+ yield first;
156
+ while (true) {
157
+ const { done, value } = await reader.read();
158
+ if (done) break;
159
+ yield value;
160
+ }
161
+ }
162
+ function hasCustomAsyncIterator(source) {
163
+ const iterator = source[Symbol.asyncIterator];
164
+ if (typeof iterator !== "function") return false;
165
+ if (!(source instanceof ReadableStream)) return true;
166
+ return Object.prototype.hasOwnProperty.call(source, Symbol.asyncIterator) || iterator !== ReadableStream.prototype[Symbol.asyncIterator];
167
+ }
168
+ async function* iterateAsyncTextEvents(source) {
169
+ const textSegmentJoiner = new TextSegmentJoiner();
170
+ for await (const value of source) {
171
+ if (typeof value === "string") {
172
+ warnDeprecatedTextStream(source);
173
+ if (value) yield textEvent(value);
174
+ continue;
175
+ }
176
+ if (!isRecord(value)) continue;
177
+ const chunk = value;
178
+ for (const event of textSegmentJoiner.pushChunk(chunk)) yield event;
179
+ switch (chunk.type) {
180
+ case "reasoning-start":
181
+ case "reasoning-end":
182
+ yield chunk;
183
+ break;
184
+ case "reasoning-delta": break;
185
+ case "finish":
186
+ yield chunk;
187
+ break;
188
+ case "error":
189
+ yield {
190
+ type: "error",
191
+ error: toError(chunk.error)
192
+ };
193
+ return;
194
+ }
195
+ }
196
+ }
197
+ function textEvent(text) {
198
+ return {
199
+ type: "text",
200
+ text
201
+ };
202
+ }
203
+ function warnDeprecatedTextStream(source) {
204
+ if (!source || !(source instanceof ReadableStream)) return;
205
+ if (warnedTextStreamSources.has(source)) return;
206
+ warnedTextStreamSources.add(source);
207
+ console.warn("[voice] AI SDK textStream is not recommended because non-adjacent text parts may be joined incorrectly. Return result.stream from onTurn() instead.");
208
+ }
209
+ function toError(error) {
210
+ if (error instanceof Error) return error;
211
+ if (typeof error === "string") return new Error(error);
212
+ return new Error("AI SDK stream error", { cause: error });
213
+ }
214
+ function isRecord(value) {
215
+ return typeof value === "object" && value !== null;
216
+ }
217
+ /**
218
+ * Parse a `ReadableStream<Uint8Array>` that contains newline-delimited JSON
219
+ * or Server-Sent Events (`data: {…}` lines). Yields each parsed JSON object.
220
+ *
221
+ * Handles the `data: [DONE]` sentinel used by OpenAI-compatible APIs.
222
+ */
223
+ async function* parseNDJSON(reader, leftOverBuffer = "") {
224
+ const decoder = new TextDecoder();
225
+ let buffer = leftOverBuffer;
226
+ while (true) {
227
+ const { done, value } = await reader.read();
228
+ if (done) break;
229
+ buffer += decoder.decode(value, { stream: true });
230
+ const lines = buffer.split("\n");
231
+ buffer = lines.pop() ?? "";
232
+ for (const line of lines) {
233
+ const parsed = parseLine(line);
234
+ if (parsed === "DONE") return;
235
+ if (parsed) yield parsed;
236
+ }
237
+ }
238
+ if (buffer.trim()) {
239
+ const remaining = buffer.split("\n").filter((l) => l.trim());
240
+ for (const line of remaining) {
241
+ const parsed = parseLine(line);
242
+ if (parsed === "DONE") return;
243
+ if (parsed) yield parsed;
244
+ }
245
+ }
246
+ }
247
+ function parseLine(line) {
248
+ const trimmed = line.trim();
249
+ if (!trimmed) return null;
250
+ if (trimmed.startsWith("data:")) {
251
+ const json = trimmed.slice(5).trim();
252
+ if (json === "[DONE]") return "DONE";
253
+ try {
254
+ return JSON.parse(json);
255
+ } catch {
256
+ console.warn("[voice] Skipping malformed SSE data:", json);
257
+ return null;
258
+ }
259
+ }
260
+ if (trimmed === "[DONE]") return "DONE";
261
+ if (trimmed.startsWith(":") || trimmed.startsWith("event:") || trimmed.startsWith("id:") || trimmed.startsWith("retry:")) return null;
262
+ try {
263
+ return JSON.parse(trimmed);
264
+ } catch {
265
+ console.warn("[voice] Skipping malformed NDJSON line:", trimmed);
266
+ return null;
267
+ }
268
+ }
269
+ //#endregion
270
+ export { iterateTextEvents as n, SentenceChunker as r, iterateText as t };
271
+
272
+ //# sourceMappingURL=text-stream-CpdiKrJB.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"text-stream-CpdiKrJB.js","names":[],"sources":["../src/voice/sentence-chunker.ts","../src/voice/text-stream.ts"],"sourcesContent":["/**\n * Sentence chunker — accumulates streaming text and yields complete sentences.\n *\n * Isolated and testable: no dependencies on the voice pipeline, Agent, or AI APIs.\n * Feed it tokens via `add()`, get back sentences via the return value.\n * Call `flush()` at end-of-stream to get any remaining text.\n *\n * Current implementation: splits on sentence-ending punctuation (. ! ?) followed\n * by a space or end-of-input. This is intentionally simple — optimize later with\n * better heuristics (abbreviations, decimal numbers, quoted speech, etc.).\n */\n\n/**\n * Punctuation characters that can end a sentence.\n */\nconst SENTENCE_TERMINATORS = new Set([\".\", \"!\", \"?\"]);\n\n/**\n * Minimum character count before we'll emit a sentence.\n * Prevents emitting fragments like \"Dr.\" or \"U.S.\" as standalone sentences,\n * while still allowing short responses like \"Sure thing!\" to stream quickly.\n */\nconst MIN_SENTENCE_LENGTH = 10;\n\nexport class SentenceChunker {\n #buffer = \"\";\n\n /**\n * Add a chunk of text (e.g. a streamed LLM token).\n * Returns an array of complete sentences extracted from the buffer.\n * May return 0, 1, or multiple sentences depending on the input.\n */\n add(text: string): string[] {\n this.#buffer += text;\n return this.#extractSentences();\n }\n\n /**\n * Flush any remaining text in the buffer as a final sentence.\n * Call this when the LLM stream ends.\n * Returns the remaining text (trimmed), or an empty array if nothing is left.\n */\n flush(): string[] {\n const remaining = this.#buffer.trim();\n this.#buffer = \"\";\n if (remaining.length > 0) {\n return [remaining];\n }\n return [];\n }\n\n /**\n * Reset the chunker, discarding any buffered text.\n */\n reset() {\n this.#buffer = \"\";\n }\n\n /**\n * Extract complete sentences from the buffer.\n * A sentence boundary is a terminator (. ! ?) followed by:\n * - a space and an uppercase letter (start of next sentence)\n * - a space and end of current buffer (likely a boundary)\n * - end of buffer after the terminator\n *\n * We leave ambiguous cases in the buffer until more text arrives.\n */\n #extractSentences(): string[] {\n const sentences: string[] = [];\n\n while (true) {\n const boundary = this.#findSentenceBoundary();\n if (boundary === -1) break;\n\n const sentence = this.#buffer.slice(0, boundary + 1).trim();\n this.#buffer = this.#buffer.slice(boundary + 1).trimStart();\n\n if (sentence.length > 0) {\n sentences.push(sentence);\n }\n }\n\n return sentences;\n }\n\n /**\n * Find the index of the end of the first complete sentence in the buffer.\n * Returns -1 if no complete sentence boundary is found.\n */\n #findSentenceBoundary(): number {\n for (let i = 0; i < this.#buffer.length; i++) {\n const char = this.#buffer[i];\n\n if (!SENTENCE_TERMINATORS.has(char)) continue;\n\n // Check what follows the terminator\n const nextChar = this.#buffer[i + 1];\n\n // If this is the last character in the buffer, don't split yet —\n // more text might follow (e.g. \"3.14\" or \"Dr. Smith\")\n if (nextChar === undefined) continue;\n\n // Terminator followed by space — likely a real sentence boundary\n if (nextChar === \" \" || nextChar === \"\\n\") {\n // But only if the sentence is long enough to be real\n const candidate = this.#buffer.slice(0, i + 1).trim();\n if (candidate.length >= MIN_SENTENCE_LENGTH) {\n return i;\n }\n }\n }\n\n return -1;\n }\n}\n","import { TextSegmentJoiner } from \"../chat/text-segment-joiner\";\nimport type { TextSegmentChunk } from \"../chat/text-segment-joiner\";\nimport type { VoiceModelFinishReason } from \"./types\";\n\n/**\n * Utilities for normalising various text-producing sources into a uniform\n * `AsyncGenerator<string>`. This lets `onTurn()` return any of:\n *\n * - A plain `string`\n * - An `AsyncIterable<string>` (deprecated for AI SDK `textStream`)\n * - An `AsyncIterable` of AI SDK `stream` parts\n * - A `ReadableStream<Uint8Array>` (e.g. a raw `fetch` response body\n * containing newline-delimited JSON / SSE)\n * - A `ReadableStream<string>`\n *\n * The generator yields individual text chunks as they become available.\n */\n\n/** Union of every source type that {@link iterateText} accepts. */\nexport type TextSource = string | TextReadableStream | AsyncIterable<unknown>;\n\n/** @internal Semantic events retained while normalizing model streams. */\nexport type TextStreamEvent =\n | { type: \"text\"; text: string }\n | { type: \"boundary\" }\n | { type: \"reasoning-start\" }\n | { type: \"reasoning-end\" }\n | { type: \"finish\"; finishReason: VoiceModelFinishReason }\n | { type: \"error\"; error: Error };\n\ntype TextReadableStreamReader = {\n read(): Promise<ReadableStreamReadResult<unknown>>;\n};\n\ninterface TextReadableStream {\n getReader(): TextReadableStreamReader;\n}\n\n/** Shape of a parsed NDJSON/SSE chunk from common AI APIs. */\ninterface AIStreamChunk {\n response?: string;\n choices?: {\n delta?: { content?: string; role?: string };\n }[];\n}\n\ninterface StructuredStreamChunk extends TextSegmentChunk {\n type?: string;\n error?: unknown;\n finishReason?: VoiceModelFinishReason;\n}\n\nconst warnedTextStreamSources = new WeakSet<object>();\n\n/**\n * Turn any {@link TextSource} into a lazy async generator of string chunks.\n *\n * - `string` → yields the string once (if non-empty).\n * - `ReadableStream<string>` → yields each chunk directly.\n * - `ReadableStream<Uint8Array>` → decodes and parses as newline-delimited\n * JSON (NDJSON) / SSE (`data: …` lines), extracting text from common AI\n * response formats.\n * - `AsyncIterable<string>` → re-yields each chunk.\n */\nexport async function* iterateText(source: TextSource): AsyncGenerator<string> {\n for await (const event of iterateTextEvents(source)) {\n if (event.type === \"text\") {\n yield event.text;\n } else if (event.type === \"error\") {\n throw toError(event.error);\n }\n }\n}\n\nexport async function* iterateTextEvents(\n source: TextSource\n): AsyncGenerator<TextStreamEvent> {\n // --- plain string ---\n if (typeof source === \"string\") {\n if (source) yield textEvent(source);\n return;\n }\n\n // --- Custom AsyncIterable ---\n // AI SDK textStream/stream are ReadableStreams with custom async\n // iterators. Prefer those custom iterators before the generic ReadableStream\n // parser, while still letting native ReadableStream async iteration fall\n // through to the stream-specific branches below.\n if (hasCustomAsyncIterator(source)) {\n for await (const event of iterateAsyncTextEvents(\n source as AsyncIterable<unknown>\n )) {\n yield event;\n }\n return;\n }\n\n // --- ReadableStream ---\n if (source instanceof ReadableStream) {\n const reader = source.getReader();\n\n const first = await reader.read();\n if (first.done || first.value === undefined) return;\n\n if (first.value instanceof Uint8Array) {\n // ReadableStream<Uint8Array> — re-assemble into an NDJSON stream\n // by pushing the already-read first chunk back into a new stream.\n const peeked = first.value as Uint8Array;\n const combined = new ReadableStream<Uint8Array>({\n async start(controller) {\n controller.enqueue(peeked);\n while (true) {\n const { done, value } = await reader.read();\n if (done) break;\n controller.enqueue(value as Uint8Array);\n }\n controller.close();\n }\n });\n\n for await (const chunk of parseNDJSON(combined.getReader())) {\n const ai = chunk as AIStreamChunk;\n if (ai.response) {\n yield textEvent(ai.response);\n } else if (ai.choices && ai.choices.length > 0) {\n const choice = ai.choices[0];\n if (choice.delta?.content && choice.delta?.role === \"assistant\") {\n yield textEvent(choice.delta.content);\n }\n }\n }\n } else {\n for await (const event of iterateAsyncTextEvents(\n readWithFirst(first.value, reader)\n )) {\n yield event;\n }\n }\n return;\n }\n\n // --- AsyncIterable ---\n if (Symbol.asyncIterator in source) {\n for await (const event of iterateAsyncTextEvents(source)) {\n yield event;\n }\n }\n}\n\nasync function* readWithFirst(\n first: unknown,\n reader: TextReadableStreamReader\n): AsyncGenerator<unknown> {\n yield first;\n\n while (true) {\n const { done, value } = await reader.read();\n if (done) break;\n yield value;\n }\n}\n\nfunction hasCustomAsyncIterator(source: Exclude<TextSource, string>): boolean {\n const iterator = (source as Partial<AsyncIterable<unknown>>)[\n Symbol.asyncIterator\n ];\n\n if (typeof iterator !== \"function\") return false;\n\n if (!(source instanceof ReadableStream)) return true;\n\n return (\n Object.prototype.hasOwnProperty.call(source, Symbol.asyncIterator) ||\n iterator !==\n (ReadableStream.prototype as Partial<AsyncIterable<unknown>>)[\n Symbol.asyncIterator\n ]\n );\n}\n\nasync function* iterateAsyncTextEvents(\n source: AsyncIterable<unknown>\n): AsyncGenerator<TextStreamEvent> {\n const textSegmentJoiner = new TextSegmentJoiner();\n\n for await (const value of source) {\n if (typeof value === \"string\") {\n warnDeprecatedTextStream(source);\n if (value) yield textEvent(value);\n continue;\n }\n\n if (!isRecord(value)) continue;\n const chunk = value as StructuredStreamChunk;\n\n for (const event of textSegmentJoiner.pushChunk(chunk)) {\n yield event;\n }\n\n switch (chunk.type) {\n case \"reasoning-start\":\n case \"reasoning-end\":\n yield chunk as TextStreamEvent;\n break;\n case \"reasoning-delta\":\n break;\n case \"finish\":\n yield chunk as TextStreamEvent;\n break;\n case \"error\":\n yield { type: \"error\", error: toError(chunk.error) };\n return;\n }\n }\n}\n\nfunction textEvent(text: string): TextStreamEvent {\n return { type: \"text\", text };\n}\n\nfunction warnDeprecatedTextStream(source?: object): void {\n if (!source || !(source instanceof ReadableStream)) return;\n if (warnedTextStreamSources.has(source)) return;\n warnedTextStreamSources.add(source);\n\n console.warn(\n \"[voice] AI SDK textStream is not recommended because non-adjacent text parts may be joined incorrectly. Return result.stream from onTurn() instead.\"\n );\n}\n\nfunction toError(error: unknown): Error {\n if (error instanceof Error) return error;\n if (typeof error === \"string\") return new Error(error);\n return new Error(\"AI SDK stream error\", { cause: error });\n}\n\nfunction isRecord(value: unknown): value is Record<string, unknown> {\n return typeof value === \"object\" && value !== null;\n}\n\n// ---------------------------------------------------------------------------\n// Internal: NDJSON / SSE stream parser\n// ---------------------------------------------------------------------------\n\n/**\n * Parse a `ReadableStream<Uint8Array>` that contains newline-delimited JSON\n * or Server-Sent Events (`data: {…}` lines). Yields each parsed JSON object.\n *\n * Handles the `data: [DONE]` sentinel used by OpenAI-compatible APIs.\n */\nasync function* parseNDJSON(\n reader: ReadableStreamDefaultReader<Uint8Array>,\n leftOverBuffer = \"\"\n): AsyncGenerator<Record<string, unknown>> {\n const decoder = new TextDecoder();\n let buffer = leftOverBuffer;\n\n while (true) {\n const { done, value } = await reader.read();\n if (done) break;\n\n buffer += decoder.decode(value, { stream: true });\n\n const lines = buffer.split(\"\\n\");\n buffer = lines.pop() ?? \"\";\n\n for (const line of lines) {\n const parsed = parseLine(line);\n if (parsed === \"DONE\") return;\n if (parsed) yield parsed;\n }\n }\n\n // Flush remaining buffer\n if (buffer.trim()) {\n const remaining = buffer.split(\"\\n\").filter((l) => l.trim());\n for (const line of remaining) {\n const parsed = parseLine(line);\n if (parsed === \"DONE\") return;\n if (parsed) yield parsed;\n }\n }\n}\n\nfunction parseLine(line: string): Record<string, unknown> | \"DONE\" | null {\n const trimmed = line.trim();\n if (!trimmed) return null;\n\n if (trimmed.startsWith(\"data:\")) {\n const json = trimmed.slice(5).trim();\n if (json === \"[DONE]\") return \"DONE\";\n try {\n return JSON.parse(json) as Record<string, unknown>;\n } catch {\n console.warn(\"[voice] Skipping malformed SSE data:\", json);\n return null;\n }\n }\n\n if (trimmed === \"[DONE]\") return \"DONE\";\n\n // Ignore SSE metadata/comment lines. Only `data:` carries payload.\n if (\n trimmed.startsWith(\":\") ||\n trimmed.startsWith(\"event:\") ||\n trimmed.startsWith(\"id:\") ||\n trimmed.startsWith(\"retry:\")\n ) {\n return null;\n }\n\n try {\n return JSON.parse(trimmed) as Record<string, unknown>;\n } catch {\n console.warn(\"[voice] Skipping malformed NDJSON line:\", trimmed);\n return null;\n }\n}\n"],"mappings":";;;;;;;;;;;;;;;;;;AAeA,MAAM,uCAAuB,IAAI,IAAI;CAAC;CAAK;CAAK;AAAG,CAAC;;;;;;AAOpD,MAAM,sBAAsB;;;AAE5B,IAAa,kBAAb,MAA6B;;;4CACjB,EAAA;;;;;;;CAOV,IAAI,MAAwB;EAC1B,uBAAA,SAAA,MAAA,uBAAA,SAAA,IAAA,IAAgB,IAAA;EAChB,OAAA,kBAAA,wBAAO,MAAA,iBAAA,CAAA,CAAA,KAAA,IAAuB;CAChC;;;;;;CAOA,QAAkB;EAChB,MAAM,YAAA,uBAAA,SAAY,IAAA,CAAA,CAAa,KAAK;EACpC,uBAAA,SAAA,MAAe,EAAA;EACf,IAAI,UAAU,SAAS,GACrB,OAAO,CAAC,SAAS;EAEnB,OAAO,CAAC;CACV;;;;CAKA,QAAQ;EACN,uBAAA,SAAA,MAAe,EAAA;CACjB;AA0DF;;;;;;;;;;AA/CE,SAAA,oBAA8B;CAC5B,MAAM,YAAsB,CAAC;CAE7B,OAAO,MAAM;EACX,MAAM,WAAA,kBAAA,wBAAW,MAAA,qBAAA,CAAA,CAAA,KAAA,IAA2B;EAC5C,IAAI,aAAa,IAAI;EAErB,MAAM,WAAA,uBAAA,SAAW,IAAA,CAAA,CAAa,MAAM,GAAG,WAAW,CAAC,CAAC,CAAC,KAAK;EAC1D,uBAAA,SAAA,MAAA,uBAAA,SAAe,IAAA,CAAA,CAAa,MAAM,WAAW,CAAC,CAAC,CAAC,UAAU,CAAA;EAE1D,IAAI,SAAS,SAAS,GACpB,UAAU,KAAK,QAAQ;CAE3B;CAEA,OAAO;AACT;;;;;AAMA,SAAA,wBAAgC;CAC9B,KAAK,IAAI,IAAI,GAAG,IAAA,uBAAA,SAAI,IAAA,CAAA,CAAa,QAAQ,KAAK;EAC5C,MAAM,OAAA,uBAAA,SAAO,IAAA,CAAA,CAAa;EAE1B,IAAI,CAAC,qBAAqB,IAAI,IAAI,GAAG;EAGrC,MAAM,WAAA,uBAAA,SAAW,IAAA,CAAA,CAAa,IAAI;EAIlC,IAAI,aAAa,KAAA,GAAW;EAG5B,IAAI,aAAa,OAAO,aAAa;uCAEjB,IAAA,CAAA,CAAa,MAAM,GAAG,IAAI,CAAC,CAAC,CAAC,KACnC,CAAC,CAAC,UAAU,qBACtB,OAAO;EAAA;CAGb;CAEA,OAAO;AACT;;;AC7DF,MAAM,0CAA0B,IAAI,QAAgB;;;;;;;;;;;AAYpD,gBAAuB,YAAY,QAA4C;CAC7E,WAAW,MAAM,SAAS,kBAAkB,MAAM,GAChD,IAAI,MAAM,SAAS,QACjB,MAAM,MAAM;MACP,IAAI,MAAM,SAAS,SACxB,MAAM,QAAQ,MAAM,KAAK;AAG/B;AAEA,gBAAuB,kBACrB,QACiC;CAEjC,IAAI,OAAO,WAAW,UAAU;EAC9B,IAAI,QAAQ,MAAM,UAAU,MAAM;EAClC;CACF;CAOA,IAAI,uBAAuB,MAAM,GAAG;EAClC,WAAW,MAAM,SAAS,uBACxB,MACF,GACE,MAAM;EAER;CACF;CAGA,IAAI,kBAAkB,gBAAgB;EACpC,MAAM,SAAS,OAAO,UAAU;EAEhC,MAAM,QAAQ,MAAM,OAAO,KAAK;EAChC,IAAI,MAAM,QAAQ,MAAM,UAAU,KAAA,GAAW;EAE7C,IAAI,MAAM,iBAAiB,YAAY;GAGrC,MAAM,SAAS,MAAM;GACrB,MAAM,WAAW,IAAI,eAA2B,EAC9C,MAAM,MAAM,YAAY;IACtB,WAAW,QAAQ,MAAM;IACzB,OAAO,MAAM;KACX,MAAM,EAAE,MAAM,UAAU,MAAM,OAAO,KAAK;KAC1C,IAAI,MAAM;KACV,WAAW,QAAQ,KAAmB;IACxC;IACA,WAAW,MAAM;GACnB,EACF,CAAC;GAED,WAAW,MAAM,SAAS,YAAY,SAAS,UAAU,CAAC,GAAG;IAC3D,MAAM,KAAK;IACX,IAAI,GAAG,UACL,MAAM,UAAU,GAAG,QAAQ;SACtB,IAAI,GAAG,WAAW,GAAG,QAAQ,SAAS,GAAG;KAC9C,MAAM,SAAS,GAAG,QAAQ;KAC1B,IAAI,OAAO,OAAO,WAAW,OAAO,OAAO,SAAS,aAClD,MAAM,UAAU,OAAO,MAAM,OAAO;IAExC;GACF;EACF,OACE,WAAW,MAAM,SAAS,uBACxB,cAAc,MAAM,OAAO,MAAM,CACnC,GACE,MAAM;EAGV;CACF;CAGA,IAAI,OAAO,iBAAiB,QAC1B,WAAW,MAAM,SAAS,uBAAuB,MAAM,GACrD,MAAM;AAGZ;AAEA,gBAAgB,cACd,OACA,QACyB;CACzB,MAAM;CAEN,OAAO,MAAM;EACX,MAAM,EAAE,MAAM,UAAU,MAAM,OAAO,KAAK;EAC1C,IAAI,MAAM;EACV,MAAM;CACR;AACF;AAEA,SAAS,uBAAuB,QAA8C;CAC5E,MAAM,WAAY,OAChB,OAAO;CAGT,IAAI,OAAO,aAAa,YAAY,OAAO;CAE3C,IAAI,EAAE,kBAAkB,iBAAiB,OAAO;CAEhD,OACE,OAAO,UAAU,eAAe,KAAK,QAAQ,OAAO,aAAa,KACjE,aACG,eAAe,UACd,OAAO;AAGf;AAEA,gBAAgB,uBACd,QACiC;CACjC,MAAM,oBAAoB,IAAI,kBAAkB;CAEhD,WAAW,MAAM,SAAS,QAAQ;EAChC,IAAI,OAAO,UAAU,UAAU;GAC7B,yBAAyB,MAAM;GAC/B,IAAI,OAAO,MAAM,UAAU,KAAK;GAChC;EACF;EAEA,IAAI,CAAC,SAAS,KAAK,GAAG;EACtB,MAAM,QAAQ;EAEd,KAAK,MAAM,SAAS,kBAAkB,UAAU,KAAK,GACnD,MAAM;EAGR,QAAQ,MAAM,MAAd;GACE,KAAK;GACL,KAAK;IACH,MAAM;IACN;GACF,KAAK,mBACH;GACF,KAAK;IACH,MAAM;IACN;GACF,KAAK;IACH,MAAM;KAAE,MAAM;KAAS,OAAO,QAAQ,MAAM,KAAK;IAAE;IACnD;EACJ;CACF;AACF;AAEA,SAAS,UAAU,MAA+B;CAChD,OAAO;EAAE,MAAM;EAAQ;CAAK;AAC9B;AAEA,SAAS,yBAAyB,QAAuB;CACvD,IAAI,CAAC,UAAU,EAAE,kBAAkB,iBAAiB;CACpD,IAAI,wBAAwB,IAAI,MAAM,GAAG;CACzC,wBAAwB,IAAI,MAAM;CAElC,QAAQ,KACN,qJACF;AACF;AAEA,SAAS,QAAQ,OAAuB;CACtC,IAAI,iBAAiB,OAAO,OAAO;CACnC,IAAI,OAAO,UAAU,UAAU,OAAO,IAAI,MAAM,KAAK;CACrD,OAAO,IAAI,MAAM,uBAAuB,EAAE,OAAO,MAAM,CAAC;AAC1D;AAEA,SAAS,SAAS,OAAkD;CAClE,OAAO,OAAO,UAAU,YAAY,UAAU;AAChD;;;;;;;AAYA,gBAAgB,YACd,QACA,iBAAiB,IACwB;CACzC,MAAM,UAAU,IAAI,YAAY;CAChC,IAAI,SAAS;CAEb,OAAO,MAAM;EACX,MAAM,EAAE,MAAM,UAAU,MAAM,OAAO,KAAK;EAC1C,IAAI,MAAM;EAEV,UAAU,QAAQ,OAAO,OAAO,EAAE,QAAQ,KAAK,CAAC;EAEhD,MAAM,QAAQ,OAAO,MAAM,IAAI;EAC/B,SAAS,MAAM,IAAI,KAAK;EAExB,KAAK,MAAM,QAAQ,OAAO;GACxB,MAAM,SAAS,UAAU,IAAI;GAC7B,IAAI,WAAW,QAAQ;GACvB,IAAI,QAAQ,MAAM;EACpB;CACF;CAGA,IAAI,OAAO,KAAK,GAAG;EACjB,MAAM,YAAY,OAAO,MAAM,IAAI,CAAC,CAAC,QAAQ,MAAM,EAAE,KAAK,CAAC;EAC3D,KAAK,MAAM,QAAQ,WAAW;GAC5B,MAAM,SAAS,UAAU,IAAI;GAC7B,IAAI,WAAW,QAAQ;GACvB,IAAI,QAAQ,MAAM;EACpB;CACF;AACF;AAEA,SAAS,UAAU,MAAuD;CACxE,MAAM,UAAU,KAAK,KAAK;CAC1B,IAAI,CAAC,SAAS,OAAO;CAErB,IAAI,QAAQ,WAAW,OAAO,GAAG;EAC/B,MAAM,OAAO,QAAQ,MAAM,CAAC,CAAC,CAAC,KAAK;EACnC,IAAI,SAAS,UAAU,OAAO;EAC9B,IAAI;GACF,OAAO,KAAK,MAAM,IAAI;EACxB,QAAQ;GACN,QAAQ,KAAK,wCAAwC,IAAI;GACzD,OAAO;EACT;CACF;CAEA,IAAI,YAAY,UAAU,OAAO;CAGjC,IACE,QAAQ,WAAW,GAAG,KACtB,QAAQ,WAAW,QAAQ,KAC3B,QAAQ,WAAW,KAAK,KACxB,QAAQ,WAAW,QAAQ,GAE3B,OAAO;CAGT,IAAI;EACF,OAAO,KAAK,MAAM,OAAO;CAC3B,QAAQ;EACN,QAAQ,KAAK,2CAA2C,OAAO;EAC/D,OAAO;CACT;AACF"}
@@ -0,0 +1,52 @@
1
+ /** Approximate token multiplier per whitespace-separated word */
2
+ const WORDS_TOKEN_MULTIPLIER = 1.3;
3
+ /** Flat token charge for an image attachment (vision-model ballpark). */
4
+ const IMAGE_ATTACHMENT_TOKENS = 1600;
5
+ /** Ceiling for a non-image attachment's bytes/4 token charge. */
6
+ const MAX_ATTACHMENT_TOKENS = 2e4;
7
+ function estimateStringTokens(text) {
8
+ if (!text) return 0;
9
+ const charEstimate = text.length / 4;
10
+ const wordEstimate = text.split(/\s+/).filter(Boolean).length * WORDS_TOKEN_MULTIPLIER;
11
+ return Math.ceil(Math.max(charEstimate, wordEstimate));
12
+ }
13
+ function estimateUnknownTokens(value) {
14
+ if (value === null || value === void 0) return 0;
15
+ if (typeof value === "string") return estimateStringTokens(value);
16
+ try {
17
+ return estimateStringTokens(JSON.stringify(value));
18
+ } catch {
19
+ return estimateStringTokens(String(value));
20
+ }
21
+ }
22
+ /** Estimate tokens for messages from their text, reasoning, and tool parts. */
23
+ function estimateMessageTokens(messages) {
24
+ let tokens = 0;
25
+ for (const msg of messages) {
26
+ tokens += 4;
27
+ for (const part of msg.parts) if (part.type === "text" || part.type === "reasoning") tokens += estimateUnknownTokens(part.text ?? part.reasoning);
28
+ else if (part.type.startsWith("tool-") || part.type === "dynamic-tool") {
29
+ tokens += estimateUnknownTokens(part.input);
30
+ tokens += estimateUnknownTokens(part.output ?? part.result);
31
+ } else if (part.text !== void 0) tokens += estimateUnknownTokens(part.text);
32
+ else if (part.result !== void 0) tokens += estimateUnknownTokens(part.result);
33
+ }
34
+ return tokens;
35
+ }
36
+ /** Heuristic token weight of one attachment so media never counts as zero. */
37
+ function estimateAttachmentTokens(mediaType, bytes) {
38
+ if (mediaType.startsWith("image/")) return IMAGE_ATTACHMENT_TOKENS;
39
+ return Math.min(Math.ceil(bytes / 4), MAX_ATTACHMENT_TOKENS);
40
+ }
41
+ /** Approximate decoded size of a `data:` URL without decoding it. */
42
+ function estimatedDataUrlBytes(url) {
43
+ const comma = url.indexOf(",");
44
+ if (!url.startsWith("data:") || comma < 0) return 0;
45
+ const header = url.slice(5, comma);
46
+ const payload = url.slice(comma + 1);
47
+ return header.endsWith(";base64") ? Math.floor(payload.length * 3 / 4) : payload.length;
48
+ }
49
+ //#endregion
50
+ export { estimatedDataUrlBytes as i, estimateMessageTokens as n, estimateStringTokens as r, estimateAttachmentTokens as t };
51
+
52
+ //# sourceMappingURL=tokens-nHAKcN6M.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"tokens-nHAKcN6M.js","names":[],"sources":["../src/sessions/tokens.ts"],"sourcesContent":["/**\n * Token estimation heuristics.\n *\n * Real tokenizers cost ~100 MB of heap, so Sessions estimates: the larger of\n * ~4 characters per token and ~1.3 tokens per word, plus a small per-message\n * framing charge. Attachments get a flat image charge or a bytes/4 charge\n * capped so a large document cannot dominate a compaction trigger. These\n * numbers gate cheap triggers only; model-reported usage stays authoritative.\n */\n\nimport type { SessionMessage } from \"./types\";\n\n/** Approximate characters per token for English text */\nexport const CHARS_PER_TOKEN = 4;\n\n/** Approximate token multiplier per whitespace-separated word */\nexport const WORDS_TOKEN_MULTIPLIER = 1.3;\n\n/** Approximate overhead tokens per message (role, framing) */\nexport const TOKENS_PER_MESSAGE = 4;\n\n/** Flat token charge for an image attachment (vision-model ballpark). */\nexport const IMAGE_ATTACHMENT_TOKENS = 1_600;\n\n/** Ceiling for a non-image attachment's bytes/4 token charge. */\nexport const MAX_ATTACHMENT_TOKENS = 20_000;\n\nexport function estimateStringTokens(text: string): number {\n if (!text) return 0;\n const charEstimate = text.length / CHARS_PER_TOKEN;\n const wordEstimate =\n text.split(/\\s+/).filter(Boolean).length * WORDS_TOKEN_MULTIPLIER;\n return Math.ceil(Math.max(charEstimate, wordEstimate));\n}\n\nfunction estimateUnknownTokens(value: unknown): number {\n if (value === null || value === undefined) return 0;\n if (typeof value === \"string\") return estimateStringTokens(value);\n try {\n return estimateStringTokens(JSON.stringify(value));\n } catch {\n return estimateStringTokens(String(value));\n }\n}\n\n/** Estimate tokens for messages from their text, reasoning, and tool parts. */\nexport function estimateMessageTokens(messages: SessionMessage[]): number {\n let tokens = 0;\n for (const msg of messages) {\n tokens += TOKENS_PER_MESSAGE;\n for (const part of msg.parts) {\n if (part.type === \"text\" || part.type === \"reasoning\") {\n tokens += estimateUnknownTokens(part.text ?? part.reasoning);\n } else if (\n part.type.startsWith(\"tool-\") ||\n part.type === \"dynamic-tool\"\n ) {\n tokens += estimateUnknownTokens(part.input);\n tokens += estimateUnknownTokens(part.output ?? part.result);\n } else if (part.text !== undefined) {\n tokens += estimateUnknownTokens(part.text);\n } else if (part.result !== undefined) {\n tokens += estimateUnknownTokens(part.result);\n }\n }\n }\n return tokens;\n}\n\n/** Heuristic token weight of one attachment so media never counts as zero. */\nexport function estimateAttachmentTokens(\n mediaType: string,\n bytes: number\n): number {\n if (mediaType.startsWith(\"image/\")) return IMAGE_ATTACHMENT_TOKENS;\n return Math.min(Math.ceil(bytes / 4), MAX_ATTACHMENT_TOKENS);\n}\n\n/** Approximate decoded size of a `data:` URL without decoding it. */\nexport function estimatedDataUrlBytes(url: string): number {\n const comma = url.indexOf(\",\");\n if (!url.startsWith(\"data:\") || comma < 0) return 0;\n const header = url.slice(\"data:\".length, comma);\n const payload = url.slice(comma + 1);\n return header.endsWith(\";base64\")\n ? Math.floor((payload.length * 3) / 4)\n : payload.length;\n}\n"],"mappings":";AAgBA,MAAa,yBAAyB;;AAMtC,MAAa,0BAA0B;;AAGvC,MAAa,wBAAwB;AAErC,SAAgB,qBAAqB,MAAsB;CACzD,IAAI,CAAC,MAAM,OAAO;CAClB,MAAM,eAAe,KAAK,SAAA;CAC1B,MAAM,eACJ,KAAK,MAAM,KAAK,CAAC,CAAC,OAAO,OAAO,CAAC,CAAC,SAAS;CAC7C,OAAO,KAAK,KAAK,KAAK,IAAI,cAAc,YAAY,CAAC;AACvD;AAEA,SAAS,sBAAsB,OAAwB;CACrD,IAAI,UAAU,QAAQ,UAAU,KAAA,GAAW,OAAO;CAClD,IAAI,OAAO,UAAU,UAAU,OAAO,qBAAqB,KAAK;CAChE,IAAI;EACF,OAAO,qBAAqB,KAAK,UAAU,KAAK,CAAC;CACnD,QAAQ;EACN,OAAO,qBAAqB,OAAO,KAAK,CAAC;CAC3C;AACF;;AAGA,SAAgB,sBAAsB,UAAoC;CACxE,IAAI,SAAS;CACb,KAAK,MAAM,OAAO,UAAU;EAC1B,UAAA;EACA,KAAK,MAAM,QAAQ,IAAI,OACrB,IAAI,KAAK,SAAS,UAAU,KAAK,SAAS,aACxC,UAAU,sBAAsB,KAAK,QAAQ,KAAK,SAAS;OACtD,IACL,KAAK,KAAK,WAAW,OAAO,KAC5B,KAAK,SAAS,gBACd;GACA,UAAU,sBAAsB,KAAK,KAAK;GAC1C,UAAU,sBAAsB,KAAK,UAAU,KAAK,MAAM;EAC5D,OAAO,IAAI,KAAK,SAAS,KAAA,GACvB,UAAU,sBAAsB,KAAK,IAAI;OACpC,IAAI,KAAK,WAAW,KAAA,GACzB,UAAU,sBAAsB,KAAK,MAAM;CAGjD;CACA,OAAO;AACT;;AAGA,SAAgB,yBACd,WACA,OACQ;CACR,IAAI,UAAU,WAAW,QAAQ,GAAG,OAAO;CAC3C,OAAO,KAAK,IAAI,KAAK,KAAK,QAAQ,CAAC,GAAG,qBAAqB;AAC7D;;AAGA,SAAgB,sBAAsB,KAAqB;CACzD,MAAM,QAAQ,IAAI,QAAQ,GAAG;CAC7B,IAAI,CAAC,IAAI,WAAW,OAAO,KAAK,QAAQ,GAAG,OAAO;CAClD,MAAM,SAAS,IAAI,MAAM,GAAgB,KAAK;CAC9C,MAAM,UAAU,IAAI,MAAM,QAAQ,CAAC;CACnC,OAAO,OAAO,SAAS,SAAS,IAC5B,KAAK,MAAO,QAAQ,SAAS,IAAK,CAAC,IACnC,QAAQ;AACd"}
@@ -0,0 +1,31 @@
1
+ //#region src/channels/tool-schema.ts
2
+ const channelMessageJsonSchema = {
3
+ type: "object",
4
+ properties: {
5
+ title: {
6
+ type: "string",
7
+ description: "Optional title or topic for the message"
8
+ },
9
+ markdown: {
10
+ type: "string",
11
+ minLength: 1,
12
+ description: "Message content formatted as Markdown"
13
+ }
14
+ },
15
+ required: ["markdown"],
16
+ additionalProperties: false
17
+ };
18
+ function parseChannelMessage(value) {
19
+ if (typeof value === "object" && value !== null) {
20
+ const candidate = value;
21
+ if (typeof candidate.markdown === "string" && candidate.markdown.length > 0 && (candidate.title === void 0 || typeof candidate.title === "string")) return {
22
+ ...typeof candidate.title === "string" ? { title: candidate.title } : {},
23
+ markdown: candidate.markdown
24
+ };
25
+ }
26
+ throw new Error("Expected an optional string title and a non-empty Markdown string");
27
+ }
28
+ //#endregion
29
+ export { parseChannelMessage as n, channelMessageJsonSchema as t };
30
+
31
+ //# sourceMappingURL=tool-schema-CBjGPrsQ.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"tool-schema-CBjGPrsQ.js","names":[],"sources":["../src/channels/tool-schema.ts"],"sourcesContent":["import type { ChannelMessage } from \"./channel\";\n\nexport const channelMessageJsonSchema: {\n type: \"object\";\n properties: {\n title: { type: \"string\"; description: string };\n markdown: { type: \"string\"; minLength: number; description: string };\n };\n required: string[];\n additionalProperties: false;\n} = {\n type: \"object\",\n properties: {\n title: {\n type: \"string\",\n description: \"Optional title or topic for the message\"\n },\n markdown: {\n type: \"string\",\n minLength: 1,\n description: \"Message content formatted as Markdown\"\n }\n },\n required: [\"markdown\"],\n additionalProperties: false\n};\n\nexport function parseChannelMessage(value: unknown): ChannelMessage {\n if (typeof value === \"object\" && value !== null) {\n const candidate = value as Record<string, unknown>;\n if (\n typeof candidate.markdown === \"string\" &&\n candidate.markdown.length > 0 &&\n (candidate.title === undefined || typeof candidate.title === \"string\")\n ) {\n return {\n ...(typeof candidate.title === \"string\"\n ? { title: candidate.title }\n : {}),\n markdown: candidate.markdown\n };\n }\n }\n\n throw new Error(\n \"Expected an optional string title and a non-empty Markdown string\"\n );\n}\n"],"mappings":";AAEA,MAAa,2BAQT;CACF,MAAM;CACN,YAAY;EACV,OAAO;GACL,MAAM;GACN,aAAa;EACf;EACA,UAAU;GACR,MAAM;GACN,WAAW;GACX,aAAa;EACf;CACF;CACA,UAAU,CAAC,UAAU;CACrB,sBAAsB;AACxB;AAEA,SAAgB,oBAAoB,OAAgC;CAClE,IAAI,OAAO,UAAU,YAAY,UAAU,MAAM;EAC/C,MAAM,YAAY;EAClB,IACE,OAAO,UAAU,aAAa,YAC9B,UAAU,SAAS,SAAS,MAC3B,UAAU,UAAU,KAAA,KAAa,OAAO,UAAU,UAAU,WAE7D,OAAO;GACL,GAAI,OAAO,UAAU,UAAU,WAC3B,EAAE,OAAO,UAAU,MAAM,IACzB,CAAC;GACL,UAAU,UAAU;EACtB;CAEJ;CAEA,MAAM,IAAI,MACR,mEACF;AACF"}
@@ -0,0 +1,202 @@
1
+ //#region src/sessions/types.d.ts
2
+ /**
3
+ * Types for the Sessions capability.
4
+ *
5
+ * @experimental The whole `agents/sessions` surface may change before
6
+ * stabilizing.
7
+ */
8
+ /**
9
+ * Minimal message part shape used by Sessions internals.
10
+ * Vercel AI SDK's `UIMessagePart` is structurally compatible.
11
+ */
12
+ interface SessionMessagePart {
13
+ type: string;
14
+ text?: string;
15
+ reasoning?: string;
16
+ toolCallId?: string;
17
+ toolName?: string;
18
+ input?: unknown;
19
+ output?: unknown;
20
+ state?: string;
21
+ result?: unknown;
22
+ /** File parts (AI SDK `FileUIPart`): IANA media type of the payload. */
23
+ mediaType?: string;
24
+ /** File parts: a `data:` URL or a remote URL. */
25
+ url?: string;
26
+ /** File parts: original filename, when one was supplied. */
27
+ filename?: string;
28
+ }
29
+ /**
30
+ * Minimal message shape used by Sessions internals.
31
+ * Vercel AI SDK's `UIMessage` is structurally compatible — you can pass
32
+ * `UIMessage` objects directly without conversion.
33
+ */
34
+ interface SessionMessage {
35
+ id: string;
36
+ role: string;
37
+ parts: SessionMessagePart[];
38
+ metadata?: unknown;
39
+ createdAt?: Date;
40
+ }
41
+ interface SearchResult {
42
+ id: string;
43
+ role: string;
44
+ content: string;
45
+ createdAt?: string;
46
+ sessionId?: string;
47
+ }
48
+ interface StoredCompaction {
49
+ id: string;
50
+ summary: string;
51
+ fromMessageId: string;
52
+ toMessageId: string;
53
+ createdAt: string;
54
+ }
55
+ /** Per-row info for the active branch path, root → leaf order. */
56
+ interface SessionRowStat {
57
+ id: string;
58
+ /** Stored message role (e.g. "user" / "assistant"). */
59
+ role: string;
60
+ /**
61
+ * Serialized size in bytes: the message row, every continuation row it
62
+ * was split across, and the payloads it points at as they read back.
63
+ */
64
+ bytes: number;
65
+ /** Token estimate stamped when the row was written. */
66
+ tokenEstimate: number;
67
+ }
68
+ /** Result of a byte-budgeted history read. */
69
+ interface RecentHistoryResult {
70
+ /**
71
+ * The most recent messages on the active branch path whose stored bytes
72
+ * fit `maxContentBytes`, root → leaf order, with compaction overlays
73
+ * applied within the window. The window always covers at least the leaf
74
+ * row; rows whose stored content fails to parse are skipped.
75
+ */
76
+ messages: SessionMessage[];
77
+ /** True when older messages were left out to satisfy the byte budget. */
78
+ truncated: boolean;
79
+ /** Summed stored size of the FULL path, in bytes. */
80
+ totalContentBytes: number;
81
+ }
82
+ /** Result of an append/upsert write. */
83
+ interface AppendResult {
84
+ /** False when the id already existed and the write was an idempotent no-op. */
85
+ inserted: boolean;
86
+ /** The STORED form of the message (sanitized). */
87
+ message: SessionMessage;
88
+ }
89
+ /** Change-feed events dispatched after each durable write. */
90
+ type SessionChangeEvent =
91
+ | {
92
+ type: "append";
93
+ sessionId: string;
94
+ message: SessionMessage;
95
+ parentId?: string | null;
96
+ inserted: boolean;
97
+ }
98
+ | {
99
+ type: "update";
100
+ sessionId: string;
101
+ message: SessionMessage;
102
+ }
103
+ | {
104
+ type: "delete";
105
+ sessionId: string;
106
+ messageIds: string[];
107
+ }
108
+ | {
109
+ type: "clear";
110
+ sessionId: string;
111
+ }
112
+ | {
113
+ type: "compact";
114
+ sessionId: string;
115
+ }
116
+ /**
117
+ * A row written verbatim by `importMessage()`. A host cache should treat
118
+ * the path as changed underneath it rather than patch itself: imports come
119
+ * in bulk with explicit parents, so re-deriving once later is the cheap
120
+ * response.
121
+ */
122
+ | {
123
+ type: "import";
124
+ sessionId: string;
125
+ message: SessionMessage;
126
+ parentId: string | null;
127
+ } /** An overlay stored directly through `addCompaction()`. */
128
+ | {
129
+ type: "compaction";
130
+ sessionId: string;
131
+ compaction: StoredCompaction;
132
+ };
133
+ type SessionChangeListener = (
134
+ event: SessionChangeEvent
135
+ ) => void | Promise<void>;
136
+ /** Options accepted by history reads. */
137
+ interface HistoryReadOptions {
138
+ /** Read the path ending at this leaf instead of the active leaf. */
139
+ leafId?: string | null;
140
+ /** Abort a streamed read between chunks. */
141
+ signal?: AbortSignal;
142
+ /**
143
+ * Yield the path leaf → root instead of root → leaf. Content is still
144
+ * fetched in bounded windows, newest window first, so a consumer that
145
+ * stops after the message it was looking for reads only the newest rows.
146
+ */
147
+ newestFirst?: boolean;
148
+ }
149
+ /** Options accepted by batched history reads. */
150
+ interface HistoryBatchReadOptions extends HistoryReadOptions {
151
+ /** Maximum messages per yielded batch. Default 50. */
152
+ batchSize?: number;
153
+ /**
154
+ * Maximum serialized bytes per yielded batch. Default 4 MiB. A single
155
+ * message larger than the limit is yielded alone.
156
+ */
157
+ maxBatchBytes?: number;
158
+ }
159
+ /** Policy for a Sessions capability. */
160
+ interface SessionsOptions {
161
+ /**
162
+ * Message-metadata keys reserved for server-side writers. Stripped from
163
+ * writes marked `source: "client"`. Default: none.
164
+ */
165
+ readonly reservedMetadataKeys?: readonly string[];
166
+ }
167
+ /** Trust policy shared by append and update writes. */
168
+ interface WriteOptions {
169
+ /**
170
+ * `"client"` marks untrusted intake: reserved metadata keys are stripped.
171
+ * Default `"server"`.
172
+ */
173
+ source?: "client" | "server";
174
+ }
175
+ /** Options accepted by append writes. */
176
+ interface AppendOptions extends WriteOptions {
177
+ /**
178
+ * - `undefined` / omitted → auto-detect: attach to the current latest leaf.
179
+ * - `null` → create a root message with no parent.
180
+ * - string → attach to the given parent id (falls back to root when the
181
+ * parent does not belong to this session).
182
+ */
183
+ parentId?: string | null;
184
+ }
185
+ //#endregion
186
+ export {
187
+ RecentHistoryResult as a,
188
+ SessionChangeListener as c,
189
+ SessionRowStat as d,
190
+ SessionsOptions as f,
191
+ HistoryReadOptions as i,
192
+ SessionMessage as l,
193
+ WriteOptions as m,
194
+ AppendResult as n,
195
+ SearchResult as o,
196
+ StoredCompaction as p,
197
+ HistoryBatchReadOptions as r,
198
+ SessionChangeEvent as s,
199
+ AppendOptions as t,
200
+ SessionMessagePart as u
201
+ };
202
+ //# sourceMappingURL=types-B7LojTe4.d.ts.map