@witqq/agent-sdk 0.7.0 → 0.8.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (147) hide show
  1. package/README.md +140 -34
  2. package/dist/{types-CqvUAYxt.d.cts → agent-CW9XbmG_.d.ts} +137 -102
  3. package/dist/{types-CqvUAYxt.d.ts → agent-DxY68NZL.d.cts} +137 -102
  4. package/dist/auth/index.cjs +72 -1
  5. package/dist/auth/index.cjs.map +1 -1
  6. package/dist/auth/index.d.cts +21 -154
  7. package/dist/auth/index.d.ts +21 -154
  8. package/dist/auth/index.js +72 -1
  9. package/dist/auth/index.js.map +1 -1
  10. package/dist/backends/claude.cjs +480 -261
  11. package/dist/backends/claude.cjs.map +1 -1
  12. package/dist/backends/claude.d.cts +3 -1
  13. package/dist/backends/claude.d.ts +3 -1
  14. package/dist/backends/claude.js +480 -261
  15. package/dist/backends/claude.js.map +1 -1
  16. package/dist/backends/copilot.cjs +329 -97
  17. package/dist/backends/copilot.cjs.map +1 -1
  18. package/dist/backends/copilot.d.cts +12 -4
  19. package/dist/backends/copilot.d.ts +12 -4
  20. package/dist/backends/copilot.js +329 -97
  21. package/dist/backends/copilot.js.map +1 -1
  22. package/dist/backends/vercel-ai.cjs +294 -61
  23. package/dist/backends/vercel-ai.cjs.map +1 -1
  24. package/dist/backends/vercel-ai.d.cts +3 -1
  25. package/dist/backends/vercel-ai.d.ts +3 -1
  26. package/dist/backends/vercel-ai.js +294 -61
  27. package/dist/backends/vercel-ai.js.map +1 -1
  28. package/dist/backends-BSrsBYFn.d.cts +39 -0
  29. package/dist/backends-BSrsBYFn.d.ts +39 -0
  30. package/dist/chat/accumulator.cjs +1 -1
  31. package/dist/chat/accumulator.cjs.map +1 -1
  32. package/dist/chat/accumulator.d.cts +5 -2
  33. package/dist/chat/accumulator.d.ts +5 -2
  34. package/dist/chat/accumulator.js +1 -1
  35. package/dist/chat/accumulator.js.map +1 -1
  36. package/dist/chat/backends.cjs +736 -746
  37. package/dist/chat/backends.cjs.map +1 -1
  38. package/dist/chat/backends.d.cts +10 -6
  39. package/dist/chat/backends.d.ts +10 -6
  40. package/dist/chat/backends.js +736 -725
  41. package/dist/chat/backends.js.map +1 -1
  42. package/dist/chat/context.cjs +50 -0
  43. package/dist/chat/context.cjs.map +1 -1
  44. package/dist/chat/context.d.cts +27 -3
  45. package/dist/chat/context.d.ts +27 -3
  46. package/dist/chat/context.js +50 -0
  47. package/dist/chat/context.js.map +1 -1
  48. package/dist/chat/core.cjs +25 -2
  49. package/dist/chat/core.cjs.map +1 -1
  50. package/dist/chat/core.d.cts +30 -381
  51. package/dist/chat/core.d.ts +30 -381
  52. package/dist/chat/core.js +24 -3
  53. package/dist/chat/core.js.map +1 -1
  54. package/dist/chat/errors.cjs +48 -26
  55. package/dist/chat/errors.cjs.map +1 -1
  56. package/dist/chat/errors.d.cts +6 -31
  57. package/dist/chat/errors.d.ts +6 -31
  58. package/dist/chat/errors.js +48 -25
  59. package/dist/chat/errors.js.map +1 -1
  60. package/dist/chat/events.cjs.map +1 -1
  61. package/dist/chat/events.d.cts +6 -2
  62. package/dist/chat/events.d.ts +6 -2
  63. package/dist/chat/events.js.map +1 -1
  64. package/dist/chat/index.cjs +1199 -1008
  65. package/dist/chat/index.cjs.map +1 -1
  66. package/dist/chat/index.d.cts +35 -10
  67. package/dist/chat/index.d.ts +35 -10
  68. package/dist/chat/index.js +1196 -987
  69. package/dist/chat/index.js.map +1 -1
  70. package/dist/chat/react/theme.css +2517 -0
  71. package/dist/chat/react.cjs +2003 -1153
  72. package/dist/chat/react.cjs.map +1 -1
  73. package/dist/chat/react.d.cts +590 -121
  74. package/dist/chat/react.d.ts +590 -121
  75. package/dist/chat/react.js +1984 -1151
  76. package/dist/chat/react.js.map +1 -1
  77. package/dist/chat/runtime.cjs +401 -186
  78. package/dist/chat/runtime.cjs.map +1 -1
  79. package/dist/chat/runtime.d.cts +92 -28
  80. package/dist/chat/runtime.d.ts +92 -28
  81. package/dist/chat/runtime.js +401 -186
  82. package/dist/chat/runtime.js.map +1 -1
  83. package/dist/chat/server.cjs +2234 -209
  84. package/dist/chat/server.cjs.map +1 -1
  85. package/dist/chat/server.d.cts +451 -90
  86. package/dist/chat/server.d.ts +451 -90
  87. package/dist/chat/server.js +2221 -210
  88. package/dist/chat/server.js.map +1 -1
  89. package/dist/chat/sessions.cjs +25 -43
  90. package/dist/chat/sessions.cjs.map +1 -1
  91. package/dist/chat/sessions.d.cts +37 -118
  92. package/dist/chat/sessions.d.ts +37 -118
  93. package/dist/chat/sessions.js +25 -43
  94. package/dist/chat/sessions.js.map +1 -1
  95. package/dist/chat/sqlite.cjs +441 -0
  96. package/dist/chat/sqlite.cjs.map +1 -0
  97. package/dist/chat/sqlite.d.cts +128 -0
  98. package/dist/chat/sqlite.d.ts +128 -0
  99. package/dist/chat/sqlite.js +435 -0
  100. package/dist/chat/sqlite.js.map +1 -0
  101. package/dist/chat/state.cjs +14 -1
  102. package/dist/chat/state.cjs.map +1 -1
  103. package/dist/chat/state.d.cts +5 -2
  104. package/dist/chat/state.d.ts +5 -2
  105. package/dist/chat/state.js +14 -1
  106. package/dist/chat/state.js.map +1 -1
  107. package/dist/chat/storage.cjs +19 -10
  108. package/dist/chat/storage.cjs.map +1 -1
  109. package/dist/chat/storage.d.cts +11 -5
  110. package/dist/chat/storage.d.ts +11 -5
  111. package/dist/chat/storage.js +19 -10
  112. package/dist/chat/storage.js.map +1 -1
  113. package/dist/errors-C-so0M4t.d.cts +33 -0
  114. package/dist/errors-C-so0M4t.d.ts +33 -0
  115. package/dist/errors-CmVvczxZ.d.cts +28 -0
  116. package/dist/errors-CmVvczxZ.d.ts +28 -0
  117. package/dist/{in-process-transport-C2oPTYs6.d.ts → in-process-transport-C1JnJGVR.d.ts} +28 -23
  118. package/dist/{in-process-transport-DG-w5G6k.d.cts → in-process-transport-C7DSqPyX.d.cts} +28 -23
  119. package/dist/index.cjs +340 -46
  120. package/dist/index.cjs.map +1 -1
  121. package/dist/index.d.cts +292 -123
  122. package/dist/index.d.ts +292 -123
  123. package/dist/index.js +334 -47
  124. package/dist/index.js.map +1 -1
  125. package/dist/provider-types-PTSlRPNB.d.cts +39 -0
  126. package/dist/provider-types-PTSlRPNB.d.ts +39 -0
  127. package/dist/refresh-manager-B81PpYBr.d.cts +153 -0
  128. package/dist/refresh-manager-Dlv_iNZi.d.ts +153 -0
  129. package/dist/testing.cjs +383 -0
  130. package/dist/testing.cjs.map +1 -0
  131. package/dist/testing.d.cts +132 -0
  132. package/dist/testing.d.ts +132 -0
  133. package/dist/testing.js +377 -0
  134. package/dist/testing.js.map +1 -0
  135. package/dist/token-store-CSUBgYwn.d.ts +48 -0
  136. package/dist/token-store-CuC4hB9Z.d.cts +48 -0
  137. package/dist/{transport-DX1Nhm4N.d.cts → transport-Cdh3M0tS.d.cts} +5 -4
  138. package/dist/{transport-D1OaUgRk.d.ts → transport-Ciap4PWK.d.ts} +5 -4
  139. package/dist/{types-CGF7AEX1.d.cts → types-4vbcmPTp.d.cts} +4 -2
  140. package/dist/{types-Bh5AhqD-.d.ts → types-BxggH0Yh.d.ts} +4 -2
  141. package/dist/types-DRgd_9R7.d.cts +363 -0
  142. package/dist/types-ajANVzf7.d.ts +363 -0
  143. package/package.json +31 -6
  144. package/dist/errors-BDLbNu9w.d.cts +0 -13
  145. package/dist/errors-BDLbNu9w.d.ts +0 -13
  146. package/dist/types-DLZzlJxt.d.ts +0 -39
  147. package/dist/types-tE0CXwBl.d.cts +0 -39
