@librechat/agents 3.2.60 → 3.2.62
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.
- package/dist/cjs/common/enum.cjs +2 -0
- package/dist/cjs/common/enum.cjs.map +1 -1
- package/dist/cjs/hooks/HookRegistry.cjs +32 -0
- package/dist/cjs/hooks/HookRegistry.cjs.map +1 -1
- package/dist/cjs/hooks/executeHooks.cjs +6 -0
- package/dist/cjs/hooks/executeHooks.cjs.map +1 -1
- package/dist/cjs/hooks/index.cjs +12 -0
- package/dist/cjs/hooks/index.cjs.map +1 -0
- package/dist/cjs/hooks/types.cjs.map +1 -1
- package/dist/cjs/langfuse.cjs +62 -1
- package/dist/cjs/langfuse.cjs.map +1 -1
- package/dist/cjs/llm/bedrock/index.cjs +266 -43
- package/dist/cjs/llm/bedrock/index.cjs.map +1 -1
- package/dist/cjs/llm/openai/index.cjs +274 -7
- package/dist/cjs/llm/openai/index.cjs.map +1 -1
- package/dist/cjs/llm/openai/streamMetadata.cjs +69 -0
- package/dist/cjs/llm/openai/streamMetadata.cjs.map +1 -0
- package/dist/cjs/llm/openrouter/index.cjs +5 -6
- package/dist/cjs/llm/openrouter/index.cjs.map +1 -1
- package/dist/cjs/main.cjs +6 -1
- package/dist/cjs/messages/format.cjs +61 -0
- package/dist/cjs/messages/format.cjs.map +1 -1
- package/dist/cjs/messages/prune.cjs +31 -19
- package/dist/cjs/messages/prune.cjs.map +1 -1
- package/dist/cjs/stream.cjs +13 -3
- package/dist/cjs/stream.cjs.map +1 -1
- package/dist/cjs/tools/ToolNode.cjs +75 -10
- package/dist/cjs/tools/ToolNode.cjs.map +1 -1
- package/dist/cjs/tools/subagent/SubagentExecutor.cjs +1 -0
- package/dist/cjs/tools/subagent/SubagentExecutor.cjs.map +1 -1
- package/dist/cjs/utils/tokens.cjs +111 -0
- package/dist/cjs/utils/tokens.cjs.map +1 -1
- package/dist/esm/common/enum.mjs +2 -0
- package/dist/esm/common/enum.mjs.map +1 -1
- package/dist/esm/hooks/HookRegistry.mjs +32 -0
- package/dist/esm/hooks/HookRegistry.mjs.map +1 -1
- package/dist/esm/hooks/executeHooks.mjs +6 -0
- package/dist/esm/hooks/executeHooks.mjs.map +1 -1
- package/dist/esm/hooks/index.mjs +12 -1
- package/dist/esm/hooks/index.mjs.map +1 -0
- package/dist/esm/hooks/types.mjs.map +1 -1
- package/dist/esm/langfuse.mjs +62 -1
- package/dist/esm/langfuse.mjs.map +1 -1
- package/dist/esm/llm/bedrock/index.mjs +266 -43
- package/dist/esm/llm/bedrock/index.mjs.map +1 -1
- package/dist/esm/llm/openai/index.mjs +274 -7
- package/dist/esm/llm/openai/index.mjs.map +1 -1
- package/dist/esm/llm/openai/streamMetadata.mjs +69 -0
- package/dist/esm/llm/openai/streamMetadata.mjs.map +1 -0
- package/dist/esm/llm/openrouter/index.mjs +5 -6
- package/dist/esm/llm/openrouter/index.mjs.map +1 -1
- package/dist/esm/main.mjs +3 -3
- package/dist/esm/messages/format.mjs +61 -0
- package/dist/esm/messages/format.mjs.map +1 -1
- package/dist/esm/messages/prune.mjs +31 -19
- package/dist/esm/messages/prune.mjs.map +1 -1
- package/dist/esm/stream.mjs +13 -3
- package/dist/esm/stream.mjs.map +1 -1
- package/dist/esm/tools/ToolNode.mjs +75 -10
- package/dist/esm/tools/ToolNode.mjs.map +1 -1
- package/dist/esm/tools/subagent/SubagentExecutor.mjs +1 -0
- package/dist/esm/tools/subagent/SubagentExecutor.mjs.map +1 -1
- package/dist/esm/utils/tokens.mjs +108 -1
- package/dist/esm/utils/tokens.mjs.map +1 -1
- package/dist/types/common/enum.d.ts +3 -1
- package/dist/types/hooks/HookRegistry.d.ts +10 -0
- package/dist/types/hooks/index.d.ts +7 -0
- package/dist/types/hooks/types.d.ts +18 -1
- package/dist/types/llm/bedrock/index.d.ts +5 -0
- package/dist/types/llm/openai/index.d.ts +19 -0
- package/dist/types/llm/openai/streamMetadata.d.ts +16 -0
- package/dist/types/messages/prune.d.ts +4 -3
- package/dist/types/tools/ToolNode.d.ts +1 -0
- package/dist/types/types/llm.d.ts +12 -2
- package/dist/types/types/tools.d.ts +1 -1
- package/dist/types/utils/tokens.d.ts +30 -0
- package/package.json +9 -9
- package/src/__tests__/stream.eagerEventExecution.test.ts +100 -2
- package/src/common/enum.ts +2 -0
- package/src/hooks/HookRegistry.ts +45 -0
- package/src/hooks/__tests__/HookRegistry.test.ts +48 -0
- package/src/hooks/__tests__/executeHooks.test.ts +85 -2
- package/src/hooks/executeHooks.ts +15 -0
- package/src/hooks/index.ts +7 -0
- package/src/hooks/types.ts +18 -1
- package/src/langfuse.ts +134 -1
- package/src/llm/bedrock/index.ts +434 -83
- package/src/llm/bedrock/streamSealDispatch.test.ts +97 -0
- package/src/llm/custom-chat-models.smoke.test.ts +7 -0
- package/src/llm/openai/index.ts +604 -6
- package/src/llm/openai/managedRequests.test.ts +182 -0
- package/src/llm/openai/streamMetadata.spec.ts +86 -0
- package/src/llm/openai/streamMetadata.ts +95 -0
- package/src/llm/openai/streamMetadataDedup.spec.ts +166 -0
- package/src/llm/openrouter/index.ts +9 -5
- package/src/messages/format.ts +96 -3
- package/src/messages/formatAgentMessages.steer.test.ts +326 -0
- package/src/messages/labelContentByAgent.test.ts +75 -0
- package/src/messages/prune.ts +56 -30
- package/src/specs/anthropic.simple.test.ts +4 -2
- package/src/specs/cache.simple.test.ts +17 -4
- package/src/specs/langfuse-callbacks.test.ts +61 -0
- package/src/specs/openai.simple.test.ts +4 -2
- package/src/specs/spec.utils.ts +12 -0
- package/src/specs/summarization.test.ts +9 -13
- package/src/specs/token-accounting-pipeline.test.ts +130 -4
- package/src/specs/tokens.test.ts +214 -0
- package/src/stream.ts +22 -3
- package/src/tools/ToolNode.ts +107 -12
- package/src/tools/__tests__/ToolNode.eagerEventExecution.test.ts +554 -0
- package/src/tools/__tests__/ToolNode.onResultCompletion.test.ts +49 -2
- package/src/tools/__tests__/hitl.test.ts +112 -0
- package/src/tools/subagent/SubagentExecutor.ts +2 -0
- package/src/types/llm.ts +12 -2
- package/src/types/tools.ts +1 -1
- package/src/utils/tokens.ts +181 -3
package/src/llm/bedrock/index.ts
CHANGED
|
@@ -39,6 +39,7 @@ import {
|
|
|
39
39
|
handleConverseStreamContentBlockDelta,
|
|
40
40
|
handleConverseStreamMetadata,
|
|
41
41
|
} from './utils';
|
|
42
|
+
import type { ContentBlockDeltaEvent } from './types';
|
|
42
43
|
import {
|
|
43
44
|
resolveBedrockPromptCacheTtl,
|
|
44
45
|
supportsBedrockToolCache,
|
|
@@ -57,6 +58,96 @@ export type ServiceTierType = 'priority' | 'default' | 'flex' | 'reserved';
|
|
|
57
58
|
export type CustomGuardrailConfiguration = GuardrailConfiguration &
|
|
58
59
|
Pick<GuardrailStreamConfiguration, 'streamProcessingMode'>;
|
|
59
60
|
|
|
61
|
+
const MAX_STREAM_QUEUE_CHUNKS = 256;
|
|
62
|
+
const MAX_STREAM_QUEUE_TEXT_CHARS = 8192;
|
|
63
|
+
const STREAM_CHUNK_MIN_SIZE = 4;
|
|
64
|
+
const STREAM_BOUNDARIES = new Set([' ', '.', ',', '!', '?', ';', ':']);
|
|
65
|
+
|
|
66
|
+
type QueuedGenerationChunk = {
|
|
67
|
+
chunk: ChatGenerationChunk;
|
|
68
|
+
callbackChunk?: ChatGenerationChunk;
|
|
69
|
+
callbackToken: string;
|
|
70
|
+
smooth: boolean;
|
|
71
|
+
textLength: number;
|
|
72
|
+
};
|
|
73
|
+
|
|
74
|
+
function findStreamChunkBoundary(text: string, minSize: number): number {
|
|
75
|
+
if (minSize >= text.length) {
|
|
76
|
+
return text.length;
|
|
77
|
+
}
|
|
78
|
+
|
|
79
|
+
for (let position = minSize; position < text.length; position++) {
|
|
80
|
+
if (STREAM_BOUNDARIES.has(text[position])) {
|
|
81
|
+
return position + 1;
|
|
82
|
+
}
|
|
83
|
+
}
|
|
84
|
+
|
|
85
|
+
return text.length;
|
|
86
|
+
}
|
|
87
|
+
|
|
88
|
+
function splitStreamToken(text: string): string[] {
|
|
89
|
+
const chunks: string[] = [];
|
|
90
|
+
let currentIndex = 0;
|
|
91
|
+
|
|
92
|
+
while (currentIndex < text.length) {
|
|
93
|
+
const remainingText = text.slice(currentIndex);
|
|
94
|
+
const chunkSize = findStreamChunkBoundary(
|
|
95
|
+
remainingText,
|
|
96
|
+
STREAM_CHUNK_MIN_SIZE
|
|
97
|
+
);
|
|
98
|
+
chunks.push(text.slice(currentIndex, currentIndex + chunkSize));
|
|
99
|
+
currentIndex += chunkSize;
|
|
100
|
+
}
|
|
101
|
+
|
|
102
|
+
return chunks;
|
|
103
|
+
}
|
|
104
|
+
|
|
105
|
+
function getCadencedStreamDelay({
|
|
106
|
+
targetDelay,
|
|
107
|
+
lastVisibleContentAt,
|
|
108
|
+
now,
|
|
109
|
+
}: {
|
|
110
|
+
targetDelay: number;
|
|
111
|
+
lastVisibleContentAt?: number;
|
|
112
|
+
now: number;
|
|
113
|
+
}): number {
|
|
114
|
+
if (targetDelay <= 0 || lastVisibleContentAt == null) {
|
|
115
|
+
return 0;
|
|
116
|
+
}
|
|
117
|
+
return Math.max(0, targetDelay - (now - lastVisibleContentAt));
|
|
118
|
+
}
|
|
119
|
+
|
|
120
|
+
async function waitForStreamDelay(
|
|
121
|
+
delay: number,
|
|
122
|
+
signal?: AbortSignal
|
|
123
|
+
): Promise<void> {
|
|
124
|
+
if (delay <= 0 || isSignalAborted(signal)) {
|
|
125
|
+
return;
|
|
126
|
+
}
|
|
127
|
+
await new Promise<void>((resolve) => {
|
|
128
|
+
const timeoutRef: { current?: ReturnType<typeof setTimeout> } = {};
|
|
129
|
+
const onAbort = (): void => {
|
|
130
|
+
if (timeoutRef.current) {
|
|
131
|
+
clearTimeout(timeoutRef.current);
|
|
132
|
+
}
|
|
133
|
+
signal?.removeEventListener('abort', onAbort);
|
|
134
|
+
resolve();
|
|
135
|
+
};
|
|
136
|
+
timeoutRef.current = setTimeout(() => {
|
|
137
|
+
signal?.removeEventListener('abort', onAbort);
|
|
138
|
+
resolve();
|
|
139
|
+
}, delay);
|
|
140
|
+
signal?.addEventListener('abort', onAbort, { once: true });
|
|
141
|
+
if (isSignalAborted(signal)) {
|
|
142
|
+
onAbort();
|
|
143
|
+
}
|
|
144
|
+
});
|
|
145
|
+
}
|
|
146
|
+
|
|
147
|
+
function isSignalAborted(signal?: AbortSignal): boolean {
|
|
148
|
+
return signal?.aborted === true;
|
|
149
|
+
}
|
|
150
|
+
|
|
60
151
|
/**
|
|
61
152
|
* Extended input interface with additional features:
|
|
62
153
|
* - applicationInferenceProfile: Use an inference profile ARN instead of model ID
|
|
@@ -78,6 +169,11 @@ export interface CustomChatBedrockConverseInput
|
|
|
78
169
|
*/
|
|
79
170
|
promptCacheTtl?: PromptCacheTtl;
|
|
80
171
|
|
|
172
|
+
/**
|
|
173
|
+
* Minimum delay in milliseconds between visible streamed content deltas.
|
|
174
|
+
*/
|
|
175
|
+
_lc_stream_delay?: number;
|
|
176
|
+
|
|
81
177
|
/**
|
|
82
178
|
* Guardrail configuration for Converse and ConverseStream invocations.
|
|
83
179
|
* `streamProcessingMode` is only used by ConverseStream.
|
|
@@ -119,6 +215,8 @@ export interface CustomChatBedrockConverseCallOptions {
|
|
|
119
215
|
}
|
|
120
216
|
|
|
121
217
|
export class CustomChatBedrockConverse extends ChatBedrockConverse {
|
|
218
|
+
_lc_stream_delay: number;
|
|
219
|
+
|
|
122
220
|
/**
|
|
123
221
|
* Whether to insert Bedrock prompt cache checkpoints when available.
|
|
124
222
|
*/
|
|
@@ -151,6 +249,7 @@ export class CustomChatBedrockConverse extends ChatBedrockConverse {
|
|
|
151
249
|
super(fields);
|
|
152
250
|
this.promptCache = fields?.promptCache;
|
|
153
251
|
this.promptCacheTtl = fields?.promptCacheTtl;
|
|
252
|
+
this._lc_stream_delay = Math.max(0, fields?._lc_stream_delay ?? 0);
|
|
154
253
|
this.applicationInferenceProfile = fields?.applicationInferenceProfile;
|
|
155
254
|
this.serviceTier = fields?.serviceTier;
|
|
156
255
|
// `super(fields)` initializes `this.model` to LangChain's default Claude
|
|
@@ -183,10 +282,10 @@ export class CustomChatBedrockConverse extends ChatBedrockConverse {
|
|
|
183
282
|
const toolConfig =
|
|
184
283
|
this.promptCache === true && supportsBedrockToolCache(this.cacheModelId)
|
|
185
284
|
? insertBedrockToolCachePoint(
|
|
186
|
-
|
|
187
|
-
|
|
188
|
-
|
|
189
|
-
|
|
285
|
+
baseParams.toolConfig,
|
|
286
|
+
true,
|
|
287
|
+
resolveBedrockPromptCacheTtl(this.promptCacheTtl, this.cacheModelId)
|
|
288
|
+
)
|
|
190
289
|
: baseParams.toolConfig;
|
|
191
290
|
|
|
192
291
|
/** Service tier from options or fall back to class-level setting */
|
|
@@ -261,102 +360,354 @@ export class CustomChatBedrockConverse extends ChatBedrockConverse {
|
|
|
261
360
|
...(params as Record<string, unknown>),
|
|
262
361
|
});
|
|
263
362
|
|
|
264
|
-
const
|
|
265
|
-
|
|
266
|
-
});
|
|
267
|
-
|
|
268
|
-
|
|
269
|
-
return;
|
|
363
|
+
const streamAbortController = new AbortController();
|
|
364
|
+
const abortStream = (): void => streamAbortController.abort();
|
|
365
|
+
options.signal?.addEventListener('abort', abortStream, { once: true });
|
|
366
|
+
if (isSignalAborted(options.signal)) {
|
|
367
|
+
abortStream();
|
|
270
368
|
}
|
|
271
369
|
|
|
272
|
-
|
|
273
|
-
|
|
274
|
-
|
|
275
|
-
|
|
276
|
-
|
|
277
|
-
|
|
278
|
-
|
|
279
|
-
|
|
280
|
-
|
|
281
|
-
|
|
282
|
-
|
|
283
|
-
|
|
284
|
-
|
|
285
|
-
|
|
286
|
-
|
|
287
|
-
|
|
288
|
-
|
|
289
|
-
|
|
290
|
-
|
|
291
|
-
|
|
292
|
-
|
|
293
|
-
|
|
370
|
+
try {
|
|
371
|
+
const response = await this.client.send(command, {
|
|
372
|
+
abortSignal: streamAbortController.signal,
|
|
373
|
+
});
|
|
374
|
+
|
|
375
|
+
const stream = response.stream;
|
|
376
|
+
if (!stream) {
|
|
377
|
+
return;
|
|
378
|
+
}
|
|
379
|
+
|
|
380
|
+
const seenBlockIndices = new Set<number>();
|
|
381
|
+
const toolUseBlockIndices = new Set<number>();
|
|
382
|
+
const queuedChunks: QueuedGenerationChunk[] = [];
|
|
383
|
+
const producerState: { done: boolean; error?: unknown } = { done: false };
|
|
384
|
+
let queuedChunkIndex = 0;
|
|
385
|
+
let bufferedTextLength = 0;
|
|
386
|
+
let consumerClosed = false;
|
|
387
|
+
let notifyConsumer: (() => void) | undefined;
|
|
388
|
+
let notifyProducer: (() => void) | undefined;
|
|
389
|
+
let hasEmittedVisibleContent = false;
|
|
390
|
+
let lastVisibleContentAt: number | undefined;
|
|
391
|
+
|
|
392
|
+
/**
|
|
393
|
+
* Guardrails can reject an already-streamed toolUse block at
|
|
394
|
+
* `messageStop` (`guardrail_intervened`), after `contentBlockStop` has
|
|
395
|
+
* passed. Only emit eager-execution seals when no guardrails are
|
|
396
|
+
* configured, so a later intervention can't race an eagerly started tool.
|
|
397
|
+
*/
|
|
398
|
+
const sealToolUseOnStop =
|
|
399
|
+
options.guardrailConfig == null && this.guardrailConfig == null;
|
|
400
|
+
|
|
401
|
+
const notifyConsumerForChunk = (): void => {
|
|
402
|
+
notifyConsumer?.();
|
|
403
|
+
notifyConsumer = undefined;
|
|
404
|
+
};
|
|
405
|
+
|
|
406
|
+
const notifyProducerForSpace = (): void => {
|
|
407
|
+
notifyProducer?.();
|
|
408
|
+
notifyProducer = undefined;
|
|
409
|
+
};
|
|
410
|
+
|
|
411
|
+
const hasQueuedChunks = (): boolean =>
|
|
412
|
+
queuedChunkIndex < queuedChunks.length;
|
|
413
|
+
|
|
414
|
+
const getQueuedChunkCount = (): number =>
|
|
415
|
+
queuedChunks.length - queuedChunkIndex;
|
|
416
|
+
|
|
417
|
+
const isQueueAtCapacity = (): boolean =>
|
|
418
|
+
getQueuedChunkCount() >= MAX_STREAM_QUEUE_CHUNKS ||
|
|
419
|
+
bufferedTextLength >= MAX_STREAM_QUEUE_TEXT_CHARS;
|
|
420
|
+
|
|
421
|
+
const waitForNextChunk = async (): Promise<void> => {
|
|
422
|
+
if (
|
|
423
|
+
hasQueuedChunks() ||
|
|
424
|
+
producerState.done ||
|
|
425
|
+
producerState.error != null
|
|
426
|
+
) {
|
|
427
|
+
return;
|
|
428
|
+
}
|
|
429
|
+
await new Promise<void>((resolve) => {
|
|
430
|
+
notifyConsumer = resolve;
|
|
431
|
+
});
|
|
432
|
+
};
|
|
433
|
+
|
|
434
|
+
const waitForQueueSpace = async (): Promise<void> => {
|
|
435
|
+
while (
|
|
436
|
+
isQueueAtCapacity() &&
|
|
437
|
+
!consumerClosed &&
|
|
438
|
+
!isSignalAborted(options.signal)
|
|
439
|
+
) {
|
|
440
|
+
await new Promise<void>((resolve) => {
|
|
441
|
+
const signal = options.signal;
|
|
442
|
+
const onAbort = (): void => {
|
|
443
|
+
signal?.removeEventListener('abort', onAbort);
|
|
444
|
+
resolve();
|
|
445
|
+
};
|
|
446
|
+
const onSpace = (): void => {
|
|
447
|
+
signal?.removeEventListener('abort', onAbort);
|
|
448
|
+
resolve();
|
|
449
|
+
};
|
|
450
|
+
notifyProducer = onSpace;
|
|
451
|
+
signal?.addEventListener('abort', onAbort, { once: true });
|
|
452
|
+
if (isSignalAborted(signal)) {
|
|
453
|
+
onAbort();
|
|
294
454
|
}
|
|
295
|
-
}
|
|
296
|
-
yield this.enrichChunk(startChunk, seenBlockIndices);
|
|
297
|
-
|
|
298
|
-
// Registered stream handlers receive chunks through callback
|
|
299
|
-
// events, not the yielded generator — dispatch the start chunk so
|
|
300
|
-
// they see the tool call's id/name (eager chunk state needs both).
|
|
301
|
-
await runManager?.handleLLMNewToken(
|
|
302
|
-
startChunk.text,
|
|
303
|
-
undefined,
|
|
304
|
-
undefined,
|
|
305
|
-
undefined,
|
|
306
|
-
undefined,
|
|
307
|
-
{ chunk: startChunk }
|
|
308
|
-
);
|
|
455
|
+
});
|
|
309
456
|
}
|
|
310
|
-
}
|
|
311
|
-
const deltaChunk = handleConverseStreamContentBlockDelta(
|
|
312
|
-
event.contentBlockDelta
|
|
313
|
-
);
|
|
457
|
+
};
|
|
314
458
|
|
|
315
|
-
|
|
459
|
+
const dequeue = (): QueuedGenerationChunk | undefined => {
|
|
460
|
+
if (!hasQueuedChunks()) {
|
|
461
|
+
return undefined;
|
|
462
|
+
}
|
|
463
|
+
const queuedChunk = queuedChunks[queuedChunkIndex];
|
|
464
|
+
queuedChunkIndex++;
|
|
465
|
+
if (
|
|
466
|
+
queuedChunkIndex > 128 &&
|
|
467
|
+
queuedChunkIndex * 2 >= queuedChunks.length
|
|
468
|
+
) {
|
|
469
|
+
queuedChunks.splice(0, queuedChunkIndex);
|
|
470
|
+
queuedChunkIndex = 0;
|
|
471
|
+
}
|
|
472
|
+
return queuedChunk;
|
|
473
|
+
};
|
|
474
|
+
|
|
475
|
+
const enqueue = async (
|
|
476
|
+
queuedChunk: QueuedGenerationChunk
|
|
477
|
+
): Promise<void> => {
|
|
478
|
+
await waitForQueueSpace();
|
|
479
|
+
if (consumerClosed || isSignalAborted(options.signal)) {
|
|
480
|
+
abortStream();
|
|
481
|
+
throw new Error('AbortError: User aborted the request.');
|
|
482
|
+
}
|
|
483
|
+
queuedChunks.push(queuedChunk);
|
|
484
|
+
if (queuedChunk.smooth) {
|
|
485
|
+
bufferedTextLength += queuedChunk.textLength;
|
|
486
|
+
}
|
|
487
|
+
notifyConsumerForChunk();
|
|
488
|
+
};
|
|
489
|
+
|
|
490
|
+
const enqueueChunk = async ({
|
|
491
|
+
chunk,
|
|
492
|
+
callbackChunk,
|
|
493
|
+
callbackToken = '',
|
|
494
|
+
smooth = false,
|
|
495
|
+
textLength = 0,
|
|
496
|
+
}: {
|
|
497
|
+
chunk: ChatGenerationChunk;
|
|
498
|
+
callbackChunk?: ChatGenerationChunk;
|
|
499
|
+
callbackToken?: string;
|
|
500
|
+
smooth?: boolean;
|
|
501
|
+
textLength?: number;
|
|
502
|
+
}): Promise<void> => {
|
|
503
|
+
await enqueue({
|
|
504
|
+
chunk,
|
|
505
|
+
callbackChunk,
|
|
506
|
+
callbackToken,
|
|
507
|
+
smooth,
|
|
508
|
+
textLength: smooth ? textLength : 0,
|
|
509
|
+
});
|
|
510
|
+
};
|
|
511
|
+
|
|
512
|
+
const enqueueDelta = async (
|
|
513
|
+
contentBlockDelta: ContentBlockDeltaEvent
|
|
514
|
+
): Promise<void> => {
|
|
515
|
+
const delta = contentBlockDelta.delta;
|
|
516
|
+
if (delta == null) {
|
|
517
|
+
throw new Error('No delta found in content block.');
|
|
518
|
+
}
|
|
519
|
+
|
|
520
|
+
const idx = contentBlockDelta.contentBlockIndex;
|
|
316
521
|
if (idx != null) {
|
|
317
522
|
seenBlockIndices.add(idx);
|
|
318
523
|
}
|
|
319
524
|
|
|
320
|
-
|
|
321
|
-
|
|
322
|
-
|
|
323
|
-
|
|
324
|
-
|
|
325
|
-
|
|
326
|
-
|
|
327
|
-
|
|
328
|
-
|
|
329
|
-
|
|
330
|
-
|
|
331
|
-
|
|
332
|
-
|
|
333
|
-
|
|
334
|
-
|
|
335
|
-
|
|
336
|
-
if (
|
|
337
|
-
|
|
338
|
-
|
|
339
|
-
|
|
340
|
-
|
|
341
|
-
|
|
525
|
+
const text = delta.text;
|
|
526
|
+
const reasoningContent = delta.reasoningContent;
|
|
527
|
+
const reasoningText = reasoningContent?.text;
|
|
528
|
+
const visibleText =
|
|
529
|
+
typeof text === 'string'
|
|
530
|
+
? text
|
|
531
|
+
: typeof reasoningText === 'string'
|
|
532
|
+
? reasoningText
|
|
533
|
+
: '';
|
|
534
|
+
const smooth = this._lc_stream_delay > 0 && visibleText !== '';
|
|
535
|
+
const tokenChunks = smooth
|
|
536
|
+
? splitStreamToken(visibleText)
|
|
537
|
+
: [visibleText];
|
|
538
|
+
|
|
539
|
+
for (const token of tokenChunks) {
|
|
540
|
+
let splitDelta = contentBlockDelta;
|
|
541
|
+
if (typeof text === 'string') {
|
|
542
|
+
splitDelta = {
|
|
543
|
+
...contentBlockDelta,
|
|
544
|
+
delta: { text: token },
|
|
545
|
+
};
|
|
546
|
+
} else if (
|
|
547
|
+
typeof reasoningText === 'string' &&
|
|
548
|
+
reasoningContent != null
|
|
549
|
+
) {
|
|
550
|
+
splitDelta = {
|
|
551
|
+
...contentBlockDelta,
|
|
552
|
+
delta: {
|
|
553
|
+
reasoningContent: {
|
|
554
|
+
...reasoningContent,
|
|
555
|
+
text: token,
|
|
556
|
+
},
|
|
557
|
+
},
|
|
558
|
+
};
|
|
559
|
+
}
|
|
560
|
+
|
|
561
|
+
const deltaChunk = handleConverseStreamContentBlockDelta(splitDelta);
|
|
562
|
+
await enqueueChunk({
|
|
563
|
+
chunk: this.enrichChunk(deltaChunk, seenBlockIndices),
|
|
564
|
+
callbackChunk: deltaChunk,
|
|
565
|
+
callbackToken: deltaChunk.text,
|
|
566
|
+
smooth,
|
|
567
|
+
textLength: token.length,
|
|
568
|
+
});
|
|
569
|
+
}
|
|
570
|
+
};
|
|
571
|
+
|
|
572
|
+
const producer = (async (): Promise<void> => {
|
|
573
|
+
try {
|
|
574
|
+
for await (const event of stream) {
|
|
575
|
+
if (isSignalAborted(options.signal)) {
|
|
576
|
+
abortStream();
|
|
577
|
+
throw new Error('AbortError: User aborted the request.');
|
|
578
|
+
}
|
|
579
|
+
|
|
580
|
+
if (event.contentBlockStart != null) {
|
|
581
|
+
const startChunk = handleConverseStreamContentBlockStart(
|
|
582
|
+
event.contentBlockStart
|
|
583
|
+
);
|
|
584
|
+
if (startChunk != null) {
|
|
585
|
+
const idx = event.contentBlockStart.contentBlockIndex;
|
|
586
|
+
if (idx != null) {
|
|
587
|
+
seenBlockIndices.add(idx);
|
|
588
|
+
if (event.contentBlockStart.start?.toolUse != null) {
|
|
589
|
+
toolUseBlockIndices.add(idx);
|
|
590
|
+
}
|
|
591
|
+
}
|
|
592
|
+
await enqueueChunk({
|
|
593
|
+
chunk: this.enrichChunk(startChunk, seenBlockIndices),
|
|
594
|
+
callbackChunk: startChunk,
|
|
595
|
+
callbackToken: startChunk.text,
|
|
596
|
+
});
|
|
597
|
+
}
|
|
598
|
+
} else if (event.contentBlockDelta != null) {
|
|
599
|
+
await enqueueDelta(event.contentBlockDelta);
|
|
600
|
+
} else if (event.metadata != null) {
|
|
601
|
+
await enqueueChunk({
|
|
602
|
+
chunk: handleConverseStreamMetadata(event.metadata, {
|
|
603
|
+
streamUsage,
|
|
604
|
+
}),
|
|
605
|
+
});
|
|
606
|
+
} else if (event.contentBlockStop != null) {
|
|
607
|
+
const stopIdx = event.contentBlockStop.contentBlockIndex;
|
|
608
|
+
if (stopIdx != null) {
|
|
609
|
+
seenBlockIndices.add(stopIdx);
|
|
610
|
+
if (sealToolUseOnStop && toolUseBlockIndices.has(stopIdx)) {
|
|
611
|
+
const sealChunk = createConverseToolUseStopChunk(stopIdx);
|
|
612
|
+
await enqueueChunk({
|
|
613
|
+
chunk: sealChunk,
|
|
614
|
+
callbackChunk: sealChunk,
|
|
615
|
+
callbackToken: sealChunk.text,
|
|
616
|
+
});
|
|
617
|
+
}
|
|
618
|
+
}
|
|
619
|
+
} else {
|
|
620
|
+
await enqueueChunk({
|
|
621
|
+
chunk: new ChatGenerationChunk({
|
|
622
|
+
text: '',
|
|
623
|
+
message: new AIMessageChunk({
|
|
624
|
+
content: '',
|
|
625
|
+
response_metadata: { ...event } as ResponseMetadata,
|
|
626
|
+
}),
|
|
627
|
+
}),
|
|
628
|
+
});
|
|
629
|
+
}
|
|
630
|
+
}
|
|
631
|
+
} catch (error) {
|
|
632
|
+
producerState.error = error;
|
|
633
|
+
} finally {
|
|
634
|
+
producerState.done = true;
|
|
635
|
+
notifyConsumerForChunk();
|
|
636
|
+
}
|
|
637
|
+
})();
|
|
638
|
+
|
|
639
|
+
try {
|
|
640
|
+
let keepStreaming = true;
|
|
641
|
+
while (keepStreaming) {
|
|
642
|
+
if (isSignalAborted(options.signal)) {
|
|
643
|
+
abortStream();
|
|
644
|
+
throw new Error('AbortError: User aborted the request.');
|
|
645
|
+
}
|
|
646
|
+
|
|
647
|
+
await waitForNextChunk();
|
|
648
|
+
const queuedChunk = dequeue();
|
|
649
|
+
|
|
650
|
+
if (!queuedChunk) {
|
|
651
|
+
if (producerState.error != null) {
|
|
652
|
+
throw producerState.error;
|
|
653
|
+
}
|
|
654
|
+
if (producerState.done) {
|
|
655
|
+
keepStreaming = false;
|
|
656
|
+
}
|
|
657
|
+
continue;
|
|
658
|
+
}
|
|
659
|
+
|
|
660
|
+
if (queuedChunk.smooth) {
|
|
661
|
+
bufferedTextLength = Math.max(
|
|
662
|
+
0,
|
|
663
|
+
bufferedTextLength - queuedChunk.textLength
|
|
664
|
+
);
|
|
665
|
+
notifyProducerForSpace();
|
|
666
|
+
await waitForStreamDelay(
|
|
667
|
+
getCadencedStreamDelay({
|
|
668
|
+
targetDelay: hasEmittedVisibleContent
|
|
669
|
+
? this._lc_stream_delay
|
|
670
|
+
: 0,
|
|
671
|
+
lastVisibleContentAt,
|
|
672
|
+
now: Date.now(),
|
|
673
|
+
}),
|
|
674
|
+
options.signal
|
|
675
|
+
);
|
|
676
|
+
if (isSignalAborted(options.signal)) {
|
|
677
|
+
abortStream();
|
|
678
|
+
throw new Error('AbortError: User aborted the request.');
|
|
679
|
+
}
|
|
680
|
+
hasEmittedVisibleContent = true;
|
|
681
|
+
lastVisibleContentAt = Date.now();
|
|
682
|
+
} else {
|
|
683
|
+
notifyProducerForSpace();
|
|
684
|
+
}
|
|
685
|
+
|
|
686
|
+
yield queuedChunk.chunk;
|
|
687
|
+
|
|
688
|
+
if (queuedChunk.callbackChunk != null) {
|
|
342
689
|
await runManager?.handleLLMNewToken(
|
|
343
|
-
|
|
690
|
+
queuedChunk.callbackToken,
|
|
344
691
|
undefined,
|
|
345
692
|
undefined,
|
|
346
693
|
undefined,
|
|
347
694
|
undefined,
|
|
348
|
-
{ chunk:
|
|
695
|
+
{ chunk: queuedChunk.callbackChunk }
|
|
349
696
|
);
|
|
350
697
|
}
|
|
351
698
|
}
|
|
352
|
-
}
|
|
353
|
-
|
|
354
|
-
|
|
355
|
-
|
|
356
|
-
|
|
357
|
-
|
|
358
|
-
|
|
359
|
-
|
|
699
|
+
} finally {
|
|
700
|
+
consumerClosed = true;
|
|
701
|
+
if (!producerState.done) {
|
|
702
|
+
abortStream();
|
|
703
|
+
notifyProducerForSpace();
|
|
704
|
+
}
|
|
705
|
+
await producer;
|
|
706
|
+
}
|
|
707
|
+
} finally {
|
|
708
|
+
options.signal?.removeEventListener('abort', abortStream);
|
|
709
|
+
if (!streamAbortController.signal.aborted) {
|
|
710
|
+
streamAbortController.abort();
|
|
360
711
|
}
|
|
361
712
|
}
|
|
362
713
|
}
|
|
@@ -155,4 +155,101 @@ describe('Converse stream seal dispatch', () => {
|
|
|
155
155
|
expect(dispatched.some(hasSeal)).toBe(false);
|
|
156
156
|
expect(dispatched).toHaveLength(1);
|
|
157
157
|
});
|
|
158
|
+
|
|
159
|
+
test('paces visible text deltas with configured stream delay', async () => {
|
|
160
|
+
const setTimeoutSpy = jest.spyOn(globalThis, 'setTimeout');
|
|
161
|
+
|
|
162
|
+
try {
|
|
163
|
+
const { yielded, dispatched } = await runStream(
|
|
164
|
+
[
|
|
165
|
+
{
|
|
166
|
+
contentBlockDelta: {
|
|
167
|
+
contentBlockIndex: 0,
|
|
168
|
+
delta: { text: 'hello' },
|
|
169
|
+
},
|
|
170
|
+
},
|
|
171
|
+
{
|
|
172
|
+
contentBlockDelta: {
|
|
173
|
+
contentBlockIndex: 0,
|
|
174
|
+
delta: { text: ' world' },
|
|
175
|
+
},
|
|
176
|
+
},
|
|
177
|
+
],
|
|
178
|
+
{ _lc_stream_delay: 35 }
|
|
179
|
+
);
|
|
180
|
+
|
|
181
|
+
expect(yielded.map((m) => m.content)).toEqual(['hello', ' world']);
|
|
182
|
+
expect(dispatched.map((m) => m.content)).toEqual(['hello', ' world']);
|
|
183
|
+
|
|
184
|
+
const timeoutDelays = setTimeoutSpy.mock.calls
|
|
185
|
+
.map(([, delay]) => delay)
|
|
186
|
+
.filter((delay): delay is number => typeof delay === 'number');
|
|
187
|
+
expect(timeoutDelays.some((delay) => delay > 0 && delay <= 35)).toBe(
|
|
188
|
+
true
|
|
189
|
+
);
|
|
190
|
+
} finally {
|
|
191
|
+
setTimeoutSpy.mockRestore();
|
|
192
|
+
}
|
|
193
|
+
});
|
|
194
|
+
|
|
195
|
+
test('splits large text deltas at Anthropic stream boundaries', async () => {
|
|
196
|
+
const { yielded, dispatched } = await runStream(
|
|
197
|
+
[
|
|
198
|
+
{
|
|
199
|
+
contentBlockDelta: {
|
|
200
|
+
contentBlockIndex: 0,
|
|
201
|
+
delta: { text: 'alpha beta gamma' },
|
|
202
|
+
},
|
|
203
|
+
},
|
|
204
|
+
],
|
|
205
|
+
{ _lc_stream_delay: 1 }
|
|
206
|
+
);
|
|
207
|
+
|
|
208
|
+
expect(yielded.map((m) => m.content)).toEqual(['alpha ', 'beta ', 'gamma']);
|
|
209
|
+
expect(dispatched.map((m) => m.content)).toEqual([
|
|
210
|
+
'alpha ',
|
|
211
|
+
'beta ',
|
|
212
|
+
'gamma',
|
|
213
|
+
]);
|
|
214
|
+
});
|
|
215
|
+
|
|
216
|
+
test('paces visible reasoning deltas with configured stream delay', async () => {
|
|
217
|
+
const setTimeoutSpy = jest.spyOn(globalThis, 'setTimeout');
|
|
218
|
+
|
|
219
|
+
try {
|
|
220
|
+
const { yielded, dispatched } = await runStream(
|
|
221
|
+
[
|
|
222
|
+
{
|
|
223
|
+
contentBlockDelta: {
|
|
224
|
+
contentBlockIndex: 0,
|
|
225
|
+
delta: { reasoningContent: { text: 'One' } },
|
|
226
|
+
},
|
|
227
|
+
},
|
|
228
|
+
{
|
|
229
|
+
contentBlockDelta: {
|
|
230
|
+
contentBlockIndex: 0,
|
|
231
|
+
delta: { reasoningContent: { text: 'Two' } },
|
|
232
|
+
},
|
|
233
|
+
},
|
|
234
|
+
],
|
|
235
|
+
{ _lc_stream_delay: 35 }
|
|
236
|
+
);
|
|
237
|
+
|
|
238
|
+
expect(yielded.map((m) => m.additional_kwargs.reasoning_content)).toEqual(
|
|
239
|
+
['One', 'Two']
|
|
240
|
+
);
|
|
241
|
+
expect(
|
|
242
|
+
dispatched.map((m) => m.additional_kwargs.reasoning_content)
|
|
243
|
+
).toEqual(['One', 'Two']);
|
|
244
|
+
|
|
245
|
+
const timeoutDelays = setTimeoutSpy.mock.calls
|
|
246
|
+
.map(([, delay]) => delay)
|
|
247
|
+
.filter((delay): delay is number => typeof delay === 'number');
|
|
248
|
+
expect(timeoutDelays.some((delay) => delay > 0 && delay <= 35)).toBe(
|
|
249
|
+
true
|
|
250
|
+
);
|
|
251
|
+
} finally {
|
|
252
|
+
setTimeoutSpy.mockRestore();
|
|
253
|
+
}
|
|
254
|
+
});
|
|
158
255
|
});
|