@librechat/agents 3.3.4 → 3.3.5

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 (159) hide show
  1. package/dist/cjs/common/constants.cjs +21 -0
  2. package/dist/cjs/common/constants.cjs.map +1 -1
  3. package/dist/cjs/events.cjs +10 -1
  4. package/dist/cjs/events.cjs.map +1 -1
  5. package/dist/cjs/graphs/Graph.cjs +456 -7
  6. package/dist/cjs/graphs/Graph.cjs.map +1 -1
  7. package/dist/cjs/hooks/HookRegistry.cjs +25 -0
  8. package/dist/cjs/hooks/HookRegistry.cjs.map +1 -1
  9. package/dist/cjs/hooks/index.cjs +12 -0
  10. package/dist/cjs/hooks/index.cjs.map +1 -1
  11. package/dist/cjs/hooks/types.cjs +1 -0
  12. package/dist/cjs/hooks/types.cjs.map +1 -1
  13. package/dist/cjs/langfuse.cjs +8 -0
  14. package/dist/cjs/langfuse.cjs.map +1 -1
  15. package/dist/cjs/llm/bedrock/utils/message_inputs.cjs +1 -3
  16. package/dist/cjs/llm/bedrock/utils/message_inputs.cjs.map +1 -1
  17. package/dist/cjs/llm/invoke.cjs +268 -3
  18. package/dist/cjs/llm/invoke.cjs.map +1 -1
  19. package/dist/cjs/llm/preempt.cjs +132 -0
  20. package/dist/cjs/llm/preempt.cjs.map +1 -0
  21. package/dist/cjs/main.cjs +17 -3
  22. package/dist/cjs/messages/alternation.cjs +92 -0
  23. package/dist/cjs/messages/alternation.cjs.map +1 -0
  24. package/dist/cjs/messages/format.cjs +72 -0
  25. package/dist/cjs/messages/format.cjs.map +1 -1
  26. package/dist/cjs/messages/handoffCue.cjs +63 -0
  27. package/dist/cjs/messages/handoffCue.cjs.map +1 -0
  28. package/dist/cjs/messages/index.cjs +3 -0
  29. package/dist/cjs/messages/injected.cjs +60 -0
  30. package/dist/cjs/messages/injected.cjs.map +1 -0
  31. package/dist/cjs/run.cjs +80 -7
  32. package/dist/cjs/run.cjs.map +1 -1
  33. package/dist/cjs/session/handlers.cjs +18 -9
  34. package/dist/cjs/session/handlers.cjs.map +1 -1
  35. package/dist/cjs/stream.cjs +28 -7
  36. package/dist/cjs/stream.cjs.map +1 -1
  37. package/dist/cjs/tools/ToolNode.cjs +3 -23
  38. package/dist/cjs/tools/ToolNode.cjs.map +1 -1
  39. package/dist/cjs/tools/search/keenable-scraper.cjs +90 -0
  40. package/dist/cjs/tools/search/keenable-scraper.cjs.map +1 -0
  41. package/dist/cjs/tools/search/tool.cjs +9 -1
  42. package/dist/cjs/tools/search/tool.cjs.map +1 -1
  43. package/dist/esm/common/constants.mjs +19 -1
  44. package/dist/esm/common/constants.mjs.map +1 -1
  45. package/dist/esm/events.mjs +10 -1
  46. package/dist/esm/events.mjs.map +1 -1
  47. package/dist/esm/graphs/Graph.mjs +455 -6
  48. package/dist/esm/graphs/Graph.mjs.map +1 -1
  49. package/dist/esm/hooks/HookRegistry.mjs +25 -0
  50. package/dist/esm/hooks/HookRegistry.mjs.map +1 -1
  51. package/dist/esm/hooks/index.mjs +12 -1
  52. package/dist/esm/hooks/index.mjs.map +1 -1
  53. package/dist/esm/hooks/types.mjs +1 -0
  54. package/dist/esm/hooks/types.mjs.map +1 -1
  55. package/dist/esm/langfuse.mjs +9 -1
  56. package/dist/esm/langfuse.mjs.map +1 -1
  57. package/dist/esm/llm/bedrock/utils/message_inputs.mjs +1 -3
  58. package/dist/esm/llm/bedrock/utils/message_inputs.mjs.map +1 -1
  59. package/dist/esm/llm/invoke.mjs +270 -6
  60. package/dist/esm/llm/invoke.mjs.map +1 -1
  61. package/dist/esm/llm/preempt.mjs +131 -0
  62. package/dist/esm/llm/preempt.mjs.map +1 -0
  63. package/dist/esm/main.mjs +8 -5
  64. package/dist/esm/messages/alternation.mjs +91 -0
  65. package/dist/esm/messages/alternation.mjs.map +1 -0
  66. package/dist/esm/messages/format.mjs +72 -0
  67. package/dist/esm/messages/format.mjs.map +1 -1
  68. package/dist/esm/messages/handoffCue.mjs +61 -0
  69. package/dist/esm/messages/handoffCue.mjs.map +1 -0
  70. package/dist/esm/messages/index.mjs +3 -0
  71. package/dist/esm/messages/injected.mjs +60 -0
  72. package/dist/esm/messages/injected.mjs.map +1 -0
  73. package/dist/esm/run.mjs +80 -7
  74. package/dist/esm/run.mjs.map +1 -1
  75. package/dist/esm/session/handlers.mjs +19 -10
  76. package/dist/esm/session/handlers.mjs.map +1 -1
  77. package/dist/esm/stream.mjs +27 -8
  78. package/dist/esm/stream.mjs.map +1 -1
  79. package/dist/esm/tools/ToolNode.mjs +3 -23
  80. package/dist/esm/tools/ToolNode.mjs.map +1 -1
  81. package/dist/esm/tools/search/keenable-scraper.mjs +88 -0
  82. package/dist/esm/tools/search/keenable-scraper.mjs.map +1 -0
  83. package/dist/esm/tools/search/tool.mjs +9 -1
  84. package/dist/esm/tools/search/tool.mjs.map +1 -1
  85. package/dist/types/common/constants.d.ts +18 -0
  86. package/dist/types/graphs/Graph.d.ts +138 -1
  87. package/dist/types/hooks/HookRegistry.d.ts +15 -0
  88. package/dist/types/hooks/index.d.ts +12 -1
  89. package/dist/types/hooks/types.d.ts +45 -6
  90. package/dist/types/index.d.ts +1 -1
  91. package/dist/types/llm/invoke.d.ts +7 -0
  92. package/dist/types/llm/preempt.d.ts +40 -0
  93. package/dist/types/messages/alternation.d.ts +23 -0
  94. package/dist/types/messages/handoffCue.d.ts +40 -0
  95. package/dist/types/messages/index.d.ts +3 -0
  96. package/dist/types/messages/injected.d.ts +3 -0
  97. package/dist/types/run.d.ts +7 -0
  98. package/dist/types/stream.d.ts +14 -0
  99. package/dist/types/tools/ToolNode.d.ts +0 -7
  100. package/dist/types/tools/search/keenable-scraper.d.ts +15 -0
  101. package/dist/types/tools/search/types.d.ts +31 -2
  102. package/dist/types/types/graph.d.ts +8 -1
  103. package/dist/types/types/run.d.ts +65 -0
  104. package/dist/types/types/stream.d.ts +0 -25
  105. package/package.json +1 -1
  106. package/src/{splitStream.test.ts → aggregator.test.ts} +59 -666
  107. package/src/common/constants.ts +21 -0
  108. package/src/events.ts +15 -1
  109. package/src/graphs/Graph.ts +568 -3
  110. package/src/graphs/__tests__/Graph.contextOverflow.test.ts +18 -7
  111. package/src/graphs/__tests__/Graph.preemptSignal.test.ts +126 -0
  112. package/src/hooks/HookRegistry.ts +40 -0
  113. package/src/hooks/__tests__/preemptBoundary.test.ts +152 -0
  114. package/src/hooks/index.ts +16 -2
  115. package/src/hooks/types.ts +47 -3
  116. package/src/index.ts +1 -1
  117. package/src/langfuse.ts +26 -1
  118. package/src/llm/bedrock/utils/message_inputs.test.ts +82 -0
  119. package/src/llm/bedrock/utils/message_inputs.ts +14 -11
  120. package/src/llm/bedrock/utils/toolResultCachePoint.test.ts +15 -0
  121. package/src/llm/invoke.alternation.test.ts +87 -0
  122. package/src/llm/invoke.handoffCue.test.ts +112 -0
  123. package/src/llm/invoke.ts +417 -8
  124. package/src/llm/preempt.test.ts +323 -0
  125. package/src/llm/preempt.ts +178 -0
  126. package/src/messages/alternation.test.ts +212 -0
  127. package/src/messages/alternation.ts +112 -0
  128. package/src/messages/format.ts +91 -0
  129. package/src/messages/formatAgentMessages.steer.test.ts +267 -0
  130. package/src/messages/handoffCue.test.ts +96 -0
  131. package/src/messages/handoffCue.ts +78 -0
  132. package/src/messages/index.ts +3 -0
  133. package/src/messages/injected.test.ts +90 -0
  134. package/src/messages/injected.ts +74 -0
  135. package/src/run.ts +91 -6
  136. package/src/scripts/preempt-probe.ts +330 -0
  137. package/src/scripts/preempt-scenarios.ts +388 -0
  138. package/src/session/handlers.ts +32 -12
  139. package/src/specs/handoffCue.test.ts +165 -0
  140. package/src/specs/langfuse-callbacks.test.ts +352 -2
  141. package/src/specs/preemptSeal.test.ts +309 -0
  142. package/src/stream.dispatch.test.ts +63 -0
  143. package/src/stream.ts +36 -8
  144. package/src/tools/ToolNode.ts +3 -31
  145. package/src/tools/search/keenable-scraper.test.ts +153 -0
  146. package/src/tools/search/keenable-scraper.ts +137 -0
  147. package/src/tools/search/tool.ts +13 -2
  148. package/src/tools/search/types.ts +50 -3
  149. package/src/types/graph.ts +12 -1
  150. package/src/types/run.ts +67 -0
  151. package/src/types/stream.ts +0 -41
  152. package/dist/cjs/splitStream.cjs +0 -151
  153. package/dist/cjs/splitStream.cjs.map +0 -1
  154. package/dist/esm/splitStream.mjs +0 -150
  155. package/dist/esm/splitStream.mjs.map +0 -1
  156. package/dist/types/mockStream.d.ts +0 -32
  157. package/dist/types/splitStream.d.ts +0 -37
  158. package/src/mockStream.ts +0 -99
  159. package/src/splitStream.ts +0 -234
