@witqq/agent-sdk 0.7.0 → 0.9.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 (154) hide show
  1. package/dist/{types-CqvUAYxt.d.ts → agent-C6H2CgJA.d.cts} +139 -102
  2. package/dist/{types-CqvUAYxt.d.cts → agent-F7oB6eKp.d.ts} +139 -102
  3. package/dist/auth/index.cjs +72 -1
  4. package/dist/auth/index.cjs.map +1 -1
  5. package/dist/auth/index.d.cts +21 -154
  6. package/dist/auth/index.d.ts +21 -154
  7. package/dist/auth/index.js +72 -1
  8. package/dist/auth/index.js.map +1 -1
  9. package/dist/backends/claude.cjs +480 -261
  10. package/dist/backends/claude.cjs.map +1 -1
  11. package/dist/backends/claude.d.cts +3 -1
  12. package/dist/backends/claude.d.ts +3 -1
  13. package/dist/backends/claude.js +480 -261
  14. package/dist/backends/claude.js.map +1 -1
  15. package/dist/backends/copilot.cjs +337 -112
  16. package/dist/backends/copilot.cjs.map +1 -1
  17. package/dist/backends/copilot.d.cts +12 -4
  18. package/dist/backends/copilot.d.ts +12 -4
  19. package/dist/backends/copilot.js +337 -112
  20. package/dist/backends/copilot.js.map +1 -1
  21. package/dist/backends/mock-llm.cjs +719 -0
  22. package/dist/backends/mock-llm.cjs.map +1 -0
  23. package/dist/backends/mock-llm.d.cts +37 -0
  24. package/dist/backends/mock-llm.d.ts +37 -0
  25. package/dist/backends/mock-llm.js +717 -0
  26. package/dist/backends/mock-llm.js.map +1 -0
  27. package/dist/backends/vercel-ai.cjs +301 -61
  28. package/dist/backends/vercel-ai.cjs.map +1 -1
  29. package/dist/backends/vercel-ai.d.cts +3 -1
  30. package/dist/backends/vercel-ai.d.ts +3 -1
  31. package/dist/backends/vercel-ai.js +301 -61
  32. package/dist/backends/vercel-ai.js.map +1 -1
  33. package/dist/backends-Cno0gZjy.d.cts +114 -0
  34. package/dist/backends-Cno0gZjy.d.ts +114 -0
  35. package/dist/chat/accumulator.cjs +1 -1
  36. package/dist/chat/accumulator.cjs.map +1 -1
  37. package/dist/chat/accumulator.d.cts +5 -2
  38. package/dist/chat/accumulator.d.ts +5 -2
  39. package/dist/chat/accumulator.js +1 -1
  40. package/dist/chat/accumulator.js.map +1 -1
  41. package/dist/chat/backends.cjs +1084 -821
  42. package/dist/chat/backends.cjs.map +1 -1
  43. package/dist/chat/backends.d.cts +10 -6
  44. package/dist/chat/backends.d.ts +10 -6
  45. package/dist/chat/backends.js +1082 -800
  46. package/dist/chat/backends.js.map +1 -1
  47. package/dist/chat/context.cjs +50 -0
  48. package/dist/chat/context.cjs.map +1 -1
  49. package/dist/chat/context.d.cts +27 -3
  50. package/dist/chat/context.d.ts +27 -3
  51. package/dist/chat/context.js +50 -0
  52. package/dist/chat/context.js.map +1 -1
  53. package/dist/chat/core.cjs +60 -27
  54. package/dist/chat/core.cjs.map +1 -1
  55. package/dist/chat/core.d.cts +41 -382
  56. package/dist/chat/core.d.ts +41 -382
  57. package/dist/chat/core.js +58 -28
  58. package/dist/chat/core.js.map +1 -1
  59. package/dist/chat/errors.cjs +48 -26
  60. package/dist/chat/errors.cjs.map +1 -1
  61. package/dist/chat/errors.d.cts +6 -31
  62. package/dist/chat/errors.d.ts +6 -31
  63. package/dist/chat/errors.js +48 -25
  64. package/dist/chat/errors.js.map +1 -1
  65. package/dist/chat/events.cjs.map +1 -1
  66. package/dist/chat/events.d.cts +6 -2
  67. package/dist/chat/events.d.ts +6 -2
  68. package/dist/chat/events.js.map +1 -1
  69. package/dist/chat/index.cjs +1612 -1125
  70. package/dist/chat/index.cjs.map +1 -1
  71. package/dist/chat/index.d.cts +35 -10
  72. package/dist/chat/index.d.ts +35 -10
  73. package/dist/chat/index.js +1600 -1097
  74. package/dist/chat/index.js.map +1 -1
  75. package/dist/chat/react/theme.css +2517 -0
  76. package/dist/chat/react.cjs +2212 -1158
  77. package/dist/chat/react.cjs.map +1 -1
  78. package/dist/chat/react.d.cts +665 -122
  79. package/dist/chat/react.d.ts +665 -122
  80. package/dist/chat/react.js +2191 -1156
  81. package/dist/chat/react.js.map +1 -1
  82. package/dist/chat/runtime.cjs +405 -186
  83. package/dist/chat/runtime.cjs.map +1 -1
  84. package/dist/chat/runtime.d.cts +92 -28
  85. package/dist/chat/runtime.d.ts +92 -28
  86. package/dist/chat/runtime.js +405 -186
  87. package/dist/chat/runtime.js.map +1 -1
  88. package/dist/chat/server.cjs +2247 -212
  89. package/dist/chat/server.cjs.map +1 -1
  90. package/dist/chat/server.d.cts +451 -90
  91. package/dist/chat/server.d.ts +451 -90
  92. package/dist/chat/server.js +2234 -213
  93. package/dist/chat/server.js.map +1 -1
  94. package/dist/chat/sessions.cjs +64 -66
  95. package/dist/chat/sessions.cjs.map +1 -1
  96. package/dist/chat/sessions.d.cts +37 -118
  97. package/dist/chat/sessions.d.ts +37 -118
  98. package/dist/chat/sessions.js +65 -67
  99. package/dist/chat/sessions.js.map +1 -1
  100. package/dist/chat/sqlite.cjs +536 -0
  101. package/dist/chat/sqlite.cjs.map +1 -0
  102. package/dist/chat/sqlite.d.cts +164 -0
  103. package/dist/chat/sqlite.d.ts +164 -0
  104. package/dist/chat/sqlite.js +527 -0
  105. package/dist/chat/sqlite.js.map +1 -0
  106. package/dist/chat/state.cjs +14 -1
  107. package/dist/chat/state.cjs.map +1 -1
  108. package/dist/chat/state.d.cts +5 -2
  109. package/dist/chat/state.d.ts +5 -2
  110. package/dist/chat/state.js +14 -1
  111. package/dist/chat/state.js.map +1 -1
  112. package/dist/chat/storage.cjs +58 -33
  113. package/dist/chat/storage.cjs.map +1 -1
  114. package/dist/chat/storage.d.cts +18 -8
  115. package/dist/chat/storage.d.ts +18 -8
  116. package/dist/chat/storage.js +59 -34
  117. package/dist/chat/storage.js.map +1 -1
  118. package/dist/errors-C-so0M4t.d.cts +33 -0
  119. package/dist/errors-C-so0M4t.d.ts +33 -0
  120. package/dist/errors-CmVvczxZ.d.cts +28 -0
  121. package/dist/errors-CmVvczxZ.d.ts +28 -0
  122. package/dist/{in-process-transport-C2oPTYs6.d.ts → in-process-transport-7EIit9Xk.d.ts} +72 -33
  123. package/dist/{in-process-transport-DG-w5G6k.d.cts → in-process-transport-Ct9YcX8I.d.cts} +72 -33
  124. package/dist/index.cjs +354 -60
  125. package/dist/index.cjs.map +1 -1
  126. package/dist/index.d.cts +294 -123
  127. package/dist/index.d.ts +294 -123
  128. package/dist/index.js +347 -60
  129. package/dist/index.js.map +1 -1
  130. package/dist/provider-types-PTSlRPNB.d.cts +39 -0
  131. package/dist/provider-types-PTSlRPNB.d.ts +39 -0
  132. package/dist/refresh-manager-B81PpYBr.d.cts +153 -0
  133. package/dist/refresh-manager-Dlv_iNZi.d.ts +153 -0
  134. package/dist/testing.cjs +1107 -0
  135. package/dist/testing.cjs.map +1 -0
  136. package/dist/testing.d.cts +144 -0
  137. package/dist/testing.d.ts +144 -0
  138. package/dist/testing.js +1101 -0
  139. package/dist/testing.js.map +1 -0
  140. package/dist/token-store-CSUBgYwn.d.ts +48 -0
  141. package/dist/token-store-CuC4hB9Z.d.cts +48 -0
  142. package/dist/{transport-DX1Nhm4N.d.cts → transport-DLWCN18G.d.cts} +5 -4
  143. package/dist/{transport-D1OaUgRk.d.ts → transport-DsuS-GeM.d.ts} +5 -4
  144. package/dist/{types-CGF7AEX1.d.cts → types-4vbcmPTp.d.cts} +4 -2
  145. package/dist/{types-Bh5AhqD-.d.ts → types-BxggH0Yh.d.ts} +4 -2
  146. package/dist/types-DgtI1hzh.d.ts +364 -0
  147. package/dist/types-DkSXALKg.d.cts +364 -0
  148. package/package.json +41 -5
  149. package/LICENSE +0 -21
  150. package/README.md +0 -948
  151. package/dist/errors-BDLbNu9w.d.cts +0 -13
  152. package/dist/errors-BDLbNu9w.d.ts +0 -13
  153. package/dist/types-DLZzlJxt.d.ts +0 -39
  154. package/dist/types-tE0CXwBl.d.cts +0 -39
@@ -1,5 +1,6 @@
1
1
  'use strict';
2
2
 
3
+ var crypto$1 = require('crypto');
3
4
  var fs = require('fs');
4
5
  var path = require('path');
5
6
 