@@ -1,6 +1,6 @@
1
1
  'use strict';
2
2
 
3
- // src/chat/core.ts
3
+ // src/chat/types.ts
4
4
  function createChatId() {
5
5
  return crypto.randomUUID();
6
6
  }
@@ -11,6 +11,8 @@ function toChatId(value) {
11
11
  }
12
12
  return value;
13
13
  }
14
+
15
+ // src/chat/bridge.ts
14
16
  function chatEventToAgentEvent(event) {
15
17
  switch (event.type) {
16
18
  case "message:delta":
@@ -36,7 +38,7 @@ function chatEventToAgentEvent(event) {
36
38
  result: event.result
37
39
  };
38
40
  case "error":
39
- return { type: "error", error: event.error, recoverable: event.recoverable };
41
+ return { type: "error", error: event.error, recoverable: event.recoverable, code: event.code };
40
42
  default:
41
43
  return null;
42
44
  }
@@ -150,6 +152,56 @@ var ContextWindowManager = class {
150
152
  });
151
153
  return { ...result, messages: updatedMessages };
152
154
  }
155
+ /**
156
+ * Trim messages using real token usage data from the previous API call.
157
+ * Uses average-based algorithm: `avgTokensPerMessage = lastPromptTokens / messageCount`.
158
+ * Removes oldest non-system messages until freed budget brings usage under modelContextWindow.
159
+ *
160
+ * @param messages - All messages in the session
161
+ * @param lastPromptTokens - Real prompt tokens from the last API response
162
+ * @param modelContextWindow - Model's total context window size in tokens
163
+ * @returns Result with fitted messages and metadata
164
+ */
165
+ fitMessagesWithUsage(messages, lastPromptTokens, modelContextWindow) {
166
+ if (messages.length === 0) {
167
+ return { messages: [], totalTokens: 0, removedCount: 0, wasTruncated: false };
168
+ }
169
+ const budget = modelContextWindow - this.config.reservedTokens;
170
+ if (budget <= 0 || lastPromptTokens <= budget) {
171
+ return {
172
+ messages: [...messages],
173
+ totalTokens: lastPromptTokens,
174
+ removedCount: 0,
175
+ wasTruncated: false
176
+ };
177
+ }
178
+ const avgTokensPerMessage = lastPromptTokens / messages.length;
179
+ const tokensToFree = lastPromptTokens - budget;
180
+ const messagesToRemove = Math.ceil(tokensToFree / avgTokensPerMessage);
181
+ const nonSystemIndices = [];
182
+ for (let i = 0; i < messages.length; i++) {
183
+ if (messages[i].role === "system") ; else {
184
+ nonSystemIndices.push(i);
185
+ }
186
+ }
187
+ const removableCount = Math.min(messagesToRemove, nonSystemIndices.length);
188
+ const removedIndices = new Set(nonSystemIndices.slice(0, removableCount));
189
+ const result = [];
190
+ for (let i = 0; i < messages.length; i++) {
191
+ if (!removedIndices.has(i)) {
192
+ result.push(messages[i]);
193
+ }
194
+ }
195
+ const estimatedTokens = Math.round(
196
+ lastPromptTokens * (result.length / messages.length)
197
+ );
198
+ return {
199
+ messages: result,
200
+ totalTokens: estimatedTokens,
201
+ removedCount: removableCount,
202
+ wasTruncated: removableCount > 0
203
+ };
204
+ }
153
205
  /**
154
206
  * Truncate oldest: keeps system messages, removes oldest non-system messages first.
155
207
  * Always keeps the most recent user message.
@@ -270,9 +322,18 @@ var ContextWindowManager = class {
270
322
  var AgentSDKError = class extends Error {
271
323
  /** @internal Marker for cross-bundle identity checks */
