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