@librechat/agents 3.9.3 → 3.9.5
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- 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 +41 -1
- package/dist/cjs/graphs/Graph.cjs.map +1 -1
- package/dist/cjs/graphs/acceptedModelResponse.cjs +168 -0
- package/dist/cjs/graphs/acceptedModelResponse.cjs.map +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 +2 -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 +8 -1
- 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/ArtifactTruncation.cjs +45 -0
- package/dist/cjs/tools/ArtifactTruncation.cjs.map +1 -0
- package/dist/cjs/tools/BashExecutor.cjs +6 -1
- package/dist/cjs/tools/BashExecutor.cjs.map +1 -1
- package/dist/cjs/tools/CodeExecutor.cjs +6 -1
- package/dist/cjs/tools/CodeExecutor.cjs.map +1 -1
- package/dist/cjs/tools/ProgrammaticToolCalling.cjs +5 -1
- package/dist/cjs/tools/ProgrammaticToolCalling.cjs.map +1 -1
- package/dist/cjs/tools/ToolNode.cjs +2 -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 +41 -1
- package/dist/esm/graphs/Graph.mjs.map +1 -1
- package/dist/esm/graphs/acceptedModelResponse.mjs +165 -0
- package/dist/esm/graphs/acceptedModelResponse.mjs.map +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 +3 -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 +8 -1
- 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/ArtifactTruncation.mjs +44 -0
- package/dist/esm/tools/ArtifactTruncation.mjs.map +1 -0
- package/dist/esm/tools/BashExecutor.mjs +6 -1
- package/dist/esm/tools/BashExecutor.mjs.map +1 -1
- package/dist/esm/tools/CodeExecutor.mjs +6 -1
- package/dist/esm/tools/CodeExecutor.mjs.map +1 -1
- package/dist/esm/tools/ProgrammaticToolCalling.mjs +5 -1
- package/dist/esm/tools/ProgrammaticToolCalling.mjs.map +1 -1
- package/dist/esm/tools/ToolNode.mjs +2 -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 +3 -1
- package/dist/types/graphs/acceptedModelResponse.d.ts +15 -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 +1 -0
- package/dist/types/tools/ArtifactTruncation.d.ts +3 -0
- package/dist/types/tools/ToolNode.d.ts +1 -1
- package/dist/types/types/graph.d.ts +5 -3
- package/dist/types/types/run.d.ts +5 -0
- package/dist/types/types/stream.d.ts +22 -1
- package/dist/types/types/tools.d.ts +14 -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 +95 -0
- package/src/graphs/acceptedModelResponse.ts +307 -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 +21 -0
- package/src/session/AgentSession.ts +2 -2
- package/src/stream.ts +21 -1
- package/src/tools/ArtifactTruncation.ts +85 -0
- package/src/tools/BashExecutor.ts +22 -5
- package/src/tools/CodeExecutor.ts +18 -1
- package/src/tools/ProgrammaticToolCalling.ts +17 -3
- package/src/tools/ToolNode.ts +5 -0
- package/src/tools/subagent/SubagentReplay.ts +10 -9
- package/src/types/graph.ts +15 -9
- package/src/types/run.ts +5 -0
- package/src/types/stream.ts +26 -0
- package/src/types/tools.ts +21 -0
- package/src/utils/acceptedToolArguments.ts +204 -0
|
@@ -0,0 +1,318 @@
|
|
|
1
|
+
import type {
|
|
2
|
+
OpenAIChatCompletionChunkChoice,
|
|
3
|
+
OpenAIToolCall,
|
|
4
|
+
OpenAIStreamTracker,
|
|
5
|
+
} from './index';
|
|
6
|
+
import type { EventHandler, ModelResponseEvent } from '@/types';
|
|
7
|
+
import { serializeToolArguments } from './arguments';
|
|
8
|
+
import { GraphEvents } from '@/common';
|
|
9
|
+
|
|
10
|
+
interface OpenAIToolCallStreamOptions {
|
|
11
|
+
/** Synchronous framing only. Async transport/backpressure is a separate boundary. */
|
|
12
|
+
emit?: (delta: OpenAIChatCompletionChunkChoice['delta']) => void;
|
|
13
|
+
signal?: AbortSignal;
|
|
14
|
+
/** Bound retained complete-before-publish output, not model execution. Default: 1024. */
|
|
15
|
+
maxToolCalls?: number;
|
|
16
|
+
/** UTF-8 bytes of serialized arguments, names and IDs retained across accepted responses. Default: 4 MiB. */
|
|
17
|
+
maxBufferedBytes?: number;
|
|
18
|
+
}
|
|
19
|
+
|
|
20
|
+
/** Streaming hosts share the finalizer's tracker; map-only hosts collect JSON output. */
|
|
21
|
+
export type OpenAIToolCallStreamConfig = OpenAIToolCallStreamOptions &
|
|
22
|
+
(
|
|
23
|
+
| {
|
|
24
|
+
tracker: OpenAIStreamTracker;
|
|
25
|
+
toolCalls?: never;
|
|
26
|
+
emit: NonNullable<OpenAIToolCallStreamOptions['emit']>;
|
|
27
|
+
}
|
|
28
|
+
| { toolCalls: Map<number, OpenAIToolCall>; tracker?: never }
|
|
29
|
+
);
|
|
30
|
+
|
|
31
|
+
export interface OpenAIToolCallStream {
|
|
32
|
+
/** Pass directly to Run.create({ customHandlers: stream.handlers }). */
|
|
33
|
+
handlers: Record<string, EventHandler>;
|
|
34
|
+
/** Publish only after the host verifies that the entire run completed successfully. */
|
|
35
|
+
finish: () => void;
|
|
36
|
+
abort: () => void;
|
|
37
|
+
}
|
|
38
|
+
|
|
39
|
+
function positiveLimit(value: number | undefined, fallback: number): number {
|
|
40
|
+
const limit = value ?? fallback;
|
|
41
|
+
if (!Number.isSafeInteger(limit) || limit <= 0) {
|
|
42
|
+
throw new Error('Tool projection limits must be positive safe integers');
|
|
43
|
+
}
|
|
44
|
+
return limit;
|
|
45
|
+
}
|
|
46
|
+
|
|
47
|
+
/** Serializes finalized, accepted tool calls. No provider fragments or attempt inference. */
|
|
48
|
+
export function createOpenAIToolCallStream(
|
|
49
|
+
config: OpenAIToolCallStreamConfig
|
|
50
|
+
): OpenAIToolCallStream {
|
|
51
|
+
const { emit, signal, tracker } = config;
|
|
52
|
+
const suppliedMap: unknown = config.toolCalls;
|
|
53
|
+
const suppliedEmitter: unknown = emit;
|
|
54
|
+
if (tracker != null && typeof suppliedEmitter !== 'function') {
|
|
55
|
+
throw new Error('A streaming tracker requires an emitter');
|
|
56
|
+
}
|
|
57
|
+
if (tracker != null && suppliedMap != null) {
|
|
58
|
+
throw new Error('Provide a tracker or a tool-call map, not both');
|
|
59
|
+
}
|
|
60
|
+
const toolCalls = tracker?.toolCalls ?? config.toolCalls;
|
|
61
|
+
if (toolCalls == null)
|
|
62
|
+
throw new Error('Provide a tracker or a tool-call map');
|
|
63
|
+
if (toolCalls.size !== 0)
|
|
64
|
+
throw new Error('Tool projection requires an empty output map');
|
|
65
|
+
let previousChunkKind: OpenAIStreamTracker['lastChunkKind'];
|
|
66
|
+
let acceptedTerminalKind: OpenAIStreamTracker['lastChunkKind'];
|
|
67
|
+
const maxCalls = positiveLimit(config.maxToolCalls, 1024);
|
|
68
|
+
const maxBytes = positiveLimit(config.maxBufferedBytes, 4 * 1024 * 1024);
|
|
69
|
+
const calls: Array<{
|
|
70
|
+
id: string;
|
|
71
|
+
name: string;
|
|
72
|
+
arguments: string;
|
|
73
|
+
agentId: string;
|
|
74
|
+
messageId?: string;
|
|
75
|
+
acceptedId: string;
|
|
76
|
+
bytes: number;
|
|
77
|
+
}> = [];
|
|
78
|
+
const providerIds = new Set<string>();
|
|
79
|
+
const acceptedIds = new Set<string>();
|
|
80
|
+
let bufferedBytes = 0;
|
|
81
|
+
let phase: 'open' | 'emitting' | 'finished' | 'aborted' = 'open';
|
|
82
|
+
|
|
83
|
+
const release = (): void => {
|
|
84
|
+
calls.length = 0;
|
|
85
|
+
acceptedIds.clear();
|
|
86
|
+
providerIds.clear();
|
|
87
|
+
bufferedBytes = 0;
|
|
88
|
+
};
|
|
89
|
+
const discard = (agentId: string, messageId?: string): void => {
|
|
90
|
+
for (let i = calls.length - 1; i >= 0; i--) {
|
|
91
|
+
const call = calls[i];
|
|
92
|
+
if (
|
|
93
|
+
call.agentId !== agentId ||
|
|
94
|
+
(messageId != null && call.messageId !== messageId)
|
|
95
|
+
)
|
|
96
|
+
continue;
|
|
97
|
+
bufferedBytes -= call.bytes;
|
|
98
|
+
calls.splice(i, 1);
|
|
99
|
+
}
|
|
100
|
+
acceptedIds.clear();
|
|
101
|
+
providerIds.clear();
|
|
102
|
+
for (const call of calls) {
|
|
103
|
+
acceptedIds.add(call.acceptedId);
|
|
104
|
+
if (call.id !== '') providerIds.add(call.id);
|
|
105
|
+
}
|
|
106
|
+
acceptedTerminalKind = calls.length > 0 ? 'tool_call' : 'text';
|
|
107
|
+
};
|
|
108
|
+
const abort = (): void => {
|
|
109
|
+
const phaseBeforeAbort = phase;
|
|
110
|
+
if (phase !== 'finished') {
|
|
111
|
+
phase = 'aborted';
|
|
112
|
+
toolCalls.clear();
|
|
113
|
+
if (tracker != null && phaseBeforeAbort === 'emitting')
|
|
114
|
+
tracker.lastChunkKind = previousChunkKind;
|
|
115
|
+
}
|
|
116
|
+
release();
|
|
117
|
+
};
|
|
118
|
+
const checkCancellation = (): void => {
|
|
119
|
+
if (signal?.aborted === true) abort();
|
|
120
|
+
if (phase === 'aborted') {
|
|
121
|
+
const error = new Error('Agent response aborted');
|
|
122
|
+
error.name = 'AbortError';
|
|
123
|
+
throw error;
|
|
124
|
+
}
|
|
125
|
+
};
|
|
126
|
+
const emitDelta = (delta: OpenAIChatCompletionChunkChoice['delta']): void => {
|
|
127
|
+
const result: unknown = emit?.(delta);
|
|
128
|
+
if (
|
|
129
|
+
result != null &&
|
|
130
|
+
typeof result === 'object' &&
|
|
131
|
+
'then' in result &&
|
|
132
|
+
typeof result.then === 'function'
|
|
133
|
+
) {
|
|
134
|
+
// Observe a mistakenly async sink's rejection, then fail closed. Never silently
|
|
135
|
+
// publish a successful terminal response before pending writes settle.
|
|
136
|
+
void Promise.resolve(result).catch(() => undefined);
|
|
137
|
+
throw new Error('Tool projection requires a synchronous emitter');
|
|
138
|
+
}
|
|
139
|
+
};
|
|
140
|
+
const accept = (result: ModelResponseEvent): void => {
|
|
141
|
+
if (phase !== 'open') return;
|
|
142
|
+
try {
|
|
143
|
+
checkCancellation();
|
|
144
|
+
if (result.invalidToolCalls.length > 0) {
|
|
145
|
+
throw new Error('Accepted model response contains invalid tool calls');
|
|
146
|
+
}
|
|
147
|
+
const ownership = result.toolCallDispositions;
|
|
148
|
+
if (
|
|
149
|
+
!Array.isArray(ownership) ||
|
|
150
|
+
ownership.length !== result.toolCalls.length ||
|
|
151
|
+
ownership.some(
|
|
152
|
+
(value) =>
|
|
153
|
+
value !== 'sdk' && value !== 'provider' && value !== 'client'
|
|
154
|
+
)
|
|
155
|
+
) {
|
|
156
|
+
throw new Error('Accepted tool calls lack trusted execution ownership');
|
|
157
|
+
}
|
|
158
|
+
const delegated = result.toolCalls.filter(
|
|
159
|
+
(_call, index) => ownership[index] === 'client'
|
|
160
|
+
);
|
|
161
|
+
if (delegated.length === 0) {
|
|
162
|
+
// A text or internal-only response supersedes this agent's request,
|
|
163
|
+
// never a sibling's. Neither 'sdk' nor 'provider' enters the wire.
|
|
164
|
+
discard(result.agentId);
|
|
165
|
+
return;
|
|
166
|
+
}
|
|
167
|
+
if (result.id.trim() === '' || acceptedIds.has(result.id)) {
|
|
168
|
+
throw new Error('Missing or repeated accepted model response identity');
|
|
169
|
+
}
|
|
170
|
+
if (calls.length + delegated.length > maxCalls) {
|
|
171
|
+
throw new Error('Tool projection call limit exceeded');
|
|
172
|
+
}
|
|
173
|
+
acceptedIds.add(result.id);
|
|
174
|
+
for (const call of delegated) {
|
|
175
|
+
if (typeof call.name !== 'string' || call.name.trim() === '') {
|
|
176
|
+
throw new Error('Accepted tool call is missing its name');
|
|
177
|
+
}
|
|
178
|
+
const id = call.id ?? '';
|
|
179
|
+
if (typeof id !== 'string')
|
|
180
|
+
throw new Error('Accepted tool call ID must be a string');
|
|
181
|
+
if (id !== '') providerIds.add(id);
|
|
182
|
+
const identityBytes =
|
|
183
|
+
Buffer.byteLength(id, 'utf8') + Buffer.byteLength(call.name, 'utf8');
|
|
184
|
+
const args = serializeToolArguments(
|
|
185
|
+
call.args,
|
|
186
|
+
maxBytes - bufferedBytes - identityBytes
|
|
187
|
+
);
|
|
188
|
+
const bytes = identityBytes + Buffer.byteLength(args, 'utf8');
|
|
189
|
+
bufferedBytes += bytes;
|
|
190
|
+
calls.push({
|
|
191
|
+
id,
|
|
192
|
+
name: call.name,
|
|
193
|
+
arguments: args,
|
|
194
|
+
bytes,
|
|
195
|
+
agentId: result.agentId,
|
|
196
|
+
messageId: result.messageId,
|
|
197
|
+
acceptedId: result.id,
|
|
198
|
+
});
|
|
199
|
+
}
|
|
200
|
+
acceptedTerminalKind = 'tool_call';
|
|
201
|
+
} catch (error) {
|
|
202
|
+
abort();
|
|
203
|
+
throw error;
|
|
204
|
+
}
|
|
205
|
+
};
|
|
206
|
+
|
|
207
|
+
return {
|
|
208
|
+
handlers: {
|
|
209
|
+
[GraphEvents.ON_MODEL_TOOLS_CLAIMED]: {
|
|
210
|
+
handle: (event, data): void => {
|
|
211
|
+
if (
|
|
212
|
+
event !== GraphEvents.ON_MODEL_TOOLS_CLAIMED ||
|
|
213
|
+
data == null ||
|
|
214
|
+
!('type' in data) ||
|
|
215
|
+
data.type !== 'model_tools_claimed'
|
|
216
|
+
)
|
|
217
|
+
return;
|
|
218
|
+
if (phase !== 'open')
|
|
219
|
+
throw new Error('Tool projection is already finalized');
|
|
220
|
+
checkCancellation();
|
|
221
|
+
discard(data.agentId, data.messageId);
|
|
222
|
+
},
|
|
223
|
+
},
|
|
224
|
+
[GraphEvents.ON_MODEL_RESPONSE]: {
|
|
225
|
+
handle: (event, data): void => {
|
|
226
|
+
if (
|
|
227
|
+
event !== GraphEvents.ON_MODEL_RESPONSE ||
|
|
228
|
+
data == null ||
|
|
229
|
+
!('type' in data) ||
|
|
230
|
+
data.type !== 'model_response'
|
|
231
|
+
)
|
|
232
|
+
return;
|
|
233
|
+
accept(data);
|
|
234
|
+
},
|
|
235
|
+
},
|
|
236
|
+
},
|
|
237
|
+
abort,
|
|
238
|
+
finish: (): void => {
|
|
239
|
+
if (phase === 'finished' || phase === 'emitting') return;
|
|
240
|
+
checkCancellation();
|
|
241
|
+
previousChunkKind = tracker?.lastChunkKind;
|
|
242
|
+
phase = 'emitting';
|
|
243
|
+
try {
|
|
244
|
+
if (toolCalls.size !== 0)
|
|
245
|
+
throw new Error(
|
|
246
|
+
'Tool projection output was modified during collection'
|
|
247
|
+
);
|
|
248
|
+
// Do not allocate synthetic IDs until every accepted response has arrived.
|
|
249
|
+
// This reserves provider IDs even when they occur in a later invocation.
|
|
250
|
+
const usedIds = new Set<string>();
|
|
251
|
+
const ready: OpenAIToolCall[] = [];
|
|
252
|
+
let reservedBytes = bufferedBytes;
|
|
253
|
+
let nextSyntheticId = 0;
|
|
254
|
+
for (const call of calls) {
|
|
255
|
+
let id = call.id;
|
|
256
|
+
if (id === '' || usedIds.has(id)) {
|
|
257
|
+
do {
|
|
258
|
+
id = `call_${nextSyntheticId++}`;
|
|
259
|
+
} while (providerIds.has(id) || usedIds.has(id));
|
|
260
|
+
reservedBytes +=
|
|
261
|
+
Buffer.byteLength(id, 'utf8') -
|
|
262
|
+
Buffer.byteLength(call.id, 'utf8');
|
|
263
|
+
if (reservedBytes > maxBytes)
|
|
264
|
+
throw new Error('Tool projection buffer limit exceeded');
|
|
265
|
+
}
|
|
266
|
+
usedIds.add(id);
|
|
267
|
+
ready.push(
|
|
268
|
+
Object.freeze({
|
|
269
|
+
id,
|
|
270
|
+
type: 'function',
|
|
271
|
+
function: Object.freeze({
|
|
272
|
+
name: call.name,
|
|
273
|
+
arguments: call.arguments,
|
|
274
|
+
}),
|
|
275
|
+
})
|
|
276
|
+
);
|
|
277
|
+
}
|
|
278
|
+
if (tracker != null && ready.length > 0 && !tracker.hasRole) {
|
|
279
|
+
tracker.hasRole = true;
|
|
280
|
+
emitDelta({ role: 'assistant' });
|
|
281
|
+
checkCancellation();
|
|
282
|
+
}
|
|
283
|
+
for (let index = 0; index < ready.length; index++) {
|
|
284
|
+
checkCancellation();
|
|
285
|
+
const call = ready[index];
|
|
286
|
+
toolCalls.set(index, call);
|
|
287
|
+
if (tracker != null) tracker.lastChunkKind = acceptedTerminalKind;
|
|
288
|
+
emitDelta({
|
|
289
|
+
tool_calls: [
|
|
290
|
+
{
|
|
291
|
+
index,
|
|
292
|
+
id: call.id,
|
|
293
|
+
type: 'function',
|
|
294
|
+
function: { name: call.function.name, arguments: '' },
|
|
295
|
+
},
|
|
296
|
+
],
|
|
297
|
+
});
|
|
298
|
+
checkCancellation();
|
|
299
|
+
emitDelta({
|
|
300
|
+
tool_calls: [
|
|
301
|
+
{ index, function: { arguments: call.function.arguments } },
|
|
302
|
+
],
|
|
303
|
+
});
|
|
304
|
+
checkCancellation();
|
|
305
|
+
}
|
|
306
|
+
if (tracker != null && acceptedTerminalKind != null) {
|
|
307
|
+
tracker.lastChunkKind = acceptedTerminalKind;
|
|
308
|
+
}
|
|
309
|
+
phase = 'finished';
|
|
310
|
+
} catch (error) {
|
|
311
|
+
abort();
|
|
312
|
+
throw error;
|
|
313
|
+
} finally {
|
|
314
|
+
release();
|
|
315
|
+
}
|
|
316
|
+
},
|
|
317
|
+
};
|
|
318
|
+
}
|
package/src/run.ts
CHANGED
|
@@ -468,6 +468,7 @@ export class Run<_T extends t.BaseGraphState> {
|
|
|
468
468
|
private langfuse?: t.LangfuseConfig;
|
|
469
469
|
private toolOutputReferences?: t.ToolOutputReferencesConfig;
|
|
470
470
|
private eagerEventToolExecution?: t.EagerEventToolExecutionConfig;
|
|
471
|
+
private clientDelegatedToolNames?: readonly string[];
|
|
471
472
|
private codeSessionToolNames?: string[];
|
|
472
473
|
private interruptingToolNames?: string[];
|
|
473
474
|
private toolExecution?: t.ToolExecutionConfig;
|
|
@@ -549,12 +550,19 @@ export class Run<_T extends t.BaseGraphState> {
|
|
|
549
550
|
}
|
|
550
551
|
}
|
|
551
552
|
|
|
553
|
+
if (
|
|
554
|
+
(config.clientDelegatedToolNames?.length ?? 0) > 0 &&
|
|
555
|
+
handlerRegistry.getHandler(GraphEvents.ON_MODEL_RESPONSE) == null
|
|
556
|
+
) {
|
|
557
|
+
throw new Error('Client tool delegation requires an accepted-result handler');
|
|
558
|
+
}
|
|
552
559
|
this.handlerRegistry = handlerRegistry;
|
|
553
560
|
this.hookRegistry = config.hooks;
|
|
554
561
|
this.humanInTheLoop = config.humanInTheLoop;
|
|
555
562
|
this.langfuse = config.langfuse;
|
|
556
563
|
this.toolOutputReferences = config.toolOutputReferences;
|
|
557
564
|
this.eagerEventToolExecution = config.eagerEventToolExecution;
|
|
565
|
+
this.clientDelegatedToolNames = config.clientDelegatedToolNames;
|
|
558
566
|
this.codeSessionToolNames = config.codeSessionToolNames;
|
|
559
567
|
this.interruptingToolNames = config.interruptingToolNames;
|
|
560
568
|
this.toolExecution = config.toolExecution;
|
|
@@ -573,6 +581,9 @@ export class Run<_T extends t.BaseGraphState> {
|
|
|
573
581
|
|
|
574
582
|
/** Handle different graph types */
|
|
575
583
|
if (config.graphConfig.type === 'multi-agent') {
|
|
584
|
+
if (this.clientDelegatedToolNames != null && this.clientDelegatedToolNames.length > 0) {
|
|
585
|
+
throw new Error('Client tool delegation requires a single-agent graph');
|
|
586
|
+
}
|
|
576
587
|
this.graphRunnable = this.createMultiAgentGraph(config.graphConfig);
|
|
577
588
|
if (this.Graph) {
|
|
578
589
|
this.Graph.handlerRegistry = handlerRegistry;
|
|
@@ -663,6 +674,7 @@ export class Run<_T extends t.BaseGraphState> {
|
|
|
663
674
|
preemption: this.preemption,
|
|
664
675
|
streamLimits: this.streamLimits,
|
|
665
676
|
toolExecution: this.toolExecution,
|
|
677
|
+
clientDelegatedToolNames: this.clientDelegatedToolNames,
|
|
666
678
|
},
|
|
667
679
|
});
|
|
668
680
|
/** Propagate compile options from graph config */
|
|
@@ -1058,6 +1070,11 @@ export class Run<_T extends t.BaseGraphState> {
|
|
|
1058
1070
|
) {
|
|
1059
1071
|
return;
|
|
1060
1072
|
}
|
|
1073
|
+
// Accepted results are graph-owned, never inferred from provider/tool callbacks.
|
|
1074
|
+
if (
|
|
1075
|
+
eventName === GraphEvents.ON_MODEL_RESPONSE ||
|
|
1076
|
+
eventName === GraphEvents.ON_MODEL_TOOLS_CLAIMED
|
|
1077
|
+
) return;
|
|
1061
1078
|
const handler = this.handlerRegistry?.getHandler(eventName);
|
|
1062
1079
|
/**
|
|
1063
1080
|
* Tool completions arriving over the custom-event channel are the only
|
|
@@ -1554,6 +1571,10 @@ export class Run<_T extends t.BaseGraphState> {
|
|
|
1554
1571
|
|
|
1555
1572
|
const modelEndAt =
|
|
1556
1573
|
eventName === GraphEvents.CHAT_MODEL_END ? Date.now() : undefined;
|
|
1574
|
+
if (
|
|
1575
|
+
eventName === GraphEvents.ON_MODEL_RESPONSE ||
|
|
1576
|
+
eventName === GraphEvents.ON_MODEL_TOOLS_CLAIMED
|
|
1577
|
+
) continue;
|
|
1557
1578
|
const handler = this.handlerRegistry?.getHandler(eventName);
|
|
1558
1579
|
if (handler) {
|
|
1559
1580
|
await handler.handle(eventName, data, metadata, this.Graph);
|
|
@@ -27,6 +27,7 @@ import {
|
|
|
27
27
|
deriveSessionMessages,
|
|
28
28
|
releaseSessionProjection,
|
|
29
29
|
} from './sessionProjection';
|
|
30
|
+
import { FADING_TIER_VERSION, isFadingTier } from '@/messages/fading';
|
|
30
31
|
import { createSummarizeNode } from '@/summarization/node';
|
|
31
32
|
import { resolveStreamLimits } from '@/llm/streamLimits';
|
|
32
33
|
import { JsonlSessionStore } from './JsonlSessionStore';
|
|
@@ -34,7 +35,6 @@ import { AgentContext } from '@/agents/AgentContext';
|
|
|
34
35
|
import { ContentTypes, GraphEvents } from '@/common';
|
|
35
36
|
import { createRunId, createSessionId } from './ids';
|
|
36
37
|
import { deriveMessages } from './deriveMessages';
|
|
37
|
-
import { isFadingTier } from '@/messages/fading';
|
|
38
38
|
import { createRunHandlers } from './handlers';
|
|
39
39
|
import { Run } from '@/run';
|
|
40
40
|
|
|
@@ -420,7 +420,7 @@ function mergeFadingTier(
|
|
|
420
420
|
masked !== current.masked ||
|
|
421
421
|
masked !== incoming.masked;
|
|
422
422
|
return {
|
|
423
|
-
v:
|
|
423
|
+
v: FADING_TIER_VERSION,
|
|
424
424
|
budgetTokens,
|
|
425
425
|
masked,
|
|
426
426
|
...(latched ? { latched: true } : {}),
|
package/src/stream.ts
CHANGED
|
@@ -56,6 +56,7 @@ import {
|
|
|
56
56
|
truncateToolResultContent,
|
|
57
57
|
} from '@/utils/truncation';
|
|
58
58
|
import { resolveToolOutcome, outcomeFieldsFromResult } from '@/tools/intentArg';
|
|
59
|
+
import { snapshotValidatedModelChunk } from '@/graphs/acceptedModelResponse';
|
|
59
60
|
import { TOOL_OUTPUT_REF_PATTERN } from '@/tools/toolOutputReferences';
|
|
60
61
|
import { PreparedSubagentError } from '@/tools/preparedSubagents';
|
|
61
62
|
import { isReasoningContentBlock } from '@/messages/core';
|
|
@@ -746,6 +747,10 @@ function createEagerToolExecutionPlan(args: {
|
|
|
746
747
|
toolCall.id == null ||
|
|
747
748
|
toolCall.id === '' ||
|
|
748
749
|
toolCall.name === '' ||
|
|
750
|
+
// A serialized parsed call is not a prepared executable object;
|
|
751
|
+
// parsing is permitted only on sealed raw tool_call_chunks.
|
|
752
|
+
typeof toolCall.args === 'string' ||
|
|
753
|
+
graph.clientDelegatedToolNames?.has(toolCall.name) === true ||
|
|
749
754
|
(!skipExisting && graph.eagerEventToolExecutions.has(toolCall.id))
|
|
750
755
|
)
|
|
751
756
|
) {
|
|
@@ -794,6 +799,9 @@ function startEagerToolExecutions(args: {
|
|
|
794
799
|
skipExisting?: boolean;
|
|
795
800
|
}): void {
|
|
796
801
|
const { graph, metadata, agentContext, toolCalls, skipExisting } = args;
|
|
802
|
+
// A later call in the same model turn may be client-delegated. Do not
|
|
803
|
+
// pre-execute an earlier SDK call in a run that rejects mixed batches.
|
|
804
|
+
if ((graph.clientDelegatedToolNames?.size ?? 0) > 0) return;
|
|
797
805
|
const entries = createEagerToolExecutionPlan({
|
|
798
806
|
graph,
|
|
799
807
|
metadata,
|
|
@@ -1345,6 +1353,10 @@ function startPreparedSubagents(
|
|
|
1345
1353
|
metadata?: Record<string, unknown>
|
|
1346
1354
|
): void {
|
|
1347
1355
|
const attempt = resolveGenerationKey(metadata);
|
|
1356
|
+
if ((graph.clientDelegatedToolNames?.size ?? 0) > 0) return;
|
|
1357
|
+
// A parsed string call is not executable even if the same event carries
|
|
1358
|
+
// sealed raw fragments. Wait for a separately validated complete call.
|
|
1359
|
+
if (chunk.tool_calls?.some((call) => typeof call.args === 'string') === true) return;
|
|
1348
1360
|
if (
|
|
1349
1361
|
(graph as Partial<StandardGraph>).canPrestartSubagents?.(agentContext) !==
|
|
1350
1362
|
true ||
|
|
@@ -1389,6 +1401,9 @@ function startPreparedSubagents(
|
|
|
1389
1401
|
for (const call of calls) {
|
|
1390
1402
|
if (
|
|
1391
1403
|
call.name === Constants.SUBAGENT &&
|
|
1404
|
+
graph.clientDelegatedToolNames?.has(call.name) !== true &&
|
|
1405
|
+
typeof call.args === 'object' &&
|
|
1406
|
+
!Array.isArray(call.args) &&
|
|
1392
1407
|
!hasToolOutputReference(call.args)
|
|
1393
1408
|
) {
|
|
1394
1409
|
graph.prestartSubagent(call, attempt, agentContext);
|
|
@@ -1643,7 +1658,7 @@ export class ChatModelStreamHandler implements t.EventHandler {
|
|
|
1643
1658
|
return;
|
|
1644
1659
|
}
|
|
1645
1660
|
|
|
1646
|
-
|
|
1661
|
+
let chunk = data.chunk as Partial<AIMessageChunk>;
|
|
1647
1662
|
|
|
1648
1663
|
/** Attempts stamp their breaker epoch into event metadata; a mismatch
|
|
1649
1664
|
* marks a straggling chunk from a failed run that outlived
|
|
@@ -1681,6 +1696,10 @@ export class ChatModelStreamHandler implements t.EventHandler {
|
|
|
1681
1696
|
}
|
|
1682
1697
|
};
|
|
1683
1698
|
|
|
1699
|
+
// Callback delivery can beat the producer's iterator. Validate before
|
|
1700
|
+
// accounting, run steps, or eager dispatch reads raw tool descriptors.
|
|
1701
|
+
chunk = snapshotValidatedModelChunk(chunk as AIMessageChunk);
|
|
1702
|
+
|
|
1684
1703
|
/**
|
|
1685
1704
|
* Enforced before every content-specific early return below
|
|
1686
1705
|
* (server-tool results, deferred mixed reasoning, late OpenRouter
|
|
@@ -1885,6 +1904,7 @@ export class ChatModelStreamHandler implements t.EventHandler {
|
|
|
1885
1904
|
chunk.response_metadata as Record<string, unknown> | undefined
|
|
1886
1905
|
);
|
|
1887
1906
|
const canStreamEager =
|
|
1907
|
+
chunk.tool_calls?.some((call) => typeof call.args === 'string') !== true &&
|
|
1888
1908
|
(allowSequentialSeal || hasExplicitStreamedToolCallSeals(chunk)) &&
|
|
1889
1909
|
!hasPotentialDirectToolInStreamContext({ graph, agentContext }) &&
|
|
1890
1910
|
isEagerToolExecutionEnabledForBatch({ graph, metadata, agentContext });
|
|
@@ -0,0 +1,85 @@
|
|
|
1
|
+
import type { ArtifactTruncation, ArtifactTruncationReason } from '@/types';
|
|
2
|
+
|
|
3
|
+
const MAX_REPORTED_TRUNCATED_PATHS = 20;
|
|
4
|
+
const ARTIFACT_TRUNCATION_REASONS = new Set<string>([
|
|
5
|
+
'max_files',
|
|
6
|
+
'depth',
|
|
7
|
+
'size',
|
|
8
|
+
'path',
|
|
9
|
+
'unreadable',
|
|
10
|
+
]);
|
|
11
|
+
|
|
12
|
+
function isNonNegativeInteger(value: unknown): value is number {
|
|
13
|
+
return typeof value === 'number' && Number.isSafeInteger(value) && value >= 0;
|
|
14
|
+
}
|
|
15
|
+
|
|
16
|
+
export function normalizeArtifactTruncation(
|
|
17
|
+
value: unknown
|
|
18
|
+
): ArtifactTruncation | undefined {
|
|
19
|
+
if (value == null || typeof value !== 'object' || Array.isArray(value)) {
|
|
20
|
+
return undefined;
|
|
21
|
+
}
|
|
22
|
+
|
|
23
|
+
const candidate = value as Partial<ArtifactTruncation>;
|
|
24
|
+
if (
|
|
25
|
+
candidate.code !== 'artifact_truncated' ||
|
|
26
|
+
!isNonNegativeInteger(candidate.skipped_count) ||
|
|
27
|
+
candidate.skipped_count === 0 ||
|
|
28
|
+
candidate.reasons == null ||
|
|
29
|
+
typeof candidate.reasons !== 'object' ||
|
|
30
|
+
Array.isArray(candidate.reasons) ||
|
|
31
|
+
!Array.isArray(candidate.skipped) ||
|
|
32
|
+
candidate.skipped.length > MAX_REPORTED_TRUNCATED_PATHS ||
|
|
33
|
+
candidate.skipped.length > candidate.skipped_count ||
|
|
34
|
+
candidate.skipped.some((path) => typeof path !== 'string')
|
|
35
|
+
) {
|
|
36
|
+
return undefined;
|
|
37
|
+
}
|
|
38
|
+
|
|
39
|
+
const reasons: ArtifactTruncation['reasons'] = {};
|
|
40
|
+
let reasonTotal = 0;
|
|
41
|
+
for (const [reason, count] of Object.entries(candidate.reasons)) {
|
|
42
|
+
if (
|
|
43
|
+
!ARTIFACT_TRUNCATION_REASONS.has(reason) ||
|
|
44
|
+
!isNonNegativeInteger(count)
|
|
45
|
+
) {
|
|
46
|
+
return undefined;
|
|
47
|
+
}
|
|
48
|
+
reasonTotal += count;
|
|
49
|
+
if (reasonTotal > candidate.skipped_count) {
|
|
50
|
+
return undefined;
|
|
51
|
+
}
|
|
52
|
+
reasons[reason as ArtifactTruncationReason] = count;
|
|
53
|
+
}
|
|
54
|
+
|
|
55
|
+
if (reasonTotal !== candidate.skipped_count) {
|
|
56
|
+
return undefined;
|
|
57
|
+
}
|
|
58
|
+
|
|
59
|
+
return {
|
|
60
|
+
code: candidate.code,
|
|
61
|
+
reasons,
|
|
62
|
+
skipped: [...candidate.skipped],
|
|
63
|
+
skipped_count: candidate.skipped_count,
|
|
64
|
+
};
|
|
65
|
+
}
|
|
66
|
+
|
|
67
|
+
export function appendArtifactTruncationWarning(
|
|
68
|
+
output: string,
|
|
69
|
+
truncation: ArtifactTruncation | undefined
|
|
70
|
+
): string {
|
|
71
|
+
if (truncation == null) {
|
|
72
|
+
return output;
|
|
73
|
+
}
|
|
74
|
+
|
|
75
|
+
const reasons = Object.entries(truncation.reasons)
|
|
76
|
+
.map(([reason, count]) => `${reason}: ${count}`)
|
|
77
|
+
.join(', ');
|
|
78
|
+
const shown = truncation.skipped.length;
|
|
79
|
+
const paths =
|
|
80
|
+
shown > 0
|
|
81
|
+
? ` Not delivered: ${truncation.skipped.join(', ')}${shown < truncation.skipped_count ? ` (${shown} of ${truncation.skipped_count} shown)` : ''}.`
|
|
82
|
+
: '';
|
|
83
|
+
const warning = `Note: ${truncation.skipped_count} file(s) were omitted from delivery${reasons ? ` (${reasons})` : ''}.${paths} Write fewer files per execution or combine them into an archive. The code itself ran; do not rerun automatically because it may have had side effects.`;
|
|
84
|
+
return `${output.trimEnd()}\n${warning}\n`;
|
|
85
|
+
}
|
|
@@ -23,14 +23,18 @@ import {
|
|
|
23
23
|
appendArtifactDeliveryWarning,
|
|
24
24
|
normalizeArtifactDeliveryFailure,
|
|
25
25
|
} from '@/tools/ArtifactDelivery';
|
|
26
|
-
import {
|
|
26
|
+
import {
|
|
27
|
+
appendArtifactTruncationWarning,
|
|
28
|
+
normalizeArtifactTruncation,
|
|
29
|
+
} from '@/tools/ArtifactTruncation';
|
|
27
30
|
import { appendExecutionArtifactFileSummary } from '@/tools/CodeSessionFileSummary';
|
|
28
31
|
import { resolveAttachedWorkspaceInstanceId } from '@/tools/workspaceIdentity';
|
|
32
|
+
import { prepareBashProgrammaticCode } from './BashProgrammaticToolCalling';
|
|
33
|
+
import { logCodeApiDiagnostic } from '@/tools/diagnostics';
|
|
34
|
+
import { makeRequest } from './ProgrammaticToolCalling';
|
|
29
35
|
import { resolveFetchProxyAgent } from '@/utils/proxy';
|
|
30
36
|
import { INTENT_PROPERTY } from '@/tools/intentArg';
|
|
31
37
|
import { Constants } from '@/common';
|
|
32
|
-
import { prepareBashProgrammaticCode } from './BashProgrammaticToolCalling';
|
|
33
|
-
import { makeRequest } from './ProgrammaticToolCalling';
|
|
34
38
|
|
|
35
39
|
config();
|
|
36
40
|
|
|
@@ -407,6 +411,13 @@ function createBashExecutionTool(
|
|
|
407
411
|
outputWithReminder,
|
|
408
412
|
artifactDelivery
|
|
409
413
|
);
|
|
414
|
+
const artifactTruncation = normalizeArtifactTruncation(
|
|
415
|
+
result.artifact_truncation
|
|
416
|
+
);
|
|
417
|
+
const outputWithWarnings = appendArtifactTruncationWarning(
|
|
418
|
+
outputWithDeliveryWarning,
|
|
419
|
+
artifactTruncation
|
|
420
|
+
);
|
|
410
421
|
const hasFiles = result.files != null && result.files.length > 0;
|
|
411
422
|
const deletionEcho =
|
|
412
423
|
result.deleted_files != null
|
|
@@ -422,11 +433,11 @@ function createBashExecutionTool(
|
|
|
422
433
|
return [
|
|
423
434
|
hasWorkspace
|
|
424
435
|
? appendExecutionArtifactFileSummary(
|
|
425
|
-
|
|
436
|
+
outputWithWarnings,
|
|
426
437
|
result.files
|
|
427
438
|
)
|
|
428
439
|
: appendCodeSessionFileSummary(
|
|
429
|
-
|
|
440
|
+
outputWithWarnings,
|
|
430
441
|
result.files
|
|
431
442
|
),
|
|
432
443
|
(hasFiles
|
|
@@ -436,6 +447,9 @@ function createBashExecutionTool(
|
|
|
436
447
|
...(artifactDelivery != null
|
|
437
448
|
? { artifact_delivery: artifactDelivery }
|
|
438
449
|
: {}),
|
|
450
|
+
...(artifactTruncation != null
|
|
451
|
+
? { artifact_truncation: artifactTruncation }
|
|
452
|
+
: {}),
|
|
439
453
|
...deletionEcho,
|
|
440
454
|
...runtimeEcho,
|
|
441
455
|
}
|
|
@@ -444,6 +458,9 @@ function createBashExecutionTool(
|
|
|
444
458
|
...(artifactDelivery != null
|
|
445
459
|
? { artifact_delivery: artifactDelivery }
|
|
446
460
|
: {}),
|
|
461
|
+
...(artifactTruncation != null
|
|
462
|
+
? { artifact_truncation: artifactTruncation }
|
|
463
|
+
: {}),
|
|
447
464
|
...deletionEcho,
|
|
448
465
|
...runtimeEcho,
|
|
449
466
|
}) satisfies t.CodeExecutionArtifact,
|
|
@@ -9,6 +9,10 @@ import {
|
|
|
9
9
|
appendArtifactDeliveryWarning,
|
|
10
10
|
normalizeArtifactDeliveryFailure,
|
|
11
11
|
} from '@/tools/ArtifactDelivery';
|
|
12
|
+
import {
|
|
13
|
+
appendArtifactTruncationWarning,
|
|
14
|
+
normalizeArtifactTruncation,
|
|
15
|
+
} from '@/tools/ArtifactTruncation';
|
|
12
16
|
import {
|
|
13
17
|
describeCodeApiError,
|
|
14
18
|
logCodeApiDiagnostic,
|
|
@@ -671,6 +675,13 @@ function createCodeExecutionTool(
|
|
|
671
675
|
outputWithReminder,
|
|
672
676
|
artifactDelivery
|
|
673
677
|
);
|
|
678
|
+
const artifactTruncation = normalizeArtifactTruncation(
|
|
679
|
+
result.artifact_truncation
|
|
680
|
+
);
|
|
681
|
+
const outputWithWarnings = appendArtifactTruncationWarning(
|
|
682
|
+
outputWithDeliveryWarning,
|
|
683
|
+
artifactTruncation
|
|
684
|
+
);
|
|
674
685
|
const hasFiles = result.files != null && result.files.length > 0;
|
|
675
686
|
const deletionEcho =
|
|
676
687
|
result.deleted_files != null
|
|
@@ -687,7 +698,7 @@ function createCodeExecutionTool(
|
|
|
687
698
|
}
|
|
688
699
|
: {};
|
|
689
700
|
return [
|
|
690
|
-
appendCodeSessionFileSummary(
|
|
701
|
+
appendCodeSessionFileSummary(outputWithWarnings, result.files),
|
|
691
702
|
(hasFiles
|
|
692
703
|
? {
|
|
693
704
|
session_id: result.session_id,
|
|
@@ -695,6 +706,9 @@ function createCodeExecutionTool(
|
|
|
695
706
|
...(artifactDelivery != null
|
|
696
707
|
? { artifact_delivery: artifactDelivery }
|
|
697
708
|
: {}),
|
|
709
|
+
...(artifactTruncation != null
|
|
710
|
+
? { artifact_truncation: artifactTruncation }
|
|
711
|
+
: {}),
|
|
698
712
|
...deletionEcho,
|
|
699
713
|
...runtimeEcho,
|
|
700
714
|
}
|
|
@@ -703,6 +717,9 @@ function createCodeExecutionTool(
|
|
|
703
717
|
...(artifactDelivery != null
|
|
704
718
|
? { artifact_delivery: artifactDelivery }
|
|
705
719
|
: {}),
|
|
720
|
+
...(artifactTruncation != null
|
|
721
|
+
? { artifact_truncation: artifactTruncation }
|
|
722
|
+
: {}),
|
|
706
723
|
...deletionEcho,
|
|
707
724
|
...runtimeEcho,
|
|
708
725
|
}) satisfies t.CodeExecutionArtifact,
|