@librechat/agents 3.9.2 → 3.9.4
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/README.md +32 -0
- package/dist/cjs/common/enum.cjs +2 -0
- package/dist/cjs/common/enum.cjs.map +1 -1
- package/dist/cjs/events.cjs +11 -0
- package/dist/cjs/events.cjs.map +1 -1
- package/dist/cjs/graphs/Graph.cjs +54 -5
- package/dist/cjs/graphs/Graph.cjs.map +1 -1
- package/dist/cjs/graphs/MultiAgentGraph.cjs +97 -5
- package/dist/cjs/graphs/MultiAgentGraph.cjs.map +1 -1
- package/dist/cjs/graphs/acceptedModelResponse.cjs +168 -0
- package/dist/cjs/graphs/acceptedModelResponse.cjs.map +1 -0
- package/dist/cjs/graphs/handoff.cjs +195 -0
- package/dist/cjs/graphs/handoff.cjs.map +1 -0
- package/dist/cjs/graphs/index.cjs +1 -0
- package/dist/cjs/llm/invoke.cjs +10 -5
- package/dist/cjs/llm/invoke.cjs.map +1 -1
- package/dist/cjs/llm/streamLimits.cjs +1 -1
- package/dist/cjs/llm/streamLimits.cjs.map +1 -1
- package/dist/cjs/main.cjs +4 -0
- package/dist/cjs/messages/fading.cjs +14 -6
- package/dist/cjs/messages/fading.cjs.map +1 -1
- package/dist/cjs/messages/prune.cjs +95 -36
- package/dist/cjs/messages/prune.cjs.map +1 -1
- package/dist/cjs/openai/index.cjs +2 -0
- package/dist/cjs/openai/index.cjs.map +1 -1
- package/dist/cjs/openai/toolProjection.cjs +196 -0
- package/dist/cjs/openai/toolProjection.cjs.map +1 -0
- package/dist/cjs/run.cjs +41 -4
- package/dist/cjs/run.cjs.map +1 -1
- package/dist/cjs/session/AgentSession.cjs +1 -1
- package/dist/cjs/session/AgentSession.cjs.map +1 -1
- package/dist/cjs/stream.cjs +9 -4
- package/dist/cjs/stream.cjs.map +1 -1
- package/dist/cjs/tools/ToolNode.cjs +5 -1
- package/dist/cjs/tools/ToolNode.cjs.map +1 -1
- package/dist/cjs/tools/subagent/SubagentReplay.cjs +4 -1
- package/dist/cjs/tools/subagent/SubagentReplay.cjs.map +1 -1
- package/dist/cjs/utils/acceptedToolArguments.cjs +143 -0
- package/dist/cjs/utils/acceptedToolArguments.cjs.map +1 -0
- package/dist/esm/common/enum.mjs +2 -0
- package/dist/esm/common/enum.mjs.map +1 -1
- package/dist/esm/events.mjs +11 -0
- package/dist/esm/events.mjs.map +1 -1
- package/dist/esm/graphs/Graph.mjs +54 -5
- package/dist/esm/graphs/Graph.mjs.map +1 -1
- package/dist/esm/graphs/MultiAgentGraph.mjs +97 -5
- package/dist/esm/graphs/MultiAgentGraph.mjs.map +1 -1
- package/dist/esm/graphs/acceptedModelResponse.mjs +165 -0
- package/dist/esm/graphs/acceptedModelResponse.mjs.map +1 -0
- package/dist/esm/graphs/handoff.mjs +193 -0
- package/dist/esm/graphs/handoff.mjs.map +1 -0
- package/dist/esm/graphs/index.mjs +1 -0
- package/dist/esm/llm/invoke.mjs +10 -5
- package/dist/esm/llm/invoke.mjs.map +1 -1
- package/dist/esm/llm/streamLimits.mjs +1 -1
- package/dist/esm/llm/streamLimits.mjs.map +1 -1
- package/dist/esm/main.mjs +4 -3
- package/dist/esm/messages/fading.mjs +14 -7
- package/dist/esm/messages/fading.mjs.map +1 -1
- package/dist/esm/messages/prune.mjs +95 -37
- package/dist/esm/messages/prune.mjs.map +1 -1
- package/dist/esm/openai/index.mjs +2 -1
- package/dist/esm/openai/index.mjs.map +1 -1
- package/dist/esm/openai/toolProjection.mjs +196 -0
- package/dist/esm/openai/toolProjection.mjs.map +1 -0
- package/dist/esm/run.mjs +41 -4
- package/dist/esm/run.mjs.map +1 -1
- package/dist/esm/session/AgentSession.mjs +1 -1
- package/dist/esm/session/AgentSession.mjs.map +1 -1
- package/dist/esm/stream.mjs +9 -4
- package/dist/esm/stream.mjs.map +1 -1
- package/dist/esm/tools/ToolNode.mjs +5 -1
- package/dist/esm/tools/ToolNode.mjs.map +1 -1
- package/dist/esm/tools/subagent/SubagentReplay.mjs +5 -2
- package/dist/esm/tools/subagent/SubagentReplay.mjs.map +1 -1
- package/dist/esm/utils/acceptedToolArguments.mjs +142 -0
- package/dist/esm/utils/acceptedToolArguments.mjs.map +1 -0
- package/dist/types/common/enum.d.ts +4 -0
- package/dist/types/graphs/Graph.d.ts +5 -1
- package/dist/types/graphs/MultiAgentGraph.d.ts +3 -0
- package/dist/types/graphs/acceptedModelResponse.d.ts +15 -0
- package/dist/types/graphs/handoff.d.ts +28 -0
- package/dist/types/graphs/index.d.ts +1 -0
- package/dist/types/messages/fading.d.ts +14 -2
- package/dist/types/messages/prune.d.ts +7 -0
- package/dist/types/openai/arguments.d.ts +2 -0
- package/dist/types/openai/index.d.ts +2 -0
- package/dist/types/openai/toolProjection.d.ts +30 -0
- package/dist/types/run.d.ts +5 -0
- package/dist/types/tools/ToolNode.d.ts +2 -1
- package/dist/types/types/graph.d.ts +51 -4
- package/dist/types/types/run.d.ts +8 -1
- package/dist/types/types/stream.d.ts +22 -1
- package/dist/types/types/tools.d.ts +5 -0
- package/dist/types/utils/acceptedToolArguments.d.ts +10 -0
- package/package.json +1 -1
- package/src/common/enum.ts +5 -0
- package/src/events.ts +24 -1
- package/src/graphs/Graph.ts +103 -0
- package/src/graphs/MultiAgentGraph.ts +142 -4
- package/src/graphs/acceptedModelResponse.ts +307 -0
- package/src/graphs/handoff.ts +263 -0
- package/src/graphs/index.ts +2 -0
- package/src/llm/invoke.ts +39 -14
- package/src/llm/streamLimits.ts +1 -1
- package/src/messages/fading.ts +30 -5
- package/src/messages/prune.ts +168 -55
- package/src/openai/arguments.ts +2 -0
- package/src/openai/index.ts +6 -0
- package/src/openai/toolProjection.ts +318 -0
- package/src/run.ts +75 -2
- package/src/session/AgentSession.ts +2 -2
- package/src/stream.ts +21 -1
- package/src/tools/ToolNode.ts +19 -6
- package/src/tools/subagent/SubagentReplay.ts +10 -9
- package/src/types/graph.ts +59 -10
- package/src/types/run.ts +8 -1
- package/src/types/stream.ts +26 -0
- package/src/types/tools.ts +5 -0
- package/src/utils/acceptedToolArguments.ts +204 -0
|
@@ -0,0 +1,263 @@
|
|
|
1
|
+
import { nanoid } from 'nanoid';
|
|
2
|
+
import { Annotation, Command, Send } from '@langchain/langgraph';
|
|
3
|
+
import type { BaseChannel, OverwriteValue } from '@langchain/langgraph';
|
|
4
|
+
import type { RunnableConfig } from '@langchain/core/runnables';
|
|
5
|
+
import type {
|
|
6
|
+
BaseGraphState,
|
|
7
|
+
HandoffOutcome,
|
|
8
|
+
HandoffState,
|
|
9
|
+
HandoffTransition,
|
|
10
|
+
} from '@/types/graph';
|
|
11
|
+
|
|
12
|
+
function isSupportedVersion(version: number): boolean {
|
|
13
|
+
return version === 1;
|
|
14
|
+
}
|
|
15
|
+
|
|
16
|
+
function isSend(value: string | Send): value is Send {
|
|
17
|
+
return typeof value !== 'string' && value.lg_name === 'Send';
|
|
18
|
+
}
|
|
19
|
+
|
|
20
|
+
export type HandoffRequest = Pick<
|
|
21
|
+
HandoffTransition,
|
|
22
|
+
'sourceAgentId' | 'targetAgentId' | 'toolCallId' | 'scope'
|
|
23
|
+
>;
|
|
24
|
+
|
|
25
|
+
type HandoffUpdate = Partial<BaseGraphState> & {
|
|
26
|
+
handoffRequest?: HandoffRequest;
|
|
27
|
+
};
|
|
28
|
+
|
|
29
|
+
function sameTransition(a: HandoffTransition, b: HandoffTransition): boolean {
|
|
30
|
+
return (
|
|
31
|
+
a.id === b.id &&
|
|
32
|
+
a.sourceAgentId === b.sourceAgentId &&
|
|
33
|
+
a.targetAgentId === b.targetAgentId &&
|
|
34
|
+
a.toolCallId === b.toolCallId &&
|
|
35
|
+
a.scope === b.scope &&
|
|
36
|
+
a.depth === b.depth
|
|
37
|
+
);
|
|
38
|
+
}
|
|
39
|
+
|
|
40
|
+
/** Union by identity makes checkpoint replay idempotent and parallel merges commutative. */
|
|
41
|
+
export function mergeHandoffState(
|
|
42
|
+
current: HandoffState | undefined,
|
|
43
|
+
update: HandoffState | undefined
|
|
44
|
+
): HandoffState | undefined {
|
|
45
|
+
if (update == null) return current;
|
|
46
|
+
if (current == null) return update;
|
|
47
|
+
if (current.executionId !== update.executionId) {
|
|
48
|
+
throw new Error('Cannot merge handoffs from different logical turns');
|
|
49
|
+
}
|
|
50
|
+
if (
|
|
51
|
+
current.entryAgentId !== update.entryAgentId ||
|
|
52
|
+
current.maxHandoffs !== update.maxHandoffs
|
|
53
|
+
) {
|
|
54
|
+
throw new Error('Conflicting handoff checkpoint configuration');
|
|
55
|
+
}
|
|
56
|
+
const transitions = new Map(
|
|
57
|
+
current.transitions.map((item) => [item.id, item])
|
|
58
|
+
);
|
|
59
|
+
for (const transition of update.transitions) {
|
|
60
|
+
const existing = transitions.get(transition.id);
|
|
61
|
+
if (existing != null && !sameTransition(existing, transition)) {
|
|
62
|
+
throw new Error('Conflicting replayed handoff transition');
|
|
63
|
+
}
|
|
64
|
+
transitions.set(transition.id, transition);
|
|
65
|
+
}
|
|
66
|
+
return {
|
|
67
|
+
...current,
|
|
68
|
+
parallel: current.parallel || update.parallel,
|
|
69
|
+
historyComplete:
|
|
70
|
+
current.historyComplete !== false && update.historyComplete !== false,
|
|
71
|
+
transitions: [...transitions.values()].sort(
|
|
72
|
+
(a, b) => a.depth - b.depth || a.id.localeCompare(b.id)
|
|
73
|
+
),
|
|
74
|
+
};
|
|
75
|
+
}
|
|
76
|
+
|
|
77
|
+
export function handoffStateAnnotation(): BaseChannel<
|
|
78
|
+
HandoffState | undefined,
|
|
79
|
+
HandoffState | OverwriteValue<HandoffState | undefined> | undefined
|
|
80
|
+
> {
|
|
81
|
+
return Annotation<HandoffState | undefined>({
|
|
82
|
+
reducer: mergeHandoffState,
|
|
83
|
+
default: () => undefined,
|
|
84
|
+
});
|
|
85
|
+
}
|
|
86
|
+
|
|
87
|
+
export class HandoffLimitError extends Error {
|
|
88
|
+
constructor(readonly limit: number) {
|
|
89
|
+
super(`Agent handoff limit (${limit}) reached`);
|
|
90
|
+
this.name = 'HandoffLimitError';
|
|
91
|
+
}
|
|
92
|
+
}
|
|
93
|
+
|
|
94
|
+
/** One owner per graph, never shared with isolated child executions. */
|
|
95
|
+
export class HandoffRouting {
|
|
96
|
+
private state: HandoffState;
|
|
97
|
+
|
|
98
|
+
constructor(
|
|
99
|
+
private readonly entryAgentId: string,
|
|
100
|
+
private readonly maxHandoffs: number | undefined,
|
|
101
|
+
private readonly parallel: boolean
|
|
102
|
+
) {
|
|
103
|
+
this.state = this.freshState();
|
|
104
|
+
}
|
|
105
|
+
|
|
106
|
+
private freshState(): HandoffState {
|
|
107
|
+
return {
|
|
108
|
+
version: 1,
|
|
109
|
+
executionId: nanoid(),
|
|
110
|
+
entryAgentId: this.entryAgentId,
|
|
111
|
+
...(this.maxHandoffs == null ? {} : { maxHandoffs: this.maxHandoffs }),
|
|
112
|
+
transitions: [],
|
|
113
|
+
parallel: this.parallel,
|
|
114
|
+
};
|
|
115
|
+
}
|
|
116
|
+
|
|
117
|
+
start(): HandoffState {
|
|
118
|
+
this.state = this.freshState();
|
|
119
|
+
return this.snapshot();
|
|
120
|
+
}
|
|
121
|
+
|
|
122
|
+
resume(state: HandoffState | undefined): void {
|
|
123
|
+
if (state != null) {
|
|
124
|
+
this.restore(state);
|
|
125
|
+
return;
|
|
126
|
+
}
|
|
127
|
+
if (this.maxHandoffs != null) {
|
|
128
|
+
throw new Error(
|
|
129
|
+
'Cannot enforce a handoff budget on a legacy checkpoint without routing state'
|
|
130
|
+
);
|
|
131
|
+
}
|
|
132
|
+
this.state = { ...this.state, historyComplete: false };
|
|
133
|
+
}
|
|
134
|
+
|
|
135
|
+
restore(state: HandoffState | undefined): void {
|
|
136
|
+
if (state == null) return;
|
|
137
|
+
if (!isSupportedVersion(state.version))
|
|
138
|
+
throw new Error('Unsupported handoff checkpoint version');
|
|
139
|
+
if (
|
|
140
|
+
state.entryAgentId !== this.entryAgentId ||
|
|
141
|
+
state.maxHandoffs !== this.maxHandoffs
|
|
142
|
+
) {
|
|
143
|
+
throw new Error('Cannot resume with a different handoff entry or budget');
|
|
144
|
+
}
|
|
145
|
+
if (this.state.executionId !== state.executionId) {
|
|
146
|
+
if (this.state.transitions.length > 0) {
|
|
147
|
+
throw new Error('Cannot resume a different handoff execution');
|
|
148
|
+
}
|
|
149
|
+
this.state = {
|
|
150
|
+
...state,
|
|
151
|
+
transitions: state.transitions.map((item) => ({ ...item })),
|
|
152
|
+
};
|
|
153
|
+
return;
|
|
154
|
+
}
|
|
155
|
+
this.state = mergeHandoffState(this.state, state)!;
|
|
156
|
+
}
|
|
157
|
+
|
|
158
|
+
snapshot(): HandoffState {
|
|
159
|
+
return {
|
|
160
|
+
...this.state,
|
|
161
|
+
transitions: this.state.transitions.map((item) => ({ ...item })),
|
|
162
|
+
};
|
|
163
|
+
}
|
|
164
|
+
|
|
165
|
+
/** Called after tools settle but before Commands can schedule recipients. */
|
|
166
|
+
finalize(
|
|
167
|
+
commands: Command[],
|
|
168
|
+
input: BaseGraphState,
|
|
169
|
+
config: RunnableConfig
|
|
170
|
+
): void {
|
|
171
|
+
this.restore(input.handoffState);
|
|
172
|
+
const updates: HandoffUpdate[] = [];
|
|
173
|
+
let parallel = this.state.parallel;
|
|
174
|
+
for (const command of commands) {
|
|
175
|
+
if (command.graph !== Command.PARENT) continue;
|
|
176
|
+
const sends = Array.isArray(command.goto)
|
|
177
|
+
? command.goto.filter(isSend)
|
|
178
|
+
: [];
|
|
179
|
+
parallel ||= sends.length > 1;
|
|
180
|
+
if (sends.length > 0) {
|
|
181
|
+
for (const send of sends) updates.push(send.args as HandoffUpdate);
|
|
182
|
+
} else if (command.update != null) {
|
|
183
|
+
updates.push(command.update as HandoffUpdate);
|
|
184
|
+
}
|
|
185
|
+
}
|
|
186
|
+
const accepted = new Map(
|
|
187
|
+
this.state.transitions.map((item) => [item.id, item])
|
|
188
|
+
);
|
|
189
|
+
const requests = updates.filter((update) => update.handoffRequest != null);
|
|
190
|
+
if (requests.length === 0) return;
|
|
191
|
+
const depth =
|
|
192
|
+
input.handoffState?.transitions.length ?? this.state.transitions.length;
|
|
193
|
+
const lastMessage = input.messages.at(-1);
|
|
194
|
+
for (const update of requests) {
|
|
195
|
+
const request = update.handoffRequest!;
|
|
196
|
+
const id = JSON.stringify([
|
|
197
|
+
this.state.executionId,
|
|
198
|
+
config.configurable?.checkpoint_ns ?? '',
|
|
199
|
+
request.sourceAgentId,
|
|
200
|
+
lastMessage?.id ?? input.messages.length,
|
|
201
|
+
request.toolCallId,
|
|
202
|
+
]);
|
|
203
|
+
const transition = { ...request, id, depth };
|
|
204
|
+
const existing = accepted.get(id);
|
|
205
|
+
if (
|
|
206
|
+
existing != null &&
|
|
207
|
+
(existing.targetAgentId !== transition.targetAgentId ||
|
|
208
|
+
existing.scope !== transition.scope)
|
|
209
|
+
)
|
|
210
|
+
throw new Error('Handoff replay changed its destination or scope');
|
|
211
|
+
accepted.set(id, existing ?? transition);
|
|
212
|
+
}
|
|
213
|
+
if (this.maxHandoffs != null && accepted.size > this.maxHandoffs) {
|
|
214
|
+
throw new HandoffLimitError(this.maxHandoffs);
|
|
215
|
+
}
|
|
216
|
+
this.state = {
|
|
217
|
+
...this.state,
|
|
218
|
+
transitions: [...accepted.values()],
|
|
219
|
+
parallel,
|
|
220
|
+
};
|
|
221
|
+
for (const update of requests) delete update.handoffRequest;
|
|
222
|
+
for (const command of commands) {
|
|
223
|
+
if (command.graph !== Command.PARENT) continue;
|
|
224
|
+
command.update = {
|
|
225
|
+
...(command.update as HandoffUpdate),
|
|
226
|
+
handoffState: this.snapshot(),
|
|
227
|
+
};
|
|
228
|
+
if (!Array.isArray(command.goto)) continue;
|
|
229
|
+
command.goto = command.goto.map((destination) =>
|
|
230
|
+
isSend(destination)
|
|
231
|
+
? new Send(destination.node, {
|
|
232
|
+
...destination.args,
|
|
233
|
+
handoffState: this.snapshot(),
|
|
234
|
+
})
|
|
235
|
+
: destination
|
|
236
|
+
);
|
|
237
|
+
}
|
|
238
|
+
}
|
|
239
|
+
|
|
240
|
+
outcome(reason?: string): HandoffOutcome {
|
|
241
|
+
const state = this.snapshot();
|
|
242
|
+
const base = {
|
|
243
|
+
executionId: state.executionId,
|
|
244
|
+
entryAgentId: state.entryAgentId,
|
|
245
|
+
transitions: state.transitions,
|
|
246
|
+
};
|
|
247
|
+
if (reason != null) return { ...base, status: 'incomplete', reason };
|
|
248
|
+
if (state.historyComplete === false)
|
|
249
|
+
return { ...base, status: 'incomplete', reason: 'legacy_checkpoint' };
|
|
250
|
+
const persistent = state.transitions.filter(
|
|
251
|
+
(item) => item.scope === 'conversation'
|
|
252
|
+
);
|
|
253
|
+
if (persistent.length === 0) return { ...base, status: 'unchanged' };
|
|
254
|
+
if (state.parallel) return { ...base, status: 'ambiguous' };
|
|
255
|
+
const last = persistent.reduce((a, b) => (a.depth > b.depth ? a : b));
|
|
256
|
+
return {
|
|
257
|
+
...base,
|
|
258
|
+
status: 'candidate',
|
|
259
|
+
agentId: last.targetAgentId,
|
|
260
|
+
transitionId: last.id,
|
|
261
|
+
};
|
|
262
|
+
}
|
|
263
|
+
}
|
package/src/graphs/index.ts
CHANGED
package/src/llm/invoke.ts
CHANGED
|
@@ -41,6 +41,11 @@ import {
|
|
|
41
41
|
resolvePreemptAction,
|
|
42
42
|
resolveRestartGraceMs,
|
|
43
43
|
} from '@/llm/preempt';
|
|
44
|
+
import {
|
|
45
|
+
detachValidatedModelToolCalls,
|
|
46
|
+
snapshotValidatedModelChunk,
|
|
47
|
+
InvalidModelToolCallError,
|
|
48
|
+
} from '@/graphs/acceptedModelResponse';
|
|
44
49
|
import {
|
|
45
50
|
assertPreparedProviderRequestFor,
|
|
46
51
|
prepareProviderRequest,
|
|
@@ -1079,7 +1084,8 @@ async function attemptInvokeBody(
|
|
|
1079
1084
|
const signal = config.signal;
|
|
1080
1085
|
if (
|
|
1081
1086
|
signal?.aborted === true &&
|
|
1082
|
-
(signal.reason instanceof StreamLimitExceededError ||
|
|
1087
|
+
(signal.reason instanceof StreamLimitExceededError ||
|
|
1088
|
+
signal.reason instanceof PreparedSubagentError)
|
|
1083
1089
|
) {
|
|
1084
1090
|
throw signal.reason;
|
|
1085
1091
|
}
|
|
@@ -1105,8 +1111,9 @@ async function attemptInvokeBody(
|
|
|
1105
1111
|
const attemptMetadata = config.metadata as
|
|
1106
1112
|
| Record<string, unknown>
|
|
1107
1113
|
| undefined;
|
|
1108
|
-
for await (const
|
|
1114
|
+
for await (const rawChunk of stream) {
|
|
1109
1115
|
throwIfBreakerTripped();
|
|
1116
|
+
const chunk = snapshotValidatedModelChunk(rawChunk);
|
|
1110
1117
|
/** An onChunk consumer replaces the stream handler entirely, so
|
|
1111
1118
|
* stream limits are enforced here for every such caller — public
|
|
1112
1119
|
* package consumers get no other accounting. The internal
|
|
@@ -1127,10 +1134,13 @@ async function attemptInvokeBody(
|
|
|
1127
1134
|
});
|
|
1128
1135
|
}
|
|
1129
1136
|
} else if (registeredStreamHandler == null) {
|
|
1130
|
-
const metadata = config.metadata as
|
|
1137
|
+
const metadata = config.metadata as
|
|
1138
|
+
| Record<string, unknown>
|
|
1139
|
+
| undefined;
|
|
1131
1140
|
const streamHandler = new ChatModelStreamHandler();
|
|
1132
|
-
for await (const
|
|
1141
|
+
for await (const rawChunk of stream) {
|
|
1133
1142
|
throwIfBreakerTripped();
|
|
1143
|
+
const chunk = snapshotValidatedModelChunk(rawChunk);
|
|
1134
1144
|
/**
|
|
1135
1145
|
* The decision is final, so stop consuming here rather than
|
|
1136
1146
|
* trusting the adapter to honor the abort. An adapter that ignores
|
|
@@ -1239,7 +1249,9 @@ async function attemptInvokeBody(
|
|
|
1239
1249
|
}
|
|
1240
1250
|
}
|
|
1241
1251
|
} else {
|
|
1242
|
-
const metadata = config.metadata as
|
|
1252
|
+
const metadata = config.metadata as
|
|
1253
|
+
| Record<string, unknown>
|
|
1254
|
+
| undefined;
|
|
1243
1255
|
/**
|
|
1244
1256
|
* The original wire chunk still reaches the registered handler through
|
|
1245
1257
|
* `streamEvents` (where the late-reasoning skip discards it AFTER the
|
|
@@ -1248,8 +1260,9 @@ async function attemptInvokeBody(
|
|
|
1248
1260
|
* once per attempt, only when a transformation occurs.
|
|
1249
1261
|
*/
|
|
1250
1262
|
let redispatchMetadata: Record<string, unknown> | undefined;
|
|
1251
|
-
for await (const
|
|
1263
|
+
for await (const rawChunk of stream) {
|
|
1252
1264
|
throwIfBreakerTripped();
|
|
1265
|
+
const chunk = snapshotValidatedModelChunk(rawChunk);
|
|
1253
1266
|
/**
|
|
1254
1267
|
* Charged synchronously, ahead of the decoupled `streamEvents`
|
|
1255
1268
|
* reader that will echo this same chunk to the registered handler:
|
|
@@ -1258,7 +1271,11 @@ async function attemptInvokeBody(
|
|
|
1258
1271
|
* throws. The chunk is marked so the echo skips accounting.
|
|
1259
1272
|
*/
|
|
1260
1273
|
if (context != null) {
|
|
1261
|
-
enforceStreamLimitsForWireChunk({
|
|
1274
|
+
enforceStreamLimitsForWireChunk({
|
|
1275
|
+
graph: context,
|
|
1276
|
+
metadata,
|
|
1277
|
+
chunk,
|
|
1278
|
+
});
|
|
1262
1279
|
}
|
|
1263
1280
|
const handlingChunk = getStreamHandlingChunk({
|
|
1264
1281
|
current: finalChunk,
|
|
@@ -1366,7 +1383,8 @@ async function attemptInvokeBody(
|
|
|
1366
1383
|
);
|
|
1367
1384
|
}
|
|
1368
1385
|
if (finalChunk != null || sealedRunId != null) {
|
|
1369
|
-
const discardedChunk =
|
|
1386
|
+
const discardedChunk =
|
|
1387
|
+
finalChunk ?? new AIMessageChunk({ content: '' });
|
|
1370
1388
|
const responseMetadata = {
|
|
1371
1389
|
...discardedChunk.response_metadata,
|
|
1372
1390
|
preempted: true,
|
|
@@ -1448,6 +1466,7 @@ async function attemptInvokeBody(
|
|
|
1448
1466
|
);
|
|
1449
1467
|
}
|
|
1450
1468
|
|
|
1469
|
+
if (finalChunk != null) detachValidatedModelToolCalls(finalChunk);
|
|
1451
1470
|
if ((finalChunk?.tool_calls?.length ?? 0) > 0) {
|
|
1452
1471
|
finalChunk!.tool_calls = finalChunk!.tool_calls?.filter(
|
|
1453
1472
|
(tool_call: ToolCall) => !!tool_call.name
|
|
@@ -1458,9 +1477,9 @@ async function attemptInvokeBody(
|
|
|
1458
1477
|
return { messages: [finalChunk as AIMessageChunk] };
|
|
1459
1478
|
}
|
|
1460
1479
|
|
|
1461
|
-
const finalMessage =
|
|
1462
|
-
messagesForProvider,
|
|
1463
|
-
|
|
1480
|
+
const finalMessage = snapshotValidatedModelChunk(
|
|
1481
|
+
await model.invoke(messagesForProvider, invocationConfig),
|
|
1482
|
+
false
|
|
1464
1483
|
);
|
|
1465
1484
|
if ((finalMessage.tool_calls?.length ?? 0) > 0) {
|
|
1466
1485
|
finalMessage.tool_calls = finalMessage.tool_calls?.filter(
|
|
@@ -1666,7 +1685,8 @@ export async function tryFallbackProviders({
|
|
|
1666
1685
|
* a run that must reject. Check before every fallback invocation. */
|
|
1667
1686
|
if (
|
|
1668
1687
|
config?.signal?.aborted === true &&
|
|
1669
|
-
(config.signal.reason instanceof StreamLimitExceededError ||
|
|
1688
|
+
(config.signal.reason instanceof StreamLimitExceededError ||
|
|
1689
|
+
config.signal.reason instanceof PreparedSubagentError)
|
|
1670
1690
|
) {
|
|
1671
1691
|
throw config.signal.reason;
|
|
1672
1692
|
}
|
|
@@ -1700,7 +1720,11 @@ export async function tryFallbackProviders({
|
|
|
1700
1720
|
* provider failure. Continuing would try the remaining fallbacks and a
|
|
1701
1721
|
* succeeding one would resolve a run that must reject.
|
|
1702
1722
|
*/
|
|
1703
|
-
if (
|
|
1723
|
+
if (
|
|
1724
|
+
e instanceof StreamLimitExceededError ||
|
|
1725
|
+
e instanceof PreparedSubagentError ||
|
|
1726
|
+
e instanceof InvalidModelToolCallError
|
|
1727
|
+
) {
|
|
1704
1728
|
throw e;
|
|
1705
1729
|
}
|
|
1706
1730
|
/** A parallel sibling's trip aborts this branch's composed signal, and
|
|
@@ -1709,7 +1733,8 @@ export async function tryFallbackProviders({
|
|
|
1709
1733
|
* abort. Rethrow the breaker's own reason instead. */
|
|
1710
1734
|
if (
|
|
1711
1735
|
config?.signal?.aborted === true &&
|
|
1712
|
-
(config.signal.reason instanceof StreamLimitExceededError ||
|
|
1736
|
+
(config.signal.reason instanceof StreamLimitExceededError ||
|
|
1737
|
+
config.signal.reason instanceof PreparedSubagentError)
|
|
1713
1738
|
) {
|
|
1714
1739
|
throw config.signal.reason;
|
|
1715
1740
|
}
|
package/src/llm/streamLimits.ts
CHANGED
package/src/messages/fading.ts
CHANGED
|
@@ -4,7 +4,12 @@ import {
|
|
|
4
4
|
calculateMaxToolResultChars,
|
|
5
5
|
} from '@/utils/truncation';
|
|
6
6
|
|
|
7
|
-
|
|
7
|
+
/**
|
|
8
|
+
* Version 2: exchange width counts only the current turn. Version 1 tiers may
|
|
9
|
+
* have latched on history whose steps storage had merged into one assistant
|
|
10
|
+
* message, so they are discarded and re-derived once.
|
|
11
|
+
*/
|
|
12
|
+
export const FADING_TIER_VERSION = 2;
|
|
8
13
|
|
|
9
14
|
/** Context pressure at which observation masking activates. */
|
|
10
15
|
export const PRESSURE_THRESHOLD_MASKING = 0.8;
|
|
@@ -34,7 +39,7 @@ export type FadingSignals = {
|
|
|
34
39
|
/** (pruningBudget − instruction tokens) ÷ calibrationRatio, in raw token space. */
|
|
35
40
|
effectiveRawTokens: number;
|
|
36
41
|
summarizationEnabled: boolean;
|
|
37
|
-
/** Largest number of parallel calls
|
|
42
|
+
/** Largest number of parallel calls in one assistant exchange of the current turn. */
|
|
38
43
|
toolExchangeWidth?: number;
|
|
39
44
|
/** Recovery paths force at least this rung on the current window's ladder. */
|
|
40
45
|
minRung?: number;
|
|
@@ -59,7 +64,10 @@ export function createFadingTier(window: number): FadingTier {
|
|
|
59
64
|
return { v: FADING_TIER_VERSION, budgetTokens: window, masked: false };
|
|
60
65
|
}
|
|
61
66
|
|
|
62
|
-
|
|
67
|
+
/** Tier version before exchange width became turn-scoped. */
|
|
68
|
+
const LEGACY_FADING_TIER_VERSION = 1;
|
|
69
|
+
|
|
70
|
+
function isFadingTierOfVersion(value: unknown, version: number): boolean {
|
|
63
71
|
if (typeof value !== 'object' || value === null) {
|
|
64
72
|
return false;
|
|
65
73
|
}
|
|
@@ -67,7 +75,7 @@ export function isFadingTier(value: unknown): value is FadingTier {
|
|
|
67
75
|
Record<keyof FadingTier, unknown>
|
|
68
76
|
>;
|
|
69
77
|
return (
|
|
70
|
-
v ===
|
|
78
|
+
v === version &&
|
|
71
79
|
typeof budgetTokens === 'number' &&
|
|
72
80
|
Number.isFinite(budgetTokens) &&
|
|
73
81
|
budgetTokens > 0 &&
|
|
@@ -76,6 +84,20 @@ export function isFadingTier(value: unknown): value is FadingTier {
|
|
|
76
84
|
);
|
|
77
85
|
}
|
|
78
86
|
|
|
87
|
+
export function isFadingTier(value: unknown): value is FadingTier {
|
|
88
|
+
return isFadingTierOfVersion(value, FADING_TIER_VERSION);
|
|
89
|
+
}
|
|
90
|
+
|
|
91
|
+
/**
|
|
92
|
+
* A well-formed tier persisted before version 2. Stored snapshots that carry
|
|
93
|
+
* one (resume manifests, session files) stay valid; every consumer then drops
|
|
94
|
+
* it through `isFadingTier`, so the tier re-derives once instead of the
|
|
95
|
+
* snapshot being rejected as corrupt.
|
|
96
|
+
*/
|
|
97
|
+
export function isLegacyFadingTier(value: unknown): boolean {
|
|
98
|
+
return isFadingTierOfVersion(value, LEGACY_FADING_TIER_VERSION);
|
|
99
|
+
}
|
|
100
|
+
|
|
79
101
|
/** Deepest rung for a window: the point where the budget reaches its floor. */
|
|
80
102
|
export function maxFadingRung(window: number): number {
|
|
81
103
|
const floor = floorBudgetTokens(window);
|
|
@@ -199,7 +221,10 @@ export function fadingRungForExchangeChars(
|
|
|
199
221
|
maxToolResultChars == null
|
|
200
222
|
? windowResultChars
|
|
201
223
|
: Math.min(windowResultChars, maxToolResultChars);
|
|
202
|
-
if (
|
|
224
|
+
if (
|
|
225
|
+
resultChars + calculateMaxToolCallInputChars(budgetTokens) <=
|
|
226
|
+
targetChars
|
|
227
|
+
) {
|
|
203
228
|
return rung;
|
|
204
229
|
}
|
|
205
230
|
}
|