@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.
Files changed (120) hide show
  1. package/README.md +32 -0
  2. package/dist/cjs/common/enum.cjs +2 -0
  3. package/dist/cjs/common/enum.cjs.map +1 -1
  4. package/dist/cjs/events.cjs +11 -0
  5. package/dist/cjs/events.cjs.map +1 -1
  6. package/dist/cjs/graphs/Graph.cjs +54 -5
  7. package/dist/cjs/graphs/Graph.cjs.map +1 -1
  8. package/dist/cjs/graphs/MultiAgentGraph.cjs +97 -5
  9. package/dist/cjs/graphs/MultiAgentGraph.cjs.map +1 -1
  10. package/dist/cjs/graphs/acceptedModelResponse.cjs +168 -0
  11. package/dist/cjs/graphs/acceptedModelResponse.cjs.map +1 -0
  12. package/dist/cjs/graphs/handoff.cjs +195 -0
  13. package/dist/cjs/graphs/handoff.cjs.map +1 -0
  14. package/dist/cjs/graphs/index.cjs +1 -0
  15. package/dist/cjs/llm/invoke.cjs +10 -5
  16. package/dist/cjs/llm/invoke.cjs.map +1 -1
  17. package/dist/cjs/llm/streamLimits.cjs +1 -1
  18. package/dist/cjs/llm/streamLimits.cjs.map +1 -1
  19. package/dist/cjs/main.cjs +4 -0
  20. package/dist/cjs/messages/fading.cjs +14 -6
  21. package/dist/cjs/messages/fading.cjs.map +1 -1
  22. package/dist/cjs/messages/prune.cjs +95 -36
  23. package/dist/cjs/messages/prune.cjs.map +1 -1
  24. package/dist/cjs/openai/index.cjs +2 -0
  25. package/dist/cjs/openai/index.cjs.map +1 -1
  26. package/dist/cjs/openai/toolProjection.cjs +196 -0
  27. package/dist/cjs/openai/toolProjection.cjs.map +1 -0
  28. package/dist/cjs/run.cjs +41 -4
  29. package/dist/cjs/run.cjs.map +1 -1
  30. package/dist/cjs/session/AgentSession.cjs +1 -1
  31. package/dist/cjs/session/AgentSession.cjs.map +1 -1
  32. package/dist/cjs/stream.cjs +9 -4
  33. package/dist/cjs/stream.cjs.map +1 -1
  34. package/dist/cjs/tools/ToolNode.cjs +5 -1
  35. package/dist/cjs/tools/ToolNode.cjs.map +1 -1
  36. package/dist/cjs/tools/subagent/SubagentReplay.cjs +4 -1
  37. package/dist/cjs/tools/subagent/SubagentReplay.cjs.map +1 -1
  38. package/dist/cjs/utils/acceptedToolArguments.cjs +143 -0
  39. package/dist/cjs/utils/acceptedToolArguments.cjs.map +1 -0
  40. package/dist/esm/common/enum.mjs +2 -0
  41. package/dist/esm/common/enum.mjs.map +1 -1
  42. package/dist/esm/events.mjs +11 -0
  43. package/dist/esm/events.mjs.map +1 -1
  44. package/dist/esm/graphs/Graph.mjs +54 -5
  45. package/dist/esm/graphs/Graph.mjs.map +1 -1
  46. package/dist/esm/graphs/MultiAgentGraph.mjs +97 -5
  47. package/dist/esm/graphs/MultiAgentGraph.mjs.map +1 -1
  48. package/dist/esm/graphs/acceptedModelResponse.mjs +165 -0
  49. package/dist/esm/graphs/acceptedModelResponse.mjs.map +1 -0
  50. package/dist/esm/graphs/handoff.mjs +193 -0
  51. package/dist/esm/graphs/handoff.mjs.map +1 -0
  52. package/dist/esm/graphs/index.mjs +1 -0
  53. package/dist/esm/llm/invoke.mjs +10 -5
  54. package/dist/esm/llm/invoke.mjs.map +1 -1
  55. package/dist/esm/llm/streamLimits.mjs +1 -1
  56. package/dist/esm/llm/streamLimits.mjs.map +1 -1
  57. package/dist/esm/main.mjs +4 -3
  58. package/dist/esm/messages/fading.mjs +14 -7
  59. package/dist/esm/messages/fading.mjs.map +1 -1
  60. package/dist/esm/messages/prune.mjs +95 -37
  61. package/dist/esm/messages/prune.mjs.map +1 -1
  62. package/dist/esm/openai/index.mjs +2 -1
  63. package/dist/esm/openai/index.mjs.map +1 -1
  64. package/dist/esm/openai/toolProjection.mjs +196 -0
  65. package/dist/esm/openai/toolProjection.mjs.map +1 -0
  66. package/dist/esm/run.mjs +41 -4
  67. package/dist/esm/run.mjs.map +1 -1
  68. package/dist/esm/session/AgentSession.mjs +1 -1
  69. package/dist/esm/session/AgentSession.mjs.map +1 -1
  70. package/dist/esm/stream.mjs +9 -4
  71. package/dist/esm/stream.mjs.map +1 -1
  72. package/dist/esm/tools/ToolNode.mjs +5 -1
  73. package/dist/esm/tools/ToolNode.mjs.map +1 -1
  74. package/dist/esm/tools/subagent/SubagentReplay.mjs +5 -2
  75. package/dist/esm/tools/subagent/SubagentReplay.mjs.map +1 -1
  76. package/dist/esm/utils/acceptedToolArguments.mjs +142 -0
  77. package/dist/esm/utils/acceptedToolArguments.mjs.map +1 -0
  78. package/dist/types/common/enum.d.ts +4 -0
  79. package/dist/types/graphs/Graph.d.ts +5 -1
  80. package/dist/types/graphs/MultiAgentGraph.d.ts +3 -0
  81. package/dist/types/graphs/acceptedModelResponse.d.ts +15 -0
  82. package/dist/types/graphs/handoff.d.ts +28 -0
  83. package/dist/types/graphs/index.d.ts +1 -0
  84. package/dist/types/messages/fading.d.ts +14 -2
  85. package/dist/types/messages/prune.d.ts +7 -0
  86. package/dist/types/openai/arguments.d.ts +2 -0
  87. package/dist/types/openai/index.d.ts +2 -0
  88. package/dist/types/openai/toolProjection.d.ts +30 -0
  89. package/dist/types/run.d.ts +5 -0
  90. package/dist/types/tools/ToolNode.d.ts +2 -1
  91. package/dist/types/types/graph.d.ts +51 -4
  92. package/dist/types/types/run.d.ts +8 -1
  93. package/dist/types/types/stream.d.ts +22 -1
  94. package/dist/types/types/tools.d.ts +5 -0
  95. package/dist/types/utils/acceptedToolArguments.d.ts +10 -0
  96. package/package.json +1 -1
  97. package/src/common/enum.ts +5 -0
  98. package/src/events.ts +24 -1
  99. package/src/graphs/Graph.ts +103 -0
  100. package/src/graphs/MultiAgentGraph.ts +142 -4
  101. package/src/graphs/acceptedModelResponse.ts +307 -0
  102. package/src/graphs/handoff.ts +263 -0
  103. package/src/graphs/index.ts +2 -0
  104. package/src/llm/invoke.ts +39 -14
  105. package/src/llm/streamLimits.ts +1 -1
  106. package/src/messages/fading.ts +30 -5
  107. package/src/messages/prune.ts +168 -55
  108. package/src/openai/arguments.ts +2 -0
  109. package/src/openai/index.ts +6 -0
  110. package/src/openai/toolProjection.ts +318 -0
  111. package/src/run.ts +75 -2
  112. package/src/session/AgentSession.ts +2 -2
  113. package/src/stream.ts +21 -1
  114. package/src/tools/ToolNode.ts +19 -6
  115. package/src/tools/subagent/SubagentReplay.ts +10 -9
  116. package/src/types/graph.ts +59 -10
  117. package/src/types/run.ts +8 -1
  118. package/src/types/stream.ts +26 -0
  119. package/src/types/tools.ts +5 -0
  120. 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: { messages: state.messages.concat(toolMessage) },
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: { messages: filteredMessages },
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 = summarizeOnlyAgentId != null ? [] : this.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
+ }