@librechat/agents 3.3.10 → 3.3.12
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/graphs/Graph.cjs +248 -27
- package/dist/cjs/graphs/Graph.cjs.map +1 -1
- package/dist/cjs/langfuseToolOutputTracing.cjs +228 -16
- package/dist/cjs/langfuseToolOutputTracing.cjs.map +1 -1
- package/dist/cjs/llm/bedrock/index.cjs +13 -2
- package/dist/cjs/llm/bedrock/index.cjs.map +1 -1
- package/dist/cjs/llm/init.cjs +1 -1
- package/dist/cjs/llm/invoke.cjs +160 -43
- package/dist/cjs/llm/invoke.cjs.map +1 -1
- package/dist/cjs/llm/openai/index.cjs +190 -13
- package/dist/cjs/llm/openai/index.cjs.map +1 -1
- package/dist/cjs/llm/streamLimits.cjs +723 -0
- package/dist/cjs/llm/streamLimits.cjs.map +1 -0
- package/dist/cjs/main.cjs +12 -3
- package/dist/cjs/messages/core.cjs +592 -27
- package/dist/cjs/messages/core.cjs.map +1 -1
- package/dist/cjs/run.cjs +7 -3
- package/dist/cjs/run.cjs.map +1 -1
- package/dist/cjs/session/AgentSession.cjs +4 -1
- package/dist/cjs/session/AgentSession.cjs.map +1 -1
- package/dist/cjs/stream.cjs +105 -14
- package/dist/cjs/stream.cjs.map +1 -1
- package/dist/cjs/summarization/node.cjs +157 -37
- package/dist/cjs/summarization/node.cjs.map +1 -1
- package/dist/cjs/tools/BashExecutor.cjs +3 -2
- package/dist/cjs/tools/BashExecutor.cjs.map +1 -1
- package/dist/cjs/tools/CodeExecutor.cjs +4 -3
- package/dist/cjs/tools/CodeExecutor.cjs.map +1 -1
- package/dist/cjs/tools/ProgrammaticToolCalling.cjs +5 -3
- package/dist/cjs/tools/ProgrammaticToolCalling.cjs.map +1 -1
- package/dist/cjs/tools/ToolNode.cjs +77 -4
- package/dist/cjs/tools/ToolNode.cjs.map +1 -1
- package/dist/cjs/tools/ToolSearch.cjs +3 -2
- package/dist/cjs/tools/ToolSearch.cjs.map +1 -1
- package/dist/cjs/tools/search/crw-scraper.cjs +7 -1
- package/dist/cjs/tools/search/crw-scraper.cjs.map +1 -1
- package/dist/cjs/tools/search/crw-search.cjs +3 -1
- package/dist/cjs/tools/search/crw-search.cjs.map +1 -1
- package/dist/cjs/tools/search/firecrawl.cjs +7 -1
- package/dist/cjs/tools/search/firecrawl.cjs.map +1 -1
- package/dist/cjs/tools/search/keenable-scraper.cjs +7 -1
- package/dist/cjs/tools/search/keenable-scraper.cjs.map +1 -1
- package/dist/cjs/tools/search/keenable-search.cjs +3 -1
- package/dist/cjs/tools/search/keenable-search.cjs.map +1 -1
- package/dist/cjs/tools/search/rerankers.cjs +26 -8
- package/dist/cjs/tools/search/rerankers.cjs.map +1 -1
- package/dist/cjs/tools/search/search.cjs +30 -10
- package/dist/cjs/tools/search/search.cjs.map +1 -1
- package/dist/cjs/tools/search/serper-scraper.cjs +7 -1
- package/dist/cjs/tools/search/serper-scraper.cjs.map +1 -1
- package/dist/cjs/tools/search/tavily-scraper.cjs +7 -1
- package/dist/cjs/tools/search/tavily-scraper.cjs.map +1 -1
- package/dist/cjs/tools/search/tavily-search.cjs +3 -1
- package/dist/cjs/tools/search/tavily-search.cjs.map +1 -1
- package/dist/cjs/tools/search/tool.cjs +17 -3
- package/dist/cjs/tools/search/tool.cjs.map +1 -1
- package/dist/cjs/tools/subagent/SubagentExecutor.cjs +54 -3
- package/dist/cjs/tools/subagent/SubagentExecutor.cjs.map +1 -1
- package/dist/cjs/utils/index.cjs +2 -1
- package/dist/cjs/utils/misc.cjs +12 -0
- package/dist/cjs/utils/misc.cjs.map +1 -1
- package/dist/cjs/utils/proxy.cjs +63 -0
- package/dist/cjs/utils/proxy.cjs.map +1 -0
- package/dist/esm/graphs/Graph.mjs +247 -26
- package/dist/esm/graphs/Graph.mjs.map +1 -1
- package/dist/esm/langfuseToolOutputTracing.mjs +228 -16
- package/dist/esm/langfuseToolOutputTracing.mjs.map +1 -1
- package/dist/esm/llm/bedrock/index.mjs +13 -2
- package/dist/esm/llm/bedrock/index.mjs.map +1 -1
- package/dist/esm/llm/init.mjs +1 -1
- package/dist/esm/llm/invoke.mjs +160 -43
- package/dist/esm/llm/invoke.mjs.map +1 -1
- package/dist/esm/llm/openai/index.mjs +192 -15
- package/dist/esm/llm/openai/index.mjs.map +1 -1
- package/dist/esm/llm/streamLimits.mjs +704 -0
- package/dist/esm/llm/streamLimits.mjs.map +1 -0
- package/dist/esm/main.mjs +8 -6
- package/dist/esm/messages/core.mjs +592 -28
- package/dist/esm/messages/core.mjs.map +1 -1
- package/dist/esm/run.mjs +7 -3
- package/dist/esm/run.mjs.map +1 -1
- package/dist/esm/session/AgentSession.mjs +4 -1
- package/dist/esm/session/AgentSession.mjs.map +1 -1
- package/dist/esm/stream.mjs +105 -14
- package/dist/esm/stream.mjs.map +1 -1
- package/dist/esm/summarization/node.mjs +157 -37
- package/dist/esm/summarization/node.mjs.map +1 -1
- package/dist/esm/tools/BashExecutor.mjs +3 -2
- package/dist/esm/tools/BashExecutor.mjs.map +1 -1
- package/dist/esm/tools/CodeExecutor.mjs +4 -3
- package/dist/esm/tools/CodeExecutor.mjs.map +1 -1
- package/dist/esm/tools/ProgrammaticToolCalling.mjs +5 -3
- package/dist/esm/tools/ProgrammaticToolCalling.mjs.map +1 -1
- package/dist/esm/tools/ToolNode.mjs +77 -4
- package/dist/esm/tools/ToolNode.mjs.map +1 -1
- package/dist/esm/tools/ToolSearch.mjs +3 -2
- package/dist/esm/tools/ToolSearch.mjs.map +1 -1
- package/dist/esm/tools/search/crw-scraper.mjs +7 -1
- package/dist/esm/tools/search/crw-scraper.mjs.map +1 -1
- package/dist/esm/tools/search/crw-search.mjs +3 -1
- package/dist/esm/tools/search/crw-search.mjs.map +1 -1
- package/dist/esm/tools/search/firecrawl.mjs +7 -1
- package/dist/esm/tools/search/firecrawl.mjs.map +1 -1
- package/dist/esm/tools/search/keenable-scraper.mjs +7 -1
- package/dist/esm/tools/search/keenable-scraper.mjs.map +1 -1
- package/dist/esm/tools/search/keenable-search.mjs +3 -1
- package/dist/esm/tools/search/keenable-search.mjs.map +1 -1
- package/dist/esm/tools/search/rerankers.mjs +26 -8
- package/dist/esm/tools/search/rerankers.mjs.map +1 -1
- package/dist/esm/tools/search/search.mjs +30 -10
- package/dist/esm/tools/search/search.mjs.map +1 -1
- package/dist/esm/tools/search/serper-scraper.mjs +7 -1
- package/dist/esm/tools/search/serper-scraper.mjs.map +1 -1
- package/dist/esm/tools/search/tavily-scraper.mjs +7 -1
- package/dist/esm/tools/search/tavily-scraper.mjs.map +1 -1
- package/dist/esm/tools/search/tavily-search.mjs +3 -1
- package/dist/esm/tools/search/tavily-search.mjs.map +1 -1
- package/dist/esm/tools/search/tool.mjs +17 -3
- package/dist/esm/tools/search/tool.mjs.map +1 -1
- package/dist/esm/tools/subagent/SubagentExecutor.mjs +54 -3
- package/dist/esm/tools/subagent/SubagentExecutor.mjs.map +1 -1
- package/dist/esm/utils/index.mjs +2 -1
- package/dist/esm/utils/misc.mjs +12 -1
- package/dist/esm/utils/misc.mjs.map +1 -1
- package/dist/esm/utils/proxy.mjs +62 -0
- package/dist/esm/utils/proxy.mjs.map +1 -0
- package/dist/types/graphs/Graph.d.ts +53 -1
- package/dist/types/index.d.ts +2 -0
- package/dist/types/langfuseToolOutputTracing.d.ts +1 -0
- package/dist/types/llm/invoke.d.ts +26 -5
- package/dist/types/llm/openai/index.d.ts +3 -0
- package/dist/types/llm/streamLimits.d.ts +314 -0
- package/dist/types/messages/core.d.ts +11 -6
- package/dist/types/run.d.ts +1 -0
- package/dist/types/summarization/node.d.ts +27 -2
- package/dist/types/tools/BashExecutor.d.ts +2 -2
- package/dist/types/tools/CodeExecutor.d.ts +3 -3
- package/dist/types/tools/ToolNode.d.ts +11 -1
- package/dist/types/tools/search/crw-scraper.d.ts +2 -0
- package/dist/types/tools/search/firecrawl.d.ts +2 -0
- package/dist/types/tools/search/keenable-scraper.d.ts +2 -0
- package/dist/types/tools/search/rerankers.d.ts +9 -5
- package/dist/types/tools/search/serper-scraper.d.ts +2 -0
- package/dist/types/tools/search/tavily-scraper.d.ts +2 -0
- package/dist/types/tools/search/types.d.ts +28 -33
- package/dist/types/tools/subagent/SubagentExecutor.d.ts +44 -0
- package/dist/types/types/graph.d.ts +7 -1
- package/dist/types/types/run.d.ts +46 -1
- package/dist/types/types/tools.d.ts +21 -0
- package/dist/types/utils/index.d.ts +1 -0
- package/dist/types/utils/misc.d.ts +7 -0
- package/dist/types/utils/proxy.d.ts +31 -0
- package/package.json +2 -1
- package/src/__tests__/stream.eagerArgsDivergence.test.ts +158 -0
- package/src/__tests__/stream.eagerEventExecution.test.ts +1 -0
- package/src/__tests__/stream.streamLimits.test.ts +1982 -0
- package/src/graphs/Graph.ts +302 -27
- package/src/graphs/__tests__/Graph.breakerLifecycle.test.ts +234 -0
- package/src/index.ts +11 -0
- package/src/langfuseToolOutputTracing.ts +410 -14
- package/src/llm/bedrock/index.ts +22 -2
- package/src/llm/custom-chat-models.smoke.test.ts +747 -0
- package/src/llm/invoke.streamLimits.test.ts +142 -0
- package/src/llm/invoke.test.ts +187 -1
- package/src/llm/invoke.ts +231 -43
- package/src/llm/openai/cacheWriteTokens.test.ts +112 -0
- package/src/llm/openai/index.ts +348 -31
- package/src/llm/openai/llm.spec.ts +107 -6
- package/src/llm/streamLimits.test.ts +450 -0
- package/src/llm/streamLimits.ts +1158 -0
- package/src/messages/core.ts +1290 -42
- package/src/messages/formatAgentMessages.test.ts +2623 -0
- package/src/run.ts +4 -0
- package/src/session/AgentSession.ts +5 -0
- package/src/specs/langfuse-tool-output-tracing.test.ts +887 -0
- package/src/specs/preemptSeal.test.ts +374 -5
- package/src/stream.ts +141 -6
- package/src/summarization/__tests__/node.test.ts +269 -0
- package/src/summarization/chunkHandler.test.ts +196 -0
- package/src/summarization/node.ts +203 -6
- package/src/tools/BashExecutor.ts +4 -3
- package/src/tools/CodeExecutor.ts +5 -4
- package/src/tools/ProgrammaticToolCalling.ts +7 -5
- package/src/tools/ToolNode.ts +109 -6
- package/src/tools/ToolSearch.ts +4 -3
- package/src/tools/__tests__/BashExecutor.test.ts +2 -2
- package/src/tools/__tests__/ProgrammaticToolCalling.test.ts +2 -4
- package/src/tools/__tests__/SubagentExecutor.test.ts +126 -0
- package/src/tools/__tests__/ToolNode.breakerSignal.test.ts +389 -0
- package/src/tools/__tests__/ToolNode.streamLimits.test.ts +69 -0
- package/src/tools/search/crw-scraper.ts +6 -0
- package/src/tools/search/crw-search.ts +6 -1
- package/src/tools/search/firecrawl.ts +6 -0
- package/src/tools/search/http-agent.test.ts +133 -0
- package/src/tools/search/keenable-scraper.ts +6 -0
- package/src/tools/search/keenable-search.ts +6 -1
- package/src/tools/search/rerankers.ts +36 -10
- package/src/tools/search/search.ts +29 -11
- package/src/tools/search/serper-scraper.ts +6 -0
- package/src/tools/search/tavily-scraper.ts +6 -0
- package/src/tools/search/tavily-search.ts +2 -0
- package/src/tools/search/tool.ts +16 -0
- package/src/tools/search/types.ts +31 -33
- package/src/tools/subagent/SubagentExecutor.ts +96 -3
- package/src/types/graph.ts +7 -0
- package/src/types/run.ts +49 -1
- package/src/types/tools.ts +21 -0
- package/src/utils/index.ts +1 -0
- package/src/utils/misc.ts +19 -0
- package/src/utils/proxy.test.ts +176 -0
- package/src/utils/proxy.ts +93 -0
|
@@ -6,14 +6,21 @@
|
|
|
6
6
|
* the dispatch-synchronous loop in `attemptInvoke` (no registered
|
|
7
7
|
* CHAT_MODEL_STREAM handler), which is the only loop allowed to seal.
|
|
8
8
|
*/
|
|
9
|
-
import { HumanMessage } from '@langchain/core/messages';
|
|
10
|
-
import type { BaseMessage } from '@langchain/core/messages';
|
|
11
9
|
import { RunnableBinding } from '@langchain/core/runnables';
|
|
10
|
+
import { AIMessageChunk, HumanMessage } from '@langchain/core/messages';
|
|
11
|
+
import {
|
|
12
|
+
type OpenAIClient,
|
|
13
|
+
convertMessagesToResponsesInput,
|
|
14
|
+
convertResponsesDeltaToChatGenerationChunk,
|
|
15
|
+
} from '@langchain/openai';
|
|
16
|
+
import type { ChatGeneration, LLMResult } from '@langchain/core/outputs';
|
|
17
|
+
import type { BaseMessage } from '@langchain/core/messages';
|
|
18
|
+
import type { HookCallback } from '@/hooks/types';
|
|
12
19
|
import type * as t from '@/types';
|
|
13
|
-
import { Providers } from '@/common';
|
|
14
20
|
import { HookRegistry } from '@/hooks/HookRegistry';
|
|
15
|
-
import type { HookCallback } from '@/hooks/types';
|
|
16
21
|
import { FakeChatModel } from '@/llm/fake';
|
|
22
|
+
import { ChatOpenAI } from '@/llm/openai';
|
|
23
|
+
import { Providers } from '@/common';
|
|
17
24
|
import { Run } from '@/run';
|
|
18
25
|
|
|
19
26
|
const FULL_RESPONSE = 'Alpha beta gamma delta epsilon zeta';
|
|
@@ -89,6 +96,133 @@ class CountingChatModel extends FakeChatModel {
|
|
|
89
96
|
}
|
|
90
97
|
}
|
|
91
98
|
|
|
99
|
+
class ResponsesReasoningChatModel extends FakeChatModel {
|
|
100
|
+
invocations: BaseMessage[][] = [];
|
|
101
|
+
includeServerToolResult = false;
|
|
102
|
+
|
|
103
|
+
_useResponsesApi(): boolean {
|
|
104
|
+
return true;
|
|
105
|
+
}
|
|
106
|
+
|
|
107
|
+
override async *_streamResponseChunks(
|
|
108
|
+
...args: Parameters<FakeChatModel['_streamResponseChunks']>
|
|
109
|
+
): ReturnType<FakeChatModel['_streamResponseChunks']> {
|
|
110
|
+
const [messages] = args;
|
|
111
|
+
this.invocations.push(messages);
|
|
112
|
+
if (this.invocations.length !== 1) {
|
|
113
|
+
yield* super._streamResponseChunks(...args);
|
|
114
|
+
return;
|
|
115
|
+
}
|
|
116
|
+
|
|
117
|
+
const outputOffset = this.includeServerToolResult ? 1 : 0;
|
|
118
|
+
const events: Parameters<
|
|
119
|
+
typeof convertResponsesDeltaToChatGenerationChunk
|
|
120
|
+
>[0][] = [
|
|
121
|
+
{
|
|
122
|
+
type: 'response.created',
|
|
123
|
+
sequence_number: 0,
|
|
124
|
+
response: {
|
|
125
|
+
id: 'resp_interrupted',
|
|
126
|
+
created_at: 0,
|
|
127
|
+
output_text: '',
|
|
128
|
+
error: null,
|
|
129
|
+
incomplete_details: null,
|
|
130
|
+
instructions: null,
|
|
131
|
+
metadata: null,
|
|
132
|
+
model: 'gpt-5.6',
|
|
133
|
+
object: 'response',
|
|
134
|
+
output: [],
|
|
135
|
+
parallel_tool_calls: true,
|
|
136
|
+
temperature: null,
|
|
137
|
+
tool_choice: 'auto',
|
|
138
|
+
tools: [],
|
|
139
|
+
top_p: null,
|
|
140
|
+
status: 'in_progress',
|
|
141
|
+
},
|
|
142
|
+
},
|
|
143
|
+
...(this.includeServerToolResult
|
|
144
|
+
? [
|
|
145
|
+
{
|
|
146
|
+
type: 'response.output_item.done' as const,
|
|
147
|
+
sequence_number: 1,
|
|
148
|
+
output_index: 0,
|
|
149
|
+
item: {
|
|
150
|
+
id: 'ci_interrupted',
|
|
151
|
+
type: 'code_interpreter_call' as const,
|
|
152
|
+
status: 'completed' as const,
|
|
153
|
+
code: 'print("server result")',
|
|
154
|
+
container_id: 'container_interrupted',
|
|
155
|
+
outputs: [
|
|
156
|
+
{ type: 'logs' as const, logs: 'server result' },
|
|
157
|
+
{
|
|
158
|
+
type: 'image' as const,
|
|
159
|
+
url: 'https://example.com/ephemeral-chart.png',
|
|
160
|
+
},
|
|
161
|
+
],
|
|
162
|
+
},
|
|
163
|
+
},
|
|
164
|
+
]
|
|
165
|
+
: []),
|
|
166
|
+
{
|
|
167
|
+
type: 'response.output_item.added',
|
|
168
|
+
sequence_number: 1 + outputOffset,
|
|
169
|
+
output_index: outputOffset,
|
|
170
|
+
item: {
|
|
171
|
+
id: 'rs_interrupted',
|
|
172
|
+
type: 'reasoning',
|
|
173
|
+
status: 'in_progress',
|
|
174
|
+
summary: [],
|
|
175
|
+
},
|
|
176
|
+
},
|
|
177
|
+
{
|
|
178
|
+
type: 'response.output_item.done',
|
|
179
|
+
sequence_number: 2 + outputOffset,
|
|
180
|
+
output_index: outputOffset,
|
|
181
|
+
item: {
|
|
182
|
+
id: 'rs_interrupted',
|
|
183
|
+
type: 'reasoning',
|
|
184
|
+
status: 'completed',
|
|
185
|
+
summary: [],
|
|
186
|
+
encrypted_content: 'encrypted-reasoning',
|
|
187
|
+
},
|
|
188
|
+
},
|
|
189
|
+
{
|
|
190
|
+
type: 'response.output_item.added',
|
|
191
|
+
sequence_number: 3 + outputOffset,
|
|
192
|
+
output_index: 1 + outputOffset,
|
|
193
|
+
item: {
|
|
194
|
+
id: 'msg_interrupted',
|
|
195
|
+
type: 'message',
|
|
196
|
+
role: 'assistant',
|
|
197
|
+
status: 'in_progress',
|
|
198
|
+
content: [],
|
|
199
|
+
},
|
|
200
|
+
},
|
|
201
|
+
{
|
|
202
|
+
type: 'response.output_text.delta',
|
|
203
|
+
sequence_number: 4 + outputOffset,
|
|
204
|
+
output_index: 1 + outputOffset,
|
|
205
|
+
content_index: 0,
|
|
206
|
+
item_id: 'msg_interrupted',
|
|
207
|
+
delta: 'Partial answer.',
|
|
208
|
+
logprobs: [],
|
|
209
|
+
},
|
|
210
|
+
];
|
|
211
|
+
for (const event of events) {
|
|
212
|
+
const chunk = convertResponsesDeltaToChatGenerationChunk(event);
|
|
213
|
+
if (chunk != null) {
|
|
214
|
+
yield chunk;
|
|
215
|
+
}
|
|
216
|
+
}
|
|
217
|
+
}
|
|
218
|
+
}
|
|
219
|
+
|
|
220
|
+
type StreamingResponsesDelegate = {
|
|
221
|
+
completionWithRetry: (
|
|
222
|
+
request: OpenAIClient.Responses.ResponseCreateParamsStreaming
|
|
223
|
+
) => Promise<AsyncIterable<OpenAIClient.Responses.ResponseStreamEvent>>;
|
|
224
|
+
};
|
|
225
|
+
|
|
92
226
|
const aiContents = (messages: BaseMessage[]): string[] =>
|
|
93
227
|
messages
|
|
94
228
|
.filter((message) => message.getType() === 'ai')
|
|
@@ -204,6 +338,43 @@ describe('cooperative seal (end-to-end via Run)', () => {
|
|
|
204
338
|
expect(ends).toBe(2);
|
|
205
339
|
});
|
|
206
340
|
|
|
341
|
+
it('preserves the preempted marker through constructor-kwargs rehydration', async () => {
|
|
342
|
+
let sealedChunk: AIMessageChunk | undefined;
|
|
343
|
+
const run = await createSealRun({
|
|
344
|
+
runId: 'seal-serialized-preempted-marker',
|
|
345
|
+
hook: async () => ({ preventContinuation: true }),
|
|
346
|
+
responses: [FULL_RESPONSE],
|
|
347
|
+
modelCallbacks: [
|
|
348
|
+
{
|
|
349
|
+
handleLLMEnd(output: LLMResult): void {
|
|
350
|
+
const generation = output.generations[0]?.[0] as
|
|
351
|
+
| ChatGeneration
|
|
352
|
+
| undefined;
|
|
353
|
+
const message = generation?.message;
|
|
354
|
+
if (
|
|
355
|
+
AIMessageChunk.isInstance(message) &&
|
|
356
|
+
message.response_metadata.preempted === true
|
|
357
|
+
) {
|
|
358
|
+
sealedChunk = message;
|
|
359
|
+
}
|
|
360
|
+
},
|
|
361
|
+
},
|
|
362
|
+
],
|
|
363
|
+
});
|
|
364
|
+
|
|
365
|
+
await run.processStream(
|
|
366
|
+
{ messages: [new HumanMessage('hello there')] },
|
|
367
|
+
streamConfig
|
|
368
|
+
);
|
|
369
|
+
|
|
370
|
+
expect(sealedChunk).toBeDefined();
|
|
371
|
+
const serializedFields = JSON.parse(
|
|
372
|
+
JSON.stringify(sealedChunk!.lc_kwargs)
|
|
373
|
+
) as ConstructorParameters<typeof AIMessageChunk>[0];
|
|
374
|
+
const rehydrated = new AIMessageChunk(serializedFields);
|
|
375
|
+
expect(rehydrated.response_metadata.preempted).toBe(true);
|
|
376
|
+
});
|
|
377
|
+
|
|
207
378
|
it('a halting boundary stops multi-agent successors, not just the sealed subgraph', async () => {
|
|
208
379
|
const registry = new HookRegistry();
|
|
209
380
|
registry.register('PreemptBoundary', {
|
|
@@ -272,7 +443,11 @@ describe('cooperative seal (end-to-end via Run)', () => {
|
|
|
272
443
|
runId: 'seal-inject-resume',
|
|
273
444
|
hook: async () => ({
|
|
274
445
|
injectedMessages: [
|
|
275
|
-
{
|
|
446
|
+
{
|
|
447
|
+
role: 'user' as const,
|
|
448
|
+
content: 'Make it shorter.',
|
|
449
|
+
source: 'steer',
|
|
450
|
+
},
|
|
276
451
|
],
|
|
277
452
|
}),
|
|
278
453
|
responses: [FULL_RESPONSE, RESUMED_RESPONSE],
|
|
@@ -306,4 +481,198 @@ describe('cooperative seal (end-to-end via Run)', () => {
|
|
|
306
481
|
expect(contents[0].length).toBeLessThan(FULL_RESPONSE.length);
|
|
307
482
|
expect(contents[1]).toBe(RESUMED_RESPONSE);
|
|
308
483
|
});
|
|
484
|
+
|
|
485
|
+
it.each(['v0', 'v1'] as const)(
|
|
486
|
+
'does not replay interrupted OpenAI Responses item ids on %s resume',
|
|
487
|
+
async (outputVersion) => {
|
|
488
|
+
const run = await createSealRun({
|
|
489
|
+
runId: `seal-openai-responses-reasoning-${outputVersion}`,
|
|
490
|
+
hook: async () => ({
|
|
491
|
+
injectedMessages: [
|
|
492
|
+
{ role: 'user' as const, content: 'Go on.', source: 'steer' },
|
|
493
|
+
],
|
|
494
|
+
}),
|
|
495
|
+
responses: [],
|
|
496
|
+
});
|
|
497
|
+
const model = new ResponsesReasoningChatModel({
|
|
498
|
+
responses: [RESUMED_RESPONSE],
|
|
499
|
+
});
|
|
500
|
+
model.outputVersion = outputVersion;
|
|
501
|
+
run.Graph!.overrideModel = model;
|
|
502
|
+
|
|
503
|
+
await run.processStream(
|
|
504
|
+
{ messages: [new HumanMessage('hello there')] },
|
|
505
|
+
streamConfig
|
|
506
|
+
);
|
|
507
|
+
|
|
508
|
+
expect(model.invocations).toHaveLength(2);
|
|
509
|
+
const sealedMessage = model.invocations[1].find(
|
|
510
|
+
(message) => message.getType() === 'ai'
|
|
511
|
+
);
|
|
512
|
+
expect(sealedMessage).toBeDefined();
|
|
513
|
+
expect(sealedMessage?.text).toBe('Partial answer.');
|
|
514
|
+
expect(sealedMessage?.response_metadata).not.toHaveProperty('id');
|
|
515
|
+
const actualOutputVersion = (
|
|
516
|
+
sealedMessage?.response_metadata as { output_version?: unknown }
|
|
517
|
+
).output_version;
|
|
518
|
+
expect(actualOutputVersion).toBe(
|
|
519
|
+
outputVersion === 'v1' ? 'v1' : undefined
|
|
520
|
+
);
|
|
521
|
+
|
|
522
|
+
const providerInput = convertMessagesToResponsesInput({
|
|
523
|
+
messages: model.invocations[1],
|
|
524
|
+
model: 'gpt-5.6',
|
|
525
|
+
zdrEnabled: false,
|
|
526
|
+
});
|
|
527
|
+
const unsafeReasoning = providerInput.find(
|
|
528
|
+
(item) =>
|
|
529
|
+
item.type === 'reasoning' &&
|
|
530
|
+
item.id === 'rs_interrupted' &&
|
|
531
|
+
(typeof item.encrypted_content !== 'string' ||
|
|
532
|
+
item.encrypted_content.length === 0)
|
|
533
|
+
);
|
|
534
|
+
expect(unsafeReasoning).toBeUndefined();
|
|
535
|
+
expect(JSON.stringify(providerInput)).toContain('Partial answer.');
|
|
536
|
+
}
|
|
537
|
+
);
|
|
538
|
+
|
|
539
|
+
it.each(['v0', 'v1'] as const)(
|
|
540
|
+
'preserves completed Responses server results on %s resume without ids',
|
|
541
|
+
async (outputVersion) => {
|
|
542
|
+
const run = await createSealRun({
|
|
543
|
+
runId: `seal-openai-responses-server-result-${outputVersion}`,
|
|
544
|
+
hook: async () => ({
|
|
545
|
+
injectedMessages: [
|
|
546
|
+
{ role: 'user' as const, content: 'Go on.', source: 'steer' },
|
|
547
|
+
],
|
|
548
|
+
}),
|
|
549
|
+
responses: [],
|
|
550
|
+
});
|
|
551
|
+
const model = new ResponsesReasoningChatModel({
|
|
552
|
+
responses: [RESUMED_RESPONSE],
|
|
553
|
+
});
|
|
554
|
+
model.outputVersion = outputVersion;
|
|
555
|
+
model.includeServerToolResult = true;
|
|
556
|
+
run.Graph!.overrideModel = model;
|
|
557
|
+
|
|
558
|
+
await run.processStream(
|
|
559
|
+
{ messages: [new HumanMessage('hello there')] },
|
|
560
|
+
streamConfig
|
|
561
|
+
);
|
|
562
|
+
|
|
563
|
+
expect(model.invocations).toHaveLength(2);
|
|
564
|
+
const sealedMessage = model.invocations[1].find(
|
|
565
|
+
(message) => message.getType() === 'ai'
|
|
566
|
+
);
|
|
567
|
+
expect(sealedMessage).toBeDefined();
|
|
568
|
+
expect(sealedMessage?.text).toContain('Partial answer.');
|
|
569
|
+
expect(sealedMessage?.text).toContain('server result');
|
|
570
|
+
expect(sealedMessage?.text).toContain('ephemeral-chart.png');
|
|
571
|
+
expect(
|
|
572
|
+
(sealedMessage?.response_metadata as { output_version?: unknown })
|
|
573
|
+
.output_version
|
|
574
|
+
).toBe('v1');
|
|
575
|
+
|
|
576
|
+
const providerInput = convertMessagesToResponsesInput({
|
|
577
|
+
messages: model.invocations[1],
|
|
578
|
+
model: 'gpt-5.6',
|
|
579
|
+
zdrEnabled: false,
|
|
580
|
+
});
|
|
581
|
+
const serializedProviderInput = JSON.stringify(providerInput);
|
|
582
|
+
expect(serializedProviderInput).toContain('server result');
|
|
583
|
+
expect(serializedProviderInput).toContain('ephemeral-chart.png');
|
|
584
|
+
expect(serializedProviderInput).not.toContain('ci_interrupted');
|
|
585
|
+
expect(serializedProviderInput).not.toContain('code_interpreter_call');
|
|
586
|
+
expect(serializedProviderInput).not.toContain('function_call_output');
|
|
587
|
+
}
|
|
588
|
+
);
|
|
589
|
+
|
|
590
|
+
it('preserves dropped raw Responses results through a real model resume', async () => {
|
|
591
|
+
const run = await createSealRun({
|
|
592
|
+
runId: 'seal-openai-responses-raw-result',
|
|
593
|
+
hook: async () => ({
|
|
594
|
+
injectedMessages: [
|
|
595
|
+
{ role: 'user' as const, content: 'Go on.', source: 'steer' },
|
|
596
|
+
],
|
|
597
|
+
}),
|
|
598
|
+
responses: [],
|
|
599
|
+
});
|
|
600
|
+
const model = new ChatOpenAI({
|
|
601
|
+
model: 'gpt-5.6',
|
|
602
|
+
apiKey: 'test-key',
|
|
603
|
+
useResponsesApi: true,
|
|
604
|
+
});
|
|
605
|
+
const responses = (
|
|
606
|
+
model as unknown as { responses: StreamingResponsesDelegate }
|
|
607
|
+
).responses;
|
|
608
|
+
const requests: OpenAIClient.Responses.ResponseCreateParamsStreaming[] = [];
|
|
609
|
+
responses.completionWithRetry = async (request) => {
|
|
610
|
+
requests.push(request);
|
|
611
|
+
const invocation = requests.length;
|
|
612
|
+
return (async function* () {
|
|
613
|
+
if (invocation === 1) {
|
|
614
|
+
yield {
|
|
615
|
+
type: 'response.output_item.done',
|
|
616
|
+
sequence_number: 0,
|
|
617
|
+
output_index: 0,
|
|
618
|
+
item: {
|
|
619
|
+
id: 'local_output_item',
|
|
620
|
+
type: 'local_shell_call_output',
|
|
621
|
+
status: 'completed',
|
|
622
|
+
output: 'local shell result',
|
|
623
|
+
},
|
|
624
|
+
} as OpenAIClient.Responses.ResponseStreamEvent;
|
|
625
|
+
yield {
|
|
626
|
+
type: 'response.output_item.added',
|
|
627
|
+
sequence_number: 1,
|
|
628
|
+
output_index: 1,
|
|
629
|
+
item: {
|
|
630
|
+
id: 'rs_interrupted',
|
|
631
|
+
type: 'reasoning',
|
|
632
|
+
status: 'in_progress',
|
|
633
|
+
summary: [],
|
|
634
|
+
},
|
|
635
|
+
} as OpenAIClient.Responses.ResponseStreamEvent;
|
|
636
|
+
yield {
|
|
637
|
+
type: 'response.output_text.delta',
|
|
638
|
+
sequence_number: 2,
|
|
639
|
+
output_index: 2,
|
|
640
|
+
content_index: 0,
|
|
641
|
+
item_id: 'msg_interrupted',
|
|
642
|
+
delta: 'Partial answer.',
|
|
643
|
+
logprobs: [],
|
|
644
|
+
} as OpenAIClient.Responses.ResponseStreamEvent;
|
|
645
|
+
return;
|
|
646
|
+
}
|
|
647
|
+
yield {
|
|
648
|
+
type: 'response.output_text.delta',
|
|
649
|
+
sequence_number: 0,
|
|
650
|
+
output_index: 0,
|
|
651
|
+
content_index: 0,
|
|
652
|
+
item_id: 'msg_resumed',
|
|
653
|
+
delta: RESUMED_RESPONSE,
|
|
654
|
+
logprobs: [],
|
|
655
|
+
} as OpenAIClient.Responses.ResponseStreamEvent;
|
|
656
|
+
})();
|
|
657
|
+
};
|
|
658
|
+
run.Graph!.overrideModel = model;
|
|
659
|
+
|
|
660
|
+
await run.processStream(
|
|
661
|
+
{ messages: [new HumanMessage('hello there')] },
|
|
662
|
+
streamConfig
|
|
663
|
+
);
|
|
664
|
+
|
|
665
|
+
expect(requests).toHaveLength(2);
|
|
666
|
+
const resumedInput = JSON.stringify(requests[1].input);
|
|
667
|
+
expect(resumedInput).toContain('Partial answer.');
|
|
668
|
+
expect(resumedInput).toContain('local shell result');
|
|
669
|
+
expect(resumedInput).toContain('serverToolResult');
|
|
670
|
+
expect(resumedInput.indexOf('local shell result')).toBeLessThan(
|
|
671
|
+
resumedInput.indexOf('Partial answer.')
|
|
672
|
+
);
|
|
673
|
+
expect(resumedInput).not.toContain('local_output_item');
|
|
674
|
+
expect(resumedInput).not.toContain('local_shell_call_output');
|
|
675
|
+
expect(resumedInput).not.toContain('rs_interrupted');
|
|
676
|
+
expect(run.getHaltReason()).toBeUndefined();
|
|
677
|
+
});
|
|
309
678
|
});
|
package/src/stream.ts
CHANGED
|
@@ -39,9 +39,21 @@ import {
|
|
|
39
39
|
calculateMaxToolResultChars,
|
|
40
40
|
truncateToolResultContent,
|
|
41
41
|
} from '@/utils/truncation';
|
|
42
|
+
import type { RunBreakerScope } from '@/llm/streamLimits';
|
|
43
|
+
import {
|
|
44
|
+
claimStreamLimitCharge,
|
|
45
|
+
combineCompleteToolCalls,
|
|
46
|
+
enforceCompleteToolCallArgLimit,
|
|
47
|
+
enforceStreamedToolCallArgLimit,
|
|
48
|
+
enforceStreamDeltaEventLimit,
|
|
49
|
+
requiresStreamLimitAccounting,
|
|
50
|
+
StreamLimitExceededError,
|
|
51
|
+
STREAM_LIMIT_EPOCH_KEY,
|
|
52
|
+
} from '@/llm/streamLimits';
|
|
42
53
|
import { resolveToolOutcome, outcomeFieldsFromResult } from '@/tools/intentArg';
|
|
43
54
|
import { TOOL_OUTPUT_REF_PATTERN } from '@/tools/toolOutputReferences';
|
|
44
55
|
import { safeDispatchCustomEvent } from '@/utils/events';
|
|
56
|
+
import { composeAbortSignals } from '@/utils/misc';
|
|
45
57
|
import { isGoogleLike } from '@/utils/llm';
|
|
46
58
|
import { getMessageId } from '@/messages';
|
|
47
59
|
|
|
@@ -166,7 +178,10 @@ function isEagerExecutionExcludedTool(
|
|
|
166
178
|
// the final request ("changed after eager execution started"), stop
|
|
167
179
|
// prestarting it so the model's retry executes normally instead of
|
|
168
180
|
// re-diverging in a loop (LibreChat#14371).
|
|
169
|
-
if (
|
|
181
|
+
if (
|
|
182
|
+
(graph.eagerEventToolSuppressions as Set<string> | undefined)?.has(name) ===
|
|
183
|
+
true
|
|
184
|
+
) {
|
|
170
185
|
return true;
|
|
171
186
|
}
|
|
172
187
|
// A code-session participant writes to the shared sandbox, so it is
|
|
@@ -778,6 +793,10 @@ function startEagerToolExecutions(args: {
|
|
|
778
793
|
| Record<string, unknown>
|
|
779
794
|
| undefined,
|
|
780
795
|
metadata,
|
|
796
|
+
signal: composeAbortSignals(
|
|
797
|
+
graph.config?.signal,
|
|
798
|
+
graph.breakerAbort.signal
|
|
799
|
+
),
|
|
781
800
|
resolve: (results): void => {
|
|
782
801
|
resultSettled = true;
|
|
783
802
|
settledResults = results;
|
|
@@ -1069,9 +1088,8 @@ function recordEagerToolCallChunks(args: {
|
|
|
1069
1088
|
const sealCoversChunk =
|
|
1070
1089
|
seal != null &&
|
|
1071
1090
|
(seal.kind === 'all' ||
|
|
1072
|
-
(seal.
|
|
1073
|
-
|
|
1074
|
-
(seal.index != null && seal.index === index))));
|
|
1091
|
+
(seal.id != null && seal.id === id) ||
|
|
1092
|
+
(seal.index != null && seal.index === index));
|
|
1075
1093
|
const next = {
|
|
1076
1094
|
id,
|
|
1077
1095
|
name,
|
|
@@ -1512,10 +1530,119 @@ export class ChatModelStreamHandler implements t.EventHandler {
|
|
|
1512
1530
|
return;
|
|
1513
1531
|
}
|
|
1514
1532
|
|
|
1515
|
-
const agentContext = graph.getAgentContext(metadata);
|
|
1516
|
-
|
|
1517
1533
|
const chunk = data.chunk as Partial<AIMessageChunk>;
|
|
1518
1534
|
|
|
1535
|
+
/** Attempts stamp their breaker epoch into event metadata; a mismatch
|
|
1536
|
+
* marks a straggling chunk from a failed run that outlived
|
|
1537
|
+
* `resetValues()`. Dropped OUTRIGHT: content handling and the eager
|
|
1538
|
+
* paths below compose the LIVE controller, so acting on a dead run's
|
|
1539
|
+
* chunk could dispatch host tools into the run now using it. Events
|
|
1540
|
+
* without a stamp (direct handler callers, partial stubs) keep the
|
|
1541
|
+
* live-controller behavior. */
|
|
1542
|
+
const eventEpoch = metadata?.[STREAM_LIMIT_EPOCH_KEY];
|
|
1543
|
+
/** Runtime-honest widening: partial handler stubs carry neither an
|
|
1544
|
+
* epoch nor a run scope despite the field types. */
|
|
1545
|
+
const liveEpoch = graph.breakerEpoch as number | undefined;
|
|
1546
|
+
if (eventEpoch != null && liveEpoch != null && eventEpoch !== liveEpoch) {
|
|
1547
|
+
return;
|
|
1548
|
+
}
|
|
1549
|
+
const eventBreaker =
|
|
1550
|
+
graph.breakerAbort instanceof AbortController
|
|
1551
|
+
? graph.breakerAbort
|
|
1552
|
+
: undefined;
|
|
1553
|
+
/** Immutable scope captured at handler entry. A reset while this
|
|
1554
|
+
* handler is suspended in an await replaces the object, so ONE
|
|
1555
|
+
* reference comparison proves the event still belongs to the live run
|
|
1556
|
+
* before anything composes `graph.breakerAbort` or `graph.config`. */
|
|
1557
|
+
const entryRunScope = graph.runScope as RunBreakerScope | undefined;
|
|
1558
|
+
const runScopeInvalidated = (): boolean =>
|
|
1559
|
+
entryRunScope != null && graph.runScope !== entryRunScope;
|
|
1560
|
+
const throwIfRunBreakerTripped = (): void => {
|
|
1561
|
+
if (
|
|
1562
|
+
eventBreaker != null &&
|
|
1563
|
+
eventBreaker.signal.aborted &&
|
|
1564
|
+
eventBreaker.signal.reason instanceof StreamLimitExceededError
|
|
1565
|
+
) {
|
|
1566
|
+
throw eventBreaker.signal.reason;
|
|
1567
|
+
}
|
|
1568
|
+
};
|
|
1569
|
+
|
|
1570
|
+
/**
|
|
1571
|
+
* Enforced before every content-specific early return below
|
|
1572
|
+
* (server-tool results, deferred mixed reasoning, late OpenRouter
|
|
1573
|
+
* reasoning): a looping provider can flood through any of those paths,
|
|
1574
|
+
* a coalesced event can carry client `tool_call_chunks` alongside a
|
|
1575
|
+
* server-tool result, and the complete-call dispatch branch further
|
|
1576
|
+
* down can prestart a side-effecting tool from an arrival-sealed
|
|
1577
|
+
* oversized call. Charging is claim-based: the producer loop and this
|
|
1578
|
+
* decoupled echo can observe the same chunk object in either order, and
|
|
1579
|
+
* only the first claimer charges it. The argument guard is
|
|
1580
|
+
* deliberately NOT gated on numeric chunk indices, so id-only or
|
|
1581
|
+
* index-less runaway streams stay bounded, and complete parsed
|
|
1582
|
+
* `tool_calls` without a raw chunk representation are judged standalone.
|
|
1583
|
+
*/
|
|
1584
|
+
if (
|
|
1585
|
+
requiresStreamLimitAccounting(graph, chunk) &&
|
|
1586
|
+
claimStreamLimitCharge(graph, data.chunk, 'consumer', metadata)
|
|
1587
|
+
) {
|
|
1588
|
+
try {
|
|
1589
|
+
enforceStreamDeltaEventLimit({ graph, metadata });
|
|
1590
|
+
/** Combined first so raw-chunk name correlation sees invalid calls
|
|
1591
|
+
* too; an unnamed raw chunk twinned with a named invalid call must
|
|
1592
|
+
* select that tool's override, not the global cap. */
|
|
1593
|
+
const completeCalls = combineCompleteToolCalls(chunk);
|
|
1594
|
+
if (chunk.tool_call_chunks && chunk.tool_call_chunks.length > 0) {
|
|
1595
|
+
enforceStreamedToolCallArgLimit({
|
|
1596
|
+
graph,
|
|
1597
|
+
metadata,
|
|
1598
|
+
toolCallChunks: chunk.tool_call_chunks,
|
|
1599
|
+
responseMetadata: chunk.response_metadata as
|
|
1600
|
+
| Record<string, unknown>
|
|
1601
|
+
| undefined,
|
|
1602
|
+
parsedToolCalls: completeCalls,
|
|
1603
|
+
});
|
|
1604
|
+
}
|
|
1605
|
+
/** Judged whenever parsed calls are present, not only when raw
|
|
1606
|
+
* chunks are absent — an adapter can pair an empty or partial raw
|
|
1607
|
+
* chunk with a complete parsed call; the standalone check is
|
|
1608
|
+
* stateless, so the common both-present case is not double-tallied.
|
|
1609
|
+
* Invalid calls are included because ToolNode processes and
|
|
1610
|
+
* promotes them. */
|
|
1611
|
+
if (completeCalls != null) {
|
|
1612
|
+
enforceCompleteToolCallArgLimit({
|
|
1613
|
+
graph,
|
|
1614
|
+
metadata,
|
|
1615
|
+
toolCalls: completeCalls,
|
|
1616
|
+
});
|
|
1617
|
+
}
|
|
1618
|
+
} catch (error) {
|
|
1619
|
+
/** A breach detected on this consumer path must still stop parallel
|
|
1620
|
+
* fan-out work: the producer skips its own enforcement once this
|
|
1621
|
+
* side has claimed the emission, so createCallModel's breaker-abort
|
|
1622
|
+
* never fires for it. Trip the EVENT's run-bound breaker before the
|
|
1623
|
+
* throw rejects the run — never the live controller of a newer run. */
|
|
1624
|
+
if (error instanceof StreamLimitExceededError && eventBreaker != null) {
|
|
1625
|
+
eventBreaker.abort(error);
|
|
1626
|
+
}
|
|
1627
|
+
throw error;
|
|
1628
|
+
}
|
|
1629
|
+
}
|
|
1630
|
+
|
|
1631
|
+
/** A parallel producer can trip the shared breaker while this event was
|
|
1632
|
+
* already queued in `streamEvents`. Stop before content handling or the
|
|
1633
|
+
* eager-tool paths below — those can dispatch a side-effecting host
|
|
1634
|
+
* tool with an already-aborted signal the handler never inspects.
|
|
1635
|
+
* Rechecked again immediately before each eager dispatch: the awaits in
|
|
1636
|
+
* between (server-tool results, tool-call handling, content dispatch)
|
|
1637
|
+
* are windows for a sibling's trip — or for a full reset, after which
|
|
1638
|
+
* this event belongs to a dead run and is dropped. */
|
|
1639
|
+
if (runScopeInvalidated()) {
|
|
1640
|
+
return;
|
|
1641
|
+
}
|
|
1642
|
+
throwIfRunBreakerTripped();
|
|
1643
|
+
|
|
1644
|
+
const agentContext = graph.getAgentContext(metadata);
|
|
1645
|
+
|
|
1519
1646
|
const content = getChunkContent({
|
|
1520
1647
|
chunk,
|
|
1521
1648
|
reasoningKey: agentContext.reasoningKey,
|
|
@@ -1576,6 +1703,10 @@ export class ChatModelStreamHandler implements t.EventHandler {
|
|
|
1576
1703
|
) {
|
|
1577
1704
|
hasToolCalls = true;
|
|
1578
1705
|
await handleToolCalls(chunk.tool_calls, metadata, graph);
|
|
1706
|
+
if (runScopeInvalidated()) {
|
|
1707
|
+
return;
|
|
1708
|
+
}
|
|
1709
|
+
throwIfRunBreakerTripped();
|
|
1579
1710
|
if (hasFinalToolCallSignal(chunk)) {
|
|
1580
1711
|
startEagerToolExecutions({
|
|
1581
1712
|
graph,
|
|
@@ -1655,6 +1786,10 @@ export class ChatModelStreamHandler implements t.EventHandler {
|
|
|
1655
1786
|
metadata,
|
|
1656
1787
|
});
|
|
1657
1788
|
if (canStreamEager) {
|
|
1789
|
+
if (runScopeInvalidated()) {
|
|
1790
|
+
return;
|
|
1791
|
+
}
|
|
1792
|
+
throwIfRunBreakerTripped();
|
|
1658
1793
|
startReadyStreamedEagerToolExecutions({
|
|
1659
1794
|
graph,
|
|
1660
1795
|
metadata,
|