@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
|
@@ -33,6 +33,7 @@ import {
|
|
|
33
33
|
HARD_MAX_TOOL_RESULT_CHARS,
|
|
34
34
|
} from '@/utils/truncation';
|
|
35
35
|
import { withInstructionlessHandoffCue } from '@/messages/handoffCue';
|
|
36
|
+
import { HandoffRouting, handoffStateAnnotation } from './handoff';
|
|
36
37
|
import { serializeToolContentBounded } from '@/utils/toolContent';
|
|
37
38
|
import { Constants, MULTI_AGENT_GRAPH_RUN_NAME } from '@/common';
|
|
38
39
|
import { StandardGraph } from './Graph';
|
|
@@ -337,6 +338,7 @@ export class MultiAgentGraph extends StandardGraph {
|
|
|
337
338
|
private handoffSourceIds = new Set<string>();
|
|
338
339
|
private readonly resultAgentId?: string;
|
|
339
340
|
private readonly memberRecursionLimit?: number;
|
|
341
|
+
private reachableAgentIds?: Set<string>;
|
|
340
342
|
private handoffPromptLabels: Map<string, Set<string>> = new Map();
|
|
341
343
|
/**
|
|
342
344
|
* Map of agentId to parallel group info.
|
|
@@ -379,9 +381,115 @@ export class MultiAgentGraph extends StandardGraph {
|
|
|
379
381
|
this.categorizeEdges();
|
|
380
382
|
this.validateCommandRoutedDirectEdges();
|
|
381
383
|
this.analyzeGraph();
|
|
384
|
+
if (input.entryAgentId != null) {
|
|
385
|
+
if (!this.agentContexts.has(input.entryAgentId)) {
|
|
386
|
+
throw new Error(
|
|
387
|
+
`MultiAgentGraph: unknown entryAgentId "${input.entryAgentId}"`
|
|
388
|
+
);
|
|
389
|
+
}
|
|
390
|
+
this.startingNodes = new Set([input.entryAgentId]);
|
|
391
|
+
this.defaultAgentId = input.entryAgentId;
|
|
392
|
+
this.computeParallelCapability();
|
|
393
|
+
this.reachableAgentIds = this.resolveEntryReachability(
|
|
394
|
+
input.entryAgentId
|
|
395
|
+
);
|
|
396
|
+
}
|
|
397
|
+
if (
|
|
398
|
+
input.maxHandoffs != null &&
|
|
399
|
+
(!Number.isSafeInteger(input.maxHandoffs) || input.maxHandoffs < 0)
|
|
400
|
+
) {
|
|
401
|
+
throw new Error(
|
|
402
|
+
'MultiAgentGraph: maxHandoffs must be a non-negative safe integer'
|
|
403
|
+
);
|
|
404
|
+
}
|
|
405
|
+
const directDestinations = new Map<string, Set<string>>();
|
|
406
|
+
for (const edge of this.directEdges) {
|
|
407
|
+
if (edge.handoffScope != null)
|
|
408
|
+
throw new Error('handoffScope is only valid on handoff edges');
|
|
409
|
+
for (const source of Array.isArray(edge.from) ? edge.from : [edge.from]) {
|
|
410
|
+
const targets = directDestinations.get(source) ?? new Set<string>();
|
|
411
|
+
for (const target of Array.isArray(edge.to) ? edge.to : [edge.to])
|
|
412
|
+
targets.add(target);
|
|
413
|
+
directDestinations.set(source, targets);
|
|
414
|
+
}
|
|
415
|
+
}
|
|
416
|
+
this.handoffRouting = new HandoffRouting(
|
|
417
|
+
this.startingNodes.values().next().value ?? this.defaultAgentId,
|
|
418
|
+
input.maxHandoffs,
|
|
419
|
+
this.startingNodes.size > 1 ||
|
|
420
|
+
[...directDestinations.values()].some((targets) => targets.size > 1)
|
|
421
|
+
);
|
|
422
|
+
this.validateHandoffScopes();
|
|
382
423
|
this.createHandoffTools();
|
|
383
424
|
}
|
|
384
425
|
|
|
426
|
+
private validateHandoffScopes(): void {
|
|
427
|
+
const scopes = new Map<string, string>();
|
|
428
|
+
for (const edge of this.handoffEdges) {
|
|
429
|
+
const scope = edge.handoffScope ?? 'turn';
|
|
430
|
+
if (!['turn', 'conversation'].includes(scope))
|
|
431
|
+
throw new Error('Invalid handoffScope');
|
|
432
|
+
for (const source of Array.isArray(edge.from) ? edge.from : [edge.from]) {
|
|
433
|
+
const destinations = Array.isArray(edge.to) ? edge.to : [edge.to];
|
|
434
|
+
const targets =
|
|
435
|
+
edge.condition != null ? ['conditional_transfer'] : destinations;
|
|
436
|
+
for (const target of targets) {
|
|
437
|
+
const key = JSON.stringify([source, target]);
|
|
438
|
+
const previous = scopes.get(key);
|
|
439
|
+
if (previous != null && previous !== scope)
|
|
440
|
+
throw new Error(
|
|
441
|
+
'Conflicting handoffScope for the same transfer tool'
|
|
442
|
+
);
|
|
443
|
+
scopes.set(key, scope);
|
|
444
|
+
}
|
|
445
|
+
}
|
|
446
|
+
}
|
|
447
|
+
}
|
|
448
|
+
|
|
449
|
+
private resolveEntryReachability(entryAgentId: string): Set<string> {
|
|
450
|
+
const reachable = new Set([entryAgentId]);
|
|
451
|
+
const direct = new Set(this.directEdges);
|
|
452
|
+
const outgoing = new Map<string, t.GraphEdge[]>();
|
|
453
|
+
const remaining = new Map<t.GraphEdge, number>();
|
|
454
|
+
for (const edge of this.edges) {
|
|
455
|
+
const sources = new Set(
|
|
456
|
+
Array.isArray(edge.from) ? edge.from : [edge.from]
|
|
457
|
+
);
|
|
458
|
+
remaining.set(edge, direct.has(edge) ? sources.size : 1);
|
|
459
|
+
for (const source of sources) {
|
|
460
|
+
const edges = outgoing.get(source) ?? [];
|
|
461
|
+
edges.push(edge);
|
|
462
|
+
outgoing.set(source, edges);
|
|
463
|
+
}
|
|
464
|
+
}
|
|
465
|
+
const queue = [entryAgentId];
|
|
466
|
+
for (let i = 0; i < queue.length; i++) {
|
|
467
|
+
for (const edge of outgoing.get(queue[i]) ?? []) {
|
|
468
|
+
const pending = remaining.get(edge) ?? 0;
|
|
469
|
+
if (pending === 0) continue;
|
|
470
|
+
remaining.set(edge, pending - 1);
|
|
471
|
+
if (pending > 1) continue;
|
|
472
|
+
for (const target of Array.isArray(edge.to) ? edge.to : [edge.to]) {
|
|
473
|
+
if (reachable.has(target)) continue;
|
|
474
|
+
reachable.add(target);
|
|
475
|
+
queue.push(target);
|
|
476
|
+
}
|
|
477
|
+
}
|
|
478
|
+
}
|
|
479
|
+
for (const edge of this.directEdges) {
|
|
480
|
+
const sources = Array.isArray(edge.from) ? edge.from : [edge.from];
|
|
481
|
+
if (
|
|
482
|
+
sources.some((source) => reachable.has(source)) &&
|
|
483
|
+
!sources.every((source) => reachable.has(source))
|
|
484
|
+
) {
|
|
485
|
+
throw new Error(
|
|
486
|
+
'entryAgentId cannot satisfy a grouped direct edge prerequisite'
|
|
487
|
+
);
|
|
488
|
+
}
|
|
489
|
+
}
|
|
490
|
+
return reachable;
|
|
491
|
+
}
|
|
492
|
+
|
|
385
493
|
/**
|
|
386
494
|
* Fails fast when an edge references an agent that is not in
|
|
387
495
|
* `agentContexts`. Without this check, the underlying LangGraph
|
|
@@ -762,6 +870,11 @@ export class MultiAgentGraph extends StandardGraph {
|
|
|
762
870
|
destination = Array.isArray(result) ? result[0] : destinations[0];
|
|
763
871
|
}
|
|
764
872
|
|
|
873
|
+
if (!destinations.includes(destination)) {
|
|
874
|
+
throw new Error(
|
|
875
|
+
`Conditional handoff selected undeclared destination "${destination}"`
|
|
876
|
+
);
|
|
877
|
+
}
|
|
765
878
|
const handoffInstructions = getHandoffInstructions(
|
|
766
879
|
input,
|
|
767
880
|
promptKey,
|
|
@@ -789,7 +902,15 @@ export class MultiAgentGraph extends StandardGraph {
|
|
|
789
902
|
|
|
790
903
|
return new Command({
|
|
791
904
|
goto: destination,
|
|
792
|
-
update: {
|
|
905
|
+
update: {
|
|
906
|
+
messages: state.messages.concat(toolMessage),
|
|
907
|
+
handoffRequest: {
|
|
908
|
+
sourceAgentId,
|
|
909
|
+
targetAgentId: destination,
|
|
910
|
+
toolCallId,
|
|
911
|
+
scope: edge.handoffScope ?? 'turn',
|
|
912
|
+
},
|
|
913
|
+
},
|
|
793
914
|
graph: Command.PARENT,
|
|
794
915
|
});
|
|
795
916
|
},
|
|
@@ -926,7 +1047,15 @@ export class MultiAgentGraph extends StandardGraph {
|
|
|
926
1047
|
|
|
927
1048
|
return new Command({
|
|
928
1049
|
goto: destination,
|
|
929
|
-
update: {
|
|
1050
|
+
update: {
|
|
1051
|
+
messages: filteredMessages,
|
|
1052
|
+
handoffRequest: {
|
|
1053
|
+
sourceAgentId,
|
|
1054
|
+
targetAgentId: destination,
|
|
1055
|
+
toolCallId,
|
|
1056
|
+
scope: edge.handoffScope ?? 'turn',
|
|
1057
|
+
},
|
|
1058
|
+
},
|
|
930
1059
|
graph: Command.PARENT,
|
|
931
1060
|
});
|
|
932
1061
|
},
|
|
@@ -1274,6 +1403,7 @@ export class MultiAgentGraph extends StandardGraph {
|
|
|
1274
1403
|
default: () => undefined,
|
|
1275
1404
|
}),
|
|
1276
1405
|
runStepState: this.createRunStepStateAnnotation(),
|
|
1406
|
+
handoffState: handoffStateAnnotation(),
|
|
1277
1407
|
});
|
|
1278
1408
|
|
|
1279
1409
|
const builder = new StateGraph(StateAnnotation);
|
|
@@ -1298,9 +1428,16 @@ export class MultiAgentGraph extends StandardGraph {
|
|
|
1298
1428
|
const agentIds =
|
|
1299
1429
|
summarizeOnlyAgentId != null
|
|
1300
1430
|
? [summarizeOnlyAgentId]
|
|
1301
|
-
: [...this.agentContexts.keys()];
|
|
1431
|
+
: [...(this.reachableAgentIds ?? this.agentContexts.keys())];
|
|
1302
1432
|
const handoffEdges = summarizeOnlyAgentId != null ? [] : this.handoffEdges;
|
|
1303
|
-
const directEdges =
|
|
1433
|
+
const directEdges =
|
|
1434
|
+
summarizeOnlyAgentId != null
|
|
1435
|
+
? []
|
|
1436
|
+
: this.directEdges.filter((edge) => {
|
|
1437
|
+
if (this.reachableAgentIds == null) return true;
|
|
1438
|
+
const sources = Array.isArray(edge.from) ? edge.from : [edge.from];
|
|
1439
|
+
return sources.every((id) => this.reachableAgentIds!.has(id));
|
|
1440
|
+
});
|
|
1304
1441
|
|
|
1305
1442
|
// Add all agents as complete subgraphs
|
|
1306
1443
|
for (const agentId of agentIds) {
|
|
@@ -1348,6 +1485,7 @@ export class MultiAgentGraph extends StandardGraph {
|
|
|
1348
1485
|
state: t.MultiAgentGraphState,
|
|
1349
1486
|
config?: LangGraphRunnableConfig
|
|
1350
1487
|
): Promise<t.MultiAgentGraphState | Command> => {
|
|
1488
|
+
this.handoffRouting?.restore(state.handoffState);
|
|
1351
1489
|
let result: t.MultiAgentGraphState;
|
|
1352
1490
|
let inputMessages = state.messages;
|
|
1353
1491
|
const agentContext = this.agentContexts.get(agentId);
|
|
@@ -0,0 +1,307 @@
|
|
|
1
|
+
import { types } from 'node:util';
|
|
2
|
+
import type { AIMessageChunk } from '@langchain/core/messages';
|
|
3
|
+
import type { ToolCall } from '@langchain/core/messages/tool';
|
|
4
|
+
import type { ModelResponseEvent } from '@/types';
|
|
5
|
+
import {
|
|
6
|
+
cloneToolArguments,
|
|
7
|
+
serializeToolArguments,
|
|
8
|
+
} from '@/utils/acceptedToolArguments';
|
|
9
|
+
import { linkStreamLimitCanonical } from '@/llm/streamLimits';
|
|
10
|
+
|
|
11
|
+
const MAX_SNAPSHOT_BYTES = 4 * 1024 * 1024;
|
|
12
|
+
const MAX_SNAPSHOT_CALLS = 1024;
|
|
13
|
+
|
|
14
|
+
export class InvalidModelToolCallError extends Error {
|
|
15
|
+
constructor(message: string) {
|
|
16
|
+
super(message);
|
|
17
|
+
this.name = 'InvalidModelToolCallError';
|
|
18
|
+
}
|
|
19
|
+
}
|
|
20
|
+
|
|
21
|
+
/** Detach executable calls before dispatch. Invalid diagnostics remain available
|
|
22
|
+
* for ToolNode to synthesize paired error results; accepted projection rejects them.
|
|
23
|
+
*/
|
|
24
|
+
export function detachValidatedModelToolCalls(
|
|
25
|
+
message: AIMessageChunk,
|
|
26
|
+
partial = false
|
|
27
|
+
): void {
|
|
28
|
+
try {
|
|
29
|
+
// Inspect every collection before replacing any provider-owned data. Raw
|
|
30
|
+
// fragments are read by accounting and handlers even when tool_calls is empty.
|
|
31
|
+
const toolCalls = snapshotToolCalls(message, true, partial);
|
|
32
|
+
const chunks = snapshotToolRecords(message, 'tool_call_chunks');
|
|
33
|
+
const invalid = snapshotToolRecords(message, 'invalid_tool_calls');
|
|
34
|
+
Object.defineProperties(message, {
|
|
35
|
+
tool_calls: {
|
|
36
|
+
value: toolCalls,
|
|
37
|
+
enumerable: true,
|
|
38
|
+
writable: true,
|
|
39
|
+
configurable: true,
|
|
40
|
+
},
|
|
41
|
+
tool_call_chunks: {
|
|
42
|
+
value: chunks,
|
|
43
|
+
enumerable: true,
|
|
44
|
+
writable: true,
|
|
45
|
+
configurable: true,
|
|
46
|
+
},
|
|
47
|
+
invalid_tool_calls: {
|
|
48
|
+
value: invalid,
|
|
49
|
+
enumerable: true,
|
|
50
|
+
writable: true,
|
|
51
|
+
configurable: true,
|
|
52
|
+
},
|
|
53
|
+
});
|
|
54
|
+
} catch (error) {
|
|
55
|
+
throw new InvalidModelToolCallError(
|
|
56
|
+
error instanceof Error
|
|
57
|
+
? error.message
|
|
58
|
+
: 'Accepted model response contains non-serializable tool calls'
|
|
59
|
+
);
|
|
60
|
+
}
|
|
61
|
+
}
|
|
62
|
+
|
|
63
|
+
/** Inspect original descriptors before a getter, proxy, or custom instance can
|
|
64
|
+
* be normalized away. The bound applies to each model message independently.
|
|
65
|
+
*/
|
|
66
|
+
function snapshotToolCalls(
|
|
67
|
+
finalResponse: AIMessageChunk,
|
|
68
|
+
allowInvalidDiagnostics: boolean,
|
|
69
|
+
allowFragmentArgs = false
|
|
70
|
+
): ToolCall[] {
|
|
71
|
+
// Read own data descriptors, not accessors supplied by a custom model. Invalid
|
|
72
|
+
// diagnostics are rejected in O(1); cloning them can run getters and bypass
|
|
73
|
+
// the valid-call snapshot's byte/count limits.
|
|
74
|
+
if (types.isProxy(finalResponse)) {
|
|
75
|
+
throw new Error(
|
|
76
|
+
'Accepted model response contains non-serializable tool calls'
|
|
77
|
+
);
|
|
78
|
+
}
|
|
79
|
+
const calls = Object.getOwnPropertyDescriptor(finalResponse, 'tool_calls');
|
|
80
|
+
const diagnostics = Object.getOwnPropertyDescriptor(
|
|
81
|
+
finalResponse,
|
|
82
|
+
'invalid_tool_calls'
|
|
83
|
+
);
|
|
84
|
+
const readArray = (descriptor: PropertyDescriptor | undefined): unknown[] => {
|
|
85
|
+
if (descriptor === undefined) return [];
|
|
86
|
+
if (!('value' in descriptor)) {
|
|
87
|
+
throw new Error(
|
|
88
|
+
'Accepted model response contains non-serializable tool calls'
|
|
89
|
+
);
|
|
90
|
+
}
|
|
91
|
+
const value: unknown = descriptor.value;
|
|
92
|
+
if (value === undefined) return [];
|
|
93
|
+
if (types.isProxy(value) || !Array.isArray(value)) {
|
|
94
|
+
throw new Error(
|
|
95
|
+
'Accepted model response contains non-serializable tool calls'
|
|
96
|
+
);
|
|
97
|
+
}
|
|
98
|
+
return value;
|
|
99
|
+
};
|
|
100
|
+
if (readArray(diagnostics).length > 0 && !allowInvalidDiagnostics) {
|
|
101
|
+
throw new Error('Accepted model response contains invalid tool calls');
|
|
102
|
+
}
|
|
103
|
+
const source = readArray(calls);
|
|
104
|
+
if (!allowInvalidDiagnostics && source.length > MAX_SNAPSHOT_CALLS) {
|
|
105
|
+
throw new Error('Accepted model response exceeds snapshot limits');
|
|
106
|
+
}
|
|
107
|
+
const toolCalls: ToolCall[] = [];
|
|
108
|
+
let remaining = allowInvalidDiagnostics ? Infinity : MAX_SNAPSHOT_BYTES;
|
|
109
|
+
for (let index = 0; index < source.length; index++) {
|
|
110
|
+
const entry = Object.getOwnPropertyDescriptor(source, String(index));
|
|
111
|
+
if (entry == null || !('value' in entry) || entry.enumerable !== true) {
|
|
112
|
+
throw new Error(
|
|
113
|
+
'Accepted model response contains non-serializable tool calls'
|
|
114
|
+
);
|
|
115
|
+
}
|
|
116
|
+
const call: unknown = entry.value;
|
|
117
|
+
if (call == null || typeof call !== 'object' || types.isProxy(call)) {
|
|
118
|
+
throw new Error(
|
|
119
|
+
'Accepted model response contains non-serializable tool calls'
|
|
120
|
+
);
|
|
121
|
+
}
|
|
122
|
+
const name = Object.getOwnPropertyDescriptor(call, 'name');
|
|
123
|
+
const originalId = Object.getOwnPropertyDescriptor(call, 'id');
|
|
124
|
+
const originalArgs = Object.getOwnPropertyDescriptor(call, 'args');
|
|
125
|
+
if (
|
|
126
|
+
name == null ||
|
|
127
|
+
!('value' in name) ||
|
|
128
|
+
typeof name.value !== 'string' ||
|
|
129
|
+
originalArgs == null ||
|
|
130
|
+
!('value' in originalArgs) ||
|
|
131
|
+
(originalId != null && !('value' in originalId))
|
|
132
|
+
) {
|
|
133
|
+
throw new Error(
|
|
134
|
+
'Accepted model response contains non-serializable tool calls'
|
|
135
|
+
);
|
|
136
|
+
}
|
|
137
|
+
const providerId: unknown = originalId?.value;
|
|
138
|
+
if (providerId !== undefined && typeof providerId !== 'string') {
|
|
139
|
+
throw new Error(
|
|
140
|
+
'Accepted model response contains non-serializable tool calls'
|
|
141
|
+
);
|
|
142
|
+
}
|
|
143
|
+
let args: ToolCall['args'];
|
|
144
|
+
if (allowInvalidDiagnostics) {
|
|
145
|
+
// Callback streams may carry a not-yet-plannable argument string. It is
|
|
146
|
+
// safe scalar data, but must not become an accepted executable call.
|
|
147
|
+
args =
|
|
148
|
+
allowFragmentArgs && typeof originalArgs.value === 'string'
|
|
149
|
+
? (originalArgs.value as unknown as ToolCall['args'])
|
|
150
|
+
: cloneToolArguments(originalArgs.value);
|
|
151
|
+
} else {
|
|
152
|
+
remaining -=
|
|
153
|
+
Buffer.byteLength(name.value, 'utf8') +
|
|
154
|
+
(typeof providerId === 'string'
|
|
155
|
+
? Buffer.byteLength(providerId, 'utf8')
|
|
156
|
+
: 0);
|
|
157
|
+
const encoded = serializeToolArguments(originalArgs.value, remaining);
|
|
158
|
+
remaining -= Buffer.byteLength(encoded, 'utf8');
|
|
159
|
+
args = JSON.parse(encoded);
|
|
160
|
+
}
|
|
161
|
+
toolCalls.push({
|
|
162
|
+
name: name.value,
|
|
163
|
+
id: providerId,
|
|
164
|
+
args,
|
|
165
|
+
type: 'tool_call',
|
|
166
|
+
});
|
|
167
|
+
}
|
|
168
|
+
return toolCalls;
|
|
169
|
+
}
|
|
170
|
+
|
|
171
|
+
/** Only the accepted final response becomes a host-visible model result. */
|
|
172
|
+
export function snapshotAcceptedModelResponse(
|
|
173
|
+
finalResponse: AIMessageChunk,
|
|
174
|
+
id: string,
|
|
175
|
+
agentId: string,
|
|
176
|
+
providerExecutedIds?: ReadonlySet<string>,
|
|
177
|
+
clientDelegatedToolNames?: ReadonlySet<string>
|
|
178
|
+
): ModelResponseEvent {
|
|
179
|
+
const toolCalls = snapshotToolCalls(finalResponse, false);
|
|
180
|
+
const hasClientCall = toolCalls.some(
|
|
181
|
+
(call) => clientDelegatedToolNames?.has(call.name) === true
|
|
182
|
+
);
|
|
183
|
+
if (
|
|
184
|
+
hasClientCall &&
|
|
185
|
+
(toolCalls.some(
|
|
186
|
+
(call) =>
|
|
187
|
+
clientDelegatedToolNames?.has(call.name) !== true ||
|
|
188
|
+
(call.id != null && providerExecutedIds?.has(call.id) === true)
|
|
189
|
+
) ||
|
|
190
|
+
(finalResponse.invalid_tool_calls?.length ?? 0) > 0)
|
|
191
|
+
) {
|
|
192
|
+
throw new InvalidModelToolCallError(
|
|
193
|
+
'Mixed client and graph-owned tool calls require separate model turns'
|
|
194
|
+
);
|
|
195
|
+
}
|
|
196
|
+
return {
|
|
197
|
+
type: 'model_response',
|
|
198
|
+
id,
|
|
199
|
+
agentId,
|
|
200
|
+
...(finalResponse.id != null ? { messageId: finalResponse.id } : {}),
|
|
201
|
+
toolCalls,
|
|
202
|
+
toolCallDispositions: toolCalls.map((call) => {
|
|
203
|
+
if (hasClientCall) return 'client';
|
|
204
|
+
return call.id != null && providerExecutedIds?.has(call.id) === true
|
|
205
|
+
? 'provider'
|
|
206
|
+
: 'sdk';
|
|
207
|
+
}),
|
|
208
|
+
invalidToolCalls: [],
|
|
209
|
+
};
|
|
210
|
+
}
|
|
211
|
+
|
|
212
|
+
/** These records contain only scalar fields, unlike parsed tool arguments. Never
|
|
213
|
+
* spread or iterate provider records until their original descriptors pass.
|
|
214
|
+
*/
|
|
215
|
+
function snapshotToolRecords(
|
|
216
|
+
message: AIMessageChunk,
|
|
217
|
+
field: 'tool_call_chunks' | 'invalid_tool_calls'
|
|
218
|
+
): Record<string, unknown>[] {
|
|
219
|
+
function invalid(): never {
|
|
220
|
+
throw new Error(
|
|
221
|
+
'Accepted model response contains non-serializable tool calls'
|
|
222
|
+
);
|
|
223
|
+
}
|
|
224
|
+
const descriptor = Object.getOwnPropertyDescriptor(message, field);
|
|
225
|
+
if (descriptor == null) return [];
|
|
226
|
+
if (!('value' in descriptor)) invalid();
|
|
227
|
+
const source: unknown = descriptor.value;
|
|
228
|
+
if (source === undefined) return [];
|
|
229
|
+
if (source == null || types.isProxy(source) || !Array.isArray(source))
|
|
230
|
+
invalid();
|
|
231
|
+
const result: Record<string, unknown>[] = [];
|
|
232
|
+
for (let index = 0; index < source.length; index++) {
|
|
233
|
+
const entry = Object.getOwnPropertyDescriptor(source, String(index));
|
|
234
|
+
if (entry == null || !('value' in entry)) invalid();
|
|
235
|
+
const record: unknown = entry.value;
|
|
236
|
+
if (record == null || typeof record !== 'object' || types.isProxy(record))
|
|
237
|
+
invalid();
|
|
238
|
+
const copy: Record<string, unknown> = {};
|
|
239
|
+
for (const key of Reflect.ownKeys(record)) {
|
|
240
|
+
if (typeof key !== 'string') invalid();
|
|
241
|
+
const property = Object.getOwnPropertyDescriptor(record, key);
|
|
242
|
+
if (property == null || !('value' in property)) invalid();
|
|
243
|
+
const value: unknown = property.value;
|
|
244
|
+
if (
|
|
245
|
+
value != null &&
|
|
246
|
+
(key === 'index'
|
|
247
|
+
? typeof value !== 'number' ||
|
|
248
|
+
!Number.isSafeInteger(value) ||
|
|
249
|
+
value < 0
|
|
250
|
+
: typeof value !== 'string')
|
|
251
|
+
)
|
|
252
|
+
invalid();
|
|
253
|
+
Object.defineProperty(copy, key, {
|
|
254
|
+
value,
|
|
255
|
+
enumerable: true,
|
|
256
|
+
writable: true,
|
|
257
|
+
configurable: true,
|
|
258
|
+
});
|
|
259
|
+
}
|
|
260
|
+
result.push(copy);
|
|
261
|
+
}
|
|
262
|
+
return result;
|
|
263
|
+
}
|
|
264
|
+
|
|
265
|
+
/** Providers can mutate and re-yield records. Inspect a per-emission snapshot,
|
|
266
|
+
* leaving their originals intact, but keep producer/consumer charge identity.
|
|
267
|
+
*/
|
|
268
|
+
export function snapshotValidatedModelChunk(
|
|
269
|
+
message: AIMessageChunk,
|
|
270
|
+
partial = true
|
|
271
|
+
): AIMessageChunk {
|
|
272
|
+
if (types.isProxy(message)) {
|
|
273
|
+
throw new InvalidModelToolCallError(
|
|
274
|
+
'Accepted model response contains non-serializable tool calls'
|
|
275
|
+
);
|
|
276
|
+
}
|
|
277
|
+
const descriptors = Object.getOwnPropertyDescriptors(message);
|
|
278
|
+
// Object.freeze on the provider result must not freeze the SDK's own
|
|
279
|
+
// envelope. LangChain updates IDs, lc_kwargs and response metadata later.
|
|
280
|
+
for (const descriptor of Object.values(descriptors)) {
|
|
281
|
+
descriptor.configurable = true;
|
|
282
|
+
if ('value' in descriptor) descriptor.writable = true;
|
|
283
|
+
}
|
|
284
|
+
const kwargs = descriptors.lc_kwargs;
|
|
285
|
+
if (Object.hasOwn(descriptors, 'lc_kwargs') && 'value' in kwargs) {
|
|
286
|
+
const value: unknown = kwargs.value;
|
|
287
|
+
if (value === null || typeof value !== 'object' || types.isProxy(value)) {
|
|
288
|
+
throw new InvalidModelToolCallError('Invalid model response metadata');
|
|
289
|
+
}
|
|
290
|
+
const fields = Object.getOwnPropertyDescriptors(value);
|
|
291
|
+
for (const field of Object.values(fields)) {
|
|
292
|
+
if (!('value' in field)) {
|
|
293
|
+
throw new InvalidModelToolCallError('Invalid model response metadata');
|
|
294
|
+
}
|
|
295
|
+
field.configurable = true;
|
|
296
|
+
field.writable = true;
|
|
297
|
+
}
|
|
298
|
+
kwargs.value = Object.create(Object.getPrototypeOf(value), fields);
|
|
299
|
+
}
|
|
300
|
+
const copy = Object.create(
|
|
301
|
+
Object.getPrototypeOf(message),
|
|
302
|
+
descriptors
|
|
303
|
+
) as AIMessageChunk;
|
|
304
|
+
detachValidatedModelToolCalls(copy, partial);
|
|
305
|
+
if (partial) linkStreamLimitCanonical(copy, message);
|
|
306
|
+
return copy;
|
|
307
|
+
}
|