272
324
  _agentSDKError = true;
325
+ /** Machine-readable error code. Prefer values from the ErrorCode enum. */
326
+ code;
327
+ /** Whether this error is safe to retry */
328
+ retryable;
329
+ /** HTTP status code hint for error classification */
330
+ httpStatus;
273
331
  constructor(message, options) {
274
332
  super(message, options);
275
333
  this.name = "AgentSDKError";
334
+ this.code = options?.code;
335
+ this.retryable = options?.retryable ?? false;
336
+ this.httpStatus = options?.httpStatus;
276
337
  }
277
338
  /** Check if an error is an AgentSDKError (works across bundled copies) */
278
339
  static is(error) {
@@ -287,7 +348,11 @@ var ChatError = class extends AgentSDKError {
287
348
  retryAfter;
288
349
  timestamp;
289
350
  constructor(message, options) {
290
- super(message, { cause: options.cause });
351
+ super(message, {
352
+ cause: options.cause,
353
+ code: options.code,
354
+ retryable: options.retryable
355
+ });
291
356
  this.name = "ChatError";
292
357
  this.code = options.code;
293
358
  this.retryable = options.retryable ?? false;
@@ -608,6 +673,35 @@ var CancellableTimeout = class {
608
673
  }
609
674
  };
610
675
 
676
+ // src/chat/listener-set.ts
677
+ var ListenerSet = class {
678
+ _listeners = /* @__PURE__ */ new Set();
679
+ /** Add a listener. Returns an unsubscribe function. */
680
+ add(callback) {
681
+ this._listeners.add(callback);
682
+ return () => {
683
+ this._listeners.delete(callback);
684
+ };
685
+ }
686
+ /** Notify all listeners with the given arguments. Errors are isolated per listener. */
687
+ notify(...args) {
688
+ for (const cb of this._listeners) {
689
+ try {
690
+ cb(...args);
691
+ } catch {
692
+ }
693
+ }
694
+ }
695
+ /** Remove all listeners. */
696
+ clear() {
697
+ this._listeners.clear();
698
+ }
699
+ /** Current number of listeners. */
700
+ get size() {
701
+ return this._listeners.size;
702
+ }
703
+ };
704
+
611
705
  // src/chat/runtime.ts
612
706
  var ChatRuntime = class {
613
707
  _state;
@@ -619,20 +713,19 @@ var ChatRuntime = class {
619
713
  _tools = /* @__PURE__ */ new Map();
620
714
  _retryConfig;
621
715
  _contextStats = /* @__PURE__ */ new Map();
716
+ _sessionUsage = /* @__PURE__ */ new Map();
717
+ _modelContextWindows = /* @__PURE__ */ new Map();
622
718
  _onContextTrimmed;
623
719
  _streamTimeoutMs;
624
- _sessionListeners = /* @__PURE__ */ new Set();
625
- _activeAdapter = null;
626
- _currentBackend;
627
- _currentModel;
628
- _activeSessionId = null;
720
+ _sessionListeners = new ListenerSet();
721
+ _adapterPool = /* @__PURE__ */ new Map();
722
+ _defaultBackend;
629
723
  _abortController = null;
630
724
  constructor(options) {
631
725
  this._state = new StateMachine("idle", RUNTIME_TRANSITIONS);
632
726
  this._guard = new ChatReentrancyGuard();
633
727
  this._backends = options.backends;
634
- this._currentBackend = options.defaultBackend;
635
- this._currentModel = options.defaultModel;
728
+ this._defaultBackend = options.defaultBackend;
636
729
  this._sessionStore = options.sessionStore;
637
730
  this._contextConfig = options.context;
638
731
  this._middleware = [...options.middleware ?? []];
@@ -645,6 +738,11 @@ var ChatRuntime = class {
645
738
  { code: "INVALID_INPUT" /* INVALID_INPUT */ }
646
739
  );
647
740
  }
741
+ if (options.tools) {
742
+ for (const tool of options.tools) {
743
+ this._tools.set(tool.name, tool);
744
+ }
745
+ }
648
746
  }
649
747
  // ── Lifecycle ──────────────────────────────────────────────
650
748
  get status() {
@@ -656,24 +754,23 @@ var ChatRuntime = class {
656
754
  this._abortController?.dispose();
657
755
  this._abortController = null;
658
756
  this._state.transition("disposed");
659
- if (this._activeAdapter) {
660
- await this._activeAdapter.dispose();
661
- this._activeAdapter = null;
757
+ for (const adapter of this._adapterPool.values()) {
758
+ try {
759
+ await adapter.dispose();
760
+ } catch {
761
+ }
662
762
  }
763
+ this._adapterPool.clear();
663
764
  }
664
765
  // ── Sessions ───────────────────────────────────────────────
665
- get activeSessionId() {
666
- return this._activeSessionId;
667
- }
668
766
  async createSession(options) {
669
767
  this.assertNotDisposed();
670
768
  const config = {
671
- model: options.config?.model ?? this._currentModel ?? "",
672
- backend: options.config?.backend ?? this._currentBackend,
769
+ model: options.config?.model ?? "",
770
+ backend: options.config?.backend ?? this._defaultBackend,
673
771
  ...options.config
674
772
  };
675
773
  const session = await this._sessionStore.createSession({ ...options, config });
676
- this._activeSessionId = session.id;
677
774
  this._notifySessionChange();
678
775
  return session;
679
776
  }
@@ -693,36 +790,12 @@ var ChatRuntime = class {
693
790
  if (!session) return;
694
791
  await this._sessionStore.deleteSession(cid);
695
792
  this._contextStats.delete(cid);
696
- if (this._activeSessionId === cid) {
697
- this._activeSessionId = null;
698
- }
793
+ this._sessionUsage.delete(cid);
699
794
  this._notifySessionChange();
700
795
  }
701
- async archiveSession(id) {
702
- this.assertNotDisposed();
703
- const cid = toChatId(id);
704
- await this._sessionStore.archiveSession(cid);
705
- this._notifySessionChange();
706
- }
707
- async switchSession(id) {
708
- this.assertNotDisposed();
709
- const cid = toChatId(id);
710
- const session = await this._sessionStore.getSession(cid);
711
- if (!session) {
712
- throw new ChatError(
713
- `Session "${id}" not found`,
714
- { code: "SESSION_NOT_FOUND" /* SESSION_NOT_FOUND */ }
715
- );
716
- }
717
- this._activeSessionId = session.id;
718
- return session;
719
- }
720
796
  // ── Messaging ──────────────────────────────────────────────
721
797
  async *send(sessionId, message, options) {
722
- this.assertNotDisposed();
723
- if (!message || message.trim().length === 0) {
724
- throw new ChatError("Message cannot be empty", { code: "INVALID_INPUT" /* INVALID_INPUT */ });
725
- }
798
+ this.validateSendInput(message, options);
726
799
  this._guard.acquire();
727
800
  const cid = toChatId(sessionId);
728
801
  this._abortController = new ChatAbortController(options?.signal);
@@ -731,150 +804,274 @@ var ChatRuntime = class {
731
804
  this._state.transition("idle");
732
805
  }
733
806
  this._state.transition("streaming");
734
- const session = await this._sessionStore.getSession(cid);
735
- if (!session) {
736
- throw new ChatError(
737
- `Session "${cid}" not found`,
738
- { code: "SESSION_NOT_FOUND" /* SESSION_NOT_FOUND */ }
739
- );
740
- }
741
- const middlewareContext = {
807
+ await this.loadSession(cid);
808
+ const mwCtx = {
742
809
  sessionId: cid,
743
810
  signal: this._abortController.signal
744
811
  };
745
- let userMessage = this.createUserMessage(message);
746
- for (const mw of this._middleware) {
747
- if (mw.onBeforeSend) {
748
- userMessage = await mw.onBeforeSend(userMessage, middlewareContext);
749
- }
750
- }
751
- await this._sessionStore.appendMessage(cid, userMessage);
752
- const updatedSession = await this._sessionStore.getSession(cid);
753
- let messagesToSend = updatedSession.messages;
754
- if (this._contextConfig) {
755
- const ctxManager = new ContextWindowManager(this._contextConfig);
756
- const result = await ctxManager.fitMessagesAsync(messagesToSend);
757
- this._contextStats.set(cid, {
758
- totalTokens: result.totalTokens,
759
- removedCount: result.removedCount,
760
- wasTruncated: result.wasTruncated,
761
- availableBudget: ctxManager.availableBudget
762
- });
763
- if (result.wasTruncated && this._onContextTrimmed) {
764
- const keptIds = new Set(result.messages.map((m) => m.id));
765
- const removed = messagesToSend.filter((m) => !keptIds.has(m.id));
766
- if (removed.length > 0) {
767
- try {
768
- this._onContextTrimmed(cid, removed);
769
- } catch {
770
- }
771
- }
772
- }
773
- messagesToSend = result.messages;
812
+ const userMessage = await this.applyBeforeSendMiddleware(
813
+ this.createUserMessage(message),
814
+ mwCtx
815
+ );
816
+ if (userMessage === null) {
817
+ this._state.transition("idle");
818
+ return;
774
819
  }
775
- const sessionForAdapter = {
776
- ...updatedSession,
777
- messages: messagesToSend
778
- };
779
- const adapter = await this.getOrCreateAdapterWithRetry();
820
+ const updatedSession = await this.persistAndReload(cid, userMessage);
821
+ const sessionForAdapter = await this.trimSessionContext(cid, updatedSession, options.model);
822
+ const stream = await this.prepareEventStream(
823
+ cid,
824
+ sessionForAdapter,
825
+ updatedSession,
826
+ message,
827
+ options
828
+ );
780
829
  const accumulator = new MessageAccumulator();
781
- const runtimeTools = this._tools.size > 0 ? this.injectToolContext([...this._tools.values()], {
782
- sessionId: cid,
783
- custom: updatedSession.metadata?.custom
784
- }) : void 0;
785
- const streamOptions = {
786
- ...options,
787
- signal: this._abortController.signal,
788
- model: options?.model ?? this._currentModel,
789
- tools: runtimeTools
790
- };
791
- const stream = await this.createStreamWithRetry(adapter, sessionForAdapter, message, streamOptions);
792
830
  const eventSource = this._streamTimeoutMs ? withStreamWatchdog(stream, { timeoutMs: this._streamTimeoutMs, signal: this._abortController.signal }) : stream;
793
831
  for await (const event of eventSource) {
794
832
  if (this._abortController.isAborted) break;
795
833
  this.feedAccumulator(accumulator, event);
796
- let processedEvent = event;
797
- for (const mw of this._middleware) {
798
- if (mw.onEvent && processedEvent) {
799
- processedEvent = await mw.onEvent(processedEvent, middlewareContext);
800
- }
801
- }
802
- if (processedEvent) {
803
- yield processedEvent;
804
- }
805
- }
806
- if (this._state.current === "disposed") {
807
- return;
808
- }
809
- let assistantMessage = accumulator.finalize();
810
- for (const mw of this._middleware) {
811
- if (mw.onAfterReceive) {
812
- assistantMessage = await mw.onAfterReceive(assistantMessage, middlewareContext);
834
+ if (event.type === "usage") {
835
+ this._sessionUsage.set(cid, {
836
+ promptTokens: event.promptTokens,
837
+ completionTokens: event.completionTokens
838
+ });
839
+ this.updateContextStatsWithUsage(cid, event.promptTokens, event.completionTokens, options);
813
840
  }
841
+ const processed = await this.applyOnEventMiddleware(event, mwCtx);
842
+ if (processed) yield processed;
814
843
  }
815
- await this._sessionStore.appendMessage(cid, assistantMessage);
816
- this._notifySessionChange();
844
+ if (this._state.current === "disposed") return;
845
+ await this.finalizeAssistantMessage(cid, accumulator, mwCtx);
817
846
  this._state.transition("idle");
818
847
  } catch (error) {
819
- let processedError = error instanceof Error ? error : new Error(String(error));
820
- const middlewareContext = {
821
- sessionId: cid,
822
- signal: this._abortController?.signal ?? new AbortController().signal
823
- };
824
- for (const mw of this._middleware) {
825
- if (mw.onError) {
826
- const result = await mw.onError(processedError, middlewareContext);
827
- if (result === null) {
828
- if (this._state.canTransition("idle")) {
829
- this._state.transition("idle");
830
- }
831
- return;
832
- }
833
- processedError = result;
834
- }
835
- }
836
- if (this._state.canTransition("error")) {
837
- this._state.transition("error");
838
- }
839
- throw processedError;
848
+ const result = await this.handleSendError(error, cid);
849
+ if (result !== null) throw result;
840
850
  } finally {
841
851
  this._guard.release();
842
852
  this._abortController?.dispose();
843
853
  this._abortController = null;
844
854
  }
845
855
  }
846
- abort() {
847
- this._abortController?.abort("User abort");
848
- }
849
- // ── Backend / Model ────────────────────────────────────────
850
- get currentBackend() {
851
- return this._currentBackend;
852
- }
853
- get currentModel() {
854
- return this._currentModel;
855
- }
856
- async switchBackend(name) {
856
+ // ── Send Pipeline Stages ──────────────────────────────────────
857
+ /** Stage 1: Validate send inputs (message content + required fields). */
858
+ validateSendInput(message, options) {
857
859
  this.assertNotDisposed();
858
- if (!this._backends[name]) {
860
+ if (!message || message.trim().length === 0) {
861
+ throw new ChatError("Message cannot be empty", { code: "INVALID_INPUT" /* INVALID_INPUT */ });
862
+ }
863
+ if (!options.model) {
864
+ throw new ChatError(
865
+ "options.model is required \u2014 caller must specify which model to use",
866
+ { code: "INVALID_INPUT" /* INVALID_INPUT */ }
867
+ );
868
+ }
869
+ if (!options.backend) {
870
+ throw new ChatError(
871
+ "options.backend is required \u2014 caller must specify which backend to use",
872
+ { code: "INVALID_INPUT" /* INVALID_INPUT */ }
873
+ );
874
+ }
875
+ if (!options.credentials) {
859
876
  throw new ChatError(
860
- `Backend "${name}" not found in backends map`,
877
+ "options.credentials is required \u2014 caller must provide authentication credentials",
861
878
  { code: "INVALID_INPUT" /* INVALID_INPUT */ }
862
879
  );
863
880
  }
864
- if (this._activeAdapter) {
865
- await this._activeAdapter.dispose();
866
- this._activeAdapter = null;
881
+ }
882
+ /** Stage 2: Load session from store. */
883
+ async loadSession(cid) {
884
+ const session = await this._sessionStore.getSession(cid);
885
+ if (!session) {
886
+ throw new ChatError(
887
+ `Session "${cid}" not found`,
888
+ { code: "SESSION_NOT_FOUND" /* SESSION_NOT_FOUND */ }
889
+ );
890
+ }
891
+ return session;
892
+ }
893
+ /** Stage 3: Apply onBeforeSend middleware pipeline. Returns null if middleware rejected the send. */
894
+ async applyBeforeSendMiddleware(userMessage, ctx) {
895
+ let msg = userMessage;
896
+ for (const mw of this._middleware) {
897
+ if (mw.onBeforeSend && msg) {
898
+ msg = await mw.onBeforeSend(msg, ctx);
899
+ if (msg === null) return null;
900
+ }
901
+ }
902
+ return msg;
903
+ }
904
+ /** Stage 4: Persist user message and reload session with full history. */
905
+ async persistAndReload(cid, userMessage) {
906
+ await this._sessionStore.appendMessage(cid, userMessage);
907
+ return await this._sessionStore.getSession(cid);
908
+ }
909
+ /** Stage 5: Auto-trim context window if configured. Returns session snapshot for adapter. */
910
+ async trimSessionContext(cid, session, model) {
911
+ if (!this._contextConfig) return session;
912
+ const ctxManager = new ContextWindowManager(this._contextConfig);
913
+ const lastUsage = this._sessionUsage.get(cid);
914
+ const modelContextWindow = model ? this._modelContextWindows.get(model) : void 0;
915
+ if (lastUsage && modelContextWindow) {
916
+ const result2 = ctxManager.fitMessagesWithUsage(
917
+ session.messages,
918
+ lastUsage.promptTokens,
919
+ modelContextWindow
920
+ );
921
+ this._contextStats.set(cid, {
922
+ totalTokens: result2.totalTokens,
923
+ removedCount: result2.removedCount,
924
+ wasTruncated: result2.wasTruncated,
925
+ availableBudget: Math.max(0, modelContextWindow - result2.totalTokens),
926
+ realPromptTokens: lastUsage.promptTokens,
927
+ realCompletionTokens: lastUsage.completionTokens,
928
+ modelContextWindow
929
+ });
930
+ if (result2.wasTruncated && this._onContextTrimmed) {
931
+ const keptIds = new Set(result2.messages.map((m) => m.id));
932
+ const removed = session.messages.filter((m) => !keptIds.has(m.id));
933
+ if (removed.length > 0) {
934
+ try {
935
+ this._onContextTrimmed(cid, removed);
936
+ } catch {
937
+ }
938
+ }
939
+ }
940
+ return { ...session, messages: result2.messages };
941
+ }
942
+ const result = await ctxManager.fitMessagesAsync(session.messages);
943
+ this._contextStats.set(cid, {
944
+ totalTokens: result.totalTokens,
945
+ removedCount: result.removedCount,
946
+ wasTruncated: result.wasTruncated,
947
+ availableBudget: ctxManager.availableBudget,
948
+ modelContextWindow
949
+ });
950
+ if (result.wasTruncated && this._onContextTrimmed) {
951
+ const keptIds = new Set(result.messages.map((m) => m.id));
952
+ const removed = session.messages.filter((m) => !keptIds.has(m.id));
953
+ if (removed.length > 0) {
954
+ try {
955
+ this._onContextTrimmed(cid, removed);
956
+ } catch {
957
+ }
958
+ }
959
+ }
960
+ return { ...session, messages: result.messages };
961
+ }
962
+ /** Update context stats with real usage data from a usage event. */
963
+ updateContextStatsWithUsage(cid, promptTokens, completionTokens, options) {
964
+ const modelContextWindow = options.model ? this._modelContextWindows.get(options.model) : void 0;
965
+ const existing = this._contextStats.get(cid);
966
+ this._contextStats.set(cid, {
967
+ totalTokens: promptTokens,
968
+ removedCount: existing?.removedCount ?? 0,
969
+ wasTruncated: existing?.wasTruncated ?? false,
970
+ availableBudget: modelContextWindow ? Math.max(0, modelContextWindow - promptTokens) : existing?.availableBudget ?? 0,
971
+ realPromptTokens: promptTokens,
972
+ realCompletionTokens: completionTokens,
973
+ modelContextWindow
974
+ });
975
+ }
976
+ /** Stage 6: Prepare event stream — adapter with retry, tool injection. */
977
+ async prepareEventStream(cid, sessionForAdapter, fullSession, message, options) {
978
+ const adapter = await this.getOrCreateAdapterWithRetry(options.backend, options.credentials);
979
+ const runtimeTools = this._tools.size > 0 ? this.injectToolContext([...this._tools.values()], {
980
+ sessionId: cid,
981
+ custom: fullSession.metadata?.custom
982
+ }) : void 0;
983
+ const streamOptions = {
984
+ signal: this._abortController.signal,
985
+ model: options.model,
986
+ systemPrompt: options.systemPrompt,
987
+ tools: runtimeTools
988
+ };
989
+ return this.createStreamWithRetry(
990
+ adapter,
991
+ sessionForAdapter,
992
+ message,
993
+ streamOptions,
994
+ options.backend,
995
+ options.credentials
996
+ );
997
+ }
998
+ /** Stage 7: Apply onEvent middleware pipeline (sequential transform/suppress). */
999
+ async applyOnEventMiddleware(event, ctx) {
1000
+ let processed = event;
1001
+ for (const mw of this._middleware) {
1002
+ if (mw.onEvent && processed) {
1003
+ processed = await mw.onEvent(processed, ctx);
1004
+ }
1005
+ }
1006
+ return processed;
1007
+ }
1008
+ /** Stage 8: Finalize accumulator, apply afterReceive middleware, persist assistant message. */
1009
+ async finalizeAssistantMessage(cid, accumulator, ctx) {
1010
+ let assistantMessage = accumulator.finalize();
1011
+ for (const mw of this._middleware) {
1012
+ if (mw.onAfterReceive) {
1013
+ assistantMessage = await mw.onAfterReceive(assistantMessage, ctx);
1014
+ }
1015
+ }
1016
+ await this._sessionStore.appendMessage(cid, assistantMessage);
1017
+ this._notifySessionChange();
1018
+ }
1019
+ /** Stage 9: Error handling — apply onError middleware, transition state. Returns null if suppressed. */
1020
+ async handleSendError(error, cid) {
1021
+ let processedError = error instanceof Error ? error : new Error(String(error));
1022
+ const ctx = {
1023
+ sessionId: cid,
1024
+ signal: this._abortController?.signal ?? new AbortController().signal
1025
+ };
1026
+ for (const mw of this._middleware) {
1027
+ if (mw.onError) {
1028
+ const result = await mw.onError(processedError, ctx);
1029
+ if (result === null) {
1030
+ if (this._state.canTransition("idle")) {
1031
+ this._state.transition("idle");
1032
+ }
1033
+ return null;
1034
+ }
1035
+ processedError = result;
1036
+ }
1037
+ }
1038
+ if (this._state.canTransition("error")) {
1039
+ this._state.transition("error");
867
1040
  }
868
- this._currentBackend = name;
1041
+ return processedError;
869
1042
  }
870
- switchModel(model) {
1043
+ abort() {
1044
+ this._abortController?.abort("User abort");
1045
+ }
1046
+ // ── Backend / Model ────────────────────────────────────────
1047
+ async listModels(options) {
871
1048
  this.assertNotDisposed();
872
- this._currentModel = model;
1049
+ let models = [];
1050
+ const firstAdapter = [...this._adapterPool.values()][0];
1051
+ if (firstAdapter) {
1052
+ try {
1053
+ models = await firstAdapter.listModels();
1054
+ } catch {
1055
+ return [];
1056
+ }
1057
+ } else if (options?.backend && options?.credentials) {
1058
+ try {
1059
+ const adapter = await this.getOrCreateAdapter(options.backend, options.credentials);
1060
+ models = await adapter.listModels();
1061
+ } catch {
1062
+ return [];
1063
+ }
1064
+ }
1065
+ for (const model of models) {
1066
+ if (model.contextWindow != null) {
1067
+ this._modelContextWindows.set(model.id, model.contextWindow);
1068
+ }
1069
+ }
1070
+ return models;
873
1071
  }
874
- async listModels() {
1072
+ async listBackends() {
875
1073
  this.assertNotDisposed();
876
- const adapter = await this.getOrCreateAdapter();
877
- return adapter.listModels();
1074
+ return Object.keys(this._backends).map((name) => ({ name }));
878
1075
  }
879
1076
  // ── Tools ──────────────────────────────────────────────────
880
1077
  get registeredTools() {
@@ -899,37 +1096,46 @@ var ChatRuntime = class {
899
1096
  if (idx >= 0) this._middleware.splice(idx, 1);
900
1097
  }
901
1098
  // ── Context Stats ─────────────────────────────────────────
902
- getContextStats(sessionId) {
1099
+ async getContextStats(sessionId) {
903
1100
  const cid = toChatId(sessionId);
904
1101
  return this._contextStats.get(cid) ?? null;
905
1102
  }
906
1103
  // ── Session Subscription ──────────────────────────────────
907
1104
  onSessionChange(callback) {
908
- this._sessionListeners.add(callback);
909
- return () => {
910
- this._sessionListeners.delete(callback);
911
- };
1105
+ return this._sessionListeners.add(callback);
912
1106
  }
913
1107
  _notifySessionChange() {
914
- for (const cb of this._sessionListeners) {
915
- try {
916
- cb();
917
- } catch {
918
- }
919
- }
1108
+ this._sessionListeners.notify();
920
1109
  }
921
1110
  // ── Private Helpers ────────────────────────────────────────
922
- async getOrCreateAdapter() {
923
- if (this._activeAdapter) return this._activeAdapter;
924
- const factory = this._backends[this._currentBackend];
1111
+ async getOrCreateAdapter(backend, credentials) {
1112
+ const key = this.getPoolKey(backend, credentials);
1113
+ const existing = this._adapterPool.get(key);
1114
+ if (existing) return existing;
1115
+ for (const [oldKey, oldAdapter] of this._adapterPool) {
1116
+ if (oldKey.startsWith(backend + ":")) {
1117
+ try {
1118
+ await oldAdapter.dispose();
1119
+ } catch {
1120
+ }
1121
+ this._adapterPool.delete(oldKey);
1122
+ }
1123
+ }
1124
+ const factory = this._backends[backend];
925
1125
  if (!factory) {
926
1126
  throw new ChatError(
927
- `Backend "${this._currentBackend}" not found`,
1127
+ `Backend "${backend}" not found`,
928
1128
  { code: "INVALID_INPUT" /* INVALID_INPUT */ }
929
1129
  );
930
1130
  }
931
- this._activeAdapter = await factory();
932
- return this._activeAdapter;
1131
+ const adapter = await factory(credentials);
1132
+ this._adapterPool.set(key, adapter);
1133
+ return adapter;
1134
+ }
1135
+ getPoolKey(backend, credentials) {
1136
+ const token = credentials.accessToken;
1137
+ const hash = token.length > 16 ? token.slice(0, 8) + token.slice(-8) : token;
1138
+ return `${backend}:${hash}`;
933
1139
  }
934
1140
  /** Wrap each tool's execute to inject ToolContext as 2nd argument */
935
1141
  injectToolContext(tools, context) {
@@ -961,17 +1167,25 @@ var ChatRuntime = class {
961
1167
  }
962
1168
  }
963
1169
  /** Get or create adapter with retry on connection errors */
964
- async getOrCreateAdapterWithRetry() {
1170
+ async getOrCreateAdapterWithRetry(backend, credentials) {
965
1171
  const maxAttempts = this._retryConfig?.maxAttempts ?? 1;
966
1172
  const delayMs = this._retryConfig?.delayMs ?? 0;
967
1173
  let lastError;
968
1174
  for (let attempt = 1; attempt <= maxAttempts; attempt++) {
969
1175
  try {
970
- return await this.getOrCreateAdapter();
1176
+ return await this.getOrCreateAdapter(backend, credentials);
971
1177
  } catch (err) {
972
1178
  lastError = err instanceof Error ? err : new Error(String(err));
973
1179
  if (attempt < maxAttempts) {
974
- this._activeAdapter = null;
1180
+ const key = this.getPoolKey(backend, credentials);
1181
+ const old = this._adapterPool.get(key);
1182
+ if (old) {
1183
+ try {
1184
+ await old.dispose();
1185
+ } catch {
1186
+ }
1187
+ }
1188
+ this._adapterPool.delete(key);
975
1189
  await delay(delayMs);
976
1190
  }
977
1191
  }
@@ -984,7 +1198,7 @@ var ChatRuntime = class {
984
1198
  * retries with a fresh adapter. Once first event is received,
985
1199
  * the stream is committed (no more retries).
986
1200
  */
987
- async createStreamWithRetry(adapter, session, message, options) {
1201
+ async createStreamWithRetry(adapter, session, message, options, backend, credentials) {
988
1202
  const maxAttempts = this._retryConfig?.maxAttempts ?? 1;
989
1203
  const delayMs = this._retryConfig?.delayMs ?? 0;
990
1204
  let lastError;
@@ -1005,13 +1219,14 @@ var ChatRuntime = class {
1005
1219
  } catch (err) {
1006
1220
  lastError = err instanceof Error ? err : new Error(String(err));
1007
1221
  if (attempt < maxAttempts) {
1008
- if (this._activeAdapter) {
1009
- await this._activeAdapter.dispose().catch(() => {
1010
- });
1222
+ try {
1223
+ await currentAdapter.dispose();
1224
+ } catch {
1011
1225
  }
1012
- this._activeAdapter = null;
1226
+ const key = this.getPoolKey(backend, credentials);
1227
+ this._adapterPool.delete(key);
1013
1228
  await delay(delayMs);
1014
- currentAdapter = await this.getOrCreateAdapter();
1229
+ currentAdapter = await this.getOrCreateAdapter(backend, credentials);
1015
1230
  }
1016
1231
  }
1017
1232
  }