@@ -0,0 +1,137 @@
1
+ import axios from 'axios';
2
+ import type * as t from './types';
3
+ import { createDefaultLogger } from './utils';
4
+
5
+ const DEFAULT_KEENABLE_SCRAPE_TIMEOUT = 15000;
6
+
7
+ /** Keyed and keyless fetch endpoints. Keenable reads any URL as clean markdown
8
+ * without a key via the public endpoint; a key only lifts rate limits. */
9
+ const KEENABLE_FETCH_API_URL = 'https://api.keenable.ai/v1/fetch';
10
+ const KEENABLE_FETCH_PUBLIC_URL = 'https://api.keenable.ai/v1/fetch/public';
11
+
12
+ export class KeenableScraper implements t.BaseScraper {
13
+ private apiKey: string | undefined;
14
+ private apiUrl: string;
15
+ private timeout: number;
16
+ private attributionTitle: string;
17
+ private logger: t.Logger;
18
+
19
+ constructor(config: t.KeenableScraperConfig = {}) {
20
+ const resolvedKey = config.apiKey ?? process.env.KEENABLE_API_KEY;
21
+ this.apiKey =
22
+ resolvedKey != null && resolvedKey !== '' ? resolvedKey : undefined;
23
+ this.apiUrl =
24
+ config.apiUrl ??
25
+ process.env.KEENABLE_FETCH_URL ??
26
+ (this.apiKey != null
27
+ ? KEENABLE_FETCH_API_URL
28
+ : KEENABLE_FETCH_PUBLIC_URL);
29
+ this.timeout = config.timeout ?? DEFAULT_KEENABLE_SCRAPE_TIMEOUT;
30
+ this.attributionTitle = config.attributionTitle ?? 'LibreChat';
31
+ this.logger = config.logger || createDefaultLogger();
32
+ }
33
+
34
+ private buildHeaders(): Record<string, string> {
35
+ /** X-Keenable-Title is used for traffic attribution and required on the
36
+ * keyless endpoint; the key only lifts rate limits, so it is sent only when
37
+ * present. */
38
+ const headers: Record<string, string> = {
39
+ 'X-Keenable-Title': this.attributionTitle,
40
+ };
41
+ if (this.apiKey != null) {
42
+ headers['X-API-Key'] = this.apiKey;
43
+ }
44
+ return headers;
45
+ }
46
+
47
+ async scrapeUrl(
48
+ url: string,
49
+ options: t.KeenableScrapeOptions = {}
50
+ ): Promise<[string, t.KeenableScrapeResponse]> {
51
+ if (!url || !url.trim()) {
52
+ return [url, { success: false, error: 'URL cannot be empty' }];
53
+ }
54
+
55
+ try {
56
+ const response = await axios.get<t.KeenableFetchResult>(this.apiUrl, {
57
+ params: { url },
58
+ headers: this.buildHeaders(),
59
+ timeout: options.timeout ?? this.timeout,
60
+ });
61
+
62
+ const data = response.data;
63
+ const content = data.content ?? '';
64
+ if (!content) {
65
+ return [
66
+ url,
67
+ { success: false, error: 'Keenable Fetch returned no content' },
68
+ ];
69
+ }
70
+
71
+ return [
72
+ url,
73
+ {
74
+ success: true,
75
+ data: {
76
+ content,
77
+ title: data.title,
78
+ description: data.description,
79
+ url: data.url ?? url,
80
+ },
81
+ },
82
+ ];
83
+ } catch (error) {
84
+ const errorMessage =
85
+ error instanceof Error ? error.message : String(error);
86
+ return [
87
+ url,
88
+ {
89
+ success: false,
90
+ error: `Keenable Fetch API request failed: ${errorMessage}`,
91
+ },
92
+ ];
93
+ }
94
+ }
95
+
96
+ async scrapeUrls(
97
+ urls: string[],
98
+ options: t.KeenableScrapeOptions = {}
99
+ ): Promise<Array<[string, t.KeenableScrapeResponse]>> {
100
+ /** Keenable fetch is single-URL; run the batch concurrently. */
101
+ return Promise.all(urls.map((url) => this.scrapeUrl(url, options)));
102
+ }
103
+
104
+ extractContent(
105
+ response: t.KeenableScrapeResponse
106
+ ): [string, undefined | t.References] {
107
+ if (!response.success || !response.data) {
108
+ return ['', undefined];
109
+ }
110
+ /** Keenable returns clean markdown with no separate media arrays, so there
111
+ * are no structured references to surface. */
112
+ return [response.data.content, undefined];
113
+ }
114
+
115
+ extractMetadata(response: t.KeenableScrapeResponse): t.GenericScrapeMetadata {
116
+ if (!response.success || !response.data) {
117
+ return {};
118
+ }
119
+ const metadata: t.GenericScrapeMetadata = {};
120
+ if (response.data.title != null) {
121
+ metadata.title = response.data.title;
122
+ }
123
+ if (response.data.description != null && response.data.description !== '') {
124
+ metadata.description = response.data.description;
125
+ }
126
+ if (response.data.url != null) {
127
+ metadata.url = response.data.url;
128
+ }
129
+ return metadata;
130
+ }
131
+ }
132
+
133
+ export const createKeenableScraper = (
134
+ config: t.KeenableScraperConfig = {}
135
+ ): KeenableScraper => {
136
+ return new KeenableScraper(config);
137
+ };
@@ -13,6 +13,7 @@ import {
13
13
  DATE_RANGE,
14
14
  } from './schema';
