@witqq/agent-sdk 0.6.1 → 0.8.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 (145) hide show
  1. package/README.md +539 -6
  2. package/dist/{types-BvwNzZCj.d.cts → agent-CW9XbmG_.d.ts} +148 -95
  3. package/dist/{types-BvwNzZCj.d.ts → agent-DxY68NZL.d.cts} +148 -95
  4. package/dist/auth/index.cjs +260 -2
  5. package/dist/auth/index.cjs.map +1 -1
  6. package/dist/auth/index.d.cts +21 -138
  7. package/dist/auth/index.d.ts +21 -138
  8. package/dist/auth/index.js +260 -3
  9. package/dist/auth/index.js.map +1 -1
  10. package/dist/backends/claude.cjs +653 -140
  11. package/dist/backends/claude.cjs.map +1 -1
  12. package/dist/backends/claude.d.cts +4 -1
  13. package/dist/backends/claude.d.ts +4 -1
  14. package/dist/backends/claude.js +653 -140
  15. package/dist/backends/claude.js.map +1 -1
  16. package/dist/backends/copilot.cjs +428 -88
  17. package/dist/backends/copilot.cjs.map +1 -1
  18. package/dist/backends/copilot.d.cts +13 -4
  19. package/dist/backends/copilot.d.ts +13 -4
  20. package/dist/backends/copilot.js +428 -88
  21. package/dist/backends/copilot.js.map +1 -1
  22. package/dist/backends/vercel-ai.cjs +349 -77
  23. package/dist/backends/vercel-ai.cjs.map +1 -1
  24. package/dist/backends/vercel-ai.d.cts +3 -1
  25. package/dist/backends/vercel-ai.d.ts +3 -1
  26. package/dist/backends/vercel-ai.js +349 -77
  27. package/dist/backends/vercel-ai.js.map +1 -1
  28. package/dist/backends-BSrsBYFn.d.cts +39 -0
  29. package/dist/backends-BSrsBYFn.d.ts +39 -0
  30. package/dist/chat/accumulator.cjs +147 -0
  31. package/dist/chat/accumulator.cjs.map +1 -0
  32. package/dist/chat/accumulator.d.cts +64 -0
  33. package/dist/chat/accumulator.d.ts +64 -0
  34. package/dist/chat/accumulator.js +145 -0
  35. package/dist/chat/accumulator.js.map +1 -0
  36. package/dist/chat/backends.cjs +3524 -0
  37. package/dist/chat/backends.cjs.map +1 -0
  38. package/dist/chat/backends.d.cts +66 -0
  39. package/dist/chat/backends.d.ts +66 -0
  40. package/dist/chat/backends.js +3512 -0
  41. package/dist/chat/backends.js.map +1 -0
  42. package/dist/chat/context.cjs +280 -0
  43. package/dist/chat/context.cjs.map +1 -0
  44. package/dist/chat/context.d.cts +191 -0
  45. package/dist/chat/context.d.ts +191 -0
  46. package/dist/chat/context.js +277 -0
  47. package/dist/chat/context.js.map +1 -0
  48. package/dist/chat/core.cjs +305 -0
  49. package/dist/chat/core.cjs.map +1 -0
  50. package/dist/chat/core.d.cts +84 -0
  51. package/dist/chat/core.d.ts +84 -0
  52. package/dist/chat/core.js +282 -0
  53. package/dist/chat/core.js.map +1 -0
  54. package/dist/chat/errors.cjs +273 -0
  55. package/dist/chat/errors.cjs.map +1 -0
  56. package/dist/chat/errors.d.cts +97 -0
  57. package/dist/chat/errors.d.ts +97 -0
  58. package/dist/chat/errors.js +266 -0
  59. package/dist/chat/errors.js.map +1 -0
  60. package/dist/chat/events.cjs +203 -0
  61. package/dist/chat/events.cjs.map +1 -0
  62. package/dist/chat/events.d.cts +245 -0
  63. package/dist/chat/events.d.ts +245 -0
  64. package/dist/chat/events.js +196 -0
  65. package/dist/chat/events.js.map +1 -0
  66. package/dist/chat/index.cjs +5550 -0
  67. package/dist/chat/index.cjs.map +1 -0
  68. package/dist/chat/index.d.cts +77 -0
  69. package/dist/chat/index.d.ts +77 -0
  70. package/dist/chat/index.js +5505 -0
  71. package/dist/chat/index.js.map +1 -0
  72. package/dist/chat/react/theme.css +2517 -0
  73. package/dist/chat/react.cjs +3589 -0
  74. package/dist/chat/react.cjs.map +1 -0
  75. package/dist/chat/react.d.cts +1088 -0
  76. package/dist/chat/react.d.ts +1088 -0
  77. package/dist/chat/react.js +3547 -0
  78. package/dist/chat/react.js.map +1 -0
  79. package/dist/chat/runtime.cjs +1245 -0
  80. package/dist/chat/runtime.cjs.map +1 -0
  81. package/dist/chat/runtime.d.cts +182 -0
  82. package/dist/chat/runtime.d.ts +182 -0
  83. package/dist/chat/runtime.js +1243 -0
  84. package/dist/chat/runtime.js.map +1 -0
  85. package/dist/chat/server.cjs +2668 -0
  86. package/dist/chat/server.cjs.map +1 -0
  87. package/dist/chat/server.d.cts +648 -0
  88. package/dist/chat/server.d.ts +648 -0
  89. package/dist/chat/server.js +2628 -0
  90. package/dist/chat/server.js.map +1 -0
  91. package/dist/chat/sessions.cjs +380 -0
  92. package/dist/chat/sessions.cjs.map +1 -0
  93. package/dist/chat/sessions.d.cts +158 -0
  94. package/dist/chat/sessions.d.ts +158 -0
  95. package/dist/chat/sessions.js +376 -0
  96. package/dist/chat/sessions.js.map +1 -0
  97. package/dist/chat/sqlite.cjs +441 -0
  98. package/dist/chat/sqlite.cjs.map +1 -0
  99. package/dist/chat/sqlite.d.cts +128 -0
  100. package/dist/chat/sqlite.d.ts +128 -0
  101. package/dist/chat/sqlite.js +435 -0
  102. package/dist/chat/sqlite.js.map +1 -0
  103. package/dist/chat/state.cjs +190 -0
  104. package/dist/chat/state.cjs.map +1 -0
  105. package/dist/chat/state.d.cts +95 -0
  106. package/dist/chat/state.d.ts +95 -0
  107. package/dist/chat/state.js +180 -0
  108. package/dist/chat/state.js.map +1 -0
  109. package/dist/chat/storage.cjs +249 -0
  110. package/dist/chat/storage.cjs.map +1 -0
  111. package/dist/chat/storage.d.cts +197 -0
  112. package/dist/chat/storage.d.ts +197 -0
  113. package/dist/chat/storage.js +245 -0
  114. package/dist/chat/storage.js.map +1 -0
  115. package/dist/errors-C-so0M4t.d.cts +33 -0
  116. package/dist/errors-C-so0M4t.d.ts +33 -0
  117. package/dist/errors-CmVvczxZ.d.cts +28 -0
  118. package/dist/errors-CmVvczxZ.d.ts +28 -0
  119. package/dist/in-process-transport-C1JnJGVR.d.ts +228 -0
  120. package/dist/in-process-transport-C7DSqPyX.d.cts +228 -0
  121. package/dist/index.cjs +365 -59
  122. package/dist/index.cjs.map +1 -1
  123. package/dist/index.d.cts +322 -125
  124. package/dist/index.d.ts +322 -125
  125. package/dist/index.js +359 -60
  126. package/dist/index.js.map +1 -1
  127. package/dist/provider-types-PTSlRPNB.d.cts +39 -0
  128. package/dist/provider-types-PTSlRPNB.d.ts +39 -0
  129. package/dist/refresh-manager-B81PpYBr.d.cts +153 -0
  130. package/dist/refresh-manager-Dlv_iNZi.d.ts +153 -0
  131. package/dist/testing.cjs +383 -0
  132. package/dist/testing.cjs.map +1 -0
  133. package/dist/testing.d.cts +132 -0
  134. package/dist/testing.d.ts +132 -0
  135. package/dist/testing.js +377 -0
  136. package/dist/testing.js.map +1 -0
  137. package/dist/token-store-CSUBgYwn.d.ts +48 -0
  138. package/dist/token-store-CuC4hB9Z.d.cts +48 -0
  139. package/dist/transport-Cdh3M0tS.d.cts +68 -0
  140. package/dist/transport-Ciap4PWK.d.ts +68 -0
  141. package/dist/types-4vbcmPTp.d.cts +143 -0
  142. package/dist/types-BxggH0Yh.d.ts +143 -0
  143. package/dist/types-DRgd_9R7.d.cts +363 -0
  144. package/dist/types-ajANVzf7.d.ts +363 -0
  145. package/package.json +178 -6