@@ -24,6 +25,101 @@ function _interopNamespace(e) {
24
25
  var fs__namespace = /*#__PURE__*/_interopNamespace(fs);
25
26
  var path__namespace = /*#__PURE__*/_interopNamespace(path);
26
27
 
28
+ // src/chat/server/utils.ts
29
+ var BodyParseError = class extends Error {
30
+ statusCode;
31
+ constructor(message, statusCode) {
32
+ super(message);
33
+ this.name = "BodyParseError";
34
+ this.statusCode = statusCode;
35
+ }
36
+ };
37
+ function readBody(req, maxSize = 1048576) {
38
+ return new Promise((resolve2, reject) => {
39
+ let body = "";
40
+ let size = 0;
41
+ let exceeded = false;
42
+ req.on("data", (chunk) => {
43
+ if (exceeded) return;
44
+ const str = chunk.toString();
45
+ size += Buffer.byteLength(str);
46
+ if (size > maxSize) {
47
+ exceeded = true;
48
+ reject(new BodyParseError("Request body too large", 413));
49
+ return;
50
+ }
51
+ body += str;
52
+ });
53
+ req.on("end", () => {
54
+ if (exceeded) return;
55
+ try {
56
+ resolve2(JSON.parse(body || "{}"));
57
+ } catch {
58
+ reject(new BodyParseError("Invalid JSON in request body", 400));
59
+ }
60
+ });
61
+ if ("once" in req && typeof req.once === "function") {
62
+ req.once(
63
+ "error",
64
+ () => reject(new BodyParseError("Request error", 500))
65
+ );
66
+ }
67
+ });
68
+ }
69
+ function json(res, data, status = 200) {
70
+ res.writeHead(status, { "Content-Type": "application/json" });
71
+ res.end(JSON.stringify(data));
72
+ }
73
+
74
+ // src/chat/server/routes/sessions.ts
75
+ var sessionRoutes = async (method, path2, req, res, ctx) => {
76
+ const { runtime, maxBodySize } = ctx;
77
+ const sessionMatch = path2.match(/^\/sessions\/([^/]+)$/);
78
+ const contextStatsMatch = path2.match(/^\/sessions\/([^/]+)\/context-stats$/);
79
+ if (method === "POST" && path2 === "/sessions/create") {
80
+ const body = await readBody(req, maxBodySize);
81
+ const session = await runtime.createSession({
82
+ title: body.title || `Chat ${(/* @__PURE__ */ new Date()).toLocaleTimeString()}`,
83
+ config: body.config || {
84
+ model: "",
85
+ backend: ""
86
+ },
87
+ ...body.tags ? { tags: body.tags } : {},
88
+ ...body.custom ? { custom: body.custom } : {}
89
+ });
90
+ json(res, session);
91
+ return true;
92
+ }
93
+ if (method === "GET" && contextStatsMatch) {
94
+ const id = decodeURIComponent(contextStatsMatch[1]);
95
+ const stats = await runtime.getContextStats(id);
96
+ json(res, stats ?? null);
97
+ return true;
98
+ }
99
+ if (method === "GET" && sessionMatch) {
100
+ const id = decodeURIComponent(sessionMatch[1]);
101
+ const session = await runtime.getSession(id);
102
+ if (!session) {
103
+ json(res, { error: "Not found" }, 404);
104
+ return true;
105
+ }
106
+ json(res, session);
107
+ return true;
108
+ }
109
+ if (method === "DELETE" && sessionMatch) {
110
+ const id = decodeURIComponent(sessionMatch[1]);
111
+ await runtime.deleteSession(id);
112
+ json(res, { ok: true });
113
+ return true;
114
+ }
115
+ if (method === "GET" && path2 === "/sessions") {
116
+ const sessions = await runtime.listSessions();
117
+ json(res, sessions);
118
+ return true;
119
+ }
120
+ return false;
121
+ };
122
+
27
123
  // src/chat/backends/transport.ts
28
124
  var SSEChatTransport = class {
29
125
  res;
@@ -99,16 +195,22 @@ var SSEChatTransport = class {
99
195
  };
100
196
  async function streamToTransport(events, transport) {
101
197
  try {
102
- let accumulatedText = "";
198
+ const textChunks = [];
199
+ let finishReason;
103
200
  for await (const event of events) {
104
201
  if (!transport.isOpen) break;
202
+ if (event.type === "done") {
203
+ finishReason = event.finishReason;
204
+ continue;
205
+ }
105
206
  transport.send(event);
106
207
  if (event.type === "message:delta") {
107
- accumulatedText += event.text;
208
+ textChunks.push(event.text);
108
209
  }
109
210
  }
110
211
  if (transport.isOpen) {
111
- transport.send({ type: "done", finalOutput: accumulatedText || void 0 });
212
+ const finalOutput = textChunks.length > 0 ? textChunks.join("") : void 0;
213
+ transport.send({ type: "done", finalOutput, finishReason });
112
214
  }
113
215
  transport.close();
114
216
  } catch (err) {
@@ -116,115 +218,332 @@ async function streamToTransport(events, transport) {
116
218
  }
117
219
  }
118
220
 
119
- // src/chat/server/handler.ts
120
- function createChatHandler(runtime, options) {
121
- const prefix = options?.prefix ?? "";
122
- const maxBodySize = options?.maxBodySize ?? 1048576;
123
- const heartbeatMs = options?.heartbeatMs;
124
- return async (req, res) => {
125
- const url = req.url || "";
126
- const method = req.method || "GET";
127
- const rawPath = prefix ? url.slice(prefix.length) : url;
128
- const path2 = rawPath.split("?")[0];
129
- const sessionMatch = path2.match(/^\/sessions\/([^/]+)$/);
130
- const archiveMatch = path2.match(/^\/sessions\/([^/]+)\/archive$/);
131
- try {
132
- if (method === "POST" && path2 === "/sessions/create") {
133
- const body = await readBody(req, maxBodySize);
134
- const session = await runtime.createSession({
135
- title: body.title || `Chat ${(/* @__PURE__ */ new Date()).toLocaleTimeString()}`,
136
- config: body.config || {
137
- model: runtime.currentModel || "",
138
- backend: runtime.currentBackend
139
- },
140
- ...body.tags ? { tags: body.tags } : {},
141
- ...body.custom ? { custom: body.custom } : {}
142
- });
143
- json(res, session);
144
- return;
221
+ // src/errors.ts
222
+ var AgentSDKError = class extends Error {
223
+ /** @internal Marker for cross-bundle identity checks */
224
+ _agentSDKError = true;
225
+ /** Machine-readable error code. Prefer values from the ErrorCode enum. */
226
+ code;
227
+ /** Whether this error is safe to retry */
228
+ retryable;
229
+ /** HTTP status code hint for error classification */
230
+ httpStatus;
231
+ constructor(message, options) {
232
+ super(message, options);
233
+ this.name = "AgentSDKError";
234
+ this.code = options?.code;
235
+ this.retryable = options?.retryable ?? false;
236
+ this.httpStatus = options?.httpStatus;
237
+ }
238
+ /** Check if an error is an AgentSDKError (works across bundled copies) */
239
+ static is(error) {
240
+ return error instanceof Error && "_agentSDKError" in error && error._agentSDKError === true;
241
+ }
242
+ };
243
+
244
+ // src/chat/errors.ts
245
+ var ChatError = class extends AgentSDKError {
246
+ code;
247
+ retryable;
248
+ retryAfter;
249
+ timestamp;
250
+ constructor(message, options) {
251
+ super(message, {
252
+ cause: options.cause,
253
+ code: options.code,
254
+ retryable: options.retryable
255
+ });
256
+ this.name = "ChatError";
257
+ this.code = options.code;
258
+ this.retryable = options.retryable ?? false;
259
+ this.retryAfter = options.retryAfter;
260
+ this.timestamp = (/* @__PURE__ */ new Date()).toISOString();
261
+ }
262
+ };
263
+
264
+ // src/chat/server/request-context.ts
265
+ async function resolveRequestContext(providerId, deps) {
266
+ const provider = await deps.providerStore.get(providerId);
267
+ if (!provider) {
268
+ throw new ChatError(`Provider "${providerId}" not found`, {
269
+ code: "PROVIDER_NOT_FOUND" /* PROVIDER_NOT_FOUND */
270
+ });
271
+ }
272
+ const credentials = await deps.tokenStore.load(provider.backend);
273
+ if (!credentials) {
274
+ throw new ChatError(
275
+ `Authentication required for backend "${provider.backend}"`,
276
+ {
277
+ code: "AUTH_REQUIRED" /* AUTH_REQUIRED */
145
278
  }
146
- if (method === "POST" && archiveMatch) {
147
- const id = decodeURIComponent(archiveMatch[1]);
148
- await runtime.archiveSession(id);
149
- json(res, { ok: true });
150
- return;
279
+ );
280
+ }
281
+ return {
282
+ backend: provider.backend,
283
+ credentials,
284
+ model: provider.model,
285
+ provider
286
+ };
287
+ }
288
+
289
+ // src/chat/server/routes/messages.ts
290
+ var messageRoutes = async (method, path2, req, res, ctx) => {
291
+ const { runtime, maxBodySize, heartbeatMs, hooks, transportFactory } = ctx;
292
+ if (method === "POST" && path2 === "/send") {
293
+ const body = await readBody(req, maxBodySize);
294
+ const sessionId = body.sessionId;
295
+ const message = body.message || body.content;
296
+ if (!sessionId || !message) {
297
+ json(res, { error: "sessionId and message are required" }, 400);
298
+ return true;
299
+ }
300
+ let model;
301
+ let reqBackend;
302
+ let reqCredentials;
303
+ const hasProviderInfra = !!(ctx.providerStore && ctx.tokenStore);
304
+ if (hasProviderInfra) {
305
+ const providerId = body.providerId;
306
+ if (!providerId || typeof providerId !== "string") {
307
+ json(res, { error: "providerId is required" }, 400);
308
+ return true;
151
309
  }
152
- if (method === "GET" && sessionMatch) {
153
- const id = decodeURIComponent(sessionMatch[1]);
154
- const session = await runtime.getSession(id);
155
- if (!session) {
156
- json(res, { error: "Not found" }, 404);
157
- return;
310
+ try {
311
+ const reqCtx = await resolveRequestContext(providerId, {
312
+ providerStore: ctx.providerStore,
313
+ tokenStore: ctx.tokenStore
314
+ });
315
+ model = reqCtx.model;
316
+ reqBackend = reqCtx.backend;
317
+ reqCredentials = reqCtx.credentials;
318
+ } catch (err) {
319
+ if (err instanceof ChatError && err.code === "PROVIDER_NOT_FOUND" /* PROVIDER_NOT_FOUND */) {
320
+ json(res, { error: err.message }, 404);
321
+ return true;
158
322
  }
159
- json(res, session);
160
- return;
323
+ if (err instanceof ChatError && err.code === "AUTH_REQUIRED" /* AUTH_REQUIRED */) {
324
+ json(res, { error: err.message }, 401);
325
+ return true;
326
+ }
327
+ throw err;
161
328
  }
162
- if (method === "DELETE" && sessionMatch) {
163
- const id = decodeURIComponent(sessionMatch[1]);
164
- await runtime.deleteSession(id);
165
- json(res, { ok: true });
166
- return;
329
+ }
330
+ const bodyModel = body.model;
331
+ if (hooks?.onModelSwitch && bodyModel && typeof bodyModel === "string") {
332
+ try {
333
+ await hooks.onModelSwitch(bodyModel);
334
+ } catch (err) {
335
+ json(res, { error: err instanceof Error ? err.message : String(err) }, 403);
336
+ return true;
167
337
  }
168
- if (method === "GET" && path2 === "/sessions") {
169
- const sessions = await runtime.listSessions();
170
- json(res, sessions);
171
- return;
338
+ }
339
+ if (hooks?.onBeforeSend) {
340
+ try {
341
+ await hooks.onBeforeSend(sessionId, message);
342
+ } catch (err) {
343
+ json(res, { error: err instanceof Error ? err.message : String(err) }, 403);
344
+ return true;
172
345
  }
173
- if (method === "POST" && path2 === "/send") {
174
- const body = await readBody(req, maxBodySize);
175
- const sessionId = body.sessionId;
176
- const message = body.message || body.content;
177
- if (!sessionId || !message) {
178
- json(res, { error: "sessionId and message are required" }, 400);
179
- return;
180
- }
181
- const transport = new SSEChatTransport(res, {
182
- heartbeatMs,
183
- request: req
184
- });
185
- try {
186
- const opts = {};
187
- if (body.model) opts.model = body.model;
188
- const stream = runtime.send(
189
- sessionId,
190
- message,
191
- Object.keys(opts).length > 0 ? opts : void 0
192
- );
193
- await streamToTransport(stream, transport);
194
- } catch (err) {
195
- transport.error(err instanceof Error ? err : new Error(String(err)));
196
- }
197
- return;
346
+ }
347
+ model = bodyModel || model;
348
+ if (!model) {
349
+ json(res, { error: "model is required (via body.model or providerId)" }, 400);
350
+ return true;
351
+ }
352
+ const transport = transportFactory ? transportFactory(req, res) : new SSEChatTransport(res, {
353
+ heartbeatMs,
354
+ request: req
355
+ });
356
+ try {
357
+ if (!reqBackend || !reqCredentials) {
358
+ json(res, { error: "backend and credentials are required (configure providerStore + tokenStore)" }, 400);
359
+ return true;
198
360
  }
199
- if (method === "POST" && path2 === "/abort") {
200
- runtime.abort();
201
- json(res, { ok: true });
202
- return;
361
+ const opts = { model, backend: reqBackend, credentials: reqCredentials };
362
+ const stream = runtime.send(sessionId, message, opts);
363
+ await streamToTransport(stream, transport);
364
+ } catch (err) {
365
+ transport.error(err instanceof Error ? err : new Error(String(err)));
366
+ }
367
+ return true;
368
+ }
369
+ if (method === "POST" && path2 === "/abort") {
370
+ runtime.abort();
371
+ json(res, { ok: true });
372
+ return true;
373
+ }
374
+ return false;
375
+ };
376
+
377
+ // src/chat/server/routes/config.ts
378
+ var configRoutes = async (method, path2, req, res, ctx) => {
379
+ const { runtime, maxBodySize, hooks, providerStore } = ctx;
380
+ if (method === "GET" && path2 === "/models") {
381
+ let models = await runtime.listModels();
382
+ if (models.length === 0 && providerStore && ctx.tokenStore) {
383
+ const providers = await providerStore.list();
384
+ for (const p of providers) {
385
+ const token = await ctx.tokenStore.load(p.backend);
386
+ if (token) {
387
+ models = await runtime.listModels({ backend: p.backend, credentials: token });
388
+ break;
389
+ }
203
390
  }
204
- if (method === "GET" && path2 === "/models") {
205
- const models = await runtime.listModels();
206
- json(res, models);
207
- return;
391
+ }
392
+ if (hooks?.filterModels) models = hooks.filterModels(models);
393
+ json(res, models);
394
+ return true;
395
+ }
396
+ if (method === "GET" && path2 === "/backends") {
397
+ const backends = await runtime.listBackends();
398
+ json(res, backends);
399
+ return true;
400
+ }
401
+ if (method === "POST" && path2 === "/model/switch") {
402
+ const body = await readBody(req, maxBodySize);
403
+ if (!body.model || typeof body.model !== "string") {
404
+ json(res, { error: "model is required" }, 400);
405
+ return true;
406
+ }
407
+ if (hooks?.onModelSwitch) {
408
+ try {
409
+ await hooks.onModelSwitch(body.model);
410
+ } catch (err) {
411
+ json(res, { error: err instanceof Error ? err.message : String(err) }, 403);
412
+ return true;
208
413
  }
209
- if (method === "POST" && path2 === "/backend/switch") {
210
- const body = await readBody(req, maxBodySize);
211
- if (!body.backend || typeof body.backend !== "string") {
212
- json(res, { error: "backend is required" }, 400);
213
- return;
214
- }
215
- await runtime.switchBackend(body.backend);
216
- json(res, { ok: true });
217
- return;
414
+ }
415
+ json(res, { ok: true });
416
+ return true;
417
+ }
418
+ if (method === "POST" && path2 === "/provider/switch") {
419
+ const body = await readBody(req, maxBodySize);
420
+ if (!body.providerId || typeof body.providerId !== "string") {
421
+ json(res, { error: "providerId is required" }, 400);
422
+ return true;
423
+ }
424
+ if (!providerStore) {
425
+ json(res, { error: "No provider store configured" }, 400);
426
+ return true;
427
+ }
428
+ const provider = await providerStore.get(body.providerId);
429
+ if (!provider) {
430
+ json(res, { error: `Provider "${body.providerId}" not found` }, 404);
431
+ return true;
432
+ }
433
+ if (hooks?.onProviderSwitch) {
434
+ try {
435
+ await hooks.onProviderSwitch({ providerId: body.providerId, backend: provider.backend });
436
+ } catch (err) {
437
+ json(res, { error: err instanceof Error ? err.message : String(err) }, 400);
438
+ return true;
218
439
  }
219
- if (method === "POST" && path2 === "/model/switch") {
220
- const body = await readBody(req, maxBodySize);
221
- if (!body.model || typeof body.model !== "string") {
222
- json(res, { error: "model is required" }, 400);
223
- return;
224
- }
225
- runtime.switchModel(body.model);
226
- json(res, { ok: true });
227
- return;
440
+ }
441
+ json(res, { ok: true });
442
+ return true;
443
+ }
444
+ return false;
445
+ };
446
+ var providerRoutes = async (method, path2, req, res, ctx) => {
447
+ const { providerStore, maxBodySize } = ctx;
448
+ if (!providerStore) return false;
449
+ const idMatch = path2.match(/^\/providers\/([^/]+)$/);
450
+ if (method === "GET" && path2 === "/providers") {
451
+ const providers = await providerStore.list();
452
+ json(res, providers);
453
+ return true;
454
+ }
455
+ if (method === "GET" && idMatch) {
456
+ const id = decodeURIComponent(idMatch[1]);
457
+ const provider = await providerStore.get(id);
458
+ if (!provider) {
459
+ json(res, { error: "Provider not found" }, 404);
460
+ return true;
461
+ }
462
+ json(res, provider);
463
+ return true;
464
+ }
465
+ if (method === "POST" && path2 === "/providers") {
466
+ const body = await readBody(req, maxBodySize);
467
+ const backend = body.backend;
468
+ const model = body.model;
469
+ const label = body.label;
470
+ if (!backend || typeof backend !== "string") {
471
+ json(res, { error: "backend is required" }, 400);
472
+ return true;
473
+ }
474
+ if (!model || typeof model !== "string") {
475
+ json(res, { error: "model is required" }, 400);
476
+ return true;
477
+ }
478
+ if (!label || typeof label !== "string") {
479
+ json(res, { error: "label is required" }, 400);
480
+ return true;
481
+ }
482
+ const config = {
483
+ id: crypto$1.randomUUID(),
484
+ backend,
485
+ model,
486
+ label,
487
+ createdAt: Date.now()
488
+ };
489
+ await providerStore.create(config);
490
+ json(res, config, 201);
491
+ return true;
492
+ }
493
+ if (method === "PUT" && idMatch) {
494
+ const id = decodeURIComponent(idMatch[1]);
495
+ const existing = await providerStore.get(id);
496
+ if (!existing) {
497
+ json(res, { error: "Provider not found" }, 404);
498
+ return true;
499
+ }
500
+ const body = await readBody(req, maxBodySize);
501
+ const changes = {};
502
+ if (body.backend && typeof body.backend === "string") changes.backend = body.backend;
503
+ if (body.model && typeof body.model === "string") changes.model = body.model;
504
+ if (body.label && typeof body.label === "string") changes.label = body.label;
505
+ await providerStore.update(id, changes);
506
+ const updated = await providerStore.get(id);
507
+ json(res, updated);
508
+ return true;
509
+ }
510
+ if (method === "DELETE" && idMatch) {
511
+ const id = decodeURIComponent(idMatch[1]);
512
+ await providerStore.delete(id);
513
+ json(res, { ok: true });
514
+ return true;
515
+ }
516
+ return false;
517
+ };
518
+
519
+ // src/chat/server/handler.ts
520
+ var ROUTE_PIPELINE = [
521
+ sessionRoutes,
522
+ messageRoutes,
523
+ configRoutes,
524
+ providerRoutes
525
+ ];
526
+ function createChatHandler(runtime, options) {
527
+ const prefix = options?.prefix ?? "";
528
+ const state = {};
529
+ const ctx = {
530
+ runtime,
531
+ maxBodySize: options?.maxBodySize ?? 1048576,
532
+ heartbeatMs: options?.heartbeatMs,
533
+ hooks: options?.hooks,
534
+ providerStore: options?.providerStore,
535
+ tokenStore: options?.tokenStore,
536
+ transportFactory: options?.transportFactory,
537
+ state
538
+ };
539
+ return async (req, res) => {
540
+ const url = req.url || "";
541
+ const method = req.method || "GET";
542
+ const rawPath = prefix ? url.slice(prefix.length) : url;
543
+ const path2 = rawPath.split("?")[0];
544
+ try {
545
+ for (const route of ROUTE_PIPELINE) {
546
+ if (await route(method, path2, req, res, ctx)) return;
228
547
  }
229
548
  json(res, { error: "Not found" }, 404);
230
549
  } catch (err) {
@@ -232,55 +551,14 @@ function createChatHandler(runtime, options) {
232
551
  json(res, { error: err.message }, err.statusCode);
233
552
  } else {
234
553
  const message = err instanceof Error ? err.message : String(err);
554
+ if (ctx.hooks?.onError) {
555
+ ctx.hooks.onError(err instanceof Error ? err : new Error(message), { route: path2, method });
556
+ }
235
557
  json(res, { error: message }, 500);
236
558
  }
237
559
  }
238
560
  };
239
561
  }
240
- var BodyParseError = class extends Error {
241
- statusCode;
242
- constructor(message, statusCode) {
243
- super(message);
244
- this.name = "BodyParseError";
245
- this.statusCode = statusCode;
246
- }
247
- };
248
- function readBody(req, maxSize) {
249
- return new Promise((resolve2, reject) => {
250
- let body = "";
251
- let size = 0;
252
- let exceeded = false;
253
- req.on("data", (chunk) => {
254
- if (exceeded) return;
255
- const str = chunk.toString();
256
- size += Buffer.byteLength(str);
257
- if (size > maxSize) {
258
- exceeded = true;
259
- reject(new BodyParseError("Request body too large", 413));
260
- return;
261
- }
262
- body += str;
263
- });
264
- req.on("end", () => {
265
- if (exceeded) return;
266
- try {
267
- resolve2(JSON.parse(body || "{}"));
268
- } catch {
269
- reject(new BodyParseError("Invalid JSON in request body", 400));
270
- }
271
- });
272
- if ("once" in req && typeof req.once === "function") {
273
- req.once(
274
- "error",
275
- () => reject(new BodyParseError("Request error", 500))
276
- );
277
- }
278
- });
279
- }
280
- function json(res, data, status = 200) {
281
- res.writeHead(status, { "Content-Type": "application/json" });
282
- res.end(JSON.stringify(data));
283
- }
284
562
 
285
563
  // src/chat/server/auth-handler.ts
286
564
  function createAuthHandler(options) {
@@ -296,74 +574,74 @@ function createAuthHandler(options) {
296
574
  const path2 = rawPath.split("?")[0];
297
575
  try {
298
576
  if (method === "POST" && path2 === "/auth/start") {
299
- const body = await readBody2(req, maxBodySize);
577
+ const body = await readBody(req, maxBodySize);
300
578
  const provider = body.provider;
301
579
  if (!provider || !isValidProvider(provider)) {
302
- json2(res, { error: "provider is required (copilot, claude, vercel-ai)" }, 400);
580
+ json(res, { error: "provider is required (copilot, claude, vercel-ai)" }, 400);
303
581
  return;
304
582
  }
305
583
  pendingCopilot = null;
306
584
  pendingClaude = null;
307
585
  if (provider === "copilot") {
308
586
  if (!options.createCopilotAuth) {
309
- json2(res, { error: "Copilot auth not configured" }, 400);
587
+ json(res, { error: "Copilot auth not configured" }, 400);
310
588
  return;
311
589
  }
312
590
  const auth = options.createCopilotAuth();
313
591
  const flow = await auth.startDeviceFlow();
314
592
  pendingCopilot = { waitForToken: flow.waitForToken };
315
- json2(res, { userCode: flow.userCode, verificationUrl: flow.verificationUrl });
593
+ json(res, { userCode: flow.userCode, verificationUrl: flow.verificationUrl });
316
594
  return;
317
595
  }
318
596
  if (provider === "claude") {
319
597
  if (!options.createClaudeAuth) {
320
- json2(res, { error: "Claude auth not configured" }, 400);
598
+ json(res, { error: "Claude auth not configured" }, 400);
321
599
  return;
322
600
  }
323
601
  const auth = options.createClaudeAuth();
324
602
  const flow = auth.startOAuthFlow();
325
603
  pendingClaude = { completeAuth: flow.completeAuth };
326
- json2(res, { authorizeUrl: flow.authorizeUrl });
604
+ json(res, { authorizeUrl: flow.authorizeUrl });
327
605
  return;
328
606
  }
329
- json2(res, { ready: true });
607
+ json(res, { ready: true });
330
608
  return;
331
609
  }
332
610
  if (method === "POST" && path2 === "/auth/copilot/poll") {
333
611
  if (!pendingCopilot) {
334
- json2(res, { error: "No active Copilot flow" }, 400);
612
+ json(res, { error: "No active Copilot flow" }, 400);
335
613
  return;
336
614
  }
337
615
  const token = await pendingCopilot.waitForToken();
338
616
  pendingCopilot = null;
339
617
  await tokenStore.save("copilot", token);
340
618
  if (onAuth) await onAuth("copilot", token);
341
- json2(res, { ok: true, login: token.login });
619
+ json(res, { ok: true, login: token.login });
342
620
  return;
343
621
  }
344
622
  if (method === "POST" && path2 === "/auth/claude/complete") {
345
623
  if (!pendingClaude) {
346
- json2(res, { error: "No active Claude flow" }, 400);
624
+ json(res, { error: "No active Claude flow" }, 400);
347
625
  return;
348
626
  }
349
- const body = await readBody2(req, maxBodySize);
627
+ const body = await readBody(req, maxBodySize);
350
628
  const code = body.code;
351
629
  if (!code || typeof code !== "string") {
352
- json2(res, { error: "code is required" }, 400);
630
+ json(res, { error: "code is required" }, 400);
353
631
  return;
354
632
  }
355
633
  const token = await pendingClaude.completeAuth(code);
356
634
  pendingClaude = null;
357
635
  await tokenStore.save("claude", token);
358
636
  if (onAuth) await onAuth("claude", token);
359
- json2(res, { ok: true });
637
+ json(res, { ok: true });
360
638
  return;
361
639
  }
362
640
  if (method === "POST" && path2 === "/auth/vercel/complete") {
363
- const body = await readBody2(req, maxBodySize);
641
+ const body = await readBody(req, maxBodySize);
364
642
  const apiKey = body.apiKey;
365
643
  if (!apiKey || typeof apiKey !== "string") {
366
- json2(res, { error: "apiKey is required" }, 400);
644
+ json(res, { error: "apiKey is required" }, 400);
367
645
  return;
368
646
  }
369
647
  const token = {
@@ -374,86 +652,57 @@ function createAuthHandler(options) {
374
652
  const storeToken = body.baseUrl ? { ...token, baseUrl: body.baseUrl } : token;
375
653
  await tokenStore.save("vercel-ai", storeToken);
376
654
  if (onAuth) await onAuth("vercel-ai", storeToken);
377
- json2(res, { ok: true });
655
+ json(res, { ok: true });
378
656
  return;
379
657
  }
380
658
  if (method === "GET" && path2 === "/tokens/saved") {
381
659
  const saved = await tokenStore.list();
382
- json2(res, { saved });
660
+ json(res, { saved });
383
661
  return;
384
662
  }
385
663
  if (method === "POST" && path2 === "/tokens/use") {
386
- const body = await readBody2(req, maxBodySize);
664
+ const body = await readBody(req, maxBodySize);
387
665
  const provider = body.provider;
388
666
  if (!provider || !isValidProvider(provider)) {
389
- json2(res, { error: "provider is required (copilot, claude, vercel-ai)" }, 400);
667
+ json(res, { error: "provider is required (copilot, claude, vercel-ai)" }, 400);
390
668
  return;
391
669
  }
392
670
  const token = await tokenStore.load(provider);
393
671
  if (!token) {
394
- json2(res, { error: `No saved token for ${provider}` }, 404);
672
+ json(res, { error: `No saved token for ${provider}` }, 404);
395
673
  return;
396
674
  }
397
675
  if (onAuth) await onAuth(provider, token);
398
- json2(res, { ok: true, provider });
676
+ json(res, { ok: true, provider });
399
677
  return;
400
678
  }
401
679
  if (method === "POST" && path2 === "/tokens/clear") {
402
680
  await tokenStore.clearAll();
403
681
  if (options.onLogout) await options.onLogout();
404
- json2(res, { ok: true });
682
+ json(res, { ok: true });
405
683
  return;
406
684
  }
407
685
  if (method === "POST" && path2 === "/auth/dispose") {
408
686
  pendingCopilot = null;
409
687
  pendingClaude = null;
410
688
  if (options.onLogout) await options.onLogout();
411
- json2(res, { ok: true });
689
+ json(res, { ok: true });
412
690
  return;
413
691
  }
414
- json2(res, { error: "Not found" }, 404);
692
+ json(res, { error: "Not found" }, 404);
415
693
  } catch (err) {
416
- const message = err instanceof Error ? err.message : String(err);
417
- json2(res, { error: message }, 500);
694
+ if (err instanceof BodyParseError) {
695
+ json(res, { error: err.message }, err.statusCode);
696
+ } else {
697
+ const message = err instanceof Error ? err.message : String(err);
698
+ json(res, { error: message }, 500);
699
+ }
418
700
  }
419
701
  };
420
702
  }
421
703
  function isValidProvider(p) {
422
704
  return p === "copilot" || p === "claude" || p === "vercel-ai";
423
705
  }
424
- function readBody2(req, maxSize) {
425
- return new Promise((resolve2) => {
426
- let body = "";
427
- let size = 0;
428
- let exceeded = false;
429
- req.on("data", (chunk) => {
430
- if (exceeded) return;
431
- const str = chunk.toString();
432
- size += Buffer.byteLength(str);
433
- if (size > maxSize) {
434
- exceeded = true;
435
- resolve2({});
436
- return;
437
- }
438
- body += str;
439
- });
440
- req.on("end", () => {
441
- if (exceeded) return;
442
- try {
443
- resolve2(JSON.parse(body || "{}"));
444
- } catch {
445
- resolve2({});
446
- }
447
- });
448
- if ("once" in req && typeof req.once === "function") {
449
- req.once("error", () => resolve2({}));
450
- }
451
- });
452
- }
453
- function json2(res, data, status = 200) {
454
- res.writeHead(status, { "Content-Type": "application/json" });
455
- res.end(JSON.stringify(data));
456
- }
457
706
 
458
707
  // src/chat/server/cors.ts
459
708
  function corsMiddleware(options) {
@@ -484,6 +733,1212 @@ function corsMiddleware(options) {
484
733
  return false;
485
734
  };
486
735
  }
736
+
737
+ // src/chat/types.ts
738
+ function createChatId() {
739
+ return crypto.randomUUID();
740
+ }
741
+ 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;
742
+ function toChatId(value) {
743
+ if (!UUID_RE.test(value)) {
744
+ throw new TypeError(`Invalid ChatId: "${value}" is not a valid UUID`);
745
+ }
746
+ return value;
747
+ }
748
+
749
+ // src/chat/bridge.ts
750
+ function chatEventToAgentEvent(event) {
751
+ switch (event.type) {
752
+ case "message:delta":
753
+ return { type: "text_delta", text: event.text };
754
+ case "thinking:start":
755
+ return { type: "thinking_start" };
756
+ case "thinking:delta":
757
+ return { type: "thinking_delta", text: event.text };
758
+ case "thinking:end":
759
+ return { type: "thinking_end" };
760
+ case "tool:start":
761
+ return {
762
+ type: "tool_call_start",
763
+ toolCallId: event.toolCallId,
764
+ toolName: event.toolName,
765
+ args: event.args
766
+ };
767
+ case "tool:complete":
768
+ return {
769
+ type: "tool_call_end",
770
+ toolCallId: event.toolCallId,
771
+ toolName: event.toolName,
772
+ result: event.result
773
+ };
774
+ case "error":
775
+ return { type: "error", error: event.error, recoverable: event.recoverable, code: event.code };
776
+ default:
777
+ return null;
778
+ }
779
+ }
780
+
781
+ // src/chat/context.ts
782
+ function estimateTokens(message, options) {
783
+ const ratio = options?.charsPerToken ?? 4;
784
+ let charCount = 0;
785
+ charCount += message.role.length + 4;
786
+ for (const part of message.parts) {
787
+ charCount += estimatePartChars(part);
788
+ }
789
+ return Math.ceil(charCount / ratio);
790
+ }
791
+ function estimatePartChars(part) {
792
+ switch (part.type) {
793
+ case "text":
794
+ return part.text.length;
795
+ case "reasoning":
796
+ return part.text.length;
797
+ case "tool_call":
798
+ return JSON.stringify(part.args).length + part.name.length + 20 + (part.result !== void 0 ? JSON.stringify(part.result).length : 0);
799
+ case "source":
800
+ return (part.title?.length ?? 0) + part.url.length + 10;
801
+ case "file":
802
+ return part.name.length + part.data.length + 20;
803
+ }
804
+ }
805
+ var ContextWindowManager = class {
806
+ config;
807
+ constructor(config) {
808
+ this.config = {
809
+ maxTokens: config.maxTokens,
810
+ reservedTokens: config.reservedTokens ?? 0,
811
+ strategy: config.strategy ?? "truncate-oldest",
812
+ estimation: config.estimation,
813
+ summarizer: config.summarizer
814
+ };
815
+ }
816
+ /** Available token budget after reserving tokens */
817
+ get availableBudget() {
818
+ return Math.max(0, this.config.maxTokens - this.config.reservedTokens);
819
+ }
820
+ /**
821
+ * Estimate tokens for a single message.
822
+ * @param message - Message to estimate
823
+ * @returns Estimated token count
824
+ */
825
+ estimateMessageTokens(message) {
826
+ return estimateTokens(message, this.config.estimation);
827
+ }
828
+ /**
829
+ * Fit messages within the token budget using the configured strategy.
830
+ * @param messages - All messages to consider
831
+ * @returns Result with fitted messages and metadata
832
+ */
833
+ fitMessages(messages) {
834
+ if (messages.length === 0) {
835
+ return { messages: [], totalTokens: 0, removedCount: 0, wasTruncated: false };
836
+ }
837
+ const budget = this.availableBudget;
838
+ const tokenCounts = messages.map((m) => this.estimateMessageTokens(m));
839
+ const totalTokens = tokenCounts.reduce((a, b) => a + b, 0);
840
+ if (totalTokens <= budget) {
841
+ return {
842
+ messages: [...messages],
843
+ totalTokens,
844
+ removedCount: 0,
845
+ wasTruncated: false
846
+ };
847
+ }
848
+ switch (this.config.strategy) {
849
+ case "truncate-oldest":
850
+ return this.truncateOldest(messages, tokenCounts, budget);
851
+ case "sliding-window":
852
+ return this.slidingWindow(messages, tokenCounts, budget);
853
+ case "summarize-placeholder":
854
+ return this.summarizePlaceholder(messages, tokenCounts, budget);
855
+ }
856
+ }
857
+ /**
858
+ * Async variant of fitMessages that supports async summarization.
859
+ * When strategy is "summarize-placeholder" and a summarizer is configured,
860
+ * calls the summarizer with removed messages and replaces the placeholder text.
861
+ * Falls back to static placeholder if summarizer throws.
862
+ * For other strategies, behaves identically to fitMessages().
863
+ */
864
+ async fitMessagesAsync(messages) {
865
+ const result = this.fitMessages(messages);
866
+ if (this.config.strategy !== "summarize-placeholder" || !result.wasTruncated || !this.config.summarizer) {
867
+ return result;
868
+ }
869
+ const keptIds = new Set(result.messages.map((m) => m.id));
870
+ const removed = messages.filter((m) => !keptIds.has(m.id));
871
+ if (removed.length === 0) return result;
872
+ let summaryText;
873
+ try {
874
+ summaryText = await this.config.summarizer(removed);
875
+ } catch {
876
+ return result;
877
+ }
878
+ const updatedMessages = result.messages.map((m) => {
879
+ if (m.metadata?.isSummary === true) {
880
+ return {
881
+ ...m,
882
+ parts: [{ type: "text", text: summaryText, status: "complete" }]
883
+ };
884
+ }
885
+ return m;
886
+ });
887
+ return { ...result, messages: updatedMessages };
888
+ }
889
+ /**
890
+ * Trim messages using real token usage data from the previous API call.
891
+ * Uses average-based algorithm: `avgTokensPerMessage = lastPromptTokens / messageCount`.
892
+ * Removes oldest non-system messages until freed budget brings usage under modelContextWindow.
893
+ *
894
+ * @param messages - All messages in the session
895
+ * @param lastPromptTokens - Real prompt tokens from the last API response
896
+ * @param modelContextWindow - Model's total context window size in tokens
897
+ * @returns Result with fitted messages and metadata
898
+ */
899
+ fitMessagesWithUsage(messages, lastPromptTokens, modelContextWindow) {
900
+ if (messages.length === 0) {
901
+ return { messages: [], totalTokens: 0, removedCount: 0, wasTruncated: false };
902
+ }
903
+ const budget = modelContextWindow - this.config.reservedTokens;
904
+ if (budget <= 0 || lastPromptTokens <= budget) {
905
+ return {
906
+ messages: [...messages],
907
+ totalTokens: lastPromptTokens,
908
+ removedCount: 0,
909
+ wasTruncated: false
910
+ };
911
+ }
912
+ const avgTokensPerMessage = lastPromptTokens / messages.length;
913
+ const tokensToFree = lastPromptTokens - budget;
914
+ const messagesToRemove = Math.ceil(tokensToFree / avgTokensPerMessage);
915
+ const nonSystemIndices = [];
916
+ for (let i = 0; i < messages.length; i++) {
917
+ if (messages[i].role === "system") ; else {
918
+ nonSystemIndices.push(i);
919
+ }
920
+ }
921
+ const removableCount = Math.min(messagesToRemove, nonSystemIndices.length);
922
+ const removedIndices = new Set(nonSystemIndices.slice(0, removableCount));
923
+ const result = [];
924
+ for (let i = 0; i < messages.length; i++) {
925
+ if (!removedIndices.has(i)) {
926
+ result.push(messages[i]);
927
+ }
928
+ }
929
+ const estimatedTokens = Math.round(
930
+ lastPromptTokens * (result.length / messages.length)
931
+ );
932
+ return {
933
+ messages: result,
934
+ totalTokens: estimatedTokens,
935
+ removedCount: removableCount,
936
+ wasTruncated: removableCount > 0
937
+ };
938
+ }
939
+ /**
940
+ * Truncate oldest: keeps system messages, removes oldest non-system messages first.
941
+ * Always keeps the most recent user message.
942
+ */
943
+ truncateOldest(messages, tokenCounts, budget) {
944
+ const systemIndices = [];
945
+ const nonSystemIndices = [];
946
+ for (let i = 0; i < messages.length; i++) {
947
+ if (messages[i].role === "system") {
948
+ systemIndices.push(i);
949
+ } else {
950
+ nonSystemIndices.push(i);
951
+ }
952
+ }
953
+ let usedTokens = systemIndices.reduce(
954
+ (sum, i) => sum + tokenCounts[i],
955
+ 0
956
+ );
957
+ const includedNonSystem = [];
958
+ for (let i = nonSystemIndices.length - 1; i >= 0; i--) {
959
+ const idx = nonSystemIndices[i];
960
+ if (usedTokens + tokenCounts[idx] <= budget) {
961
+ includedNonSystem.unshift(idx);
962
+ usedTokens += tokenCounts[idx];
963
+ }
964
+ }
965
+ const includedSet = /* @__PURE__ */ new Set([...systemIndices, ...includedNonSystem]);
966
+ const result = [];
967
+ let resultTokens = 0;
968
+ for (let i = 0; i < messages.length; i++) {
969
+ if (includedSet.has(i)) {
970
+ result.push(messages[i]);
971
+ resultTokens += tokenCounts[i];
972
+ }
973
+ }
974
+ return {
975
+ messages: result,
976
+ totalTokens: resultTokens,
977
+ removedCount: messages.length - result.length,
978
+ wasTruncated: true
979
+ };
980
+ }
981
+ /**
982
+ * Sliding window: keeps the most recent messages that fit within budget.
983
+ */
984
+ slidingWindow(messages, tokenCounts, budget) {
985
+ const result = [];
986
+ let usedTokens = 0;
987
+ for (let i = messages.length - 1; i >= 0; i--) {
988
+ if (usedTokens + tokenCounts[i] <= budget) {
989
+ result.unshift(messages[i]);
990
+ usedTokens += tokenCounts[i];
991
+ } else {
992
+ break;
993
+ }
994
+ }
995
+ return {
996
+ messages: result,
997
+ totalTokens: usedTokens,
998
+ removedCount: messages.length - result.length,
999
+ wasTruncated: true
1000
+ };
1001
+ }
1002
+ /**
1003
+ * Summarize placeholder: replaces truncated messages with a placeholder,
1004
+ * preserving system messages and recent context.
1005
+ */
1006
+ summarizePlaceholder(messages, tokenCounts, budget) {
1007
+ const systemMessages = [];
1008
+ const nonSystem = [];
1009
+ for (let i = 0; i < messages.length; i++) {
1010
+ if (messages[i].role === "system") {
1011
+ systemMessages.push({ msg: messages[i], tokens: tokenCounts[i] });
1012
+ } else {
1013
+ nonSystem.push({ msg: messages[i], tokens: tokenCounts[i], idx: i });
1014
+ }
1015
+ }
1016
+ let usedTokens = systemMessages.reduce((s, m) => s + m.tokens, 0);
1017
+ const placeholderTokens = 20;
1018
+ usedTokens += placeholderTokens;
1019
+ const recentKept = [];
1020
+ for (let i = nonSystem.length - 1; i >= 0; i--) {
1021
+ if (usedTokens + nonSystem[i].tokens <= budget) {
1022
+ recentKept.unshift(nonSystem[i]);
1023
+ usedTokens += nonSystem[i].tokens;
1024
+ } else {
1025
+ break;
1026
+ }
1027
+ }
1028
+ const removedCount = messages.length - systemMessages.length - recentKept.length;
1029
+ const result = [];
1030
+ for (const sm of systemMessages) {
1031
+ result.push(sm.msg);
1032
+ }
1033
+ if (removedCount > 0) {
1034
+ result.push({
1035
+ id: "context-placeholder",
1036
+ role: "system",
1037
+ parts: [{ type: "text", text: `[${removedCount} earlier message${removedCount === 1 ? "" : "s"} omitted for context window]`, status: "complete" }],
1038
+ metadata: { isSummary: true },
1039
+ createdAt: (/* @__PURE__ */ new Date()).toISOString(),
1040
+ status: "complete"
1041
+ });
1042
+ }
1043
+ for (const m of recentKept) {
1044
+ result.push(m.msg);
1045
+ }
1046
+ return {
1047
+ messages: result,
1048
+ totalTokens: usedTokens,
1049
+ removedCount,
1050
+ wasTruncated: true
1051
+ };
1052
+ }
1053
+ };
1054
+
1055
+ // src/chat/state.ts
1056
+ var StateMachine = class {
1057
+ constructor(initial, transitions) {
1058
+ this.initial = initial;
1059
+ this.transitions = transitions;
1060
+ this._current = initial;
1061
+ }
1062
+ _current;
1063
+ /** Current state */
1064
+ get current() {
1065
+ return this._current;
1066
+ }
1067
+ /**
1068
+ * Check whether transitioning to `next` is allowed from current state
1069
+ * @param next - Target state to check
1070
+ * @returns True if transition is allowed
1071
+ */
1072
+ canTransition(next) {
1073
+ const allowed = this.transitions[this._current];
1074
+ return allowed !== void 0 && allowed.includes(next);
1075
+ }
1076
+ /**
1077
+ * Transition to `next` state.
1078
+ * @throws ChatError(INVALID_TRANSITION) if the transition is not allowed
1079
+ */
1080
+ transition(next) {
1081
+ if (!this.canTransition(next)) {
1082
+ throw new ChatError(
1083
+ `Invalid transition: ${this._current} \u2192 ${next}`,
1084
+ { code: "INVALID_TRANSITION" /* INVALID_TRANSITION */ }
1085
+ );
1086
+ }
1087
+ this._current = next;
1088
+ }
1089
+ /** Reset to initial state */
1090
+ reset() {
1091
+ this._current = this.initial;
1092
+ }
1093
+ };
1094
+ var RUNTIME_TRANSITIONS = {
1095
+ idle: ["streaming", "disposed"],
1096
+ streaming: ["idle", "error", "disposed"],
1097
+ error: ["idle", "disposed"],
1098
+ disposed: []
1099
+ };
1100
+ var ChatReentrancyGuard = class {
1101
+ _acquired = false;
1102
+ /** Whether the guard is currently held */
1103
+ get isAcquired() {
1104
+ return this._acquired;
1105
+ }
1106
+ /**
1107
+ * Acquire the guard. Throws if already acquired.
1108
+ * @throws ChatError with code REENTRANCY
1109
+ */
1110
+ acquire() {
1111
+ if (this._acquired) {
1112
+ throw new ChatError(
1113
+ "Concurrent operation detected: a send is already in progress",
1114
+ { code: "REENTRANCY" /* REENTRANCY */ }
1115
+ );
1116
+ }
1117
+ this._acquired = true;
1118
+ }
1119
+ /** Release the guard. Safe to call even if not acquired. */
1120
+ release() {
1121
+ this._acquired = false;
1122
+ }
1123
+ };
1124
+ var ChatAbortController = class {
1125
+ _controller;
1126
+ _onExternalAbort;
1127
+ _externalSignal;
1128
+ constructor(externalSignal) {
1129
+ this._controller = new AbortController();
1130
+ this._externalSignal = externalSignal;
1131
+ if (externalSignal) {
1132
+ if (externalSignal.aborted) {
1133
+ this._controller.abort(externalSignal.reason);
1134
+ } else {
1135
+ this._onExternalAbort = () => {
1136
+ this._controller.abort(externalSignal.reason);
1137
+ };
1138
+ externalSignal.addEventListener("abort", this._onExternalAbort, { once: true });
1139
+ }
1140
+ }
1141
+ }
1142
+ /** The AbortSignal for this controller */
1143
+ get signal() {
1144
+ return this._controller.signal;
1145
+ }
1146
+ /** Whether the operation has been aborted */
1147
+ get isAborted() {
1148
+ return this._controller.signal.aborted;
1149
+ }
1150
+ /**
1151
+ * Abort the operation.
1152
+ * @param reason - Optional abort reason
1153
+ */
1154
+ abort(reason) {
1155
+ this._controller.abort(reason);
1156
+ }
1157
+ /** Clean up external signal listener to prevent memory leaks */
1158
+ dispose() {
1159
+ if (this._onExternalAbort && this._externalSignal) {
1160
+ this._externalSignal.removeEventListener("abort", this._onExternalAbort);
1161
+ }
1162
+ }
1163
+ };
1164
+
1165
+ // src/chat/accumulator.ts
1166
+ var MessageAccumulator = class {
1167
+ messageId;
1168
+ parts = [];
1169
+ status = "pending";
1170
+ currentTextPart = null;
1171
+ currentReasoningPart = null;
1172
+ toolCallParts = /* @__PURE__ */ new Map();
1173
+ _finalized = false;
1174
+ constructor(messageId) {
1175
+ this.messageId = messageId ?? createChatId();
1176
+ }
1177
+ /** Get current message ID */
1178
+ get id() {
1179
+ return this.messageId;
1180
+ }
1181
+ /**
1182
+ * Apply an AgentEvent to accumulate into the message
1183
+ * @param event - AgentEvent to process
1184
+ * @throws Error if accumulator is already finalized
1185
+ */
1186
+ apply(event) {
1187
+ if (this._finalized) throw new Error("Cannot apply events to finalized accumulator");
1188
+ if (this.status === "pending") {
1189
+ this.status = "streaming";
1190
+ }
1191
+ switch (event.type) {
1192
+ case "text_delta":
1193
+ this.handleTextDelta(event.text);
1194
+ break;
1195
+ case "thinking_start":
1196
+ this.finalizeCurrentText();
1197
+ this.currentReasoningPart = { type: "reasoning", text: "", status: "streaming" };
1198
+ this.parts.push(this.currentReasoningPart);
1199
+ break;
1200
+ case "thinking_delta":
1201
+ if (this.currentReasoningPart) {
1202
+ this.currentReasoningPart.text += event.text;
1203
+ }
1204
+ break;
1205
+ case "thinking_end":
1206
+ if (this.currentReasoningPart) {
1207
+ this.currentReasoningPart.status = "complete";
1208
+ this.currentReasoningPart = null;
1209
+ }
1210
+ break;
1211
+ case "tool_call_start": {
1212
+ this.finalizeCurrentText();
1213
+ const toolPart = {
1214
+ type: "tool_call",
1215
+ toolCallId: event.toolCallId,
1216
+ name: event.toolName,
1217
+ args: event.args,
1218
+ status: "running"
1219
+ };
1220
+ this.toolCallParts.set(event.toolCallId, toolPart);
1221
+ this.parts.push(toolPart);
1222
+ break;
1223
+ }
1224
+ case "tool_call_end": {
1225
+ const existing = this.toolCallParts.get(event.toolCallId);
1226
+ if (existing) {
1227
+ existing.result = event.result;
1228
+ existing.status = "complete";
1229
+ }
1230
+ break;
1231
+ }
1232
+ case "error":
1233
+ this.status = "error";
1234
+ break;
1235
+ }
1236
+ }
1237
+ handleTextDelta(text) {
1238
+ if (!this.currentTextPart) {
1239
+ this.currentTextPart = { type: "text", text: "", status: "streaming" };
1240
+ this.parts.push(this.currentTextPart);
1241
+ }
1242
+ this.currentTextPart.text += text;
1243
+ }
1244
+ finalizeCurrentText() {
1245
+ if (this.currentTextPart) {
1246
+ this.currentTextPart.status = "complete";
1247
+ this.currentTextPart = null;
1248
+ }
1249
+ }
1250
+ /**
1251
+ * Get a snapshot of the current accumulated message (for streaming UI)
1252
+ * @returns ChatMessage with current parts and "streaming" status
1253
+ */
1254
+ snapshot() {
1255
+ const now = (/* @__PURE__ */ new Date()).toISOString();
1256
+ return {
1257
+ id: this.messageId,
1258
+ role: "assistant",
1259
+ parts: this.parts.map((p) => ({ ...p })),
1260
+ status: this.status === "pending" ? "pending" : "streaming",
1261
+ createdAt: now,
1262
+ updatedAt: now
1263
+ };
1264
+ }
1265
+ /**
1266
+ * Finalize the accumulator and return the complete ChatMessage
1267
+ * @returns Completed ChatMessage with all parts finalized
1268
+ * @throws Error if accumulator is already finalized
1269
+ */
1270
+ finalize() {
1271
+ if (this._finalized) throw new Error("Accumulator already finalized");
1272
+ this._finalized = true;
1273
+ this.finalizeCurrentText();
1274
+ if (this.currentReasoningPart) {
1275
+ this.currentReasoningPart.status = "complete";
1276
+ this.currentReasoningPart = null;
1277
+ }
1278
+ for (const [, toolPart] of this.toolCallParts) {
1279
+ if (toolPart.status === "running" || toolPart.status === "pending") {
1280
+ toolPart.status = "error";
1281
+ }
1282
+ }
1283
+ if (this.status !== "error" && this.status !== "cancelled") {
1284
+ this.status = "complete";
1285
+ }
1286
+ const now = (/* @__PURE__ */ new Date()).toISOString();
1287
+ return {
1288
+ id: this.messageId,
1289
+ role: "assistant",
1290
+ parts: this.parts,
1291
+ status: this.status,
1292
+ createdAt: now,
1293
+ updatedAt: now
1294
+ };
1295
+ }
1296
+ /** Check if the accumulator has been finalized */
1297
+ get finalized() {
1298
+ return this._finalized;
1299
+ }
1300
+ };
1301
+
1302
+ // src/chat/watchdog.ts
1303
+ async function* withStreamWatchdog(source, config) {
1304
+ const { timeoutMs, signal } = config;
1305
+ const iterator = source[Symbol.asyncIterator]();
1306
+ let aborted = false;
1307
+ if (signal?.aborted) {
1308
+ iterator.return?.();
1309
+ return;
1310
+ }
1311
+ const onAbort = () => {
1312
+ aborted = true;
1313
+ iterator.return?.();
1314
+ };
1315
+ signal?.addEventListener("abort", onAbort, { once: true });
1316
+ try {
1317
+ while (true) {
1318
+ if (aborted) break;
1319
+ const timeout = new CancellableTimeout(timeoutMs);
1320
+ try {
1321
+ const result = await Promise.race([
1322
+ iterator.next(),
1323
+ timeout.promise
1324
+ ]);
1325
+ timeout.cancel();
1326
+ if (result.done) break;
1327
+ yield result.value;
1328
+ } catch (err) {
1329
+ timeout.cancel();
1330
+ throw err;
1331
+ }
1332
+ }
1333
+ } finally {
1334
+ signal?.removeEventListener("abort", onAbort);
1335
+ iterator.return?.();
1336
+ }
1337
+ }
1338
+ var CancellableTimeout = class {
1339
+ promise;
1340
+ _timer;
1341
+ _cancelled = false;
1342
+ constructor(ms) {
1343
+ this.promise = new Promise((_, reject) => {
1344
+ this._timer = setTimeout(() => {
1345
+ if (!this._cancelled) {
1346
+ reject(
1347
+ new ChatError(
1348
+ `Stream timed out after ${ms}ms of inactivity`,
1349
+ { code: "TIMEOUT" /* TIMEOUT */ }
1350
+ )
1351
+ );
1352
+ }
1353
+ }, ms);
1354
+ });
1355
+ this.promise.catch(() => {
1356
+ });
1357
+ }
1358
+ cancel() {
1359
+ this._cancelled = true;
1360
+ if (this._timer !== void 0) {
1361
+ clearTimeout(this._timer);
1362
+ this._timer = void 0;
1363
+ }
1364
+ }
1365
+ };
1366
+
1367
+ // src/chat/listener-set.ts
1368
+ var ListenerSet = class {
1369
+ _listeners = /* @__PURE__ */ new Set();
1370
+ /** Add a listener. Returns an unsubscribe function. */
1371
+ add(callback) {
1372
+ this._listeners.add(callback);
1373
+ return () => {
1374
+ this._listeners.delete(callback);
1375
+ };
1376
+ }
1377
+ /** Notify all listeners with the given arguments. Errors are isolated per listener. */
1378
+ notify(...args) {
1379
+ for (const cb of this._listeners) {
1380
+ try {
1381
+ cb(...args);
1382
+ } catch {
1383
+ }
1384
+ }
1385
+ }
1386
+ /** Remove all listeners. */
1387
+ clear() {
1388
+ this._listeners.clear();
1389
+ }
1390
+ /** Current number of listeners. */
1391
+ get size() {
1392
+ return this._listeners.size;
1393
+ }
1394
+ };
1395
+
1396
+ // src/chat/runtime.ts
1397
+ var ChatRuntime = class {
1398
+ _state;
1399
+ _guard;
1400
+ _backends;
1401
+ _sessionStore;
1402
+ _contextConfig;
1403
+ _ctxManager;
1404
+ _middleware;
1405
+ _tools = /* @__PURE__ */ new Map();
1406
+ _retryConfig;
1407
+ _contextStats = /* @__PURE__ */ new Map();
1408
+ _sessionUsage = /* @__PURE__ */ new Map();
1409
+ _modelContextWindows = /* @__PURE__ */ new Map();
1410
+ _onContextTrimmed;
1411
+ _streamTimeoutMs;
1412
+ _sessionListeners = new ListenerSet();
1413
+ _adapterPool = /* @__PURE__ */ new Map();
1414
+ _defaultBackend;
1415
+ _abortController = null;
1416
+ constructor(options) {
1417
+ this._state = new StateMachine("idle", RUNTIME_TRANSITIONS);
1418
+ this._guard = new ChatReentrancyGuard();
1419
+ this._backends = options.backends;
1420
+ this._defaultBackend = options.defaultBackend;
1421
+ this._sessionStore = options.sessionStore;
1422
+ this._contextConfig = options.context;
1423
+ if (this._contextConfig) {
1424
+ this._ctxManager = new ContextWindowManager(this._contextConfig);
1425
+ }
1426
+ this._middleware = [...options.middleware ?? []];
1427
+ this._retryConfig = options.retryConfig;
1428
+ this._onContextTrimmed = options.onContextTrimmed;
1429
+ this._streamTimeoutMs = options.streamTimeoutMs;
1430
+ if (!options.backends[options.defaultBackend]) {
1431
+ throw new ChatError(
1432
+ `Default backend "${options.defaultBackend}" not found in backends map`,
1433
+ { code: "INVALID_INPUT" /* INVALID_INPUT */ }
1434
+ );
1435
+ }
1436
+ if (options.tools) {
1437
+ for (const tool of options.tools) {
1438
+ this._tools.set(tool.name, tool);
1439
+ }
1440
+ }
1441
+ }
1442
+ // ── Lifecycle ──────────────────────────────────────────────
1443
+ get status() {
1444
+ return this._state.current;
1445
+ }
1446
+ async dispose() {
1447
+ if (this._state.current === "disposed") return;
1448
+ this._abortController?.abort("Runtime disposed");
1449
+ this._abortController?.dispose();
1450
+ this._abortController = null;
1451
+ this._state.transition("disposed");
1452
+ for (const adapter of this._adapterPool.values()) {
1453
+ try {
1454
+ await adapter.dispose();
1455
+ } catch {
1456
+ }
1457
+ }
1458
+ this._adapterPool.clear();
1459
+ }
1460
+ // ── Sessions ───────────────────────────────────────────────
1461
+ async createSession(options) {
1462
+ this.assertNotDisposed();
1463
+ const config = {
1464
+ model: options.config?.model ?? "",
1465
+ backend: options.config?.backend ?? this._defaultBackend,
1466
+ ...options.config
1467
+ };
1468
+ const session = await this._sessionStore.createSession({ ...options, config });
1469
+ this._notifySessionChange();
1470
+ return session;
1471
+ }
1472
+ async getSession(id) {
1473
+ this.assertNotDisposed();
1474
+ const cid = toChatId(id);
1475
+ return this._sessionStore.getSession(cid);
1476
+ }
1477
+ async listSessions(options) {
1478
+ this.assertNotDisposed();
1479
+ return this._sessionStore.listSessions(options);
1480
+ }
1481
+ async deleteSession(id) {
1482
+ this.assertNotDisposed();
1483
+ const cid = toChatId(id);
1484
+ const session = await this._sessionStore.getSession(cid);
1485
+ if (!session) return;
1486
+ await this._sessionStore.deleteSession(cid);
1487
+ this._contextStats.delete(cid);
1488
+ this._sessionUsage.delete(cid);
1489
+ this._notifySessionChange();
1490
+ }
1491
+ // ── Messaging ──────────────────────────────────────────────
1492
+ async *send(sessionId, message, options) {
1493
+ this.validateSendInput(message, options);
1494
+ this._guard.acquire();
1495
+ const cid = toChatId(sessionId);
1496
+ this._abortController = new ChatAbortController(options?.signal);
1497
+ try {
1498
+ if (this._state.current === "error") {
1499
+ this._state.transition("idle");
1500
+ }
1501
+ this._state.transition("streaming");
1502
+ await this.loadSession(cid);
1503
+ const mwCtx = {
1504
+ sessionId: cid,
1505
+ signal: this._abortController.signal
1506
+ };
1507
+ const userMessage = await this.applyBeforeSendMiddleware(
1508
+ this.createUserMessage(message),
1509
+ mwCtx
1510
+ );
1511
+ if (userMessage === null) {
1512
+ this._state.transition("idle");
1513
+ return;
1514
+ }
1515
+ const updatedSession = await this.persistAndReload(cid, userMessage);
1516
+ const sessionForAdapter = await this.trimSessionContext(cid, updatedSession, options.model);
1517
+ const stream = await this.prepareEventStream(
1518
+ cid,
1519
+ sessionForAdapter,
1520
+ updatedSession,
1521
+ message,
1522
+ options
1523
+ );
1524
+ const accumulator = new MessageAccumulator();
1525
+ const eventSource = this._streamTimeoutMs ? withStreamWatchdog(stream, { timeoutMs: this._streamTimeoutMs, signal: this._abortController.signal }) : stream;
1526
+ for await (const event of eventSource) {
1527
+ if (this._abortController.isAborted) break;
1528
+ this.feedAccumulator(accumulator, event);
1529
+ if (event.type === "usage") {
1530
+ this._sessionUsage.set(cid, {
1531
+ promptTokens: event.promptTokens,
1532
+ completionTokens: event.completionTokens
1533
+ });
1534
+ this.updateContextStatsWithUsage(cid, event.promptTokens, event.completionTokens, options);
1535
+ }
1536
+ const processed = await this.applyOnEventMiddleware(event, mwCtx);
1537
+ if (processed) yield processed;
1538
+ }
1539
+ if (this._state.current === "disposed") return;
1540
+ await this.finalizeAssistantMessage(cid, accumulator, mwCtx);
1541
+ this._state.transition("idle");
1542
+ } catch (error) {
1543
+ const result = await this.handleSendError(error, cid);
1544
+ if (result !== null) throw result;
1545
+ } finally {
1546
+ this._guard.release();
1547
+ this._abortController?.dispose();
1548
+ this._abortController = null;
1549
+ }
1550
+ }
1551
+ // ── Send Pipeline Stages ──────────────────────────────────────
1552
+ /** Stage 1: Validate send inputs (message content + required fields). */
1553
+ validateSendInput(message, options) {
1554
+ this.assertNotDisposed();
1555
+ if (!message || message.trim().length === 0) {
1556
+ throw new ChatError("Message cannot be empty", { code: "INVALID_INPUT" /* INVALID_INPUT */ });
1557
+ }
1558
+ if (!options.model) {
1559
+ throw new ChatError(
1560
+ "options.model is required \u2014 caller must specify which model to use",
1561
+ { code: "INVALID_INPUT" /* INVALID_INPUT */ }
1562
+ );
1563
+ }
1564
+ if (!options.backend) {
1565
+ throw new ChatError(
1566
+ "options.backend is required \u2014 caller must specify which backend to use",
1567
+ { code: "INVALID_INPUT" /* INVALID_INPUT */ }
1568
+ );
1569
+ }
1570
+ if (!options.credentials) {
1571
+ throw new ChatError(
1572
+ "options.credentials is required \u2014 caller must provide authentication credentials",
1573
+ { code: "INVALID_INPUT" /* INVALID_INPUT */ }
1574
+ );
1575
+ }
1576
+ }
1577
+ /** Stage 2: Load session from store. */
1578
+ async loadSession(cid) {
1579
+ const session = await this._sessionStore.getSession(cid);
1580
+ if (!session) {
1581
+ throw new ChatError(
1582
+ `Session "${cid}" not found`,
1583
+ { code: "SESSION_NOT_FOUND" /* SESSION_NOT_FOUND */ }
1584
+ );
1585
+ }
1586
+ return session;
1587
+ }
1588
+ /** Stage 3: Apply onBeforeSend middleware pipeline. Returns null if middleware rejected the send. */
1589
+ async applyBeforeSendMiddleware(userMessage, ctx) {
1590
+ let msg = userMessage;
1591
+ for (const mw of this._middleware) {
1592
+ if (mw.onBeforeSend && msg) {
1593
+ msg = await mw.onBeforeSend(msg, ctx);
1594
+ if (msg === null) return null;
1595
+ }
1596
+ }
1597
+ return msg;
1598
+ }
1599
+ /** Stage 4: Persist user message and reload session with full history. */
1600
+ async persistAndReload(cid, userMessage) {
1601
+ await this._sessionStore.appendMessage(cid, userMessage);
1602
+ return await this._sessionStore.getSession(cid);
1603
+ }
1604
+ /** Stage 5: Auto-trim context window if configured. Returns session snapshot for adapter. */
1605
+ async trimSessionContext(cid, session, model) {
1606
+ if (!this._ctxManager) return session;
1607
+ const ctxManager = this._ctxManager;
1608
+ const lastUsage = this._sessionUsage.get(cid);
1609
+ const modelContextWindow = model ? this._modelContextWindows.get(model) : void 0;
1610
+ if (lastUsage && modelContextWindow) {
1611
+ const result2 = ctxManager.fitMessagesWithUsage(
1612
+ session.messages,
1613
+ lastUsage.promptTokens,
1614
+ modelContextWindow
1615
+ );
1616
+ this._contextStats.set(cid, {
1617
+ totalTokens: result2.totalTokens,
1618
+ removedCount: result2.removedCount,
1619
+ wasTruncated: result2.wasTruncated,
1620
+ availableBudget: Math.max(0, modelContextWindow - result2.totalTokens),
1621
+ realPromptTokens: lastUsage.promptTokens,
1622
+ realCompletionTokens: lastUsage.completionTokens,
1623
+ modelContextWindow
1624
+ });
1625
+ if (result2.wasTruncated && this._onContextTrimmed) {
1626
+ const keptIds = new Set(result2.messages.map((m) => m.id));
1627
+ const removed = session.messages.filter((m) => !keptIds.has(m.id));
1628
+ if (removed.length > 0) {
1629
+ try {
1630
+ this._onContextTrimmed(cid, removed);
1631
+ } catch {
1632
+ }
1633
+ }
1634
+ }
1635
+ return { ...session, messages: result2.messages };
1636
+ }
1637
+ const result = await ctxManager.fitMessagesAsync(session.messages);
1638
+ this._contextStats.set(cid, {
1639
+ totalTokens: result.totalTokens,
1640
+ removedCount: result.removedCount,
1641
+ wasTruncated: result.wasTruncated,
1642
+ availableBudget: ctxManager.availableBudget,
1643
+ modelContextWindow
1644
+ });
1645
+ if (result.wasTruncated && this._onContextTrimmed) {
1646
+ const keptIds = new Set(result.messages.map((m) => m.id));
1647
+ const removed = session.messages.filter((m) => !keptIds.has(m.id));
1648
+ if (removed.length > 0) {
1649
+ try {
1650
+ this._onContextTrimmed(cid, removed);
1651
+ } catch {
1652
+ }
1653
+ }
1654
+ }
1655
+ return { ...session, messages: result.messages };
1656
+ }
1657
+ /** Update context stats with real usage data from a usage event. */
1658
+ updateContextStatsWithUsage(cid, promptTokens, completionTokens, options) {
1659
+ const modelContextWindow = options.model ? this._modelContextWindows.get(options.model) : void 0;
1660
+ const existing = this._contextStats.get(cid);
1661
+ this._contextStats.set(cid, {
1662
+ totalTokens: promptTokens,
1663
+ removedCount: existing?.removedCount ?? 0,
1664
+ wasTruncated: existing?.wasTruncated ?? false,
1665
+ availableBudget: modelContextWindow ? Math.max(0, modelContextWindow - promptTokens) : existing?.availableBudget ?? 0,
1666
+ realPromptTokens: promptTokens,
1667
+ realCompletionTokens: completionTokens,
1668
+ modelContextWindow
1669
+ });
1670
+ }
1671
+ /** Stage 6: Prepare event stream — adapter with retry, tool injection. */
1672
+ async prepareEventStream(cid, sessionForAdapter, fullSession, message, options) {
1673
+ const adapter = await this.getOrCreateAdapterWithRetry(options.backend, options.credentials);
1674
+ const runtimeTools = this._tools.size > 0 ? this.injectToolContext([...this._tools.values()], {
1675
+ sessionId: cid,
1676
+ custom: fullSession.metadata?.custom
1677
+ }) : void 0;
1678
+ const streamOptions = {
1679
+ signal: this._abortController.signal,
1680
+ model: options.model,
1681
+ systemPrompt: options.systemPrompt,
1682
+ tools: runtimeTools
1683
+ };
1684
+ return this.createStreamWithRetry(
1685
+ adapter,
1686
+ sessionForAdapter,
1687
+ message,
1688
+ streamOptions,
1689
+ options.backend,
1690
+ options.credentials
1691
+ );
1692
+ }
1693
+ /** Stage 7: Apply onEvent middleware pipeline (sequential transform/suppress). */
1694
+ async applyOnEventMiddleware(event, ctx) {
1695
+ let processed = event;
1696
+ for (const mw of this._middleware) {
1697
+ if (mw.onEvent && processed) {
1698
+ processed = await mw.onEvent(processed, ctx);
1699
+ }
1700
+ }
1701
+ return processed;
1702
+ }
1703
+ /** Stage 8: Finalize accumulator, apply afterReceive middleware, persist assistant message. */
1704
+ async finalizeAssistantMessage(cid, accumulator, ctx) {
1705
+ let assistantMessage = accumulator.finalize();
1706
+ for (const mw of this._middleware) {
1707
+ if (mw.onAfterReceive) {
1708
+ assistantMessage = await mw.onAfterReceive(assistantMessage, ctx);
1709
+ }
1710
+ }
1711
+ await this._sessionStore.appendMessage(cid, assistantMessage);
1712
+ this._notifySessionChange();
1713
+ }
1714
+ /** Stage 9: Error handling — apply onError middleware, transition state. Returns null if suppressed. */
1715
+ async handleSendError(error, cid) {
1716
+ let processedError = error instanceof Error ? error : new Error(String(error));
1717
+ const ctx = {
1718
+ sessionId: cid,
1719
+ signal: this._abortController?.signal ?? new AbortController().signal
1720
+ };
1721
+ for (const mw of this._middleware) {
1722
+ if (mw.onError) {
1723
+ const result = await mw.onError(processedError, ctx);
1724
+ if (result === null) {
1725
+ if (this._state.canTransition("idle")) {
1726
+ this._state.transition("idle");
1727
+ }
1728
+ return null;
1729
+ }
1730
+ processedError = result;
1731
+ }
1732
+ }
1733
+ if (this._state.canTransition("error")) {
1734
+ this._state.transition("error");
1735
+ }
1736
+ return processedError;
1737
+ }
1738
+ abort() {
1739
+ this._abortController?.abort("User abort");
1740
+ }
1741
+ // ── Backend / Model ────────────────────────────────────────
1742
+ async listModels(options) {
1743
+ this.assertNotDisposed();
1744
+ let models = [];
1745
+ const firstAdapter = [...this._adapterPool.values()][0];
1746
+ if (firstAdapter) {
1747
+ try {
1748
+ models = await firstAdapter.listModels();
1749
+ } catch {
1750
+ return [];
1751
+ }
1752
+ } else if (options?.backend && options?.credentials) {
1753
+ try {
1754
+ const adapter = await this.getOrCreateAdapter(options.backend, options.credentials);
1755
+ models = await adapter.listModels();
1756
+ } catch {
1757
+ return [];
1758
+ }
1759
+ }
1760
+ for (const model of models) {
1761
+ if (model.contextWindow != null) {
1762
+ this._modelContextWindows.set(model.id, model.contextWindow);
1763
+ }
1764
+ }
1765
+ return models;
1766
+ }
1767
+ async listBackends() {
1768
+ this.assertNotDisposed();
1769
+ return Object.keys(this._backends).map((name) => ({ name }));
1770
+ }
1771
+ // ── Tools ──────────────────────────────────────────────────
1772
+ get registeredTools() {
1773
+ return this._tools;
1774
+ }
1775
+ registerTool(tool) {
1776
+ this.assertNotDisposed();
1777
+ this._tools.set(tool.name, tool);
1778
+ }
1779
+ removeTool(name) {
1780
+ this.assertNotDisposed();
1781
+ this._tools.delete(name);
1782
+ }
1783
+ // ── Middleware ──────────────────────────────────────────────
1784
+ use(middleware) {
1785
+ this.assertNotDisposed();
1786
+ this._middleware.push(middleware);
1787
+ }
1788
+ removeMiddleware(middleware) {
1789
+ this.assertNotDisposed();
1790
+ const idx = this._middleware.indexOf(middleware);
1791
+ if (idx >= 0) this._middleware.splice(idx, 1);
1792
+ }
1793
+ // ── Context Stats ─────────────────────────────────────────
1794
+ async getContextStats(sessionId) {
1795
+ const cid = toChatId(sessionId);
1796
+ return this._contextStats.get(cid) ?? null;
1797
+ }
1798
+ // ── Session Subscription ──────────────────────────────────
1799
+ onSessionChange(callback) {
1800
+ return this._sessionListeners.add(callback);
1801
+ }
1802
+ _notifySessionChange() {
1803
+ this._sessionListeners.notify();
1804
+ }
1805
+ // ── Private Helpers ────────────────────────────────────────
1806
+ async getOrCreateAdapter(backend, credentials) {
1807
+ const key = this.getPoolKey(backend, credentials);
1808
+ const existing = this._adapterPool.get(key);
1809
+ if (existing) return existing;
1810
+ for (const [oldKey, oldAdapter] of this._adapterPool) {
1811
+ if (oldKey.startsWith(backend + ":")) {
1812
+ try {
1813
+ await oldAdapter.dispose();
1814
+ } catch {
1815
+ }
1816
+ this._adapterPool.delete(oldKey);
1817
+ }
1818
+ }
1819
+ const factory = this._backends[backend];
1820
+ if (!factory) {
1821
+ throw new ChatError(
1822
+ `Backend "${backend}" not found`,
1823
+ { code: "INVALID_INPUT" /* INVALID_INPUT */ }
1824
+ );
1825
+ }
1826
+ const adapter = await factory(credentials);
1827
+ this._adapterPool.set(key, adapter);
1828
+ return adapter;
1829
+ }
1830
+ getPoolKey(backend, credentials) {
1831
+ const token = credentials.accessToken;
1832
+ const hash = token.length > 16 ? token.slice(0, 8) + token.slice(-8) : token;
1833
+ return `${backend}:${hash}`;
1834
+ }
1835
+ /** Wrap each tool's execute to inject ToolContext as 2nd argument */
1836
+ injectToolContext(tools, context) {
1837
+ return tools.map((tool) => ({
1838
+ ...tool,
1839
+ execute: (params) => tool.execute(params, context)
1840
+ }));
1841
+ }
1842
+ /** Map ChatEvent to AgentEvent for MessageAccumulator */
1843
+ feedAccumulator(acc, event) {
1844
+ const agentEvent = chatEventToAgentEvent(event);
1845
+ if (agentEvent) acc.apply(agentEvent);
1846
+ }
1847
+ createUserMessage(text) {
1848
+ return {
1849
+ id: createChatId(),
1850
+ role: "user",
1851
+ parts: [{ type: "text", text, status: "complete" }],
1852
+ createdAt: (/* @__PURE__ */ new Date()).toISOString(),
1853
+ status: "complete"
1854
+ };
1855
+ }
1856
+ assertNotDisposed() {
1857
+ if (this._state.current === "disposed") {
1858
+ throw new ChatError(
1859
+ "Runtime is disposed",
1860
+ { code: "DISPOSED" /* DISPOSED */ }
1861
+ );
1862
+ }
1863
+ }
1864
+ /** Get or create adapter with retry on connection errors */
1865
+ async getOrCreateAdapterWithRetry(backend, credentials) {
1866
+ const maxAttempts = this._retryConfig?.maxAttempts ?? 1;
1867
+ const delayMs = this._retryConfig?.delayMs ?? 0;
1868
+ let lastError;
1869
+ for (let attempt = 1; attempt <= maxAttempts; attempt++) {
1870
+ try {
1871
+ return await this.getOrCreateAdapter(backend, credentials);
1872
+ } catch (err) {
1873
+ lastError = err instanceof Error ? err : new Error(String(err));
1874
+ if (attempt < maxAttempts) {
1875
+ const key = this.getPoolKey(backend, credentials);
1876
+ const old = this._adapterPool.get(key);
1877
+ if (old) {
1878
+ try {
1879
+ await old.dispose();
1880
+ } catch {
1881
+ }
1882
+ }
1883
+ this._adapterPool.delete(key);
1884
+ await delay(delayMs);
1885
+ }
1886
+ }
1887
+ }
1888
+ throw lastError;
1889
+ }
1890
+ /**
1891
+ * Create stream with retry for pre-stream connection errors.
1892
+ * Tries to get the first event from the stream; if that fails,
1893
+ * retries with a fresh adapter. Once first event is received,
1894
+ * the stream is committed (no more retries).
1895
+ */
1896
+ async createStreamWithRetry(adapter, session, message, options, backend, credentials) {
1897
+ const maxAttempts = this._retryConfig?.maxAttempts ?? 1;
1898
+ const delayMs = this._retryConfig?.delayMs ?? 0;
1899
+ let lastError;
1900
+ let currentAdapter = adapter;
1901
+ for (let attempt = 1; attempt <= maxAttempts; attempt++) {
1902
+ try {
1903
+ const stream = currentAdapter.streamMessage(session, message, options);
1904
+ const iterator = stream[Symbol.asyncIterator]();
1905
+ const first = await iterator.next();
1906
+ return (async function* () {
1907
+ if (!first.done) yield first.value;
1908
+ while (true) {
1909
+ const next = await iterator.next();
1910
+ if (next.done) break;
1911
+ yield next.value;
1912
+ }
1913
+ })();
1914
+ } catch (err) {
1915
+ lastError = err instanceof Error ? err : new Error(String(err));
1916
+ if (attempt < maxAttempts) {
1917
+ try {
1918
+ await currentAdapter.dispose();
1919
+ } catch {
1920
+ }
1921
+ const key = this.getPoolKey(backend, credentials);
1922
+ this._adapterPool.delete(key);
1923
+ await delay(delayMs);
1924
+ currentAdapter = await this.getOrCreateAdapter(backend, credentials);
1925
+ }
1926
+ }
1927
+ }
1928
+ throw lastError;
1929
+ }
1930
+ };
1931
+ function delay(ms) {
1932
+ return new Promise((resolve2) => setTimeout(resolve2, ms));
1933
+ }
1934
+ function createChatRuntime(options) {
1935
+ return new ChatRuntime(options);
1936
+ }
1937
+ var DEFAULT_PROVIDER_MODELS = {
1938
+ copilot: "gpt-5-mini",
1939
+ claude: "claude-sonnet-4-5-20250514",
1940
+ "vercel-ai": "gpt-4.1-mini"
1941
+ };
487
1942
  var MIME_TYPES = {
488
1943
  ".html": "text/html",
489
1944
  ".css": "text/css",
@@ -503,15 +1958,25 @@ var MIME_TYPES = {
503
1958
  ".map": "application/json"
504
1959
  };
505
1960
  function createChatServer(options) {
1961
+ const runtime = options.runtime ?? (options.runtimeConfig ? createChatRuntime(options.runtimeConfig) : (() => {
1962
+ throw new Error("Either `runtime` or `runtimeConfig` must be provided to createChatServer");
1963
+ })());
506
1964
  const chatPrefix = options.chatPrefix ?? "/api/chat";
507
1965
  const authPrefix = options.authPrefix ?? "/api/auth";
508
1966
  const staticPrefix = options.staticPrefix ?? "/";
509
1967
  const staticDir = options.staticDir ? path__namespace.resolve(options.staticDir) : void 0;
510
- const chatHandler = createChatHandler(options.runtime, {
1968
+ const healthPath = options.healthPath !== false ? options.healthPath ?? "/api/health" : void 0;
1969
+ const authOptions = wrapAuthWithServiceManager(
1970
+ wrapAuthWithAutoProviders(options),
1971
+ options.serviceManager
1972
+ );
1973
+ const chatHandler = createChatHandler(runtime, {
511
1974
  prefix: chatPrefix,
1975
+ providerStore: options.providers?.providerStore,
1976
+ hooks: options.hooks,
512
1977
  ...options.chatHandlerOptions
513
1978
  });
514
- const authHandler = options.auth ? createAuthHandler(options.auth) : void 0;
1979
+ const authHandler = authOptions ? createAuthHandler(authOptions) : void 0;
515
1980
  const cors = options.cors !== false ? corsMiddleware(options.cors) : void 0;
516
1981
  return async (req, res) => {
517
1982
  const url = req.url || "/";
@@ -527,6 +1992,10 @@ function createChatServer(options) {
527
1992
  return;
528
1993
  }
529
1994
  }
1995
+ if (healthPath && urlPath === healthPath) {
1996
+ json(res, { ok: true }, 200);
1997
+ return;
1998
+ }
530
1999
  if (urlPath.startsWith(chatPrefix + "/") || urlPath === chatPrefix) {
531
2000
  await chatHandler(req, res);
532
2001
  return;
@@ -563,13 +2032,51 @@ function createChatServer(options) {
563
2032
  } catch {
564
2033
  }
565
2034
  }
566
- json3(res, 404, { error: "Not found" });
2035
+ json(res, { error: "Not found" }, 404);
567
2036
  };
568
2037
  }
569
- function json3(res, status, body) {
570
- const data = JSON.stringify(body);
571
- res.writeHead(status, { "Content-Type": "application/json" });
572
- res.end(data);
2038
+ function wrapAuthWithAutoProviders(options) {
2039
+ if (!options.auth) return void 0;
2040
+ if (!options.autoCreateProviders || !options.providers) return options.auth;
2041
+ const providerStore = options.providers.providerStore;
2042
+ const modelMap = typeof options.autoCreateProviders === "object" ? options.autoCreateProviders : DEFAULT_PROVIDER_MODELS;
2043
+ const userOnAuth = options.auth.onAuth;
2044
+ const wrappedOnAuth = async (backend, token) => {
2045
+ if (userOnAuth) await userOnAuth(backend, token);
2046
+ try {
2047
+ const existing = await providerStore.list();
2048
+ const hasBackend = existing.some((p) => p.backend === backend);
2049
+ if (!hasBackend) {
2050
+ const model = modelMap[backend] ?? "default";
2051
+ const label = `${backend.charAt(0).toUpperCase() + backend.slice(1)} ${model}`;
2052
+ await providerStore.create({
2053
+ id: crypto.randomUUID(),
2054
+ backend,
2055
+ model,
2056
+ label,
2057
+ createdAt: Date.now()
2058
+ });
2059
+ }
2060
+ } catch {
2061
+ }
2062
+ };
2063
+ return { ...options.auth, onAuth: wrappedOnAuth };
2064
+ }
2065
+ function wrapAuthWithServiceManager(authOptions, serviceManager) {
2066
+ if (!authOptions || !serviceManager) return authOptions;
2067
+ const userOnAuth = authOptions.onAuth;
2068
+ const userOnLogout = authOptions.onLogout;
2069
+ return {
2070
+ ...authOptions,
2071
+ onAuth: async (backend, token) => {
2072
+ if (userOnAuth) await userOnAuth(backend, token);
2073
+ await serviceManager.handleAuth(backend, token);
2074
+ },
2075
+ onLogout: async () => {
2076
+ if (userOnLogout) await userOnLogout();
2077
+ await serviceManager.handleLogout();
2078
+ }
2079
+ };
573
2080
  }
574
2081
  var InMemoryTokenStore = class {
575
2082
  tokens = /* @__PURE__ */ new Map();
@@ -632,12 +2139,540 @@ var FileTokenStore = class {
632
2139
  return path.join(this.dir, `${provider}-token.json`);
633
2140
  }
634
2141
  };
2142
+ function createProviderHandler(options) {
2143
+ const { providerStore } = options;
2144
+ return async (req, res) => {
2145
+ const url = req.url || "";
2146
+ const method = req.method || "GET";
2147
+ const path2 = url.split("?")[0];
2148
+ const idMatch = path2.match(/^\/providers\/([^/]+)$/);
2149
+ try {
2150
+ if (method === "GET" && path2 === "/providers") {
2151
+ const providers = await providerStore.list();
2152
+ json(res, providers);
2153
+ return;
2154
+ }
2155
+ if (method === "GET" && idMatch) {
2156
+ const id = decodeURIComponent(idMatch[1]);
2157
+ const provider = await providerStore.get(id);
2158
+ if (!provider) {
2159
+ json(res, { error: "Provider not found" }, 404);
2160
+ return;
2161
+ }
2162
+ json(res, provider);
2163
+ return;
2164
+ }
2165
+ if (method === "POST" && path2 === "/providers") {
2166
+ const body = await readBody(req);
2167
+ const backend = body.backend;
2168
+ const model = body.model;
2169
+ const label = body.label;
2170
+ if (!backend || typeof backend !== "string") {
2171
+ json(res, { error: "backend is required" }, 400);
2172
+ return;
2173
+ }
2174
+ if (!model || typeof model !== "string") {
2175
+ json(res, { error: "model is required" }, 400);
2176
+ return;
2177
+ }
2178
+ if (!label || typeof label !== "string") {
2179
+ json(res, { error: "label is required" }, 400);
2180
+ return;
2181
+ }
2182
+ const config = {
2183
+ id: crypto$1.randomUUID(),
2184
+ backend,
2185
+ model,
2186
+ label,
2187
+ createdAt: Date.now()
2188
+ };
2189
+ await providerStore.create(config);
2190
+ json(res, config, 201);
2191
+ return;
2192
+ }
2193
+ if (method === "PUT" && idMatch) {
2194
+ const id = decodeURIComponent(idMatch[1]);
2195
+ const existing = await providerStore.get(id);
2196
+ if (!existing) {
2197
+ json(res, { error: "Provider not found" }, 404);
2198
+ return;
2199
+ }
2200
+ const body = await readBody(req);
2201
+ const changes = {};
2202
+ if (body.backend && typeof body.backend === "string") changes.backend = body.backend;
2203
+ if (body.model && typeof body.model === "string") changes.model = body.model;
2204
+ if (body.label && typeof body.label === "string") changes.label = body.label;
2205
+ await providerStore.update(id, changes);
2206
+ const updated = await providerStore.get(id);
2207
+ json(res, updated);
2208
+ return;
2209
+ }
2210
+ if (method === "DELETE" && idMatch) {
2211
+ const id = decodeURIComponent(idMatch[1]);
2212
+ await providerStore.delete(id);
2213
+ json(res, { ok: true });
2214
+ return;
2215
+ }
2216
+ json(res, { error: "Not found" }, 404);
2217
+ } catch (err) {
2218
+ const message = err instanceof Error ? err.message : String(err);
2219
+ json(res, { error: message }, 500);
2220
+ }
2221
+ };
2222
+ }
2223
+ var InMemoryProviderStore = class {
2224
+ providers = /* @__PURE__ */ new Map();
2225
+ async create(config) {
2226
+ const id = config.id || crypto$1.randomUUID();
2227
+ this.providers.set(id, { ...config, id });
2228
+ }
2229
+ async get(id) {
2230
+ const p = this.providers.get(id);
2231
+ return p ? { ...p } : null;
2232
+ }
2233
+ async update(id, changes) {
2234
+ const existing = this.providers.get(id);
2235
+ if (!existing) {
2236
+ throw new Error(`Provider "${id}" not found`);
2237
+ }
2238
+ this.providers.set(id, { ...existing, ...changes, id: existing.id, createdAt: existing.createdAt });
2239
+ }
2240
+ async delete(id) {
2241
+ this.providers.delete(id);
2242
+ }
2243
+ async list() {
2244
+ return [...this.providers.values()].map((p) => ({ ...p }));
2245
+ }
2246
+ };
2247
+ var FileProviderStore = class {
2248
+ dir;
2249
+ constructor(options) {
2250
+ this.dir = options.directory;
2251
+ }
2252
+ async create(config) {
2253
+ const id = config.id || crypto$1.randomUUID();
2254
+ const data = { ...config, id };
2255
+ fs.mkdirSync(this.dir, { recursive: true });
2256
+ fs.writeFileSync(this.filePath(id), JSON.stringify(data));
2257
+ }
2258
+ async get(id) {
2259
+ try {
2260
+ const data = fs.readFileSync(this.filePath(id), "utf-8");
2261
+ return JSON.parse(data);
2262
+ } catch {
2263
+ return null;
2264
+ }
2265
+ }
2266
+ async update(id, changes) {
2267
+ const existing = await this.get(id);
2268
+ if (!existing) {
2269
+ throw new Error(`Provider "${id}" not found`);
2270
+ }
2271
+ const updated = { ...existing, ...changes, id: existing.id, createdAt: existing.createdAt };
2272
+ fs.writeFileSync(this.filePath(id), JSON.stringify(updated));
2273
+ }
2274
+ async delete(id) {
2275
+ try {
2276
+ fs.unlinkSync(this.filePath(id));
2277
+ } catch {
2278
+ }
2279
+ }
2280
+ async list() {
2281
+ if (!fs.existsSync(this.dir)) return [];
2282
+ return fs.readdirSync(this.dir).filter((f) => f.endsWith("-provider.json")).map((f) => {
2283
+ try {
2284
+ const data = fs.readFileSync(path.join(this.dir, f), "utf-8");
2285
+ return JSON.parse(data);
2286
+ } catch {
2287
+ return null;
2288
+ }
2289
+ }).filter((p) => p !== null);
2290
+ }
2291
+ filePath(id) {
2292
+ return path.join(this.dir, `${id}-provider.json`);
2293
+ }
2294
+ };
2295
+
2296
+ // src/auth/refresh-manager.ts
2297
+ var TokenRefreshManager = class {
2298
+ currentToken;
2299
+ refreshFn;
2300
+ threshold;
2301
+ maxRetries;
2302
+ retryDelayMs;
2303
+ minDelayMs;
2304
+ timerId = null;
2305
+ running = false;
2306
+ disposed = false;
2307
+ listeners = {
2308
+ refreshed: /* @__PURE__ */ new Set(),
2309
+ error: /* @__PURE__ */ new Set(),
2310
+ expired: /* @__PURE__ */ new Set(),
2311
+ disposed: /* @__PURE__ */ new Set()
2312
+ };
2313
+ constructor(options) {
2314
+ this.currentToken = { ...options.token };
2315
+ this.refreshFn = options.refresh;
2316
+ this.threshold = options.refreshThreshold ?? 0.8;
2317
+ this.maxRetries = options.maxRetries ?? 3;
2318
+ this.retryDelayMs = options.retryDelayMs ?? 1e3;
2319
+ this.minDelayMs = options.minDelayMs ?? 1e3;
2320
+ }
2321
+ /** Register an event listener */
2322
+ on(event, listener) {
2323
+ this.listeners[event].add(listener);
2324
+ return this;
2325
+ }
2326
+ /** Remove an event listener */
2327
+ off(event, listener) {
2328
+ this.listeners[event].delete(listener);
2329
+ return this;
2330
+ }
2331
+ /** Current token managed by this instance */
2332
+ get token() {
2333
+ return { ...this.currentToken };
2334
+ }
2335
+ /** Whether the manager is currently running */
2336
+ get isRunning() {
2337
+ return this.running;
2338
+ }
2339
+ /** Whether the manager has been disposed */
2340
+ get isDisposed() {
2341
+ return this.disposed;
2342
+ }
2343
+ /**
2344
+ * Start automatic refresh scheduling.
2345
+ * If the token is already expired, emits "expired" immediately.
2346
+ * If the token has no expiresIn, does nothing (long-lived token).
2347
+ */
2348
+ start() {
2349
+ if (this.disposed) return;
2350
+ if (this.running) return;
2351
+ this.running = true;
2352
+ this.schedule();
2353
+ }
2354
+ /** Stop automatic refresh (can be restarted with start()) */
2355
+ stop() {
2356
+ this.running = false;
2357
+ this.clearTimer();
2358
+ }
2359
+ /**
2360
+ * Update the managed token (e.g. after manual refresh).
2361
+ * Reschedules automatic refresh if running.
2362
+ */
2363
+ updateToken(token) {
2364
+ if (this.disposed) return;
2365
+ this.currentToken = { ...token };
2366
+ if (this.running) {
2367
+ this.clearTimer();
2368
+ this.schedule();
2369
+ }
2370
+ }
2371
+ /** Stop and clean up all resources */
2372
+ dispose() {
2373
+ if (this.disposed) return;
2374
+ this.stop();
2375
+ this.disposed = true;
2376
+ this.emit("disposed");
2377
+ for (const set of Object.values(this.listeners)) {
2378
+ set.clear();
2379
+ }
2380
+ }
2381
+ // ─── Private ──────────────────────────────────────────────────
2382
+ schedule() {
2383
+ if (!this.running || this.disposed) return;
2384
+ const delayMs = this.computeRefreshDelay();
2385
+ if (delayMs === null) {
2386
+ return;
2387
+ }
2388
+ if (delayMs <= 0) {
2389
+ this.timerId = setTimeout(() => {
2390
+ this.timerId = null;
2391
+ if (!this.running || this.disposed) return;
2392
+ void this.performRefresh();
2393
+ }, 0);
2394
+ return;
2395
+ }
2396
+ this.timerId = setTimeout(() => {
2397
+ this.timerId = null;
2398
+ if (!this.running || this.disposed) return;
2399
+ void this.performRefresh();
2400
+ }, Math.max(delayMs, this.minDelayMs));
2401
+ }
2402
+ async performRefresh(attempt = 1) {
2403
+ if (!this.running || this.disposed) return;
2404
+ try {
2405
+ const newToken = await this.refreshFn(this.currentToken);
2406
+ if (!this.running || this.disposed) return;
2407
+ this.currentToken = { ...newToken };
2408
+ this.emit("refreshed", newToken);
2409
+ this.schedule();
2410
+ } catch (err) {
2411
+ if (!this.running || this.disposed) return;
2412
+ const error = err instanceof Error ? err : new Error(String(err));
2413
+ this.emit("error", error, attempt);
2414
+ if (attempt < this.maxRetries) {
2415
+ const delay2 = this.retryDelayMs * Math.pow(2, attempt - 1);
2416
+ this.timerId = setTimeout(() => {
2417
+ this.timerId = null;
2418
+ if (!this.running || this.disposed) return;
2419
+ void this.performRefresh(attempt + 1);
2420
+ }, delay2);
2421
+ } else {
2422
+ if (this.isTokenExpired()) {
2423
+ this.running = false;
2424
+ this.emit("expired");
2425
+ } else {
2426
+ const expiresIn = this.currentToken.expiresIn;
2427
+ if (expiresIn == null) return;
2428
+ const expiresAt = this.currentToken.obtainedAt + expiresIn * 1e3;
2429
+ const waitMs = Math.max(expiresAt - Date.now(), this.minDelayMs);
2430
+ this.timerId = setTimeout(() => {
2431
+ this.timerId = null;
2432
+ if (!this.running || this.disposed) return;
2433
+ void this.performRefresh();
2434
+ }, waitMs);
2435
+ }
2436
+ }
2437
+ }
2438
+ }
2439
+ computeRefreshDelay() {
2440
+ if (this.currentToken.expiresIn == null) return null;
2441
+ const lifetimeMs = this.currentToken.expiresIn * 1e3;
2442
+ const refreshAtMs = this.currentToken.obtainedAt + lifetimeMs * this.threshold;
2443
+ const now = Date.now();
2444
+ const delay2 = refreshAtMs - now;
2445
+ return delay2;
2446
+ }
2447
+ isTokenExpired() {
2448
+ if (this.currentToken.expiresIn == null) return false;
2449
+ const expiresAt = this.currentToken.obtainedAt + this.currentToken.expiresIn * 1e3;
2450
+ return Date.now() >= expiresAt;
2451
+ }
2452
+ clearTimer() {
2453
+ if (this.timerId !== null) {
2454
+ clearTimeout(this.timerId);
2455
+ this.timerId = null;
2456
+ }
2457
+ }
2458
+ emit(event, ...args) {
2459
+ for (const listener of this.listeners[event]) {
2460
+ try {
2461
+ listener(...args);
2462
+ } catch {
2463
+ }
2464
+ }
2465
+ }
2466
+ };
2467
+
2468
+ // src/chat/server/service-manager.ts
2469
+ var ServiceManager = class {
2470
+ _services = /* @__PURE__ */ new Map();
2471
+ _refreshManagers = /* @__PURE__ */ new Map();
2472
+ _options;
2473
+ constructor(options) {
2474
+ this._options = options;
2475
+ }
2476
+ /**
2477
+ * Handle auth event: dispose old service (if any) and create new one.
2478
+ * If the token is refreshable and refreshFactory is configured, starts a
2479
+ * TokenRefreshManager that auto-refreshes and recreates the service.
2480
+ */
2481
+ async handleAuth(backend, token) {
2482
+ this._stopRefreshManager(backend);
2483
+ const old = this._services.get(backend);
2484
+ if (old) {
2485
+ try {
2486
+ await old.dispose();
2487
+ } catch {
2488
+ }
2489
+ }
2490
+ const service = await this._options.createService(backend, token);
2491
+ this._services.set(backend, service);
2492
+ this._startRefreshManager(backend, token);
2493
+ return service;
2494
+ }
2495
+ /**
2496
+ * Handle logout: dispose all services, stop all refresh managers, clear cache.
2497
+ */
2498
+ async handleLogout() {
2499
+ for (const backend of [...this._refreshManagers.keys()]) {
2500
+ this._stopRefreshManager(backend);
2501
+ }
2502
+ for (const [, service] of this._services) {
2503
+ try {
2504
+ await service.dispose();
2505
+ } catch {
2506
+ }
2507
+ }
2508
+ this._services.clear();
2509
+ }
2510
+ /**
2511
+ * Dispose the ServiceManager — stops all refresh managers and disposes all services.
2512
+ */
2513
+ async dispose() {
2514
+ await this.handleLogout();
2515
+ }
2516
+ /** Get cached service for a backend (undefined if not authenticated). */
2517
+ getService(backend) {
2518
+ return this._services.get(backend);
2519
+ }
2520
+ /** Check if a service exists for the given backend. */
2521
+ hasService(backend) {
2522
+ return this._services.has(backend);
2523
+ }
2524
+ /** Get all backend names with active services. */
2525
+ get activeBackends() {
2526
+ return [...this._services.keys()];
2527
+ }
2528
+ /** Get active refresh manager for a backend (for testing/introspection). */
2529
+ getRefreshManager(backend) {
2530
+ return this._refreshManagers.get(backend);
2531
+ }
2532
+ // ── Private ─────────────────────────────────────────────────
2533
+ _startRefreshManager(backend, token) {
2534
+ if (!this._options.refreshFactory) return;
2535
+ if (token.expiresIn == null) return;
2536
+ const refreshFn = this._options.refreshFactory(backend);
2537
+ if (!refreshFn) return;
2538
+ const manager = new TokenRefreshManager({
2539
+ token,
2540
+ refresh: refreshFn,
2541
+ refreshThreshold: this._options.refreshOptions?.refreshThreshold,
2542
+ maxRetries: this._options.refreshOptions?.maxRetries,
2543
+ retryDelayMs: this._options.refreshOptions?.retryDelayMs
2544
+ });
2545
+ manager.on("refreshed", (newToken) => {
2546
+ void this._recreateService(backend, newToken);
2547
+ });
2548
+ manager.on("expired", () => {
2549
+ this._options.onTokenExpired?.(backend);
2550
+ void this._logoutBackend(backend);
2551
+ });
2552
+ this._refreshManagers.set(backend, manager);
2553
+ manager.start();
2554
+ }
2555
+ _stopRefreshManager(backend) {
2556
+ const manager = this._refreshManagers.get(backend);
2557
+ if (manager) {
2558
+ manager.dispose();
2559
+ this._refreshManagers.delete(backend);
2560
+ }
2561
+ }
2562
+ async _recreateService(backend, token) {
2563
+ const old = this._services.get(backend);
2564
+ if (old) {
2565
+ try {
2566
+ await old.dispose();
2567
+ } catch {
2568
+ }
2569
+ }
2570
+ try {
2571
+ const service = await this._options.createService(backend, token);
2572
+ this._services.set(backend, service);
2573
+ } catch {
2574
+ this._services.delete(backend);
2575
+ }
2576
+ }
2577
+ async _logoutBackend(backend) {
2578
+ this._stopRefreshManager(backend);
2579
+ const service = this._services.get(backend);
2580
+ if (service) {
2581
+ try {
2582
+ await service.dispose();
2583
+ } catch {
2584
+ }
2585
+ this._services.delete(backend);
2586
+ }
2587
+ }
2588
+ };
2589
+
2590
+ // src/chat/server/adapter-pool.ts
2591
+ var AdapterPool = class {
2592
+ _cached = /* @__PURE__ */ new Map();
2593
+ _pending = /* @__PURE__ */ new Map();
2594
+ _factory;
2595
+ _disposed = false;
2596
+ constructor(options) {
2597
+ this._factory = options.factory;
2598
+ }
2599
+ /**
2600
+ * Get or create an adapter for the given backend.
2601
+ * Concurrent calls for the same backend share one creation promise.
2602
+ * Failed creations are NOT cached — next call retries.
2603
+ */
2604
+ async getAdapter(backend) {
2605
+ if (this._disposed) {
2606
+ throw new Error("AdapterPool is disposed");
2607
+ }
2608
+ const cached = this._cached.get(backend);
2609
+ if (cached) return cached;
2610
+ const pending = this._pending.get(backend);
2611
+ if (pending) return pending;
2612
+ const promise = this._create(backend);
2613
+ this._pending.set(backend, promise);
2614
+ try {
2615
+ const adapter = await promise;
2616
+ this._cached.set(backend, adapter);
2617
+ return adapter;
2618
+ } finally {
2619
+ this._pending.delete(backend);
2620
+ }
2621
+ }
2622
+ /**
2623
+ * Evict (dispose and remove) the cached adapter for a backend.
2624
+ * Use after token rotation to force re-creation on next getAdapter().
2625
+ */
2626
+ async evict(backend) {
2627
+ const cached = this._cached.get(backend);
2628
+ if (cached) {
2629
+ this._cached.delete(backend);
2630
+ try {
2631
+ await cached.dispose();
2632
+ } catch {
2633
+ }
2634
+ }
2635
+ }
2636
+ /** Check if a backend has a cached adapter. */
2637
+ has(backend) {
2638
+ return this._cached.has(backend);
2639
+ }
2640
+ /** Get all backend names with cached adapters. */
2641
+ get activeBackends() {
2642
+ return [...this._cached.keys()];
2643
+ }
2644
+ /** Dispose all cached adapters and mark pool as unusable. */
2645
+ async dispose() {
2646
+ this._disposed = true;
2647
+ const backends = [...this._cached.keys()];
2648
+ for (const backend of backends) {
2649
+ await this.evict(backend);
2650
+ }
2651
+ }
2652
+ async _create(backend) {
2653
+ return this._factory(backend);
2654
+ }
2655
+ };
635
2656
 
2657
+ exports.AdapterPool = AdapterPool;
2658
+ exports.BodyParseError = BodyParseError;
2659
+ exports.DEFAULT_PROVIDER_MODELS = DEFAULT_PROVIDER_MODELS;
2660
+ exports.FileProviderStore = FileProviderStore;
636
2661
  exports.FileTokenStore = FileTokenStore;
2662
+ exports.InMemoryProviderStore = InMemoryProviderStore;
637
2663
  exports.InMemoryTokenStore = InMemoryTokenStore;
2664
+ exports.ServiceManager = ServiceManager;
2665
+ exports.configRoutes = configRoutes;
638
2666
  exports.corsMiddleware = corsMiddleware;
639
2667
  exports.createAuthHandler = createAuthHandler;
640
2668
  exports.createChatHandler = createChatHandler;
641
2669
  exports.createChatServer = createChatServer;
2670
+ exports.createProviderHandler = createProviderHandler;
2671
+ exports.json = json;
2672
+ exports.messageRoutes = messageRoutes;
2673
+ exports.providerRoutes = providerRoutes;
2674
+ exports.readBody = readBody;
2675
+ exports.resolveRequestContext = resolveRequestContext;
2676
+ exports.sessionRoutes = sessionRoutes;
642
2677
  //# sourceMappingURL=server.cjs.map
643
2678
  //# sourceMappingURL=server.cjs.map