@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,4 +1,4 @@
1
- // src/chat/core.ts
1
+ // src/chat/types.ts
2
2
  function createChatId() {
3
3
  return crypto.randomUUID();
4
4
  }
@@ -9,6 +9,8 @@ function toChatId(value) {
9
9
  }
10
10
  return value;
11
11
  }
12
+
13
+ // src/chat/bridge.ts
12
14
  function chatEventToAgentEvent(event) {
13
15
  switch (event.type) {
14
16
  case "message:delta":
@@ -34,7 +36,7 @@ function chatEventToAgentEvent(event) {
34
36
  result: event.result
35
37
  };
36
38
  case "error":
37
- return { type: "error", error: event.error, recoverable: event.recoverable };
39
+ return { type: "error", error: event.error, recoverable: event.recoverable, code: event.code };
38
40
  default:
39
41
  return null;
40
42
  }
@@ -148,6 +150,56 @@ var ContextWindowManager = class {
148
150
  });
149
151
  return { ...result, messages: updatedMessages };
150
152
  }
153
+ /**
154
+ * Trim messages using real token usage data from the previous API call.
155
+ * Uses average-based algorithm: `avgTokensPerMessage = lastPromptTokens / messageCount`.
156
+ * Removes oldest non-system messages until freed budget brings usage under modelContextWindow.
157
+ *
158
+ * @param messages - All messages in the session
159
+ * @param lastPromptTokens - Real prompt tokens from the last API response
160
+ * @param modelContextWindow - Model's total context window size in tokens
161
+ * @returns Result with fitted messages and metadata
162
+ */
163
+ fitMessagesWithUsage(messages, lastPromptTokens, modelContextWindow) {
164
+ if (messages.length === 0) {
165
+ return { messages: [], totalTokens: 0, removedCount: 0, wasTruncated: false };
166
+ }
167
+ const budget = modelContextWindow - this.config.reservedTokens;
168
+ if (budget <= 0 || lastPromptTokens <= budget) {
169
+ return {
170
+ messages: [...messages],
171
+ totalTokens: lastPromptTokens,
172
+ removedCount: 0,
173
+ wasTruncated: false
174
+ };
175
+ }
176
+ const avgTokensPerMessage = lastPromptTokens / messages.length;
177
+ const tokensToFree = lastPromptTokens - budget;
178
+ const messagesToRemove = Math.ceil(tokensToFree / avgTokensPerMessage);
179
+ const nonSystemIndices = [];
180
+ for (let i = 0; i < messages.length; i++) {
181
+ if (messages[i].role === "system") ; else {
182
+ nonSystemIndices.push(i);
183
+ }
184
+ }
185
+ const removableCount = Math.min(messagesToRemove, nonSystemIndices.length);
186
+ const removedIndices = new Set(nonSystemIndices.slice(0, removableCount));
187
+ const result = [];
188
+ for (let i = 0; i < messages.length; i++) {
189
+ if (!removedIndices.has(i)) {
190
+ result.push(messages[i]);
191
+ }
192
+ }
193
+ const estimatedTokens = Math.round(
194
+ lastPromptTokens * (result.length / messages.length)
195
+ );
196
+ return {
197
+ messages: result,
198
+ totalTokens: estimatedTokens,
199
+ removedCount: removableCount,
200
+ wasTruncated: removableCount > 0
201
+ };
202
+ }
151
203
  /**
152
204
  * Truncate oldest: keeps system messages, removes oldest non-system messages first.
153
205
  * Always keeps the most recent user message.
@@ -268,9 +320,18 @@ var ContextWindowManager = class {
268
320
  var AgentSDKError = class extends Error {
269
321
  /** @internal Marker for cross-bundle identity checks */
270
322
  _agentSDKError = true;
323
+ /** Machine-readable error code. Prefer values from the ErrorCode enum. */
324
+ code;
325
+ /** Whether this error is safe to retry */
326
+ retryable;
327
+ /** HTTP status code hint for error classification */
328
+ httpStatus;
271
329
  constructor(message, options) {
272
330
  super(message, options);
273
331
  this.name = "AgentSDKError";
332
+ this.code = options?.code;
333
+ this.retryable = options?.retryable ?? false;
334
+ this.httpStatus = options?.httpStatus;
274
335
  }
