@witqq/agent-sdk 0.6.0 → 0.7.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 (122) hide show
  1. package/README.md +433 -6
  2. package/dist/auth/index.cjs +188 -1
  3. package/dist/auth/index.cjs.map +1 -1
  4. package/dist/auth/index.d.cts +154 -138
  5. package/dist/auth/index.d.ts +154 -138
  6. package/dist/auth/index.js +188 -2
  7. package/dist/auth/index.js.map +1 -1
  8. package/dist/backends/claude.cjs +341 -22
  9. package/dist/backends/claude.cjs.map +1 -1
  10. package/dist/backends/claude.d.cts +2 -1
  11. package/dist/backends/claude.d.ts +2 -1
  12. package/dist/backends/claude.js +341 -22
  13. package/dist/backends/claude.js.map +1 -1
  14. package/dist/backends/copilot.cjs +133 -25
  15. package/dist/backends/copilot.cjs.map +1 -1
  16. package/dist/backends/copilot.d.cts +2 -1
  17. package/dist/backends/copilot.d.ts +2 -1
  18. package/dist/backends/copilot.js +133 -25
  19. package/dist/backends/copilot.js.map +1 -1
  20. package/dist/backends/vercel-ai.cjs +66 -19
  21. package/dist/backends/vercel-ai.cjs.map +1 -1
  22. package/dist/backends/vercel-ai.d.cts +1 -1
  23. package/dist/backends/vercel-ai.d.ts +1 -1
  24. package/dist/backends/vercel-ai.js +66 -19
  25. package/dist/backends/vercel-ai.js.map +1 -1
  26. package/dist/chat/accumulator.cjs +147 -0
  27. package/dist/chat/accumulator.cjs.map +1 -0
  28. package/dist/chat/accumulator.d.cts +61 -0
  29. package/dist/chat/accumulator.d.ts +61 -0
  30. package/dist/chat/accumulator.js +145 -0
  31. package/dist/chat/accumulator.js.map +1 -0
  32. package/dist/chat/backends.cjs +3534 -0
  33. package/dist/chat/backends.cjs.map +1 -0
  34. package/dist/chat/backends.d.cts +62 -0
  35. package/dist/chat/backends.d.ts +62 -0
  36. package/dist/chat/backends.js +3501 -0
  37. package/dist/chat/backends.js.map +1 -0
  38. package/dist/chat/context.cjs +230 -0
  39. package/dist/chat/context.cjs.map +1 -0
  40. package/dist/chat/context.d.cts +167 -0
  41. package/dist/chat/context.d.ts +167 -0
  42. package/dist/chat/context.js +227 -0
  43. package/dist/chat/context.js.map +1 -0
  44. package/dist/chat/core.cjs +282 -0
  45. package/dist/chat/core.cjs.map +1 -0
  46. package/dist/chat/core.d.cts +435 -0
  47. package/dist/chat/core.d.ts +435 -0
  48. package/dist/chat/core.js +261 -0
  49. package/dist/chat/core.js.map +1 -0
  50. package/dist/chat/errors.cjs +251 -0
  51. package/dist/chat/errors.cjs.map +1 -0
  52. package/dist/chat/errors.d.cts +122 -0
  53. package/dist/chat/errors.d.ts +122 -0
  54. package/dist/chat/errors.js +243 -0
  55. package/dist/chat/errors.js.map +1 -0
  56. package/dist/chat/events.cjs +203 -0
  57. package/dist/chat/events.cjs.map +1 -0
  58. package/dist/chat/events.d.cts +241 -0
  59. package/dist/chat/events.d.ts +241 -0
  60. package/dist/chat/events.js +196 -0
  61. package/dist/chat/events.js.map +1 -0
  62. package/dist/chat/index.cjs +5359 -0
  63. package/dist/chat/index.cjs.map +1 -0
  64. package/dist/chat/index.d.cts +52 -0
  65. package/dist/chat/index.d.ts +52 -0
  66. package/dist/chat/index.js +5296 -0
  67. package/dist/chat/index.js.map +1 -0
  68. package/dist/chat/react.cjs +2739 -0
  69. package/dist/chat/react.cjs.map +1 -0
  70. package/dist/chat/react.d.cts +619 -0
  71. package/dist/chat/react.d.ts +619 -0
  72. package/dist/chat/react.js +2714 -0
  73. package/dist/chat/react.js.map +1 -0
  74. package/dist/chat/runtime.cjs +1030 -0
  75. package/dist/chat/runtime.cjs.map +1 -0
  76. package/dist/chat/runtime.d.cts +118 -0
  77. package/dist/chat/runtime.d.ts +118 -0
  78. package/dist/chat/runtime.js +1028 -0
  79. package/dist/chat/runtime.js.map +1 -0
  80. package/dist/chat/server.cjs +643 -0
  81. package/dist/chat/server.cjs.map +1 -0
  82. package/dist/chat/server.d.cts +287 -0
  83. package/dist/chat/server.d.ts +287 -0
  84. package/dist/chat/server.js +617 -0
  85. package/dist/chat/server.js.map +1 -0
  86. package/dist/chat/sessions.cjs +398 -0
  87. package/dist/chat/sessions.cjs.map +1 -0
  88. package/dist/chat/sessions.d.cts +239 -0
  89. package/dist/chat/sessions.d.ts +239 -0
  90. package/dist/chat/sessions.js +394 -0
  91. package/dist/chat/sessions.js.map +1 -0
  92. package/dist/chat/state.cjs +177 -0
  93. package/dist/chat/state.cjs.map +1 -0
  94. package/dist/chat/state.d.cts +92 -0
  95. package/dist/chat/state.d.ts +92 -0
  96. package/dist/chat/state.js +167 -0
  97. package/dist/chat/state.js.map +1 -0
  98. package/dist/chat/storage.cjs +240 -0
  99. package/dist/chat/storage.cjs.map +1 -0
  100. package/dist/chat/storage.d.cts +191 -0
  101. package/dist/chat/storage.d.ts +191 -0
  102. package/dist/chat/storage.js +236 -0
  103. package/dist/chat/storage.js.map +1 -0
  104. package/dist/errors-BDLbNu9w.d.cts +13 -0
  105. package/dist/errors-BDLbNu9w.d.ts +13 -0
  106. package/dist/in-process-transport-C2oPTYs6.d.ts +223 -0
  107. package/dist/in-process-transport-DG-w5G6k.d.cts +223 -0
  108. package/dist/index.cjs +25 -13
  109. package/dist/index.cjs.map +1 -1
  110. package/dist/index.d.cts +32 -4
  111. package/dist/index.d.ts +32 -4
  112. package/dist/index.js +25 -13
  113. package/dist/index.js.map +1 -1
  114. package/dist/transport-D1OaUgRk.d.ts +67 -0
  115. package/dist/transport-DX1Nhm4N.d.cts +67 -0
  116. package/dist/types-Bh5AhqD-.d.ts +141 -0
  117. package/dist/types-CGF7AEX1.d.cts +141 -0
  118. package/dist/{types-BvwNzZCj.d.cts → types-CqvUAYxt.d.cts} +21 -3
  119. package/dist/{types-BvwNzZCj.d.ts → types-CqvUAYxt.d.ts} +21 -3
  120. package/dist/types-DLZzlJxt.d.ts +39 -0
  121. package/dist/types-tE0CXwBl.d.cts +39 -0
  122. package/package.json +149 -2