@@ -0,0 +1,1245 @@
1
+ 'use strict';
2
+
3
+ // src/chat/types.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
+
15
+ // src/chat/bridge.ts
16
+ function chatEventToAgentEvent(event) {
17
+ switch (event.type) {
18
+ case "message:delta":
19
+ return { type: "text_delta", text: event.text };
20
+ case "thinking:start":
21
+ return { type: "thinking_start" };
22
+ case "thinking:delta":
23
+ return { type: "thinking_delta", text: event.text };
24
+ case "thinking:end":
25
+ return { type: "thinking_end" };
26
+ case "tool:start":
27
+ return {
28
+ type: "tool_call_start",
29
+ toolCallId: event.toolCallId,
30
+ toolName: event.toolName,
31
+ args: event.args
32
+ };
33
+ case "tool:complete":
34
+ return {
35
+ type: "tool_call_end",
36
+ toolCallId: event.toolCallId,
37
+ toolName: event.toolName,
38
+ result: event.result
39
+ };
40
+ case "error":
41
+ return { type: "error", error: event.error, recoverable: event.recoverable, code: event.code };
42
+ default:
43
+ return null;
44
+ }
45
+ }
46
+
47
+ // src/chat/context.ts
48
+ function estimateTokens(message, options) {
49
+ const ratio = options?.charsPerToken ?? 4;
50
+ let charCount = 0;
51
+ charCount += message.role.length + 4;
52
+ for (const part of message.parts) {
53
+ charCount += estimatePartChars(part);
54
+ }
55
+ return Math.ceil(charCount / ratio);
56
+ }
57
+ function estimatePartChars(part) {
58
+ switch (part.type) {
59
+ case "text":
60
+ return part.text.length;
61
+ case "reasoning":
62
+ return part.text.length;
63
+ case "tool_call":
64
+ return JSON.stringify(part.args).length + part.name.length + 20 + (part.result !== void 0 ? JSON.stringify(part.result).length : 0);
65
+ case "source":
66
+ return (part.title?.length ?? 0) + part.url.length + 10;
67
+ case "file":
68
+ return part.name.length + part.data.length + 20;
69
+ }
70
+ }
71
+ var ContextWindowManager = class {
72
+ config;
73
+ constructor(config) {
74
+ this.config = {
75
+ maxTokens: config.maxTokens,
76
+ reservedTokens: config.reservedTokens ?? 0,
77
+ strategy: config.strategy ?? "truncate-oldest",
78
+ estimation: config.estimation,
79
+ summarizer: config.summarizer
80
+ };
81
+ }
82
+ /** Available token budget after reserving tokens */
83
+ get availableBudget() {
84
+ return Math.max(0, this.config.maxTokens - this.config.reservedTokens);
85
+ }
86
+ /**
87
+ * Estimate tokens for a single message.
88
+ * @param message - Message to estimate
89
+ * @returns Estimated token count
90
+ */
91
+ estimateMessageTokens(message) {
92
+ return estimateTokens(message, this.config.estimation);
93
+ }
94
+ /**
95
+ * Fit messages within the token budget using the configured strategy.
96
+ * @param messages - All messages to consider
97
+ * @returns Result with fitted messages and metadata
98
+ */
99
+ fitMessages(messages) {
100
+ if (messages.length === 0) {
101
+ return { messages: [], totalTokens: 0, removedCount: 0, wasTruncated: false };
102
+ }
103
+ const budget = this.availableBudget;
104
+ const tokenCounts = messages.map((m) => this.estimateMessageTokens(m));
105
+ const totalTokens = tokenCounts.reduce((a, b) => a + b, 0);
106
+ if (totalTokens <= budget) {
107
+ return {
108
+ messages: [...messages],
109
+ totalTokens,
110
+ removedCount: 0,
111
+ wasTruncated: false
112
+ };
113
+ }
114
+ switch (this.config.strategy) {
115
+ case "truncate-oldest":
116
+ return this.truncateOldest(messages, tokenCounts, budget);
117
+ case "sliding-window":
118
+ return this.slidingWindow(messages, tokenCounts, budget);
119
+ case "summarize-placeholder":
120
+ return this.summarizePlaceholder(messages, tokenCounts, budget);
121
+ }
122
+ }
123
+ /**
124
+ * Async variant of fitMessages that supports async summarization.
125
+ * When strategy is "summarize-placeholder" and a summarizer is configured,
126
+ * calls the summarizer with removed messages and replaces the placeholder text.
127
+ * Falls back to static placeholder if summarizer throws.
128
+ * For other strategies, behaves identically to fitMessages().
129
+ */
130
+ async fitMessagesAsync(messages) {
131
+ const result = this.fitMessages(messages);
132
+ if (this.config.strategy !== "summarize-placeholder" || !result.wasTruncated || !this.config.summarizer) {
133
+ return result;
134
+ }
135
+ const keptIds = new Set(result.messages.map((m) => m.id));
136
+ const removed = messages.filter((m) => !keptIds.has(m.id));
137
+ if (removed.length === 0) return result;
138
+ let summaryText;
139
+ try {
140
+ summaryText = await this.config.summarizer(removed);
141
+ } catch {
142
+ return result;
143
+ }
144
+ const updatedMessages = result.messages.map((m) => {
145
+ if (m.metadata?.isSummary === true) {
146
+ return {
147
+ ...m,
148
+ parts: [{ type: "text", text: summaryText, status: "complete" }]
149
+ };
150
+ }
151
+ return m;
152
+ });
153
+ return { ...result, messages: updatedMessages };
154
+ }
155
+ /**
156
+ * Trim messages using real token usage data from the previous API call.
157
+ * Uses average-based algorithm: `avgTokensPerMessage = lastPromptTokens / messageCount`.
158
+ * Removes oldest non-system messages until freed budget brings usage under modelContextWindow.
159
+ *
160
+ * @param messages - All messages in the session
161
+ * @param lastPromptTokens - Real prompt tokens from the last API response
162
+ * @param modelContextWindow - Model's total context window size in tokens
163
+ * @returns Result with fitted messages and metadata
164
+ */
165
+ fitMessagesWithUsage(messages, lastPromptTokens, modelContextWindow) {
166
+ if (messages.length === 0) {
167
+ return { messages: [], totalTokens: 0, removedCount: 0, wasTruncated: false };
168
+ }
169
+ const budget = modelContextWindow - this.config.reservedTokens;
170
+ if (budget <= 0 || lastPromptTokens <= budget) {
171
+ return {
172
+ messages: [...messages],
173
+ totalTokens: lastPromptTokens,
174
+ removedCount: 0,
175
+ wasTruncated: false
176
+ };
177
+ }
178
+ const avgTokensPerMessage = lastPromptTokens / messages.length;
179
+ const tokensToFree = lastPromptTokens - budget;
180
+ const messagesToRemove = Math.ceil(tokensToFree / avgTokensPerMessage);
181
+ const nonSystemIndices = [];
182
+ for (let i = 0; i < messages.length; i++) {
183
+ if (messages[i].role === "system") ; else {
184
+ nonSystemIndices.push(i);
185
+ }
186
+ }
187
+ const removableCount = Math.min(messagesToRemove, nonSystemIndices.length);
188
+ const removedIndices = new Set(nonSystemIndices.slice(0, removableCount));
189
+ const result = [];
190
+ for (let i = 0; i < messages.length; i++) {
191
+ if (!removedIndices.has(i)) {
192
+ result.push(messages[i]);
193
+ }
194
+ }
195
+ const estimatedTokens = Math.round(
196
+ lastPromptTokens * (result.length / messages.length)
197
+ );
198
+ return {
199
+ messages: result,
200
+ totalTokens: estimatedTokens,
201
+ removedCount: removableCount,
202
+ wasTruncated: removableCount > 0
203
+ };
204
+ }
205
+ /**
206
+ * Truncate oldest: keeps system messages, removes oldest non-system messages first.
207
+ * Always keeps the most recent user message.
208
+ */
209
+ truncateOldest(messages, tokenCounts, budget) {
210
+ const systemIndices = [];
211
+ const nonSystemIndices = [];
212
+ for (let i = 0; i < messages.length; i++) {
213
+ if (messages[i].role === "system") {
214
+ systemIndices.push(i);
215
+ } else {
216
+ nonSystemIndices.push(i);
217
+ }
218
+ }
219
+ let usedTokens = systemIndices.reduce(
220
+ (sum, i) => sum + tokenCounts[i],
221
+ 0
222
+ );
223
+ const includedNonSystem = [];
224
+ for (let i = nonSystemIndices.length - 1; i >= 0; i--) {
225
+ const idx = nonSystemIndices[i];
226
+ if (usedTokens + tokenCounts[idx] <= budget) {
227
+ includedNonSystem.unshift(idx);
228
+ usedTokens += tokenCounts[idx];
229
+ }
230
+ }
231
+ const includedSet = /* @__PURE__ */ new Set([...systemIndices, ...includedNonSystem]);
232
+ const result = [];
233
+ let resultTokens = 0;
234
+ for (let i = 0; i < messages.length; i++) {
235
+ if (includedSet.has(i)) {
236
+ result.push(messages[i]);
237
+ resultTokens += tokenCounts[i];
238
+ }
239
+ }
240
+ return {
241
+ messages: result,
242
+ totalTokens: resultTokens,
243
+ removedCount: messages.length - result.length,
244
+ wasTruncated: true
245
+ };
246
+ }
247
+ /**
248
+ * Sliding window: keeps the most recent messages that fit within budget.
249
+ */
250
+ slidingWindow(messages, tokenCounts, budget) {
251
+ const result = [];
252
+ let usedTokens = 0;
253
+ for (let i = messages.length - 1; i >= 0; i--) {
254
+ if (usedTokens + tokenCounts[i] <= budget) {
255
+ result.unshift(messages[i]);
256
+ usedTokens += tokenCounts[i];
257
+ } else {
258
+ break;
259
+ }
260
+ }
261
+ return {
262
+ messages: result,
263
+ totalTokens: usedTokens,
264
+ removedCount: messages.length - result.length,
265
+ wasTruncated: true
266
+ };
267
+ }
268
+ /**
269
+ * Summarize placeholder: replaces truncated messages with a placeholder,
270
+ * preserving system messages and recent context.
271
+ */
272
+ summarizePlaceholder(messages, tokenCounts, budget) {
273
+ const systemMessages = [];
274
+ const nonSystem = [];
275
+ for (let i = 0; i < messages.length; i++) {
276
+ if (messages[i].role === "system") {
277
+ systemMessages.push({ msg: messages[i], tokens: tokenCounts[i] });
278
+ } else {
279
+ nonSystem.push({ msg: messages[i], tokens: tokenCounts[i], idx: i });
280
+ }
281
+ }
282
+ let usedTokens = systemMessages.reduce((s, m) => s + m.tokens, 0);
283
+ const placeholderTokens = 20;
284
+ usedTokens += placeholderTokens;
285
+ const recentKept = [];
286
+ for (let i = nonSystem.length - 1; i >= 0; i--) {
287
+ if (usedTokens + nonSystem[i].tokens <= budget) {
288
+ recentKept.unshift(nonSystem[i]);
289
+ usedTokens += nonSystem[i].tokens;
290
+ } else {
291
+ break;
292
+ }
293
+ }
294
+ const removedCount = messages.length - systemMessages.length - recentKept.length;
295
+ const result = [];
296
+ for (const sm of systemMessages) {
297
+ result.push(sm.msg);
298
+ }
299
+ if (removedCount > 0) {
300
+ result.push({
301
+ id: "context-placeholder",
302
+ role: "system",
303
+ parts: [{ type: "text", text: `[${removedCount} earlier message${removedCount === 1 ? "" : "s"} omitted for context window]`, status: "complete" }],
304
+ metadata: { isSummary: true },
305
+ createdAt: (/* @__PURE__ */ new Date()).toISOString(),
306
+ status: "complete"
307
+ });
308
+ }
309
+ for (const m of recentKept) {
310
+ result.push(m.msg);
311
+ }
312
+ return {
313
+ messages: result,
314
+ totalTokens: usedTokens,
315
+ removedCount,
316
+ wasTruncated: true
317
+ };
318
+ }
319
+ };
320
+
321
+ // src/errors.ts
322
+ var AgentSDKError = class extends Error {
323
+ /** @internal Marker for cross-bundle identity checks */
324
+ _agentSDKError = true;
325
+ /** Machine-readable error code. Prefer values from the ErrorCode enum. */
326
+ code;
327
+ /** Whether this error is safe to retry */
328
+ retryable;
329
+ /** HTTP status code hint for error classification */
330
+ httpStatus;
331
+ constructor(message, options) {
332
+ super(message, options);
333
+ this.name = "AgentSDKError";
334
+ this.code = options?.code;
335
+ this.retryable = options?.retryable ?? false;
336
+ this.httpStatus = options?.httpStatus;
337
+ }
338
+ /** Check if an error is an AgentSDKError (works across bundled copies) */
339
+ static is(error) {
340
+ return error instanceof Error && "_agentSDKError" in error && error._agentSDKError === true;
341
+ }
342
+ };
343
+
344
+ // src/chat/errors.ts
345
+ var ChatError = class extends AgentSDKError {
346
+ code;
347
+ retryable;
348
+ retryAfter;
349
+ timestamp;
350
+ constructor(message, options) {
351
+ super(message, {
352
+ cause: options.cause,
353
+ code: options.code,
354
+ retryable: options.retryable
355
+ });
356
+ this.name = "ChatError";
357
+ this.code = options.code;
358
+ this.retryable = options.retryable ?? false;
359
+ this.retryAfter = options.retryAfter;
360
+ this.timestamp = (/* @__PURE__ */ new Date()).toISOString();
361
+ }
362
+ };
363
+
364
+ // src/chat/state.ts
365
+ var StateMachine = class {
366
+ constructor(initial, transitions) {
367
+ this.initial = initial;
368
+ this.transitions = transitions;
369
+ this._current = initial;
370
+ }
371
+ _current;
372
+ /** Current state */
373
+ get current() {
374
+ return this._current;
375
+ }
376
+ /**
377
+ * Check whether transitioning to `next` is allowed from current state
378
+ * @param next - Target state to check
379
+ * @returns True if transition is allowed
380
+ */
381
+ canTransition(next) {
382
+ const allowed = this.transitions[this._current];
383
+ return allowed !== void 0 && allowed.includes(next);
384
+ }
385
+ /**
386
+ * Transition to `next` state.
387
+ * @throws ChatError(INVALID_TRANSITION) if the transition is not allowed
388
+ */
389
+ transition(next) {
390
+ if (!this.canTransition(next)) {
391
+ throw new ChatError(
392
+ `Invalid transition: ${this._current} \u2192 ${next}`,
393
+ { code: "INVALID_TRANSITION" /* INVALID_TRANSITION */ }
394
+ );
395
+ }
396
+ this._current = next;
397
+ }
398
+ /** Reset to initial state */
399
+ reset() {
400
+ this._current = this.initial;
401
+ }
402
+ };
403
+ var RUNTIME_TRANSITIONS = {
404
+ idle: ["streaming", "disposed"],
405
+ streaming: ["idle", "error", "disposed"],
406
+ error: ["idle", "disposed"],
407
+ disposed: []
408
+ };
409
+ var ChatReentrancyGuard = class {
410
+ _acquired = false;
411
+ /** Whether the guard is currently held */
412
+ get isAcquired() {
413
+ return this._acquired;
414
+ }
415
+ /**
416
+ * Acquire the guard. Throws if already acquired.
417
+ * @throws ChatError with code REENTRANCY
418
+ */
419
+ acquire() {
420
+ if (this._acquired) {
421
+ throw new ChatError(
422
+ "Concurrent operation detected: a send is already in progress",
423
+ { code: "REENTRANCY" /* REENTRANCY */ }
424
+ );
425
+ }
426
+ this._acquired = true;
427
+ }
428
+ /** Release the guard. Safe to call even if not acquired. */
429
+ release() {
430
+ this._acquired = false;
431
+ }
432
+ };
433
+ var ChatAbortController = class {
434
+ _controller;
435
+ _onExternalAbort;
436
+ _externalSignal;
437
+ constructor(externalSignal) {
438
+ this._controller = new AbortController();
439
+ this._externalSignal = externalSignal;
440
+ if (externalSignal) {
441
+ if (externalSignal.aborted) {
442
+ this._controller.abort(externalSignal.reason);
443
+ } else {
444
+ this._onExternalAbort = () => {
445
+ this._controller.abort(externalSignal.reason);
446
+ };
447
+ externalSignal.addEventListener("abort", this._onExternalAbort, { once: true });
448
+ }
449
+ }
450
+ }
451
+ /** The AbortSignal for this controller */
452
+ get signal() {
453
+ return this._controller.signal;
454
+ }
455
+ /** Whether the operation has been aborted */
456
+ get isAborted() {
457
+ return this._controller.signal.aborted;
458
+ }
459
+ /**
460
+ * Abort the operation.
461
+ * @param reason - Optional abort reason
462
+ */
463
+ abort(reason) {
464
+ this._controller.abort(reason);
465
+ }
466
+ /** Clean up external signal listener to prevent memory leaks */
467
+ dispose() {
468
+ if (this._onExternalAbort && this._externalSignal) {
469
+ this._externalSignal.removeEventListener("abort", this._onExternalAbort);
470
+ }
471
+ }
472
+ };
473
+
474
+ // src/chat/accumulator.ts
475
+ var MessageAccumulator = class {
476
+ messageId;
477
+ parts = [];
478
+ status = "pending";
479
+ currentTextPart = null;
480
+ currentReasoningPart = null;
481
+ toolCallParts = /* @__PURE__ */ new Map();
482
+ _finalized = false;
483
+ constructor(messageId) {
484
+ this.messageId = messageId ?? createChatId();
485
+ }
486
+ /** Get current message ID */
487
+ get id() {
488
+ return this.messageId;
489
+ }
490
+ /**
491
+ * Apply an AgentEvent to accumulate into the message
492
+ * @param event - AgentEvent to process
493
+ * @throws Error if accumulator is already finalized
494
+ */
495
+ apply(event) {
496
+ if (this._finalized) throw new Error("Cannot apply events to finalized accumulator");
497
+ if (this.status === "pending") {
498
+ this.status = "streaming";
499
+ }
500
+ switch (event.type) {
501
+ case "text_delta":
502
+ this.handleTextDelta(event.text);
503
+ break;
504
+ case "thinking_start":
505
+ this.finalizeCurrentText();
506
+ this.currentReasoningPart = { type: "reasoning", text: "", status: "streaming" };
507
+ this.parts.push(this.currentReasoningPart);
508
+ break;
509
+ case "thinking_delta":
510
+ if (this.currentReasoningPart) {
511
+ this.currentReasoningPart.text += event.text;
512
+ }
513
+ break;
514
+ case "thinking_end":
515
+ if (this.currentReasoningPart) {
516
+ this.currentReasoningPart.status = "complete";
517
+ this.currentReasoningPart = null;
518
+ }
519
+ break;
520
+ case "tool_call_start": {
521
+ this.finalizeCurrentText();
522
+ const toolPart = {
523
+ type: "tool_call",
524
+ toolCallId: event.toolCallId,
525
+ name: event.toolName,
526
+ args: event.args,
527
+ status: "running"
528
+ };
529
+ this.toolCallParts.set(event.toolCallId, toolPart);
530
+ this.parts.push(toolPart);
531
+ break;
532
+ }
533
+ case "tool_call_end": {
534
+ const existing = this.toolCallParts.get(event.toolCallId);
535
+ if (existing) {
536
+ existing.result = event.result;
537
+ existing.status = "complete";
538
+ }
539
+ break;
540
+ }
541
+ case "error":
542
+ this.status = "error";
543
+ break;
544
+ }
545
+ }
546
+ handleTextDelta(text) {
547
+ if (!this.currentTextPart) {
548
+ this.currentTextPart = { type: "text", text: "", status: "streaming" };
549
+ this.parts.push(this.currentTextPart);
550
+ }
551
+ this.currentTextPart.text += text;
552
+ }
553
+ finalizeCurrentText() {
554
+ if (this.currentTextPart) {
555
+ this.currentTextPart.status = "complete";
556
+ this.currentTextPart = null;
557
+ }
558
+ }
559
+ /**
560
+ * Get a snapshot of the current accumulated message (for streaming UI)
561
+ * @returns ChatMessage with current parts and "streaming" status
562
+ */
563
+ snapshot() {
564
+ const now = (/* @__PURE__ */ new Date()).toISOString();
565
+ return {
566
+ id: this.messageId,
567
+ role: "assistant",
568
+ parts: this.parts.map((p) => ({ ...p })),
569
+ status: this.status === "pending" ? "pending" : "streaming",
570
+ createdAt: now,
571
+ updatedAt: now
572
+ };
573
+ }
574
+ /**
575
+ * Finalize the accumulator and return the complete ChatMessage
576
+ * @returns Completed ChatMessage with all parts finalized
577
+ * @throws Error if accumulator is already finalized
578
+ */
579
+ finalize() {
580
+ if (this._finalized) throw new Error("Accumulator already finalized");
581
+ this._finalized = true;
582
+ this.finalizeCurrentText();
583
+ if (this.currentReasoningPart) {
584
+ this.currentReasoningPart.status = "complete";
585
+ this.currentReasoningPart = null;
586
+ }
587
+ for (const [, toolPart] of this.toolCallParts) {
588
+ if (toolPart.status === "running" || toolPart.status === "pending") {
589
+ toolPart.status = "error";
590
+ }
591
+ }
592
+ if (this.status !== "error" && this.status !== "cancelled") {
593
+ this.status = "complete";
594
+ }
595
+ const now = (/* @__PURE__ */ new Date()).toISOString();
596
+ return {
597
+ id: this.messageId,
598
+ role: "assistant",
599
+ parts: this.parts,
600
+ status: this.status,
601
+ createdAt: now,
602
+ updatedAt: now
603
+ };
604
+ }
605
+ /** Check if the accumulator has been finalized */
606
+ get finalized() {
607
+ return this._finalized;
608
+ }
609
+ };
610
+
611
+ // src/chat/watchdog.ts
612
+ async function* withStreamWatchdog(source, config) {
613
+ const { timeoutMs, signal } = config;
614
+ const iterator = source[Symbol.asyncIterator]();
615
+ let aborted = false;
616
+ if (signal?.aborted) {
617
+ iterator.return?.();
618
+ return;
619
+ }
620
+ const onAbort = () => {
621
+ aborted = true;
622
+ iterator.return?.();
623
+ };
624
+ signal?.addEventListener("abort", onAbort, { once: true });
625
+ try {
626
+ while (true) {
627
+ if (aborted) break;
628
+ const timeout = new CancellableTimeout(timeoutMs);
629
+ try {
630
+ const result = await Promise.race([
631
+ iterator.next(),
632
+ timeout.promise
633
+ ]);
634
+ timeout.cancel();
635
+ if (result.done) break;
636
+ yield result.value;
637
+ } catch (err) {
638
+ timeout.cancel();
639
+ throw err;
640
+ }
641
+ }
642
+ } finally {
643
+ signal?.removeEventListener("abort", onAbort);
644
+ iterator.return?.();
645
+ }
646
+ }
647
+ var CancellableTimeout = class {
648
+ promise;
649
+ _timer;
650
+ _cancelled = false;
651
+ constructor(ms) {
652
+ this.promise = new Promise((_, reject) => {
653
+ this._timer = setTimeout(() => {
654
+ if (!this._cancelled) {
655
+ reject(
656
+ new ChatError(
657
+ `Stream timed out after ${ms}ms of inactivity`,
658
+ { code: "TIMEOUT" /* TIMEOUT */ }
659
+ )
660
+ );
661
+ }
662
+ }, ms);
663
+ });
664
+ this.promise.catch(() => {
665
+ });
666
+ }
667
+ cancel() {
668
+ this._cancelled = true;
669
+ if (this._timer !== void 0) {
670
+ clearTimeout(this._timer);
671
+ this._timer = void 0;
672
+ }
673
+ }
674
+ };
675
+
676
+ // src/chat/listener-set.ts
677
+ var ListenerSet = class {
678
+ _listeners = /* @__PURE__ */ new Set();
679
+ /** Add a listener. Returns an unsubscribe function. */
680
+ add(callback) {
681
+ this._listeners.add(callback);
682
+ return () => {
683
+ this._listeners.delete(callback);
684
+ };
685
+ }
686
+ /** Notify all listeners with the given arguments. Errors are isolated per listener. */
687
+ notify(...args) {
688
+ for (const cb of this._listeners) {
689
+ try {
690
+ cb(...args);
691
+ } catch {
692
+ }
693
+ }
694
+ }
695
+ /** Remove all listeners. */
696
+ clear() {
697
+ this._listeners.clear();
698
+ }
699
+ /** Current number of listeners. */
700
+ get size() {
701
+ return this._listeners.size;
702
+ }
703
+ };
704
+
705
+ // src/chat/runtime.ts
706
+ var ChatRuntime = class {
707
+ _state;
708
+ _guard;
709
+ _backends;
710
+ _sessionStore;
711
+ _contextConfig;
712
+ _middleware;
713
+ _tools = /* @__PURE__ */ new Map();
714
+ _retryConfig;
715
+ _contextStats = /* @__PURE__ */ new Map();
716
+ _sessionUsage = /* @__PURE__ */ new Map();
717
+ _modelContextWindows = /* @__PURE__ */ new Map();
718
+ _onContextTrimmed;
719
+ _streamTimeoutMs;
720
+ _sessionListeners = new ListenerSet();
721
+ _adapterPool = /* @__PURE__ */ new Map();
722
+ _defaultBackend;
723
+ _abortController = null;
724
+ constructor(options) {
725
+ this._state = new StateMachine("idle", RUNTIME_TRANSITIONS);
726
+ this._guard = new ChatReentrancyGuard();
727
+ this._backends = options.backends;
728
+ this._defaultBackend = options.defaultBackend;
729
+ this._sessionStore = options.sessionStore;
730
+ this._contextConfig = options.context;
731
+ this._middleware = [...options.middleware ?? []];
732
+ this._retryConfig = options.retryConfig;
733
+ this._onContextTrimmed = options.onContextTrimmed;
734
+ this._streamTimeoutMs = options.streamTimeoutMs;
735
+ if (!options.backends[options.defaultBackend]) {
736
+ throw new ChatError(
737
+ `Default backend "${options.defaultBackend}" not found in backends map`,
738
+ { code: "INVALID_INPUT" /* INVALID_INPUT */ }
739
+ );
740
+ }
741
+ if (options.tools) {
742
+ for (const tool of options.tools) {
743
+ this._tools.set(tool.name, tool);
744
+ }
745
+ }
746
+ }
747
+ // ── Lifecycle ──────────────────────────────────────────────
748
+ get status() {
749
+ return this._state.current;
750
+ }
751
+ async dispose() {
752
+ if (this._state.current === "disposed") return;
753
+ this._abortController?.abort("Runtime disposed");
754
+ this._abortController?.dispose();
755
+ this._abortController = null;
756
+ this._state.transition("disposed");
757
+ for (const adapter of this._adapterPool.values()) {
758
+ try {
759
+ await adapter.dispose();
760
+ } catch {
761
+ }
762
+ }
763
+ this._adapterPool.clear();
764
+ }
765
+ // ── Sessions ───────────────────────────────────────────────
766
+ async createSession(options) {
767
+ this.assertNotDisposed();
768
+ const config = {
769
+ model: options.config?.model ?? "",
770
+ backend: options.config?.backend ?? this._defaultBackend,
771
+ ...options.config
772
+ };
773
+ const session = await this._sessionStore.createSession({ ...options, config });
774
+ this._notifySessionChange();
775
+ return session;
776
+ }
777
+ async getSession(id) {
778
+ this.assertNotDisposed();
779
+ const cid = toChatId(id);
780
+ return this._sessionStore.getSession(cid);
781
+ }
782
+ async listSessions(options) {
783
+ this.assertNotDisposed();
784
+ return this._sessionStore.listSessions(options);
785
+ }
786
+ async deleteSession(id) {
787
+ this.assertNotDisposed();
788
+ const cid = toChatId(id);
789
+ const session = await this._sessionStore.getSession(cid);
790
+ if (!session) return;
791
+ await this._sessionStore.deleteSession(cid);
792
+ this._contextStats.delete(cid);
793
+ this._sessionUsage.delete(cid);
794
+ this._notifySessionChange();
795
+ }
796
+ // ── Messaging ──────────────────────────────────────────────
797
+ async *send(sessionId, message, options) {
798
+ this.validateSendInput(message, options);
799
+ this._guard.acquire();
800
+ const cid = toChatId(sessionId);
801
+ this._abortController = new ChatAbortController(options?.signal);
802
+ try {
803
+ if (this._state.current === "error") {
804
+ this._state.transition("idle");
805
+ }
806
+ this._state.transition("streaming");
807
+ await this.loadSession(cid);
808
+ const mwCtx = {
809
+ sessionId: cid,
810
+ signal: this._abortController.signal
811
+ };
812
+ const userMessage = await this.applyBeforeSendMiddleware(
813
+ this.createUserMessage(message),
814
+ mwCtx
815
+ );
816
+ if (userMessage === null) {
817
+ this._state.transition("idle");
818
+ return;
819
+ }
820
+ const updatedSession = await this.persistAndReload(cid, userMessage);
821
+ const sessionForAdapter = await this.trimSessionContext(cid, updatedSession, options.model);
822
+ const stream = await this.prepareEventStream(
823
+ cid,
824
+ sessionForAdapter,
825
+ updatedSession,
826
+ message,
827
+ options
828
+ );
829
+ const accumulator = new MessageAccumulator();
830
+ const eventSource = this._streamTimeoutMs ? withStreamWatchdog(stream, { timeoutMs: this._streamTimeoutMs, signal: this._abortController.signal }) : stream;
831
+ for await (const event of eventSource) {
832
+ if (this._abortController.isAborted) break;
833
+ this.feedAccumulator(accumulator, event);
834
+ if (event.type === "usage") {
835
+ this._sessionUsage.set(cid, {
836
+ promptTokens: event.promptTokens,
837
+ completionTokens: event.completionTokens
838
+ });
839
+ this.updateContextStatsWithUsage(cid, event.promptTokens, event.completionTokens, options);
840
+ }
841
+ const processed = await this.applyOnEventMiddleware(event, mwCtx);
842
+ if (processed) yield processed;
843
+ }
844
+ if (this._state.current === "disposed") return;
845
+ await this.finalizeAssistantMessage(cid, accumulator, mwCtx);
846
+ this._state.transition("idle");
847
+ } catch (error) {
848
+ const result = await this.handleSendError(error, cid);
849
+ if (result !== null) throw result;
850
+ } finally {
851
+ this._guard.release();
852
+ this._abortController?.dispose();
853
+ this._abortController = null;
854
+ }
855
+ }
856
+ // ── Send Pipeline Stages ──────────────────────────────────────
857
+ /** Stage 1: Validate send inputs (message content + required fields). */
858
+ validateSendInput(message, options) {
859
+ this.assertNotDisposed();
860
+ if (!message || message.trim().length === 0) {
861
+ throw new ChatError("Message cannot be empty", { code: "INVALID_INPUT" /* INVALID_INPUT */ });
862
+ }
863
+ if (!options.model) {
864
+ throw new ChatError(
865
+ "options.model is required \u2014 caller must specify which model to use",
866
+ { code: "INVALID_INPUT" /* INVALID_INPUT */ }
867
+ );
868
+ }
869
+ if (!options.backend) {
870
+ throw new ChatError(
871
+ "options.backend is required \u2014 caller must specify which backend to use",
872
+ { code: "INVALID_INPUT" /* INVALID_INPUT */ }
873
+ );
874
+ }
875
+ if (!options.credentials) {
876
+ throw new ChatError(
877
+ "options.credentials is required \u2014 caller must provide authentication credentials",
878
+ { code: "INVALID_INPUT" /* INVALID_INPUT */ }
879
+ );
880
+ }
881
+ }
882
+ /** Stage 2: Load session from store. */
883
+ async loadSession(cid) {
884
+ const session = await this._sessionStore.getSession(cid);
885
+ if (!session) {
886
+ throw new ChatError(
887
+ `Session "${cid}" not found`,
888
+ { code: "SESSION_NOT_FOUND" /* SESSION_NOT_FOUND */ }
889
+ );
890
+ }
891
+ return session;
892
+ }
893
+ /** Stage 3: Apply onBeforeSend middleware pipeline. Returns null if middleware rejected the send. */
894
+ async applyBeforeSendMiddleware(userMessage, ctx) {
895
+ let msg = userMessage;
896
+ for (const mw of this._middleware) {
897
+ if (mw.onBeforeSend && msg) {
898
+ msg = await mw.onBeforeSend(msg, ctx);
899
+ if (msg === null) return null;
900
+ }
901
+ }
902
+ return msg;
903
+ }
904
+ /** Stage 4: Persist user message and reload session with full history. */
905
+ async persistAndReload(cid, userMessage) {
906
+ await this._sessionStore.appendMessage(cid, userMessage);
907
+ return await this._sessionStore.getSession(cid);
908
+ }
909
+ /** Stage 5: Auto-trim context window if configured. Returns session snapshot for adapter. */
910
+ async trimSessionContext(cid, session, model) {
911
+ if (!this._contextConfig) return session;
912
+ const ctxManager = new ContextWindowManager(this._contextConfig);
913
+ const lastUsage = this._sessionUsage.get(cid);
914
+ const modelContextWindow = model ? this._modelContextWindows.get(model) : void 0;
915
+ if (lastUsage && modelContextWindow) {
916
+ const result2 = ctxManager.fitMessagesWithUsage(
917
+ session.messages,
918
+ lastUsage.promptTokens,
919
+ modelContextWindow
920
+ );
921
+ this._contextStats.set(cid, {
922
+ totalTokens: result2.totalTokens,
923
+ removedCount: result2.removedCount,
924
+ wasTruncated: result2.wasTruncated,
925
+ availableBudget: Math.max(0, modelContextWindow - result2.totalTokens),
926
+ realPromptTokens: lastUsage.promptTokens,
927
+ realCompletionTokens: lastUsage.completionTokens,
928
+ modelContextWindow
929
+ });
930
+ if (result2.wasTruncated && this._onContextTrimmed) {
931
+ const keptIds = new Set(result2.messages.map((m) => m.id));
932
+ const removed = session.messages.filter((m) => !keptIds.has(m.id));
933
+ if (removed.length > 0) {
934
+ try {
935
+ this._onContextTrimmed(cid, removed);
936
+ } catch {
937
+ }
938
+ }
939
+ }
940
+ return { ...session, messages: result2.messages };
941
+ }
942
+ const result = await ctxManager.fitMessagesAsync(session.messages);
943
+ this._contextStats.set(cid, {
944
+ totalTokens: result.totalTokens,
945
+ removedCount: result.removedCount,
946
+ wasTruncated: result.wasTruncated,
947
+ availableBudget: ctxManager.availableBudget,
948
+ modelContextWindow
949
+ });
950
+ if (result.wasTruncated && this._onContextTrimmed) {
951
+ const keptIds = new Set(result.messages.map((m) => m.id));
952
+ const removed = session.messages.filter((m) => !keptIds.has(m.id));
953
+ if (removed.length > 0) {
954
+ try {
955
+ this._onContextTrimmed(cid, removed);
956
+ } catch {
957
+ }
958
+ }
959
+ }
960
+ return { ...session, messages: result.messages };
961
+ }
962
+ /** Update context stats with real usage data from a usage event. */
963
+ updateContextStatsWithUsage(cid, promptTokens, completionTokens, options) {
964
+ const modelContextWindow = options.model ? this._modelContextWindows.get(options.model) : void 0;
965
+ const existing = this._contextStats.get(cid);
966
+ this._contextStats.set(cid, {
967
+ totalTokens: promptTokens,
968
+ removedCount: existing?.removedCount ?? 0,
969
+ wasTruncated: existing?.wasTruncated ?? false,
970
+ availableBudget: modelContextWindow ? Math.max(0, modelContextWindow - promptTokens) : existing?.availableBudget ?? 0,
971
+ realPromptTokens: promptTokens,
972
+ realCompletionTokens: completionTokens,
973
+ modelContextWindow
974
+ });
975
+ }
976
+ /** Stage 6: Prepare event stream — adapter with retry, tool injection. */
977
+ async prepareEventStream(cid, sessionForAdapter, fullSession, message, options) {
978
+ const adapter = await this.getOrCreateAdapterWithRetry(options.backend, options.credentials);
979
+ const runtimeTools = this._tools.size > 0 ? this.injectToolContext([...this._tools.values()], {
980
+ sessionId: cid,
981
+ custom: fullSession.metadata?.custom
982
+ }) : void 0;
983
+ const streamOptions = {
984
+ signal: this._abortController.signal,
985
+ model: options.model,
986
+ systemPrompt: options.systemPrompt,
987
+ tools: runtimeTools
988
+ };
989
+ return this.createStreamWithRetry(
990
+ adapter,
991
+ sessionForAdapter,
992
+ message,
993
+ streamOptions,
994
+ options.backend,
995
+ options.credentials
996
+ );
997
+ }
998
+ /** Stage 7: Apply onEvent middleware pipeline (sequential transform/suppress). */
999
+ async applyOnEventMiddleware(event, ctx) {
1000
+ let processed = event;
1001
+ for (const mw of this._middleware) {
1002
+ if (mw.onEvent && processed) {
1003
+ processed = await mw.onEvent(processed, ctx);
1004
+ }
1005
+ }
1006
+ return processed;
1007
+ }
1008
+ /** Stage 8: Finalize accumulator, apply afterReceive middleware, persist assistant message. */
1009
+ async finalizeAssistantMessage(cid, accumulator, ctx) {
1010
+ let assistantMessage = accumulator.finalize();
1011
+ for (const mw of this._middleware) {
1012
+ if (mw.onAfterReceive) {
1013
+ assistantMessage = await mw.onAfterReceive(assistantMessage, ctx);
1014
+ }
1015
+ }
1016
+ await this._sessionStore.appendMessage(cid, assistantMessage);
1017
+ this._notifySessionChange();
1018
+ }
1019
+ /** Stage 9: Error handling — apply onError middleware, transition state. Returns null if suppressed. */
1020
+ async handleSendError(error, cid) {
1021
+ let processedError = error instanceof Error ? error : new Error(String(error));
1022
+ const ctx = {
1023
+ sessionId: cid,
1024
+ signal: this._abortController?.signal ?? new AbortController().signal
1025
+ };
1026
+ for (const mw of this._middleware) {
1027
+ if (mw.onError) {
1028
+ const result = await mw.onError(processedError, ctx);
1029
+ if (result === null) {
1030
+ if (this._state.canTransition("idle")) {
1031
+ this._state.transition("idle");
1032
+ }
1033
+ return null;
1034
+ }
1035
+ processedError = result;
1036
+ }
1037
+ }
1038
+ if (this._state.canTransition("error")) {
1039
+ this._state.transition("error");
1040
+ }
1041
+ return processedError;
1042
+ }
1043
+ abort() {
1044
+ this._abortController?.abort("User abort");
1045
+ }
1046
+ // ── Backend / Model ────────────────────────────────────────
1047
+ async listModels(options) {
1048
+ this.assertNotDisposed();
1049
+ let models = [];
1050
+ const firstAdapter = [...this._adapterPool.values()][0];
1051
+ if (firstAdapter) {
1052
+ try {
1053
+ models = await firstAdapter.listModels();
1054
+ } catch {
1055
+ return [];
1056
+ }
1057
+ } else if (options?.backend && options?.credentials) {
1058
+ try {
1059
+ const adapter = await this.getOrCreateAdapter(options.backend, options.credentials);
1060
+ models = await adapter.listModels();
1061
+ } catch {
1062
+ return [];
1063
+ }
1064
+ }
1065
+ for (const model of models) {
1066
+ if (model.contextWindow != null) {
1067
+ this._modelContextWindows.set(model.id, model.contextWindow);
1068
+ }
1069
+ }
1070
+ return models;
1071
+ }
1072
+ async listBackends() {
1073
+ this.assertNotDisposed();
1074
+ return Object.keys(this._backends).map((name) => ({ name }));
1075
+ }
1076
+ // ── Tools ──────────────────────────────────────────────────
1077
+ get registeredTools() {
1078
+ return this._tools;
1079
+ }
1080
+ registerTool(tool) {
1081
+ this.assertNotDisposed();
1082
+ this._tools.set(tool.name, tool);
1083
+ }
1084
+ removeTool(name) {
1085
+ this.assertNotDisposed();
1086
+ this._tools.delete(name);
1087
+ }
1088
+ // ── Middleware ──────────────────────────────────────────────
1089
+ use(middleware) {
1090
+ this.assertNotDisposed();
1091
+ this._middleware.push(middleware);
1092
+ }
1093
+ removeMiddleware(middleware) {
1094
+ this.assertNotDisposed();
1095
+ const idx = this._middleware.indexOf(middleware);
1096
+ if (idx >= 0) this._middleware.splice(idx, 1);
1097
+ }
1098
+ // ── Context Stats ─────────────────────────────────────────
1099
+ async getContextStats(sessionId) {
1100
+ const cid = toChatId(sessionId);
1101
+ return this._contextStats.get(cid) ?? null;
1102
+ }
1103
+ // ── Session Subscription ──────────────────────────────────
1104
+ onSessionChange(callback) {
1105
+ return this._sessionListeners.add(callback);
1106
+ }
1107
+ _notifySessionChange() {
1108
+ this._sessionListeners.notify();
1109
+ }
1110
+ // ── Private Helpers ────────────────────────────────────────
1111
+ async getOrCreateAdapter(backend, credentials) {
1112
+ const key = this.getPoolKey(backend, credentials);
1113
+ const existing = this._adapterPool.get(key);
1114
+ if (existing) return existing;
1115
+ for (const [oldKey, oldAdapter] of this._adapterPool) {
1116
+ if (oldKey.startsWith(backend + ":")) {
1117
+ try {
1118
+ await oldAdapter.dispose();
1119
+ } catch {
1120
+ }
1121
+ this._adapterPool.delete(oldKey);
1122
+ }
1123
+ }
1124
+ const factory = this._backends[backend];
1125
+ if (!factory) {
1126
+ throw new ChatError(
1127
+ `Backend "${backend}" not found`,
1128
+ { code: "INVALID_INPUT" /* INVALID_INPUT */ }
1129
+ );
1130
+ }
1131
+ const adapter = await factory(credentials);
1132
+ this._adapterPool.set(key, adapter);
1133
+ return adapter;
1134
+ }
1135
+ getPoolKey(backend, credentials) {
1136
+ const token = credentials.accessToken;
1137
+ const hash = token.length > 16 ? token.slice(0, 8) + token.slice(-8) : token;
1138
+ return `${backend}:${hash}`;
1139
+ }
1140
+ /** Wrap each tool's execute to inject ToolContext as 2nd argument */
1141
+ injectToolContext(tools, context) {
1142
+ return tools.map((tool) => ({
1143
+ ...tool,
1144
+ execute: (params) => tool.execute(params, context)
1145
+ }));
1146
+ }
1147
+ /** Map ChatEvent to AgentEvent for MessageAccumulator */
1148
+ feedAccumulator(acc, event) {
1149
+ const agentEvent = chatEventToAgentEvent(event);
1150
+ if (agentEvent) acc.apply(agentEvent);
1151
+ }
1152
+ createUserMessage(text) {
1153
+ return {
1154
+ id: createChatId(),
1155
+ role: "user",
1156
+ parts: [{ type: "text", text, status: "complete" }],
1157
+ createdAt: (/* @__PURE__ */ new Date()).toISOString(),
1158
+ status: "complete"
1159
+ };
1160
+ }
1161
+ assertNotDisposed() {
1162
+ if (this._state.current === "disposed") {
1163
+ throw new ChatError(
1164
+ "Runtime is disposed",
1165
+ { code: "DISPOSED" /* DISPOSED */ }
1166
+ );
1167
+ }
1168
+ }
1169
+ /** Get or create adapter with retry on connection errors */
1170
+ async getOrCreateAdapterWithRetry(backend, credentials) {
1171
+ const maxAttempts = this._retryConfig?.maxAttempts ?? 1;
1172
+ const delayMs = this._retryConfig?.delayMs ?? 0;
1173
+ let lastError;
1174
+ for (let attempt = 1; attempt <= maxAttempts; attempt++) {
1175
+ try {
1176
+ return await this.getOrCreateAdapter(backend, credentials);
1177
+ } catch (err) {
1178
+ lastError = err instanceof Error ? err : new Error(String(err));
1179
+ if (attempt < maxAttempts) {
1180
+ const key = this.getPoolKey(backend, credentials);
1181
+ const old = this._adapterPool.get(key);
1182
+ if (old) {
1183
+ try {
1184
+ await old.dispose();
1185
+ } catch {
1186
+ }
1187
+ }
1188
+ this._adapterPool.delete(key);
1189
+ await delay(delayMs);
1190
+ }
1191
+ }
1192
+ }
1193
+ throw lastError;
1194
+ }
1195
+ /**
1196
+ * Create stream with retry for pre-stream connection errors.
1197
+ * Tries to get the first event from the stream; if that fails,
1198
+ * retries with a fresh adapter. Once first event is received,
1199
+ * the stream is committed (no more retries).
1200
+ */
1201
+ async createStreamWithRetry(adapter, session, message, options, backend, credentials) {
1202
+ const maxAttempts = this._retryConfig?.maxAttempts ?? 1;
1203
+ const delayMs = this._retryConfig?.delayMs ?? 0;
1204
+ let lastError;
1205
+ let currentAdapter = adapter;
1206
+ for (let attempt = 1; attempt <= maxAttempts; attempt++) {
1207
+ try {
1208
+ const stream = currentAdapter.streamMessage(session, message, options);
1209
+ const iterator = stream[Symbol.asyncIterator]();
1210
+ const first = await iterator.next();
1211
+ return (async function* () {
1212
+ if (!first.done) yield first.value;
1213
+ while (true) {
1214
+ const next = await iterator.next();
1215
+ if (next.done) break;
1216
+ yield next.value;
1217
+ }
1218
+ })();
1219
+ } catch (err) {
1220
+ lastError = err instanceof Error ? err : new Error(String(err));
1221
+ if (attempt < maxAttempts) {
1222
+ try {
1223
+ await currentAdapter.dispose();
1224
+ } catch {
1225
+ }
1226
+ const key = this.getPoolKey(backend, credentials);
1227
+ this._adapterPool.delete(key);
1228
+ await delay(delayMs);
1229
+ currentAdapter = await this.getOrCreateAdapter(backend, credentials);
1230
+ }
1231
+ }
1232
+ }
1233
+ throw lastError;
1234
+ }
1235
+ };
1236
+ function delay(ms) {
1237
+ return new Promise((resolve) => setTimeout(resolve, ms));
1238
+ }
1239
+ function createChatRuntime(options) {
1240
+ return new ChatRuntime(options);
1241
+ }
1242
+
1243
+ exports.createChatRuntime = createChatRuntime;
1244
+ //# sourceMappingURL=runtime.cjs.map
1245
+ //# sourceMappingURL=runtime.cjs.map