275
336
  /** Check if an error is an AgentSDKError (works across bundled copies) */
276
337
  static is(error) {
@@ -285,7 +346,11 @@ var ChatError = class extends AgentSDKError {
285
346
  retryAfter;
286
347
  timestamp;
287
348
  constructor(message, options) {
288
- super(message, { cause: options.cause });
349
+ super(message, {
350
+ cause: options.cause,
351
+ code: options.code,
352
+ retryable: options.retryable
353
+ });
289
354
  this.name = "ChatError";
290
355
  this.code = options.code;
291
356
  this.retryable = options.retryable ?? false;
@@ -606,6 +671,35 @@ var CancellableTimeout = class {
606
671
  }
607
672
  };
608
673
 
674
+ // src/chat/listener-set.ts
675
+ var ListenerSet = class {
676
+ _listeners = /* @__PURE__ */ new Set();
677
+ /** Add a listener. Returns an unsubscribe function. */
678
+ add(callback) {
679
+ this._listeners.add(callback);
680
+ return () => {
681
+ this._listeners.delete(callback);
682
+ };
683
+ }
684
+ /** Notify all listeners with the given arguments. Errors are isolated per listener. */
685
+ notify(...args) {
686
+ for (const cb of this._listeners) {
687
+ try {
688
+ cb(...args);
689
+ } catch {
690
+ }
691
+ }
692
+ }
693
+ /** Remove all listeners. */
694
+ clear() {
695
+ this._listeners.clear();
696
+ }
697
+ /** Current number of listeners. */
698
+ get size() {
699
+ return this._listeners.size;
700
+ }
701
+ };
702
+
609
703
  // src/chat/runtime.ts
610
704
  var ChatRuntime = class {
611
705
  _state;
@@ -613,26 +707,29 @@ var ChatRuntime = class {
613
707
  _backends;
614
708
  _sessionStore;
615
709
  _contextConfig;
710
+ _ctxManager;
616
711
  _middleware;
617
712
  _tools = /* @__PURE__ */ new Map();
618
713
  _retryConfig;
619
714
  _contextStats = /* @__PURE__ */ new Map();
715
+ _sessionUsage = /* @__PURE__ */ new Map();
716
+ _modelContextWindows = /* @__PURE__ */ new Map();
620
717
  _onContextTrimmed;
621
718
  _streamTimeoutMs;
622
- _sessionListeners = /* @__PURE__ */ new Set();
623
- _activeAdapter = null;
624
- _currentBackend;
625
- _currentModel;
626
- _activeSessionId = null;
719
+ _sessionListeners = new ListenerSet();
720
+ _adapterPool = /* @__PURE__ */ new Map();
721
+ _defaultBackend;
627
722
  _abortController = null;
628
723
  constructor(options) {
629
724
  this._state = new StateMachine("idle", RUNTIME_TRANSITIONS);
630
725
  this._guard = new ChatReentrancyGuard();
631
726
  this._backends = options.backends;
632
- this._currentBackend = options.defaultBackend;
633
- this._currentModel = options.defaultModel;
727
+ this._defaultBackend = options.defaultBackend;
634
728
  this._sessionStore = options.sessionStore;
635
729
  this._contextConfig = options.context;
730
+ if (this._contextConfig) {
731
+ this._ctxManager = new ContextWindowManager(this._contextConfig);
732
+ }
636
733
  this._middleware = [...options.middleware ?? []];
637
734
  this._retryConfig = options.retryConfig;
638
735
  this._onContextTrimmed = options.onContextTrimmed;
@@ -643,6 +740,11 @@ var ChatRuntime = class {
643
740
  { code: "INVALID_INPUT" /* INVALID_INPUT */ }
644
741
  );
645
742
  }
743
+ if (options.tools) {
744
+ for (const tool of options.tools) {
745
+ this._tools.set(tool.name, tool);
746
+ }
747
+ }
646
748
  }
647
749
  // ── Lifecycle ──────────────────────────────────────────────
648
750
  get status() {
@@ -654,24 +756,23 @@ var ChatRuntime = class {
654
756
  this._abortController?.dispose();
655
757
  this._abortController = null;
656
758
  this._state.transition("disposed");
657
- if (this._activeAdapter) {
658
- await this._activeAdapter.dispose();
659
- this._activeAdapter = null;
759
+ for (const adapter of this._adapterPool.values()) {
760
+ try {
761
+ await adapter.dispose();
762
+ } catch {
763
+ }
660
764
  }
765
+ this._adapterPool.clear();
661
766
  }
662
767
  // ── Sessions ───────────────────────────────────────────────
663
- get activeSessionId() {
664
- return this._activeSessionId;
665
- }
666
768
  async createSession(options) {
667
769
  this.assertNotDisposed();
668
770
  const config = {
669
- model: options.config?.model ?? this._currentModel ?? "",
670
- backend: options.config?.backend ?? this._currentBackend,
771
+ model: options.config?.model ?? "",
772
+ backend: options.config?.backend ?? this._defaultBackend,
671
773
  ...options.config
672
774
  };
673
775
  const session = await this._sessionStore.createSession({ ...options, config });
674
- this._activeSessionId = session.id;
675
776
  this._notifySessionChange();
676
777
  return session;
677
778
  }
@@ -691,36 +792,12 @@ var ChatRuntime = class {
691
792
  if (!session) return;
692
793
  await this._sessionStore.deleteSession(cid);
693
794
  this._contextStats.delete(cid);
694
- if (this._activeSessionId === cid) {
695
- this._activeSessionId = null;
696
- }
795
+ this._sessionUsage.delete(cid);
697
796
  this._notifySessionChange();
698
797
  }
699
- async archiveSession(id) {
700
- this.assertNotDisposed();
701
- const cid = toChatId(id);
702
- await this._sessionStore.archiveSession(cid);
703
- this._notifySessionChange();
704
- }
705
- async switchSession(id) {
706
- this.assertNotDisposed();
707
- const cid = toChatId(id);
708
- const session = await this._sessionStore.getSession(cid);
709
- if (!session) {
710
- throw new ChatError(
711
- `Session "${id}" not found`,
712
- { code: "SESSION_NOT_FOUND" /* SESSION_NOT_FOUND */ }
713
- );
714
- }
715
- this._activeSessionId = session.id;
716
- return session;
717
- }
718
798
  // ── Messaging ──────────────────────────────────────────────
719
799
  async *send(sessionId, message, options) {
720
- this.assertNotDisposed();
721
- if (!message || message.trim().length === 0) {
722
- throw new ChatError("Message cannot be empty", { code: "INVALID_INPUT" /* INVALID_INPUT */ });
723
- }
800
+ this.validateSendInput(message, options);
724
801
  this._guard.acquire();
725
802
  const cid = toChatId(sessionId);
726
803
  this._abortController = new ChatAbortController(options?.signal);
@@ -729,150 +806,274 @@ var ChatRuntime = class {
729
806
  this._state.transition("idle");
730
807
  }
731
808
  this._state.transition("streaming");
732
- const session = await this._sessionStore.getSession(cid);
733
- if (!session) {
734
- throw new ChatError(
735
- `Session "${cid}" not found`,
736
- { code: "SESSION_NOT_FOUND" /* SESSION_NOT_FOUND */ }
737
- );
738
- }
739
- const middlewareContext = {
809
+ await this.loadSession(cid);
810
+ const mwCtx = {
740
811
  sessionId: cid,
741
812
  signal: this._abortController.signal
742
813
  };
743
- let userMessage = this.createUserMessage(message);
744
- for (const mw of this._middleware) {
745
- if (mw.onBeforeSend) {
746
- userMessage = await mw.onBeforeSend(userMessage, middlewareContext);
747
- }
748
- }
749
- await this._sessionStore.appendMessage(cid, userMessage);
750
- const updatedSession = await this._sessionStore.getSession(cid);
751
- let messagesToSend = updatedSession.messages;
752
- if (this._contextConfig) {
753
- const ctxManager = new ContextWindowManager(this._contextConfig);
754
- const result = await ctxManager.fitMessagesAsync(messagesToSend);
755
- this._contextStats.set(cid, {
756
- totalTokens: result.totalTokens,
757
- removedCount: result.removedCount,
758
- wasTruncated: result.wasTruncated,
759
- availableBudget: ctxManager.availableBudget
760
- });
761
- if (result.wasTruncated && this._onContextTrimmed) {
762
- const keptIds = new Set(result.messages.map((m) => m.id));
763
- const removed = messagesToSend.filter((m) => !keptIds.has(m.id));
764
- if (removed.length > 0) {
765
- try {
766
- this._onContextTrimmed(cid, removed);
767
- } catch {
768
- }
769
- }
770
- }
771
- messagesToSend = result.messages;
814
+ const userMessage = await this.applyBeforeSendMiddleware(
815
+ this.createUserMessage(message),
816
+ mwCtx
817
+ );
818
+ if (userMessage === null) {
819
+ this._state.transition("idle");
820
+ return;
772
821
  }
773
- const sessionForAdapter = {
774
- ...updatedSession,
775
- messages: messagesToSend
776
- };
777
- const adapter = await this.getOrCreateAdapterWithRetry();
822
+ const updatedSession = await this.persistAndReload(cid, userMessage);
823
+ const sessionForAdapter = await this.trimSessionContext(cid, updatedSession, options.model);
824
+ const stream = await this.prepareEventStream(
825
+ cid,
826
+ sessionForAdapter,
827
+ updatedSession,
828
+ message,
829
+ options
830
+ );
778
831
  const accumulator = new MessageAccumulator();
779
- const runtimeTools = this._tools.size > 0 ? this.injectToolContext([...this._tools.values()], {
780
- sessionId: cid,
781
- custom: updatedSession.metadata?.custom
782
- }) : void 0;
783
- const streamOptions = {
784
- ...options,
785
- signal: this._abortController.signal,
786
- model: options?.model ?? this._currentModel,
787
- tools: runtimeTools
788
- };
789
- const stream = await this.createStreamWithRetry(adapter, sessionForAdapter, message, streamOptions);
790
832
  const eventSource = this._streamTimeoutMs ? withStreamWatchdog(stream, { timeoutMs: this._streamTimeoutMs, signal: this._abortController.signal }) : stream;
791
833
  for await (const event of eventSource) {
792
834
  if (this._abortController.isAborted) break;
793
835
  this.feedAccumulator(accumulator, event);
794
- let processedEvent = event;
795
- for (const mw of this._middleware) {
796
- if (mw.onEvent && processedEvent) {
797
- processedEvent = await mw.onEvent(processedEvent, middlewareContext);
798
- }
799
- }
800
- if (processedEvent) {
801
- yield processedEvent;
802
- }
803
- }
804
- if (this._state.current === "disposed") {
805
- return;
806
- }
807
- let assistantMessage = accumulator.finalize();
808
- for (const mw of this._middleware) {
809
- if (mw.onAfterReceive) {
810
- assistantMessage = await mw.onAfterReceive(assistantMessage, middlewareContext);
836
+ if (event.type === "usage") {
837
+ this._sessionUsage.set(cid, {
838
+ promptTokens: event.promptTokens,
839
+ completionTokens: event.completionTokens
840
+ });
841
+ this.updateContextStatsWithUsage(cid, event.promptTokens, event.completionTokens, options);
811
842
  }
843
+ const processed = await this.applyOnEventMiddleware(event, mwCtx);
844
+ if (processed) yield processed;
812
845
  }
813
- await this._sessionStore.appendMessage(cid, assistantMessage);
814
- this._notifySessionChange();
846
+ if (this._state.current === "disposed") return;
847
+ await this.finalizeAssistantMessage(cid, accumulator, mwCtx);
815
848
  this._state.transition("idle");
816
849
  } catch (error) {
817
- let processedError = error instanceof Error ? error : new Error(String(error));
818
- const middlewareContext = {
819
- sessionId: cid,
820
- signal: this._abortController?.signal ?? new AbortController().signal
821
- };
822
- for (const mw of this._middleware) {
823
- if (mw.onError) {
824
- const result = await mw.onError(processedError, middlewareContext);
825
- if (result === null) {
826
- if (this._state.canTransition("idle")) {
827
- this._state.transition("idle");
828
- }
829
- return;
830
- }
831
- processedError = result;
832
- }
833
- }
834
- if (this._state.canTransition("error")) {
835
- this._state.transition("error");
836
- }
837
- throw processedError;
850
+ const result = await this.handleSendError(error, cid);
851
+ if (result !== null) throw result;
838
852
  } finally {
839
853
  this._guard.release();
840
854
  this._abortController?.dispose();
841
855
  this._abortController = null;
842
856
  }
843
857
  }
844
- abort() {
845
- this._abortController?.abort("User abort");
846
- }
847
- // ── Backend / Model ────────────────────────────────────────
848
- get currentBackend() {
849
- return this._currentBackend;
850
- }
851
- get currentModel() {
852
- return this._currentModel;
853
- }
854
- async switchBackend(name) {
858
+ // ── Send Pipeline Stages ──────────────────────────────────────
859
+ /** Stage 1: Validate send inputs (message content + required fields). */
860
+ validateSendInput(message, options) {
855
861
  this.assertNotDisposed();
856
- if (!this._backends[name]) {
862
+ if (!message || message.trim().length === 0) {
863
+ throw new ChatError("Message cannot be empty", { code: "INVALID_INPUT" /* INVALID_INPUT */ });
864
+ }
865
+ if (!options.model) {
857
866
  throw new ChatError(
858
- `Backend "${name}" not found in backends map`,
867
+ "options.model is required \u2014 caller must specify which model to use",
859
868
  { code: "INVALID_INPUT" /* INVALID_INPUT */ }
860
869
  );
861
870
  }
862
- if (this._activeAdapter) {
863
- await this._activeAdapter.dispose();
864
- this._activeAdapter = null;
871
+ if (!options.backend) {
872
+ throw new ChatError(
873
+ "options.backend is required \u2014 caller must specify which backend to use",
874
+ { code: "INVALID_INPUT" /* INVALID_INPUT */ }
875
+ );
876
+ }
877
+ if (!options.credentials) {
878
+ throw new ChatError(
879
+ "options.credentials is required \u2014 caller must provide authentication credentials",
880
+ { code: "INVALID_INPUT" /* INVALID_INPUT */ }
881
+ );
865
882
  }
866
- this._currentBackend = name;
867
883
  }
868
- switchModel(model) {
884
+ /** Stage 2: Load session from store. */
885
+ async loadSession(cid) {
886
+ const session = await this._sessionStore.getSession(cid);
887
+ if (!session) {
888
+ throw new ChatError(
889
+ `Session "${cid}" not found`,
890
+ { code: "SESSION_NOT_FOUND" /* SESSION_NOT_FOUND */ }
891
+ );
892
+ }
893
+ return session;
894
+ }
895
+ /** Stage 3: Apply onBeforeSend middleware pipeline. Returns null if middleware rejected the send. */
896
+ async applyBeforeSendMiddleware(userMessage, ctx) {
897
+ let msg = userMessage;
898
+ for (const mw of this._middleware) {
899
+ if (mw.onBeforeSend && msg) {
900
+ msg = await mw.onBeforeSend(msg, ctx);
901
+ if (msg === null) return null;
902
+ }
903
+ }
904
+ return msg;
905
+ }
906
+ /** Stage 4: Persist user message and reload session with full history. */
907
+ async persistAndReload(cid, userMessage) {
908
+ await this._sessionStore.appendMessage(cid, userMessage);
909
+ return await this._sessionStore.getSession(cid);
910
+ }
911
+ /** Stage 5: Auto-trim context window if configured. Returns session snapshot for adapter. */
912
+ async trimSessionContext(cid, session, model) {
913
+ if (!this._ctxManager) return session;
914
+ const ctxManager = this._ctxManager;
915
+ const lastUsage = this._sessionUsage.get(cid);
916
+ const modelContextWindow = model ? this._modelContextWindows.get(model) : void 0;
917
+ if (lastUsage && modelContextWindow) {
918
+ const result2 = ctxManager.fitMessagesWithUsage(
919
+ session.messages,
920
+ lastUsage.promptTokens,
921
+ modelContextWindow
922
+ );
923
+ this._contextStats.set(cid, {
924
+ totalTokens: result2.totalTokens,
925
+ removedCount: result2.removedCount,
926
+ wasTruncated: result2.wasTruncated,
927
+ availableBudget: Math.max(0, modelContextWindow - result2.totalTokens),
928
+ realPromptTokens: lastUsage.promptTokens,
929
+ realCompletionTokens: lastUsage.completionTokens,
930
+ modelContextWindow
931
+ });
932
+ if (result2.wasTruncated && this._onContextTrimmed) {
933
+ const keptIds = new Set(result2.messages.map((m) => m.id));
934
+ const removed = session.messages.filter((m) => !keptIds.has(m.id));
935
+ if (removed.length > 0) {
936
+ try {
937
+ this._onContextTrimmed(cid, removed);
938
+ } catch {
939
+ }
940
+ }
941
+ }
942
+ return { ...session, messages: result2.messages };
943
+ }
944
+ const result = await ctxManager.fitMessagesAsync(session.messages);
945
+ this._contextStats.set(cid, {
946
+ totalTokens: result.totalTokens,
947
+ removedCount: result.removedCount,
948
+ wasTruncated: result.wasTruncated,
949
+ availableBudget: ctxManager.availableBudget,
950
+ modelContextWindow
951
+ });
952
+ if (result.wasTruncated && this._onContextTrimmed) {
953
+ const keptIds = new Set(result.messages.map((m) => m.id));
954
+ const removed = session.messages.filter((m) => !keptIds.has(m.id));
955
+ if (removed.length > 0) {
956
+ try {
957
+ this._onContextTrimmed(cid, removed);
958
+ } catch {
959
+ }
960
+ }
961
+ }
962
+ return { ...session, messages: result.messages };
963
+ }
964
+ /** Update context stats with real usage data from a usage event. */
965
+ updateContextStatsWithUsage(cid, promptTokens, completionTokens, options) {
966
+ const modelContextWindow = options.model ? this._modelContextWindows.get(options.model) : void 0;
967
+ const existing = this._contextStats.get(cid);
968
+ this._contextStats.set(cid, {
969
+ totalTokens: promptTokens,
970
+ removedCount: existing?.removedCount ?? 0,
971
+ wasTruncated: existing?.wasTruncated ?? false,
972
+ availableBudget: modelContextWindow ? Math.max(0, modelContextWindow - promptTokens) : existing?.availableBudget ?? 0,
973
+ realPromptTokens: promptTokens,
974
+ realCompletionTokens: completionTokens,
975
+ modelContextWindow
976
+ });
977
+ }
978
+ /** Stage 6: Prepare event stream — adapter with retry, tool injection. */
979
+ async prepareEventStream(cid, sessionForAdapter, fullSession, message, options) {
980
+ const adapter = await this.getOrCreateAdapterWithRetry(options.backend, options.credentials);
981
+ const runtimeTools = this._tools.size > 0 ? this.injectToolContext([...this._tools.values()], {
982
+ sessionId: cid,
983
+ custom: fullSession.metadata?.custom
984
+ }) : void 0;
985
+ const streamOptions = {
986
+ signal: this._abortController.signal,
987
+ model: options.model,
988
+ systemPrompt: options.systemPrompt,
989
+ tools: runtimeTools
990
+ };
991
+ return this.createStreamWithRetry(
992
+ adapter,
993
+ sessionForAdapter,
994
+ message,
995
+ streamOptions,
996
+ options.backend,
997
+ options.credentials
998
+ );
999
+ }
1000
+ /** Stage 7: Apply onEvent middleware pipeline (sequential transform/suppress). */
1001
+ async applyOnEventMiddleware(event, ctx) {
1002
+ let processed = event;
1003
+ for (const mw of this._middleware) {
1004
+ if (mw.onEvent && processed) {
1005
+ processed = await mw.onEvent(processed, ctx);
1006
+ }
1007
+ }
1008
+ return processed;
1009
+ }
1010
+ /** Stage 8: Finalize accumulator, apply afterReceive middleware, persist assistant message. */
1011
+ async finalizeAssistantMessage(cid, accumulator, ctx) {
1012
+ let assistantMessage = accumulator.finalize();
1013
+ for (const mw of this._middleware) {
1014
+ if (mw.onAfterReceive) {
1015
+ assistantMessage = await mw.onAfterReceive(assistantMessage, ctx);
1016
+ }
1017
+ }
1018
+ await this._sessionStore.appendMessage(cid, assistantMessage);
1019
+ this._notifySessionChange();
1020
+ }
1021
+ /** Stage 9: Error handling — apply onError middleware, transition state. Returns null if suppressed. */
1022
+ async handleSendError(error, cid) {
1023
+ let processedError = error instanceof Error ? error : new Error(String(error));
1024
+ const ctx = {
1025
+ sessionId: cid,
1026
+ signal: this._abortController?.signal ?? new AbortController().signal
1027
+ };
1028
+ for (const mw of this._middleware) {
1029
+ if (mw.onError) {
1030
+ const result = await mw.onError(processedError, ctx);
1031
+ if (result === null) {
1032
+ if (this._state.canTransition("idle")) {
1033
+ this._state.transition("idle");
1034
+ }
1035
+ return null;
1036
+ }
1037
+ processedError = result;
1038
+ }
1039
+ }
1040
+ if (this._state.canTransition("error")) {
1041
+ this._state.transition("error");
1042
+ }
1043
+ return processedError;
1044
+ }
1045
+ abort() {
1046
+ this._abortController?.abort("User abort");
1047
+ }
1048
+ // ── Backend / Model ────────────────────────────────────────
1049
+ async listModels(options) {
869
1050
  this.assertNotDisposed();
870
- this._currentModel = model;
1051
+ let models = [];
1052
+ const firstAdapter = [...this._adapterPool.values()][0];
1053
+ if (firstAdapter) {
1054
+ try {
1055
+ models = await firstAdapter.listModels();
1056
+ } catch {
1057
+ return [];
1058
+ }
1059
+ } else if (options?.backend && options?.credentials) {
1060
+ try {
1061
+ const adapter = await this.getOrCreateAdapter(options.backend, options.credentials);
1062
+ models = await adapter.listModels();
1063
+ } catch {
1064
+ return [];
1065
+ }
1066
+ }
1067
+ for (const model of models) {
1068
+ if (model.contextWindow != null) {
1069
+ this._modelContextWindows.set(model.id, model.contextWindow);
1070
+ }
1071
+ }
1072
+ return models;
871
1073
  }
872
- async listModels() {
1074
+ async listBackends() {
873
1075
  this.assertNotDisposed();
874
- const adapter = await this.getOrCreateAdapter();
875
- return adapter.listModels();
1076
+ return Object.keys(this._backends).map((name) => ({ name }));
876
1077
  }
877
1078
  // ── Tools ──────────────────────────────────────────────────
878
1079
  get registeredTools() {
@@ -897,37 +1098,46 @@ var ChatRuntime = class {
897
1098
  if (idx >= 0) this._middleware.splice(idx, 1);
898
1099
  }
899
1100
  // ── Context Stats ─────────────────────────────────────────
900
- getContextStats(sessionId) {
1101
+ async getContextStats(sessionId) {
901
1102
  const cid = toChatId(sessionId);
902
1103
  return this._contextStats.get(cid) ?? null;
903
1104
  }
904
1105
  // ── Session Subscription ──────────────────────────────────
905
1106
  onSessionChange(callback) {
906
- this._sessionListeners.add(callback);
907
- return () => {
908
- this._sessionListeners.delete(callback);
909
- };
1107
+ return this._sessionListeners.add(callback);
910
1108
  }
911
1109
  _notifySessionChange() {
912
- for (const cb of this._sessionListeners) {
913
- try {
914
- cb();
915
- } catch {
916
- }
917
- }
1110
+ this._sessionListeners.notify();
918
1111
  }
919
1112
  // ── Private Helpers ────────────────────────────────────────
920
- async getOrCreateAdapter() {
921
- if (this._activeAdapter) return this._activeAdapter;
922
- const factory = this._backends[this._currentBackend];
1113
+ async getOrCreateAdapter(backend, credentials) {
1114
+ const key = this.getPoolKey(backend, credentials);
1115
+ const existing = this._adapterPool.get(key);
1116
+ if (existing) return existing;
1117
+ for (const [oldKey, oldAdapter] of this._adapterPool) {
1118
+ if (oldKey.startsWith(backend + ":")) {
1119
+ try {
1120
+ await oldAdapter.dispose();
1121
+ } catch {
1122
+ }
1123
+ this._adapterPool.delete(oldKey);
1124
+ }
1125
+ }
1126
+ const factory = this._backends[backend];
923
1127
  if (!factory) {
924
1128
  throw new ChatError(
925
- `Backend "${this._currentBackend}" not found`,
1129
+ `Backend "${backend}" not found`,
926
1130
  { code: "INVALID_INPUT" /* INVALID_INPUT */ }
927
1131
  );
928
1132
  }
929
- this._activeAdapter = await factory();
930
- return this._activeAdapter;
1133
+ const adapter = await factory(credentials);
1134
+ this._adapterPool.set(key, adapter);
1135
+ return adapter;
1136
+ }
1137
+ getPoolKey(backend, credentials) {
1138
+ const token = credentials.accessToken;
1139
+ const hash = token.length > 16 ? token.slice(0, 8) + token.slice(-8) : token;
1140
+ return `${backend}:${hash}`;
931
1141
  }
932
1142
  /** Wrap each tool's execute to inject ToolContext as 2nd argument */
933
1143
  injectToolContext(tools, context) {
@@ -959,17 +1169,25 @@ var ChatRuntime = class {
959
1169
  }
960
1170
  }
961
1171
  /** Get or create adapter with retry on connection errors */
962
- async getOrCreateAdapterWithRetry() {
1172
+ async getOrCreateAdapterWithRetry(backend, credentials) {
963
1173
  const maxAttempts = this._retryConfig?.maxAttempts ?? 1;
964
1174
  const delayMs = this._retryConfig?.delayMs ?? 0;
965
1175
  let lastError;
966
1176
  for (let attempt = 1; attempt <= maxAttempts; attempt++) {
967
1177
  try {
968
- return await this.getOrCreateAdapter();
1178
+ return await this.getOrCreateAdapter(backend, credentials);
969
1179
  } catch (err) {
970
1180
  lastError = err instanceof Error ? err : new Error(String(err));
971
1181
  if (attempt < maxAttempts) {
972
- this._activeAdapter = null;
1182
+ const key = this.getPoolKey(backend, credentials);
1183
+ const old = this._adapterPool.get(key);
1184
+ if (old) {
1185
+ try {
1186
+ await old.dispose();
1187
+ } catch {
1188
+ }
1189
+ }
1190
+ this._adapterPool.delete(key);
973
1191
  await delay(delayMs);
974
1192
  }
975
1193
  }
@@ -982,7 +1200,7 @@ var ChatRuntime = class {
982
1200
  * retries with a fresh adapter. Once first event is received,
983
1201
  * the stream is committed (no more retries).
984
1202
  */
985
- async createStreamWithRetry(adapter, session, message, options) {
1203
+ async createStreamWithRetry(adapter, session, message, options, backend, credentials) {
986
1204
  const maxAttempts = this._retryConfig?.maxAttempts ?? 1;
987
1205
  const delayMs = this._retryConfig?.delayMs ?? 0;
988
1206
  let lastError;
@@ -1003,13 +1221,14 @@ var ChatRuntime = class {
1003
1221
  } catch (err) {
1004
1222
  lastError = err instanceof Error ? err : new Error(String(err));
1005
1223
  if (attempt < maxAttempts) {
1006
- if (this._activeAdapter) {
1007
- await this._activeAdapter.dispose().catch(() => {
1008
- });
1224
+ try {
1225
+ await currentAdapter.dispose();
1226
+ } catch {
1009
1227
  }
1010
- this._activeAdapter = null;
1228
+ const key = this.getPoolKey(backend, credentials);
1229
+ this._adapterPool.delete(key);
1011
1230
  await delay(delayMs);
1012
- currentAdapter = await this.getOrCreateAdapter();
1231
+ currentAdapter = await this.getOrCreateAdapter(backend, credentials);
1013
1232
  }
1014
1233
  }
1015
1234
  }