@@ -0,0 +1,1028 @@
1
+ // src/chat/core.ts
2
+ function createChatId() {
3
+ return crypto.randomUUID();
4
+ }
5
+ var UUID_RE = /^[0-9a-f]{8}-[0-9a-f]{4}-4[0-9a-f]{3}-[89ab][0-9a-f]{3}-[0-9a-f]{12}$/i;
6
+ function toChatId(value) {
7
+ if (!UUID_RE.test(value)) {
8
+ throw new TypeError(`Invalid ChatId: "${value}" is not a valid UUID`);
9
+ }
10
+ return value;
11
+ }
12
+ function chatEventToAgentEvent(event) {
13
+ switch (event.type) {
14
+ case "message:delta":
15
+ return { type: "text_delta", text: event.text };
16
+ case "thinking:start":
17
+ return { type: "thinking_start" };
18
+ case "thinking:delta":
19
+ return { type: "thinking_delta", text: event.text };
20
+ case "thinking:end":
21
+ return { type: "thinking_end" };
22
+ case "tool:start":
23
+ return {
24
+ type: "tool_call_start",
25
+ toolCallId: event.toolCallId,
26
+ toolName: event.toolName,
27
+ args: event.args
28
+ };
29
+ case "tool:complete":
30
+ return {
31
+ type: "tool_call_end",
32
+ toolCallId: event.toolCallId,
33
+ toolName: event.toolName,
34
+ result: event.result
35
+ };
36
+ case "error":
37
+ return { type: "error", error: event.error, recoverable: event.recoverable };
38
+ default:
39
+ return null;
40
+ }
41
+ }
42
+
43
+ // src/chat/context.ts
44
+ function estimateTokens(message, options) {
45
+ const ratio = options?.charsPerToken ?? 4;
46
+ let charCount = 0;
47
+ charCount += message.role.length + 4;
48
+ for (const part of message.parts) {
49
+ charCount += estimatePartChars(part);
50
+ }
51
+ return Math.ceil(charCount / ratio);
52
+ }
53
+ function estimatePartChars(part) {
54
+ switch (part.type) {
55
+ case "text":
56
+ return part.text.length;
57
+ case "reasoning":
58
+ return part.text.length;
59
+ case "tool_call":
60
+ return JSON.stringify(part.args).length + part.name.length + 20 + (part.result !== void 0 ? JSON.stringify(part.result).length : 0);
61
+ case "source":
62
+ return (part.title?.length ?? 0) + part.url.length + 10;
63
+ case "file":
64
+ return part.name.length + part.data.length + 20;
65
+ }
66
+ }
67
+ var ContextWindowManager = class {
68
+ config;
69
+ constructor(config) {
70
+ this.config = {
71
+ maxTokens: config.maxTokens,
72
+ reservedTokens: config.reservedTokens ?? 0,
73
+ strategy: config.strategy ?? "truncate-oldest",
74
+ estimation: config.estimation,
75
+ summarizer: config.summarizer
76
+ };
77
+ }
78
+ /** Available token budget after reserving tokens */
79
+ get availableBudget() {
80
+ return Math.max(0, this.config.maxTokens - this.config.reservedTokens);
81
+ }
82
+ /**
83
+ * Estimate tokens for a single message.
84
+ * @param message - Message to estimate
85
+ * @returns Estimated token count
86
+ */
87
+ estimateMessageTokens(message) {
88
+ return estimateTokens(message, this.config.estimation);
89
+ }
90
+ /**
91
+ * Fit messages within the token budget using the configured strategy.
92
+ * @param messages - All messages to consider
93
+ * @returns Result with fitted messages and metadata
94
+ */
95
+ fitMessages(messages) {
96
+ if (messages.length === 0) {
97
+ return { messages: [], totalTokens: 0, removedCount: 0, wasTruncated: false };
98
+ }
99
+ const budget = this.availableBudget;
100
+ const tokenCounts = messages.map((m) => this.estimateMessageTokens(m));
101
+ const totalTokens = tokenCounts.reduce((a, b) => a + b, 0);
102
+ if (totalTokens <= budget) {
103
+ return {
104
+ messages: [...messages],
105
+ totalTokens,
106
+ removedCount: 0,
107
+ wasTruncated: false
108
+ };
109
+ }
110
+ switch (this.config.strategy) {
111
+ case "truncate-oldest":
112
+ return this.truncateOldest(messages, tokenCounts, budget);
113
+ case "sliding-window":
114
+ return this.slidingWindow(messages, tokenCounts, budget);
115
+ case "summarize-placeholder":
116
+ return this.summarizePlaceholder(messages, tokenCounts, budget);
117
+ }
118
+ }
119
+ /**
120
+ * Async variant of fitMessages that supports async summarization.
121
+ * When strategy is "summarize-placeholder" and a summarizer is configured,
122
+ * calls the summarizer with removed messages and replaces the placeholder text.
123
+ * Falls back to static placeholder if summarizer throws.
124
+ * For other strategies, behaves identically to fitMessages().
125
+ */
126
+ async fitMessagesAsync(messages) {
127
+ const result = this.fitMessages(messages);
128
+ if (this.config.strategy !== "summarize-placeholder" || !result.wasTruncated || !this.config.summarizer) {
129
+ return result;
130
+ }
131
+ const keptIds = new Set(result.messages.map((m) => m.id));
132
+ const removed = messages.filter((m) => !keptIds.has(m.id));
133
+ if (removed.length === 0) return result;
134
+ let summaryText;
135
+ try {
136
+ summaryText = await this.config.summarizer(removed);
137
+ } catch {
138
+ return result;
139
+ }
140
+ const updatedMessages = result.messages.map((m) => {
141
+ if (m.metadata?.isSummary === true) {
142
+ return {
143
+ ...m,
144
+ parts: [{ type: "text", text: summaryText, status: "complete" }]
145
+ };
146
+ }
147
+ return m;
148
+ });
149
+ return { ...result, messages: updatedMessages };
150
+ }
151
+ /**
152
+ * Truncate oldest: keeps system messages, removes oldest non-system messages first.
153
+ * Always keeps the most recent user message.
154
+ */
155
+ truncateOldest(messages, tokenCounts, budget) {
156
+ const systemIndices = [];
157
+ const nonSystemIndices = [];
158
+ for (let i = 0; i < messages.length; i++) {
159
+ if (messages[i].role === "system") {
160
+ systemIndices.push(i);
161
+ } else {
162
+ nonSystemIndices.push(i);
163
+ }
164
+ }
165
+ let usedTokens = systemIndices.reduce(
166
+ (sum, i) => sum + tokenCounts[i],
167
+ 0
168
+ );
169
+ const includedNonSystem = [];
170
+ for (let i = nonSystemIndices.length - 1; i >= 0; i--) {
171
+ const idx = nonSystemIndices[i];
172
+ if (usedTokens + tokenCounts[idx] <= budget) {
173
+ includedNonSystem.unshift(idx);
174
+ usedTokens += tokenCounts[idx];
175
+ }
176
+ }
177
+ const includedSet = /* @__PURE__ */ new Set([...systemIndices, ...includedNonSystem]);
178
+ const result = [];
179
+ let resultTokens = 0;
180
+ for (let i = 0; i < messages.length; i++) {
181
+ if (includedSet.has(i)) {
182
+ result.push(messages[i]);
183
+ resultTokens += tokenCounts[i];
184
+ }
185
+ }
186
+ return {
187
+ messages: result,
188
+ totalTokens: resultTokens,
189
+ removedCount: messages.length - result.length,
190
+ wasTruncated: true
191
+ };
192
+ }
193
+ /**
194
+ * Sliding window: keeps the most recent messages that fit within budget.
195
+ */
196
+ slidingWindow(messages, tokenCounts, budget) {
197
+ const result = [];
198
+ let usedTokens = 0;
199
+ for (let i = messages.length - 1; i >= 0; i--) {
200
+ if (usedTokens + tokenCounts[i] <= budget) {
201
+ result.unshift(messages[i]);
202
+ usedTokens += tokenCounts[i];
203
+ } else {
204
+ break;
205
+ }
206
+ }
207
+ return {
208
+ messages: result,
209
+ totalTokens: usedTokens,
210
+ removedCount: messages.length - result.length,
211
+ wasTruncated: true
212
+ };
213
+ }
214
+ /**
215
+ * Summarize placeholder: replaces truncated messages with a placeholder,
216
+ * preserving system messages and recent context.
217
+ */
218
+ summarizePlaceholder(messages, tokenCounts, budget) {
219
+ const systemMessages = [];
220
+ const nonSystem = [];
221
+ for (let i = 0; i < messages.length; i++) {
222
+ if (messages[i].role === "system") {
223
+ systemMessages.push({ msg: messages[i], tokens: tokenCounts[i] });
224
+ } else {
225
+ nonSystem.push({ msg: messages[i], tokens: tokenCounts[i], idx: i });
226
+ }
227
+ }
228
+ let usedTokens = systemMessages.reduce((s, m) => s + m.tokens, 0);
229
+ const placeholderTokens = 20;
230
+ usedTokens += placeholderTokens;
231
+ const recentKept = [];
232
+ for (let i = nonSystem.length - 1; i >= 0; i--) {
233
+ if (usedTokens + nonSystem[i].tokens <= budget) {
234
+ recentKept.unshift(nonSystem[i]);
235
+ usedTokens += nonSystem[i].tokens;
236
+ } else {
237
+ break;
238
+ }
239
+ }
240
+ const removedCount = messages.length - systemMessages.length - recentKept.length;
241
+ const result = [];
242
+ for (const sm of systemMessages) {
243
+ result.push(sm.msg);
244
+ }
245
+ if (removedCount > 0) {
246
+ result.push({
247
+ id: "context-placeholder",
248
+ role: "system",
249
+ parts: [{ type: "text", text: `[${removedCount} earlier message${removedCount === 1 ? "" : "s"} omitted for context window]`, status: "complete" }],
250
+ metadata: { isSummary: true },
251
+ createdAt: (/* @__PURE__ */ new Date()).toISOString(),
252
+ status: "complete"
253
+ });
254
+ }
255
+ for (const m of recentKept) {
256
+ result.push(m.msg);
257
+ }
258
+ return {
259
+ messages: result,
260
+ totalTokens: usedTokens,
261
+ removedCount,
262
+ wasTruncated: true
263
+ };
264
+ }
265
+ };
266
+
267
+ // src/errors.ts
268
+ var AgentSDKError = class extends Error {
269
+ /** @internal Marker for cross-bundle identity checks */
270
+ _agentSDKError = true;
271
+ constructor(message, options) {
272
+ super(message, options);
273
+ this.name = "AgentSDKError";
274
+ }
275
+ /** Check if an error is an AgentSDKError (works across bundled copies) */
276
+ static is(error) {
277
+ return error instanceof Error && "_agentSDKError" in error && error._agentSDKError === true;
278
+ }
279
+ };
280
+
281
+ // src/chat/errors.ts
282
+ var ChatError = class extends AgentSDKError {
283
+ code;
284
+ retryable;
285
+ retryAfter;
286
+ timestamp;
287
+ constructor(message, options) {
288
+ super(message, { cause: options.cause });
289
+ this.name = "ChatError";
290
+ this.code = options.code;
291
+ this.retryable = options.retryable ?? false;
292
+ this.retryAfter = options.retryAfter;
293
+ this.timestamp = (/* @__PURE__ */ new Date()).toISOString();
294
+ }
295
+ };
296
+
297
+ // src/chat/state.ts
298
+ var StateMachine = class {
299
+ constructor(initial, transitions) {
300
+ this.initial = initial;
301
+ this.transitions = transitions;
302
+ this._current = initial;
303
+ }
304
+ _current;
305
+ /** Current state */
306
+ get current() {
307
+ return this._current;
308
+ }
309
+ /**
310
+ * Check whether transitioning to `next` is allowed from current state
311
+ * @param next - Target state to check
312
+ * @returns True if transition is allowed
313
+ */
314
+ canTransition(next) {
315
+ const allowed = this.transitions[this._current];
316
+ return allowed !== void 0 && allowed.includes(next);
317
+ }
318
+ /**
319
+ * Transition to `next` state.
320
+ * @throws ChatError(INVALID_TRANSITION) if the transition is not allowed
321
+ */
322
+ transition(next) {
323
+ if (!this.canTransition(next)) {
324
+ throw new ChatError(
325
+ `Invalid transition: ${this._current} \u2192 ${next}`,
326
+ { code: "INVALID_TRANSITION" /* INVALID_TRANSITION */ }
327
+ );
328
+ }
329
+ this._current = next;
330
+ }
331
+ /** Reset to initial state */
332
+ reset() {
333
+ this._current = this.initial;
334
+ }
335
+ };
336
+ var RUNTIME_TRANSITIONS = {
337
+ idle: ["streaming", "disposed"],
338
+ streaming: ["idle", "error", "disposed"],
339
+ error: ["idle", "disposed"],
340
+ disposed: []
341
+ };
342
+ var ChatReentrancyGuard = class {
343
+ _acquired = false;
344
+ /** Whether the guard is currently held */
345
+ get isAcquired() {
346
+ return this._acquired;
347
+ }
348
+ /**
349
+ * Acquire the guard. Throws if already acquired.
350
+ * @throws ChatError with code REENTRANCY
351
+ */
352
+ acquire() {
353
+ if (this._acquired) {
354
+ throw new ChatError(
355
+ "Concurrent operation detected: a send is already in progress",
356
+ { code: "REENTRANCY" /* REENTRANCY */ }
357
+ );
358
+ }
359
+ this._acquired = true;
360
+ }
361
+ /** Release the guard. Safe to call even if not acquired. */
362
+ release() {
363
+ this._acquired = false;
364
+ }
365
+ };
366
+ var ChatAbortController = class {
367
+ _controller;
368
+ _onExternalAbort;
369
+ _externalSignal;
370
+ constructor(externalSignal) {
371
+ this._controller = new AbortController();
372
+ this._externalSignal = externalSignal;
373
+ if (externalSignal) {
374
+ if (externalSignal.aborted) {
375
+ this._controller.abort(externalSignal.reason);
376
+ } else {
377
+ this._onExternalAbort = () => {
378
+ this._controller.abort(externalSignal.reason);
379
+ };
380
+ externalSignal.addEventListener("abort", this._onExternalAbort, { once: true });
381
+ }
382
+ }
383
+ }
384
+ /** The AbortSignal for this controller */
385
+ get signal() {
386
+ return this._controller.signal;
387
+ }
388
+ /** Whether the operation has been aborted */
389
+ get isAborted() {
390
+ return this._controller.signal.aborted;
391
+ }
392
+ /**
393
+ * Abort the operation.
394
+ * @param reason - Optional abort reason
395
+ */
396
+ abort(reason) {
397
+ this._controller.abort(reason);
398
+ }
399
+ /** Clean up external signal listener to prevent memory leaks */
400
+ dispose() {
401
+ if (this._onExternalAbort && this._externalSignal) {
402
+ this._externalSignal.removeEventListener("abort", this._onExternalAbort);
403
+ }
404
+ }
405
+ };
406
+
407
+ // src/chat/accumulator.ts
408
+ var MessageAccumulator = class {
409
+ messageId;
410
+ parts = [];
411
+ status = "pending";
412
+ currentTextPart = null;
413
+ currentReasoningPart = null;
414
+ toolCallParts = /* @__PURE__ */ new Map();
415
+ _finalized = false;
416
+ constructor(messageId) {
417
+ this.messageId = messageId ?? createChatId();
418
+ }
419
+ /** Get current message ID */
420
+ get id() {
421
+ return this.messageId;
422
+ }
423
+ /**
424
+ * Apply an AgentEvent to accumulate into the message
425
+ * @param event - AgentEvent to process
426
+ * @throws Error if accumulator is already finalized
427
+ */
428
+ apply(event) {
429
+ if (this._finalized) throw new Error("Cannot apply events to finalized accumulator");
430
+ if (this.status === "pending") {
431
+ this.status = "streaming";
432
+ }
433
+ switch (event.type) {
434
+ case "text_delta":
435
+ this.handleTextDelta(event.text);
436
+ break;
437
+ case "thinking_start":
438
+ this.finalizeCurrentText();
439
+ this.currentReasoningPart = { type: "reasoning", text: "", status: "streaming" };
440
+ this.parts.push(this.currentReasoningPart);
441
+ break;
442
+ case "thinking_delta":
443
+ if (this.currentReasoningPart) {
444
+ this.currentReasoningPart.text += event.text;
445
+ }
446
+ break;
447
+ case "thinking_end":
448
+ if (this.currentReasoningPart) {
449
+ this.currentReasoningPart.status = "complete";
450
+ this.currentReasoningPart = null;
451
+ }
452
+ break;
453
+ case "tool_call_start": {
454
+ this.finalizeCurrentText();
455
+ const toolPart = {
456
+ type: "tool_call",
457
+ toolCallId: event.toolCallId,
458
+ name: event.toolName,
459
+ args: event.args,
460
+ status: "running"
461
+ };
462
+ this.toolCallParts.set(event.toolCallId, toolPart);
463
+ this.parts.push(toolPart);
464
+ break;
465
+ }
466
+ case "tool_call_end": {
467
+ const existing = this.toolCallParts.get(event.toolCallId);
468
+ if (existing) {
469
+ existing.result = event.result;
470
+ existing.status = "complete";
471
+ }
472
+ break;
473
+ }
474
+ case "error":
475
+ this.status = "error";
476
+ break;
477
+ }
478
+ }
479
+ handleTextDelta(text) {
480
+ if (!this.currentTextPart) {
481
+ this.currentTextPart = { type: "text", text: "", status: "streaming" };
482
+ this.parts.push(this.currentTextPart);
483
+ }
484
+ this.currentTextPart.text += text;
485
+ }
486
+ finalizeCurrentText() {
487
+ if (this.currentTextPart) {
488
+ this.currentTextPart.status = "complete";
489
+ this.currentTextPart = null;
490
+ }
491
+ }
492
+ /**
493
+ * Get a snapshot of the current accumulated message (for streaming UI)
494
+ * @returns ChatMessage with current parts and "streaming" status
495
+ */
496
+ snapshot() {
497
+ const now = (/* @__PURE__ */ new Date()).toISOString();
498
+ return {
499
+ id: this.messageId,
500
+ role: "assistant",
501
+ parts: this.parts.map((p) => ({ ...p })),
502
+ status: this.status === "pending" ? "pending" : "streaming",
503
+ createdAt: now,
504
+ updatedAt: now
505
+ };
506
+ }
507
+ /**
508
+ * Finalize the accumulator and return the complete ChatMessage
509
+ * @returns Completed ChatMessage with all parts finalized
510
+ * @throws Error if accumulator is already finalized
511
+ */
512
+ finalize() {
513
+ if (this._finalized) throw new Error("Accumulator already finalized");
514
+ this._finalized = true;
515
+ this.finalizeCurrentText();
516
+ if (this.currentReasoningPart) {
517
+ this.currentReasoningPart.status = "complete";
518
+ this.currentReasoningPart = null;
519
+ }
520
+ for (const [, toolPart] of this.toolCallParts) {
521
+ if (toolPart.status === "running" || toolPart.status === "pending") {
522
+ toolPart.status = "error";
523
+ }
524
+ }
525
+ if (this.status !== "error" && this.status !== "cancelled") {
526
+ this.status = "complete";
527
+ }
528
+ const now = (/* @__PURE__ */ new Date()).toISOString();
529
+ return {
530
+ id: this.messageId,
531
+ role: "assistant",
532
+ parts: this.parts,
533
+ status: this.status,
534
+ createdAt: now,
535
+ updatedAt: now
536
+ };
537
+ }
538
+ /** Check if the accumulator has been finalized */
539
+ get finalized() {
540
+ return this._finalized;
541
+ }
542
+ };
543
+
544
+ // src/chat/watchdog.ts
545
+ async function* withStreamWatchdog(source, config) {
546
+ const { timeoutMs, signal } = config;
547
+ const iterator = source[Symbol.asyncIterator]();
548
+ let aborted = false;
549
+ if (signal?.aborted) {
550
+ iterator.return?.();
551
+ return;
552
+ }
553
+ const onAbort = () => {
554
+ aborted = true;
555
+ iterator.return?.();
556
+ };
557
+ signal?.addEventListener("abort", onAbort, { once: true });
558
+ try {
559
+ while (true) {
560
+ if (aborted) break;
561
+ const timeout = new CancellableTimeout(timeoutMs);
562
+ try {
563
+ const result = await Promise.race([
564
+ iterator.next(),
565
+ timeout.promise
566
+ ]);
567
+ timeout.cancel();
568
+ if (result.done) break;
569
+ yield result.value;
570
+ } catch (err) {
571
+ timeout.cancel();
572
+ throw err;
573
+ }
574
+ }
575
+ } finally {
576
+ signal?.removeEventListener("abort", onAbort);
577
+ iterator.return?.();
578
+ }
579
+ }
580
+ var CancellableTimeout = class {
581
+ promise;
582
+ _timer;
583
+ _cancelled = false;
584
+ constructor(ms) {
585
+ this.promise = new Promise((_, reject) => {
586
+ this._timer = setTimeout(() => {
587
+ if (!this._cancelled) {
588
+ reject(
589
+ new ChatError(
590
+ `Stream timed out after ${ms}ms of inactivity`,
591
+ { code: "TIMEOUT" /* TIMEOUT */ }
592
+ )
593
+ );
594
+ }
595
+ }, ms);
596
+ });
597
+ this.promise.catch(() => {
598
+ });
599
+ }
600
+ cancel() {
601
+ this._cancelled = true;
602
+ if (this._timer !== void 0) {
603
+ clearTimeout(this._timer);
604
+ this._timer = void 0;
605
+ }
606
+ }
607
+ };
608
+
609
+ // src/chat/runtime.ts
610
+ var ChatRuntime = class {
611
+ _state;
612
+ _guard;
613
+ _backends;
614
+ _sessionStore;
615
+ _contextConfig;
616
+ _middleware;
617
+ _tools = /* @__PURE__ */ new Map();
618
+ _retryConfig;
619
+ _contextStats = /* @__PURE__ */ new Map();
620
+ _onContextTrimmed;
621
+ _streamTimeoutMs;
622
+ _sessionListeners = /* @__PURE__ */ new Set();
623
+ _activeAdapter = null;
624
+ _currentBackend;
625
+ _currentModel;
626
+ _activeSessionId = null;
627
+ _abortController = null;
628
+ constructor(options) {
629
+ this._state = new StateMachine("idle", RUNTIME_TRANSITIONS);
630
+ this._guard = new ChatReentrancyGuard();
631
+ this._backends = options.backends;
632
+ this._currentBackend = options.defaultBackend;
633
+ this._currentModel = options.defaultModel;
634
+ this._sessionStore = options.sessionStore;
635
+ this._contextConfig = options.context;
636
+ this._middleware = [...options.middleware ?? []];
637
+ this._retryConfig = options.retryConfig;
638
+ this._onContextTrimmed = options.onContextTrimmed;
639
+ this._streamTimeoutMs = options.streamTimeoutMs;
640
+ if (!options.backends[options.defaultBackend]) {
641
+ throw new ChatError(
642
+ `Default backend "${options.defaultBackend}" not found in backends map`,
643
+ { code: "INVALID_INPUT" /* INVALID_INPUT */ }
644
+ );
645
+ }
646
+ }
647
+ // ── Lifecycle ──────────────────────────────────────────────
648
+ get status() {
649
+ return this._state.current;
650
+ }
651
+ async dispose() {
652
+ if (this._state.current === "disposed") return;
653
+ this._abortController?.abort("Runtime disposed");
654
+ this._abortController?.dispose();
655
+ this._abortController = null;
656
+ this._state.transition("disposed");
657
+ if (this._activeAdapter) {
658
+ await this._activeAdapter.dispose();
659
+ this._activeAdapter = null;
660
+ }
661
+ }
662
+ // ── Sessions ───────────────────────────────────────────────
663
+ get activeSessionId() {
664
+ return this._activeSessionId;
665
+ }
666
+ async createSession(options) {
667
+ this.assertNotDisposed();
668
+ const config = {
669
+ model: options.config?.model ?? this._currentModel ?? "",
670
+ backend: options.config?.backend ?? this._currentBackend,
671
+ ...options.config
672
+ };
673
+ const session = await this._sessionStore.createSession({ ...options, config });
674
+ this._activeSessionId = session.id;
675
+ this._notifySessionChange();
676
+ return session;
677
+ }
678
+ async getSession(id) {
679
+ this.assertNotDisposed();
680
+ const cid = toChatId(id);
681
+ return this._sessionStore.getSession(cid);
682
+ }
683
+ async listSessions(options) {
684
+ this.assertNotDisposed();
685
+ return this._sessionStore.listSessions(options);
686
+ }
687
+ async deleteSession(id) {
688
+ this.assertNotDisposed();
689
+ const cid = toChatId(id);
690
+ const session = await this._sessionStore.getSession(cid);
691
+ if (!session) return;
692
+ await this._sessionStore.deleteSession(cid);
693
+ this._contextStats.delete(cid);
694
+ if (this._activeSessionId === cid) {
695
+ this._activeSessionId = null;
696
+ }
697
+ this._notifySessionChange();
698
+ }
699
+ async archiveSession(id) {
700
+ this.assertNotDisposed();
701
+ const cid = toChatId(id);
702
+ await this._sessionStore.archiveSession(cid);
703
+ this._notifySessionChange();
704
+ }
705
+ async switchSession(id) {
706
+ this.assertNotDisposed();
707
+ const cid = toChatId(id);
708
+ const session = await this._sessionStore.getSession(cid);
709
+ if (!session) {
710
+ throw new ChatError(
711
+ `Session "${id}" not found`,
712
+ { code: "SESSION_NOT_FOUND" /* SESSION_NOT_FOUND */ }
713
+ );
714
+ }
715
+ this._activeSessionId = session.id;
716
+ return session;
717
+ }
718
+ // ── Messaging ──────────────────────────────────────────────
719
+ async *send(sessionId, message, options) {
720
+ this.assertNotDisposed();
721
+ if (!message || message.trim().length === 0) {
722
+ throw new ChatError("Message cannot be empty", { code: "INVALID_INPUT" /* INVALID_INPUT */ });
723
+ }
724
+ this._guard.acquire();
725
+ const cid = toChatId(sessionId);
726
+ this._abortController = new ChatAbortController(options?.signal);
727
+ try {
728
+ if (this._state.current === "error") {
729
+ this._state.transition("idle");
730
+ }
731
+ this._state.transition("streaming");
732
+ const session = await this._sessionStore.getSession(cid);
733
+ if (!session) {
734
+ throw new ChatError(
735
+ `Session "${cid}" not found`,
736
+ { code: "SESSION_NOT_FOUND" /* SESSION_NOT_FOUND */ }
737
+ );
738
+ }
739
+ const middlewareContext = {
740
+ sessionId: cid,
741
+ signal: this._abortController.signal
742
+ };
743
+ let userMessage = this.createUserMessage(message);
744
+ for (const mw of this._middleware) {
745
+ if (mw.onBeforeSend) {
746
+ userMessage = await mw.onBeforeSend(userMessage, middlewareContext);
747
+ }
748
+ }
749
+ await this._sessionStore.appendMessage(cid, userMessage);
750
+ const updatedSession = await this._sessionStore.getSession(cid);
751
+ let messagesToSend = updatedSession.messages;
752
+ if (this._contextConfig) {
753
+ const ctxManager = new ContextWindowManager(this._contextConfig);
754
+ const result = await ctxManager.fitMessagesAsync(messagesToSend);
755
+ this._contextStats.set(cid, {
756
+ totalTokens: result.totalTokens,
757
+ removedCount: result.removedCount,
758
+ wasTruncated: result.wasTruncated,
759
+ availableBudget: ctxManager.availableBudget
760
+ });
761
+ if (result.wasTruncated && this._onContextTrimmed) {
762
+ const keptIds = new Set(result.messages.map((m) => m.id));
763
+ const removed = messagesToSend.filter((m) => !keptIds.has(m.id));
764
+ if (removed.length > 0) {
765
+ try {
766
+ this._onContextTrimmed(cid, removed);
767
+ } catch {
768
+ }
769
+ }
770
+ }
771
+ messagesToSend = result.messages;
772
+ }
773
+ const sessionForAdapter = {
774
+ ...updatedSession,
775
+ messages: messagesToSend
776
+ };
777
+ const adapter = await this.getOrCreateAdapterWithRetry();
778
+ const accumulator = new MessageAccumulator();
779
+ const runtimeTools = this._tools.size > 0 ? this.injectToolContext([...this._tools.values()], {
780
+ sessionId: cid,
781
+ custom: updatedSession.metadata?.custom
782
+ }) : void 0;
783
+ const streamOptions = {
784
+ ...options,
785
+ signal: this._abortController.signal,
786
+ model: options?.model ?? this._currentModel,
787
+ tools: runtimeTools
788
+ };
789
+ const stream = await this.createStreamWithRetry(adapter, sessionForAdapter, message, streamOptions);
790
+ const eventSource = this._streamTimeoutMs ? withStreamWatchdog(stream, { timeoutMs: this._streamTimeoutMs, signal: this._abortController.signal }) : stream;
791
+ for await (const event of eventSource) {
792
+ if (this._abortController.isAborted) break;
793
+ this.feedAccumulator(accumulator, event);
794
+ let processedEvent = event;
795
+ for (const mw of this._middleware) {
796
+ if (mw.onEvent && processedEvent) {
797
+ processedEvent = await mw.onEvent(processedEvent, middlewareContext);
798
+ }
799
+ }
800
+ if (processedEvent) {
801
+ yield processedEvent;
802
+ }
803
+ }
804
+ if (this._state.current === "disposed") {
805
+ return;
806
+ }
807
+ let assistantMessage = accumulator.finalize();
808
+ for (const mw of this._middleware) {
809
+ if (mw.onAfterReceive) {
810
+ assistantMessage = await mw.onAfterReceive(assistantMessage, middlewareContext);
811
+ }
812
+ }
813
+ await this._sessionStore.appendMessage(cid, assistantMessage);
814
+ this._notifySessionChange();
815
+ this._state.transition("idle");
816
+ } catch (error) {
817
+ let processedError = error instanceof Error ? error : new Error(String(error));
818
+ const middlewareContext = {
819
+ sessionId: cid,
820
+ signal: this._abortController?.signal ?? new AbortController().signal
821
+ };
822
+ for (const mw of this._middleware) {
823
+ if (mw.onError) {
824
+ const result = await mw.onError(processedError, middlewareContext);
825
+ if (result === null) {
826
+ if (this._state.canTransition("idle")) {
827
+ this._state.transition("idle");
828
+ }
829
+ return;
830
+ }
831
+ processedError = result;
832
+ }
833
+ }
834
+ if (this._state.canTransition("error")) {
835
+ this._state.transition("error");
836
+ }
837
+ throw processedError;
838
+ } finally {
839
+ this._guard.release();
840
+ this._abortController?.dispose();
841
+ this._abortController = null;
842
+ }
843
+ }
844
+ abort() {
845
+ this._abortController?.abort("User abort");
846
+ }
847
+ // ── Backend / Model ────────────────────────────────────────
848
+ get currentBackend() {
849
+ return this._currentBackend;
850
+ }
851
+ get currentModel() {
852
+ return this._currentModel;
853
+ }
854
+ async switchBackend(name) {
855
+ this.assertNotDisposed();
856
+ if (!this._backends[name]) {
857
+ throw new ChatError(
858
+ `Backend "${name}" not found in backends map`,
859
+ { code: "INVALID_INPUT" /* INVALID_INPUT */ }
860
+ );
861
+ }
862
+ if (this._activeAdapter) {
863
+ await this._activeAdapter.dispose();
864
+ this._activeAdapter = null;
865
+ }
866
+ this._currentBackend = name;
867
+ }
868
+ switchModel(model) {
869
+ this.assertNotDisposed();
870
+ this._currentModel = model;
871
+ }
872
+ async listModels() {
873
+ this.assertNotDisposed();
874
+ const adapter = await this.getOrCreateAdapter();
875
+ return adapter.listModels();
876
+ }
877
+ // ── Tools ──────────────────────────────────────────────────
878
+ get registeredTools() {
879
+ return this._tools;
880
+ }
881
+ registerTool(tool) {
882
+ this.assertNotDisposed();
883
+ this._tools.set(tool.name, tool);
884
+ }
885
+ removeTool(name) {
886
+ this.assertNotDisposed();
887
+ this._tools.delete(name);
888
+ }
889
+ // ── Middleware ──────────────────────────────────────────────
890
+ use(middleware) {
891
+ this.assertNotDisposed();
892
+ this._middleware.push(middleware);
893
+ }
894
+ removeMiddleware(middleware) {
895
+ this.assertNotDisposed();
896
+ const idx = this._middleware.indexOf(middleware);
897
+ if (idx >= 0) this._middleware.splice(idx, 1);
898
+ }
899
+ // ── Context Stats ─────────────────────────────────────────
900
+ getContextStats(sessionId) {
901
+ const cid = toChatId(sessionId);
902
+ return this._contextStats.get(cid) ?? null;
903
+ }
904
+ // ── Session Subscription ──────────────────────────────────
905
+ onSessionChange(callback) {
906
+ this._sessionListeners.add(callback);
907
+ return () => {
908
+ this._sessionListeners.delete(callback);
909
+ };
910
+ }
911
+ _notifySessionChange() {
912
+ for (const cb of this._sessionListeners) {
913
+ try {
914
+ cb();
915
+ } catch {
916
+ }
917
+ }
918
+ }
919
+ // ── Private Helpers ────────────────────────────────────────
920
+ async getOrCreateAdapter() {
921
+ if (this._activeAdapter) return this._activeAdapter;
922
+ const factory = this._backends[this._currentBackend];
923
+ if (!factory) {
924
+ throw new ChatError(
925
+ `Backend "${this._currentBackend}" not found`,
926
+ { code: "INVALID_INPUT" /* INVALID_INPUT */ }
927
+ );
928
+ }
929
+ this._activeAdapter = await factory();
930
+ return this._activeAdapter;
931
+ }
932
+ /** Wrap each tool's execute to inject ToolContext as 2nd argument */
933
+ injectToolContext(tools, context) {
934
+ return tools.map((tool) => ({
935
+ ...tool,
936
+ execute: (params) => tool.execute(params, context)
937
+ }));
938
+ }
939
+ /** Map ChatEvent to AgentEvent for MessageAccumulator */
940
+ feedAccumulator(acc, event) {
941
+ const agentEvent = chatEventToAgentEvent(event);
942
+ if (agentEvent) acc.apply(agentEvent);
943
+ }
944
+ createUserMessage(text) {
945
+ return {
946
+ id: createChatId(),
947
+ role: "user",
948
+ parts: [{ type: "text", text, status: "complete" }],
949
+ createdAt: (/* @__PURE__ */ new Date()).toISOString(),
950
+ status: "complete"
951
+ };
952
+ }
953
+ assertNotDisposed() {
954
+ if (this._state.current === "disposed") {
955
+ throw new ChatError(
956
+ "Runtime is disposed",
957
+ { code: "DISPOSED" /* DISPOSED */ }
958
+ );
959
+ }
960
+ }
961
+ /** Get or create adapter with retry on connection errors */
962
+ async getOrCreateAdapterWithRetry() {
963
+ const maxAttempts = this._retryConfig?.maxAttempts ?? 1;
964
+ const delayMs = this._retryConfig?.delayMs ?? 0;
965
+ let lastError;
966
+ for (let attempt = 1; attempt <= maxAttempts; attempt++) {
967
+ try {
968
+ return await this.getOrCreateAdapter();
969
+ } catch (err) {
970
+ lastError = err instanceof Error ? err : new Error(String(err));
971
+ if (attempt < maxAttempts) {
972
+ this._activeAdapter = null;
973
+ await delay(delayMs);
974
+ }
975
+ }
976
+ }
977
+ throw lastError;
978
+ }
979
+ /**
980
+ * Create stream with retry for pre-stream connection errors.
981
+ * Tries to get the first event from the stream; if that fails,
982
+ * retries with a fresh adapter. Once first event is received,
983
+ * the stream is committed (no more retries).
984
+ */
985
+ async createStreamWithRetry(adapter, session, message, options) {
986
+ const maxAttempts = this._retryConfig?.maxAttempts ?? 1;
987
+ const delayMs = this._retryConfig?.delayMs ?? 0;
988
+ let lastError;
989
+ let currentAdapter = adapter;
990
+ for (let attempt = 1; attempt <= maxAttempts; attempt++) {
991
+ try {
992
+ const stream = currentAdapter.streamMessage(session, message, options);
993
+ const iterator = stream[Symbol.asyncIterator]();
994
+ const first = await iterator.next();
995
+ return (async function* () {
996
+ if (!first.done) yield first.value;
997
+ while (true) {
998
+ const next = await iterator.next();
999
+ if (next.done) break;
1000
+ yield next.value;
1001
+ }
1002
+ })();
1003
+ } catch (err) {
1004
+ lastError = err instanceof Error ? err : new Error(String(err));
1005
+ if (attempt < maxAttempts) {
1006
+ if (this._activeAdapter) {
1007
+ await this._activeAdapter.dispose().catch(() => {
1008
+ });
1009
+ }
1010
+ this._activeAdapter = null;
1011
+ await delay(delayMs);
1012
+ currentAdapter = await this.getOrCreateAdapter();
1013
+ }
1014
+ }
1015
+ }
1016
+ throw lastError;
1017
+ }
1018
+ };
1019
+ function delay(ms) {
1020
+ return new Promise((resolve) => setTimeout(resolve, ms));
1021
+ }
1022
+ function createChatRuntime(options) {
1023
+ return new ChatRuntime(options);
1024
+ }
1025
+
1026
+ export { createChatRuntime };
1027
+ //# sourceMappingURL=runtime.js.map
1028
+ //# sourceMappingURL=runtime.js.map