15
15
  import { createSearchAPI, createSourceProcessor } from './search';
16
+ import { createKeenableScraper } from './keenable-scraper';
16
17
  import { createSerperScraper } from './serper-scraper';
17
18
  import { createTavilyScraper } from './tavily-scraper';
18
19
  import { createFirecrawlScraper } from './firecrawl';
@@ -378,6 +379,7 @@ export const createSearchTool = (
378
379
  keenableApiKey,
379
380
  keenableApiUrl,
380
381
  keenableSearchOptions,
382
+ keenableScraperOptions,
381
383
  rerankerType = 'cohere',
382
384
  rerankerTimeout,
383
385
  topResults = 5,
@@ -476,13 +478,22 @@ export const createSearchTool = (
476
478
  } else if (scraperProvider === 'crw') {
477
479
  scraperInstance = createCrwScraper({
478
480
  ...crwScraperOptions,
479
- apiKey:
480
- crwScraperOptions?.apiKey ?? crwApiKey ?? process.env.CRW_API_KEY,
481
+ apiKey: crwScraperOptions?.apiKey ?? crwApiKey ?? process.env.CRW_API_KEY,
481
482
  apiUrl: crwScraperOptions?.apiUrl ?? crwApiUrl,
482
483
  timeout: scraperTimeout ?? crwScraperOptions?.timeout,
483
484
  formats: crwScraperOptions?.formats ?? ['markdown', 'rawHtml'],
484
485
  logger,
485
486
  });
487
+ } else if (scraperProvider === 'keenable') {
488
+ scraperInstance = createKeenableScraper({
489
+ ...keenableScraperOptions,
490
+ apiKey: keenableScraperOptions?.apiKey ?? keenableApiKey,
491
+ timeout: scraperTimeout ?? keenableScraperOptions?.timeout,
492
+ attributionTitle:
493
+ keenableScraperOptions?.attributionTitle ??
494
+ keenableSearchOptions?.attributionTitle,
495
+ logger,
496
+ });
486
497
  } else {
487
498
  scraperInstance = createFirecrawlScraper({
488
499
  ...firecrawlOptions,
@@ -3,8 +3,18 @@ import type { Logger as WinstonLogger } from 'winston';
3
3
  import type { BaseReranker } from './rerankers';
4
4
  import { DATE_RANGE } from './schema';
5
5
 
6
- export type SearchProvider = 'serper' | 'searxng' | 'tavily' | 'keenable' | 'crw';
7
- export type ScraperProvider = 'firecrawl' | 'serper' | 'tavily' | 'crw';
6
+ export type SearchProvider =
7
+ | 'serper'
8
+ | 'searxng'
9
+ | 'tavily'
10
+ | 'keenable'
11
+ | 'crw';
12
+ export type ScraperProvider =
13
+ | 'firecrawl'
14
+ | 'serper'
15
+ | 'tavily'
16
+ | 'crw'
17
+ | 'keenable';
8
18
  export type RerankerType = 'infinity' | 'jina' | 'cohere' | 'none';
9
19
 
10
20
  export interface Highlight {
@@ -198,6 +208,41 @@ export interface KeenableSearchResponse {
198
208
  results?: KeenableSearchResult[];
199
209
  }
200
210
 
211
+ export interface KeenableScraperConfig {
212
+ apiKey?: string;
213
+ /** Override the fetch endpoint base (default: public keyless, keyed when a
214
+ * key is set). Env fallback: KEENABLE_FETCH_URL. */
215
+ apiUrl?: string;
216
+ timeout?: number;
217
+ logger?: Logger;
218
+ /** Sent as the X-Keenable-Title attribution header. Defaults to "LibreChat". */
219
+ attributionTitle?: string;
220
+ }
221
+
222
+ export type KeenableScrapeOptions = Omit<
223
+ KeenableScraperConfig,
224
+ 'apiKey' | 'apiUrl' | 'logger'
225
+ >;
226
+
227
+ /** Raw JSON shape returned by GET /v1/fetch{,/public}?url=... */
228
+ export interface KeenableFetchResult {
229
+ url?: string;
230
+ title?: string;
231
+ content?: string;
232
+ description?: string;
233
+ }
234
+
235
+ export interface KeenableScrapeResponse {
236
+ success: boolean;
237
+ data?: {
238
+ content: string;
239
+ title?: string;
240
+ description?: string;
241
+ url?: string;
242
+ };
243
+ error?: string;
244
+ }
245
+
201
246
  export type References = {
202
247
  links: MediaReference[];
203
248
  images: MediaReference[];
@@ -311,6 +356,7 @@ export interface SearchToolConfig
311
356
  FirecrawlConfig {
312
357
  tavilyScraperOptions?: TavilyScraperConfig;
313
358
  crwScraperOptions?: CrwScraperConfig;
359
+ keenableScraperOptions?: KeenableScraperConfig;
314
360
  /** Max chars of highlight content this tool feeds the MODEL per search (the
315
361
  * dominant, otherwise-unbounded part of the output). Distinct from
316
362
  * `maxContentLength`, which caps scraped/reranked content per source — full
@@ -351,7 +397,8 @@ export type AnyScraperResponse =
351
397
  | FirecrawlScrapeResponse
352
398
  | SerperScrapeResponse
353
399
  | TavilyScrapeResponse
354
- | CrwScrapeResponse;
400
+ | CrwScrapeResponse
401
+ | KeenableScrapeResponse;
355
402
 
356
403
  /** Base Scraper Interface */
357
404
  export interface BaseScraper {
@@ -30,7 +30,11 @@ import type {
30
30
  MessageDeltaEvent,
31
31
  ReasoningDeltaEvent,
32
32
  } from '@/types/stream';
33
- import type { TokenCounter, TokenBudgetBreakdown } from '@/types/run';
33
+ import type {
34
+ TokenCounter,
35
+ StreamPreemption,
36
+ TokenBudgetBreakdown,
37
+ } from '@/types/run';
34
38
  import type { Providers, Callback, GraphNodeKeys } from '@/common';
35
39
  import type { StandardGraph, MultiAgentGraph } from '@/graphs';
36
40
  import type { ClientOptions } from '@/types/llm';
@@ -346,6 +350,13 @@ export type StandardGraphInput = {
346
350
  * hook inputs carry only `executingAgentId`.
347
351
  */
348
352
  subagentScope?: boolean;
353
+ /**
354
+ * Cooperative preemption, forwarded from `RunConfig.preemption`. Only ever
355
+ * set on the top-level graph: a steer targets the conversation, so subagent
356
+ * children must run to completion and `buildChildInputs` does not propagate
357
+ * this field.
358
+ */
359
+ preemption?: StreamPreemption;
349
360
  };
350
361
 
351
362
  export type GraphEdge = {
package/src/types/run.ts CHANGED
@@ -115,6 +115,65 @@ export type StandardGraphConfig = Omit<
115
115
  'edges' | 'type'
116
116
  > & { type?: 'standard'; signal?: AbortSignal };
117
117
 
118
+ /**
119
+ * Cooperative mid-generation preemption. Lets a host seal the live model
120
+ * stream at the next provider-safe token boundary — the run is never
121
+ * aborted, the partial assistant turn is kept, and the graph self-loops
122
+ * into a fresh model call once the `PreemptBoundary` hook has injected
123
+ * whatever the host queued.
124
+ *
125
+ * Preconditions the host MUST satisfy:
126
+ * - `shouldPreempt` is polled once per streamed chunk on the top-level
127
+ * graph. It must be synchronous, allocation-free and O(1) — never I/O.
128
+ * It must also be LEVEL-TRIGGERED (non-consuming): the SDK never clears
129
+ * the host's request, and a true result is only honored once the
130
+ * accumulated chunk is provider-safe, so the predicate may be polled
131
+ * many times before a seal. A one-shot read that clears its own pending
132
+ * flag would silently lose the request on an unsafe chunk (leading
133
+ * whitespace/reasoning, an in-flight tool call) — keep returning true
134
+ * until the `PreemptBoundary` drain hands over the queued injection,
135
+ * then disarm there.
136
+ * - Sealing is only honored on the SDK's own dispatch loop. A run whose
137
+ * registered `CHAT_MODEL_STREAM` handler IS the SDK dispatcher — or wraps
138
+ * it, which `composeEventHandlers` and `createRunHandlers` both do —
139
+ * consumes chunks through a decoupled `streamEvents` reader that can lag
140
+ * the accumulated chunk, so those runs never seal.
141
+ *
142
+ * Detection is by capability, not identity: the dispatcher carries
143
+ * `SDK_STREAM_DISPATCH` and the SDK's wrappers propagate it. A handler
144
+ * that merely OBSERVES the raw chunk echo — LibreChat's no-op
145
+ * `OpenAIChatModelStreamHandler`, say — is unbranded and does NOT disable
146
+ * sealing, because it assigns no content-part indices and so cannot be
147
+ * inverted. A host that renders from the raw feed and wants the opt-out
148
+ * should brand its handler with `SDK_STREAM_DISPATCH`.
149
+ * - `RunConfig.tokenCounter` should be set. A sealed turn ends before most
150
+ * providers send their usage chunk, so the synthetic `CHAT_MODEL_END`
151
+ * falls back to the counter to report `output_tokens`. Without one that
152
+ * fallback silently no-ops and the sealed turn's usage is lost — verified
153
+ * live: OpenAI, Azure OpenAI and DeepSeek report no usage for the sealed
154
+ * segment without a counter, while Anthropic streams usage incrementally
155
+ * and reports it either way.
156
+ */
157
+ export interface StreamPreemption {
158
+ /**
159
+ * Polled once per streamed chunk. Synchronous, allocation-free, O(1), and
160
+ * level-triggered — keep returning true until the `PreemptBoundary` drain
161
+ * consumes the request; a self-clearing read loses it on an unsafe chunk.
162
+ */
163
+ shouldPreempt: () => boolean;
164
+ /**
165
+ * Max cooperative seals per run. Each seal costs one extra superstep, so
166
+ * this also bounds the recursion-limit headroom the run reserves.
167
+ */
168
+ maxSeals?: number;
169
+ }
170
+
171
+ /** Seals honored and boundaries that had nothing to inject, per run. */
172
+ export type PreemptStats = {
173
+ seals: number;
174
+ emptyBoundaries: number;
175
+ };
176
+
118
177
  export type RunConfig = {
119
178
  runId: string;
120
179
  graphConfig: LegacyGraphConfig | StandardGraphConfig | MultiAgentGraphConfig;
@@ -146,6 +205,14 @@ export type RunConfig = {
146
205
  * block to prevent leaks.
147
206
  */
148
207
  hooks?: HookRegistry;
208
+ /**
209
+ * Opt-in cooperative preemption for this run. Requires a `hooks` registry
210
+ * with a `PreemptBoundary` matcher — the seal only stops the stream, the
211
+ * hook is what supplies the messages to resume with. Omit to keep the
212
+ * pre-preemption behavior, where a mid-run injection can only land at a
213
+ * tool boundary.
214
+ */
215
+ preemption?: StreamPreemption;
149
216
  returnContent?: boolean;
150
217
  tokenCounter?: TokenCounter;
151
218
  indexTokenCountMap?: Record<string, number>;
@@ -8,7 +8,6 @@ import type {
8
8
  import type { ToolCall, ToolCallChunk } from '@langchain/core/messages/tool';
9
9
  import type { LLMResult, Generation } from '@langchain/core/outputs';
10
10
  import type { Command } from '@langchain/langgraph';
11
- import type OpenAITypes from 'openai';
12
11
  import type { AnthropicContentBlock } from '@/llm/anthropic/types';
13
12
  import type { SummarizeCompleteEvent } from '@/types/summarize';
14
13
  import type { ToolEndEvent } from '@/types/tools';
@@ -393,46 +392,6 @@ export interface TMessage {
393
392
 
394
393
  export type TPayload = Array<Partial<TMessage>>;
395
394
 
396
- export type CustomChunkDelta =
397
- | null
398
- | undefined
399
- | (Partial<OpenAITypes.Chat.Completions.ChatCompletionChunk.Choice.Delta> & {
400
- reasoning?: string | null;
401
- reasoning_content?: string | null;
402
- });
403
- export type CustomChunkChoice = Partial<
404
- Omit<OpenAITypes.Chat.Completions.ChatCompletionChunk.Choice, 'delta'> & {
405
- delta?: CustomChunkDelta;
406
- }
407
- >;
408
- export type CustomChunk = Partial<OpenAITypes.ChatCompletionChunk> & {
409
- choices?: Partial<Array<CustomChunkChoice>>;
410
- };
411
-
412
- export type SplitStreamHandlers = Partial<{
413
- [GraphEvents.ON_RUN_STEP]: ({
414
- event,
415
- data,
416
- }: {
417
- event: GraphEvents;
418
- data: RunStep;
419
- }) => void;
420
- [GraphEvents.ON_MESSAGE_DELTA]: ({
421
- event,
422
- data,
423
- }: {
424
- event: GraphEvents;
425
- data: MessageDeltaEvent;
426
- }) => void;
427
- [GraphEvents.ON_REASONING_DELTA]: ({
428
- event,
429
- data,
430
- }: {
431
- event: GraphEvents;
432
- data: ReasoningDeltaEvent;
433
- }) => void;
434
- }>;
435
-
436
395
  export type SummarizeDeltaData = {
437
396
  id: string;
438
397
  delta: {
@@ -1,151 +0,0 @@
1
- require("./common/enum.cjs");
2
- require("./common/index.cjs");
3
- let nanoid = require("nanoid");
4
- //#region src/splitStream.ts
5
- const SEPARATORS = [
6
- ". ",
7
- "?",
8
- "!",
9
- "۔",
10
- "- ",
11
- "。",
12
- "‥",
13
- ";",
14
- "¡",
15
- "¿",
16
- "\n",
17
- "```"
18
- ];
19
- var SplitStreamHandler = class {
20
- inCodeBlock = false;
21
- inThinkBlock = false;
22
- accumulate;
23
- tokens = [];
24
- lastToken = "";
25
- reasoningTokens = [];
26
- currentStepId;
27
- currentMessageId;
28
- currentType;
29
- currentLength = 0;
30
- reasoningKey = "reasoning_content";
31
- currentIndex = -1;
32
- blockThreshold = 4500;
33
- /** The run ID AKA the Message ID associated with the complete generation */
34
- runId;
35
- handlers;
36
- constructor({ runId, handlers, accumulate, reasoningKey, blockThreshold }) {
37
- this.runId = runId;
38
- this.handlers = handlers;
39
- if (reasoningKey) this.reasoningKey = reasoningKey;
40
- if (blockThreshold != null) this.blockThreshold = blockThreshold;
41
- this.accumulate = accumulate ?? false;
42
- }
43
- getMessageId = () => {
44
- const messageId = this.currentMessageId;
45
- if (messageId != null && messageId) return messageId;
46
- };
47
- createMessageStep = (type) => {
48
- if (type != null && this.currentType !== type) this.currentType = type;
49
- this.currentLength = 0;
50
- this.currentIndex += 1;
51
- this.currentStepId = `step_${(0, nanoid.nanoid)()}`;
52
- this.currentMessageId = `msg_${(0, nanoid.nanoid)()}`;
53
- return [this.currentStepId, this.currentMessageId];
54
- };
55
- dispatchRunStep = (stepId, stepDetails) => {
56
- const runStep = {
57
- id: stepId,
58
- runId: this.runId,
59
- type: stepDetails.type,
60
- index: this.currentIndex,
61
- stepDetails
62
- };
63
- this.handlers?.["on_run_step"]?.({
64
- event: "on_run_step",
65
- data: runStep
66
- });
67
- };
68
- dispatchMessageDelta = (stepId, delta) => {
69
- const messageDelta = {
70
- id: stepId,
71
- delta
72
- };
73
- this.handlers?.["on_message_delta"]?.({
74
- event: "on_message_delta",
75
- data: messageDelta
76
- });
77
- };
78
- dispatchReasoningDelta = (stepId, delta) => {
79
- const reasoningDelta = {
80
- id: stepId,
81
- delta
82
- };
83
- this.handlers?.["on_reasoning_delta"]?.({
84
- event: "on_reasoning_delta",
85
- data: reasoningDelta
86
- });
87
- };
88
- handleContent = (content, _type) => {
89
- let type = _type;
90
- if (this.inThinkBlock && type === "text") type = "think";
91
- if (this.accumulate) if (type === "think") this.reasoningTokens.push(content);
92
- else this.tokens.push(content);
93
- if (this.currentType !== type) {
94
- const [newStepId, newMessageId] = this.createMessageStep(type);
95
- this.dispatchRunStep(newStepId, {
96
- type: "message_creation",
97
- message_creation: { message_id: newMessageId }
98
- });
99
- }
100
- const stepId = this.currentStepId ?? "";
101
- if (type === "think") this.dispatchReasoningDelta(stepId, { content: [{
102
- type: "think",
103
- think: content
104
- }] });
105
- else this.dispatchMessageDelta(stepId, { content: [{
106
- type: "text",
107
- text: content
108
- }] });
109
- this.currentLength += content.length;
110
- if (this.inCodeBlock) return;
111
- if (this.currentLength > this.blockThreshold && SEPARATORS.some((sep) => content.includes(sep))) {
112
- const [newStepId, newMessageId] = this.createMessageStep(type);
113
- this.dispatchRunStep(newStepId, {
114
- type: "message_creation",
115
- message_creation: { message_id: newMessageId }
116
- });
117
- }
118
- };
119
- getDeltaContent(chunk) {
120
- return (chunk?.choices?.[0]?.delta)?.content ?? "";
121
- }
122
- getReasoningDelta(chunk) {
123
- return (chunk?.choices?.[0]?.delta)?.[this.reasoningKey] ?? "";
124
- }
125
- handle(chunk) {
126
- if (!chunk) return;
127
- const content = this.getDeltaContent(chunk);
128
- const reasoning_content = this.getReasoningDelta(chunk);
129
- if (!content.length && !reasoning_content.length) return;
130
- if (content.includes("```")) this.inCodeBlock = !this.inCodeBlock;
131
- if (content.includes("<think>") && !this.inCodeBlock) this.inThinkBlock = true;
132
- else if (this.lastToken.includes("</think>") && !this.inCodeBlock) this.inThinkBlock = false;
133
- this.lastToken = content;
134
- if (!(this.getMessageId() ?? "")) {
135
- const initialContentType = this.inThinkBlock ? "think" : "text";
136
- const initialType = reasoning_content ? "think" : initialContentType;
137
- const [stepId, message_id] = this.createMessageStep(initialType);
138
- this.dispatchRunStep(stepId, {
139
- type: "message_creation",
140
- message_creation: { message_id }
141
- });
142
- }
143
- if (reasoning_content) this.handleContent(reasoning_content, "think");
144
- else this.handleContent(content, "text");
145
- }
146
- };
147
- //#endregion
148
- exports.SEPARATORS = SEPARATORS;
149
- exports.SplitStreamHandler = SplitStreamHandler;
150
-
151
- //# sourceMappingURL=splitStream.cjs.map
@@ -1 +0,0 @@
1
- {"version":3,"file":"splitStream.cjs","names":[],"sources":["../../src/splitStream.ts"],"sourcesContent":["import { nanoid } from 'nanoid';\nimport type * as t from '@/types';\nimport { ContentTypes, GraphEvents, StepTypes } from '@/common';\n\nexport const SEPARATORS = [\n '. ',\n '?',\n '!',\n '۔',\n '- ',\n '。',\n '‥',\n ';',\n '¡',\n '¿',\n '\\n',\n '```',\n];\n\nexport class SplitStreamHandler {\n private inCodeBlock = false;\n private inThinkBlock = false;\n private accumulate: boolean;\n tokens: string[] = [];\n lastToken = '';\n reasoningTokens: string[] = [];\n currentStepId?: string;\n currentMessageId?: string;\n currentType?: ContentTypes.TEXT | ContentTypes.THINK;\n currentLength = 0;\n reasoningKey: 'reasoning_content' | 'reasoning' = 'reasoning_content';\n currentIndex = -1;\n blockThreshold = 4500;\n /** The run ID AKA the Message ID associated with the complete generation */\n runId: string;\n handlers?: t.SplitStreamHandlers;\n constructor({\n runId,\n handlers,\n accumulate,\n reasoningKey,\n blockThreshold,\n }: {\n runId: string;\n accumulate?: boolean;\n handlers: t.SplitStreamHandlers;\n blockThreshold?: number;\n reasoningKey?: 'reasoning_content' | 'reasoning';\n }) {\n this.runId = runId;\n this.handlers = handlers;\n if (reasoningKey) {\n this.reasoningKey = reasoningKey;\n }\n if (blockThreshold != null) {\n this.blockThreshold = blockThreshold;\n }\n this.accumulate = accumulate ?? false;\n }\n getMessageId = (): string | undefined => {\n const messageId = this.currentMessageId;\n if (messageId != null && messageId) {\n return messageId;\n }\n return undefined;\n };\n createMessageStep = (\n type?: ContentTypes.TEXT | ContentTypes.THINK\n ): [string, string] => {\n if (type != null && this.currentType !== type) {\n this.currentType = type;\n }\n this.currentLength = 0;\n this.currentIndex += 1;\n this.currentStepId = `step_${nanoid()}`;\n this.currentMessageId = `msg_${nanoid()}`;\n return [this.currentStepId, this.currentMessageId];\n };\n dispatchRunStep = (stepId: string, stepDetails: t.StepDetails): void => {\n const runStep: t.RunStep = {\n id: stepId,\n runId: this.runId,\n type: stepDetails.type,\n index: this.currentIndex,\n stepDetails,\n // usage: null,\n };\n this.handlers?.[GraphEvents.ON_RUN_STEP]?.({\n event: GraphEvents.ON_RUN_STEP,\n data: runStep,\n });\n };\n dispatchMessageDelta = (stepId: string, delta: t.MessageDelta): void => {\n const messageDelta: t.MessageDeltaEvent = {\n id: stepId,\n delta,\n };\n this.handlers?.[GraphEvents.ON_MESSAGE_DELTA]?.({\n event: GraphEvents.ON_MESSAGE_DELTA,\n data: messageDelta,\n });\n };\n dispatchReasoningDelta = (stepId: string, delta: t.ReasoningDelta): void => {\n const reasoningDelta: t.ReasoningDeltaEvent = {\n id: stepId,\n delta,\n };\n this.handlers?.[GraphEvents.ON_REASONING_DELTA]?.({\n event: GraphEvents.ON_REASONING_DELTA,\n data: reasoningDelta,\n });\n };\n handleContent = (\n content: string,\n _type: ContentTypes.TEXT | ContentTypes.THINK\n ): void => {\n let type = _type;\n if (this.inThinkBlock && type === ContentTypes.TEXT) {\n type = ContentTypes.THINK;\n }\n if (this.accumulate) {\n if (type === ContentTypes.THINK) {\n this.reasoningTokens.push(content);\n } else {\n this.tokens.push(content);\n }\n }\n\n if (this.currentType !== type) {\n const [newStepId, newMessageId] = this.createMessageStep(type);\n this.dispatchRunStep(newStepId, {\n type: StepTypes.MESSAGE_CREATION,\n message_creation: {\n message_id: newMessageId,\n },\n });\n }\n\n const stepId = this.currentStepId ?? '';\n if (type === ContentTypes.THINK) {\n this.dispatchReasoningDelta(stepId, {\n content: [\n {\n type: ContentTypes.THINK,\n think: content,\n },\n ],\n });\n } else {\n this.dispatchMessageDelta(stepId, {\n content: [\n {\n type: ContentTypes.TEXT,\n text: content,\n },\n ],\n });\n }\n\n this.currentLength += content.length;\n if (this.inCodeBlock) {\n return;\n }\n\n if (\n this.currentLength > this.blockThreshold &&\n SEPARATORS.some((sep) => content.includes(sep))\n ) {\n const [newStepId, newMessageId] = this.createMessageStep(type);\n this.dispatchRunStep(newStepId, {\n type: StepTypes.MESSAGE_CREATION,\n message_creation: {\n message_id: newMessageId,\n },\n });\n }\n };\n getDeltaContent(chunk?: t.CustomChunk): string {\n return (chunk?.choices?.[0]?.delta as t.CustomChunkDelta)?.content ?? '';\n }\n getReasoningDelta(chunk?: t.CustomChunk): string {\n return (\n (chunk?.choices?.[0]?.delta as t.CustomChunkDelta)?.[this.reasoningKey] ??\n ''\n );\n }\n handle(chunk?: t.CustomChunk): void {\n if (!chunk) {\n return;\n }\n\n const content = this.getDeltaContent(chunk);\n const reasoning_content = this.getReasoningDelta(chunk);\n if (!content.length && !reasoning_content.length) {\n return;\n }\n\n if (content.includes('```')) {\n this.inCodeBlock = !this.inCodeBlock;\n }\n\n if (content.includes('<think>') && !this.inCodeBlock) {\n this.inThinkBlock = true;\n } else if (this.lastToken.includes('</think>') && !this.inCodeBlock) {\n this.inThinkBlock = false;\n }\n\n this.lastToken = content;\n\n const message_id = this.getMessageId() ?? '';\n\n if (!message_id) {\n const initialContentType = this.inThinkBlock\n ? ContentTypes.THINK\n : ContentTypes.TEXT;\n const initialType = reasoning_content\n ? ContentTypes.THINK\n : initialContentType;\n const [stepId, message_id] = this.createMessageStep(initialType);\n this.dispatchRunStep(stepId, {\n type: StepTypes.MESSAGE_CREATION,\n message_creation: {\n message_id,\n },\n });\n }\n\n if (reasoning_content) {\n this.handleContent(reasoning_content, ContentTypes.THINK);\n } else {\n this.handleContent(content, ContentTypes.TEXT);\n }\n }\n}\n"],"mappings":"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"}