@librechat/agents 3.9.3 → 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 (104) 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 +41 -1
  7. package/dist/cjs/graphs/Graph.cjs.map +1 -1
  8. package/dist/cjs/graphs/acceptedModelResponse.cjs +168 -0
  9. package/dist/cjs/graphs/acceptedModelResponse.cjs.map +1 -0
  10. package/dist/cjs/llm/invoke.cjs +10 -5
  11. package/dist/cjs/llm/invoke.cjs.map +1 -1
  12. package/dist/cjs/llm/streamLimits.cjs +1 -1
  13. package/dist/cjs/llm/streamLimits.cjs.map +1 -1
  14. package/dist/cjs/main.cjs +2 -0
  15. package/dist/cjs/messages/fading.cjs +14 -6
  16. package/dist/cjs/messages/fading.cjs.map +1 -1
  17. package/dist/cjs/messages/prune.cjs +95 -36
  18. package/dist/cjs/messages/prune.cjs.map +1 -1
  19. package/dist/cjs/openai/index.cjs +2 -0
  20. package/dist/cjs/openai/index.cjs.map +1 -1
  21. package/dist/cjs/openai/toolProjection.cjs +196 -0
  22. package/dist/cjs/openai/toolProjection.cjs.map +1 -0
  23. package/dist/cjs/run.cjs +8 -1
  24. package/dist/cjs/run.cjs.map +1 -1
  25. package/dist/cjs/session/AgentSession.cjs +1 -1
  26. package/dist/cjs/session/AgentSession.cjs.map +1 -1
  27. package/dist/cjs/stream.cjs +9 -4
  28. package/dist/cjs/stream.cjs.map +1 -1
  29. package/dist/cjs/tools/ToolNode.cjs +2 -1
  30. package/dist/cjs/tools/ToolNode.cjs.map +1 -1
  31. package/dist/cjs/tools/subagent/SubagentReplay.cjs +4 -1
  32. package/dist/cjs/tools/subagent/SubagentReplay.cjs.map +1 -1
  33. package/dist/cjs/utils/acceptedToolArguments.cjs +143 -0
  34. package/dist/cjs/utils/acceptedToolArguments.cjs.map +1 -0
  35. package/dist/esm/common/enum.mjs +2 -0
  36. package/dist/esm/common/enum.mjs.map +1 -1
  37. package/dist/esm/events.mjs +11 -0
  38. package/dist/esm/events.mjs.map +1 -1
  39. package/dist/esm/graphs/Graph.mjs +41 -1
  40. package/dist/esm/graphs/Graph.mjs.map +1 -1
  41. package/dist/esm/graphs/acceptedModelResponse.mjs +165 -0
  42. package/dist/esm/graphs/acceptedModelResponse.mjs.map +1 -0
  43. package/dist/esm/llm/invoke.mjs +10 -5
  44. package/dist/esm/llm/invoke.mjs.map +1 -1
  45. package/dist/esm/llm/streamLimits.mjs +1 -1
  46. package/dist/esm/llm/streamLimits.mjs.map +1 -1
  47. package/dist/esm/main.mjs +3 -3
  48. package/dist/esm/messages/fading.mjs +14 -7
  49. package/dist/esm/messages/fading.mjs.map +1 -1
  50. package/dist/esm/messages/prune.mjs +95 -37
  51. package/dist/esm/messages/prune.mjs.map +1 -1
  52. package/dist/esm/openai/index.mjs +2 -1
  53. package/dist/esm/openai/index.mjs.map +1 -1
  54. package/dist/esm/openai/toolProjection.mjs +196 -0
  55. package/dist/esm/openai/toolProjection.mjs.map +1 -0
  56. package/dist/esm/run.mjs +8 -1
  57. package/dist/esm/run.mjs.map +1 -1
  58. package/dist/esm/session/AgentSession.mjs +1 -1
  59. package/dist/esm/session/AgentSession.mjs.map +1 -1
  60. package/dist/esm/stream.mjs +9 -4
  61. package/dist/esm/stream.mjs.map +1 -1
  62. package/dist/esm/tools/ToolNode.mjs +2 -1
  63. package/dist/esm/tools/ToolNode.mjs.map +1 -1
  64. package/dist/esm/tools/subagent/SubagentReplay.mjs +5 -2
  65. package/dist/esm/tools/subagent/SubagentReplay.mjs.map +1 -1
  66. package/dist/esm/utils/acceptedToolArguments.mjs +142 -0
  67. package/dist/esm/utils/acceptedToolArguments.mjs.map +1 -0
  68. package/dist/types/common/enum.d.ts +4 -0
  69. package/dist/types/graphs/Graph.d.ts +3 -1
  70. package/dist/types/graphs/acceptedModelResponse.d.ts +15 -0
  71. package/dist/types/messages/fading.d.ts +14 -2
  72. package/dist/types/messages/prune.d.ts +7 -0
  73. package/dist/types/openai/arguments.d.ts +2 -0
  74. package/dist/types/openai/index.d.ts +2 -0
  75. package/dist/types/openai/toolProjection.d.ts +30 -0
  76. package/dist/types/run.d.ts +1 -0
  77. package/dist/types/tools/ToolNode.d.ts +1 -1
  78. package/dist/types/types/graph.d.ts +5 -3
  79. package/dist/types/types/run.d.ts +5 -0
  80. package/dist/types/types/stream.d.ts +22 -1
  81. package/dist/types/types/tools.d.ts +2 -0
  82. package/dist/types/utils/acceptedToolArguments.d.ts +10 -0
  83. package/package.json +1 -1
  84. package/src/common/enum.ts +5 -0
  85. package/src/events.ts +24 -1
  86. package/src/graphs/Graph.ts +95 -0
  87. package/src/graphs/acceptedModelResponse.ts +307 -0
  88. package/src/llm/invoke.ts +39 -14
  89. package/src/llm/streamLimits.ts +1 -1
  90. package/src/messages/fading.ts +30 -5
  91. package/src/messages/prune.ts +168 -55
  92. package/src/openai/arguments.ts +2 -0
  93. package/src/openai/index.ts +6 -0
  94. package/src/openai/toolProjection.ts +318 -0
  95. package/src/run.ts +21 -0
  96. package/src/session/AgentSession.ts +2 -2
  97. package/src/stream.ts +21 -1
  98. package/src/tools/ToolNode.ts +5 -0
  99. package/src/tools/subagent/SubagentReplay.ts +10 -9
  100. package/src/types/graph.ts +15 -9
  101. package/src/types/run.ts +5 -0
  102. package/src/types/stream.ts +26 -0
  103. package/src/types/tools.ts +2 -0
  104. package/src/utils/acceptedToolArguments.ts +204 -0
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: 1,
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
- const chunk = data.chunk as Partial<AIMessageChunk>;
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 });
@@ -1021,6 +1021,7 @@ export class ToolNode<T = any> extends RunnableCallable<T, T> {
1021
1021
  preparedSubagents,
1022
1022
  restoreRunStepResumeState,
1023
1023
  createRunStepResumeState,
1024
+ onToolCallsClaimed,
1024
1025
  }: t.ToolNodeConstructorParams) {
1025
1026
  super({
1026
1027
  name: name ?? TOOL_NODE_RUN_NAME,
@@ -1155,6 +1156,10 @@ export class ToolNode<T = any> extends RunnableCallable<T, T> {
1155
1156
  }
1156
1157
  const state = input as T & Pick<t.BaseGraphState, 'runStepState'>;
1157
1158
  restoreRunStepResumeState?.(state.runStepState, config);
1159
+ // Freeze replay authority before observers can yield to sibling tasks.
1160
+ if (onToolCallsClaimed != null && assistantBatch?.message.id != null) {
1161
+ await onToolCallsClaimed(assistantBatch.message.id, config);
1162
+ }
1158
1163
  let result: T;
1159
1164
  try {
1160
1165
  result = await this.run(input, config, referenceReplay);
@@ -8,7 +8,7 @@ import type {
8
8
  RunStepResumeState,
9
9
  ToolSessionContext,
10
10
  } from '@/types';
11
- import { isFadingTier } from '@/messages/fading';
11
+ import { isFadingTier, isLegacyFadingTier } from '@/messages/fading';
12
12
  import {
13
13
  attachRunStepResumeState,
14
14
  getRunStepResumeState,
@@ -290,6 +290,11 @@ export function isToolOutputReferenceState(
290
290
  );
291
291
  }
292
292
 
293
+ /** A current tier, or a legacy one the run then drops so it re-derives. */
294
+ function isStoredFadingTier(value: unknown): boolean {
295
+ return isFadingTier(value) || isLegacyFadingTier(value);
296
+ }
297
+
293
298
  function isGraphResumeState(value: unknown): value is SubagentGraphResumeState {
294
299
  if (value == null || typeof value !== 'object') {
295
300
  return false;
@@ -306,15 +311,14 @@ function isGraphResumeState(value: unknown): value is SubagentGraphResumeState {
306
311
  !state.eagerToolUsage.every(isEagerToolUsageState) ||
307
312
  !Array.isArray(state.eagerToolSuppressions) ||
308
313
  !state.eagerToolSuppressions.every(isString) ||
309
- (state.runStepState != null &&
310
- !isRunStepResumeState(state.runStepState)) ||
314
+ (state.runStepState != null && !isRunStepResumeState(state.runStepState)) ||
311
315
  (state.toolOutputReferences != null &&
312
316
  !isToolOutputReferenceState(state.toolOutputReferences)) ||
313
- (state.fadingTier != null && !isFadingTier(state.fadingTier)) ||
317
+ (state.fadingTier != null && !isStoredFadingTier(state.fadingTier)) ||
314
318
  (state.fadingTiers != null &&
315
319
  (typeof state.fadingTiers !== 'object' ||
316
320
  Array.isArray(state.fadingTiers) ||
317
- !Object.values(state.fadingTiers).every(isFadingTier)))
321
+ !Object.values(state.fadingTiers).every(isStoredFadingTier)))
318
322
  ) {
319
323
  return false;
320
324
  }
@@ -515,10 +519,7 @@ export function attachSubagentResumeManifest(
515
519
  const runStepState = getRunStepResumeState(payload);
516
520
  if (runStepState != null) {
517
521
  return attachRunStepResumeState(
518
- attachSubagentResumeManifest(
519
- stripRunStepResumeState(payload),
520
- manifest
521
- ),
522
+ attachSubagentResumeManifest(stripRunStepResumeState(payload), manifest),
522
523
  runStepState
523
524
  );
524
525
  }
@@ -10,22 +10,24 @@ import type { START, StateGraph, StateGraphArgs } from '@langchain/langgraph';
10
10
  import type { RunnableConfig, Runnable } from '@langchain/core/runnables';
11
11
  import type { ChatGenerationChunk } from '@langchain/core/outputs';
12
12
  import type { GoogleAIToolType } from '@langchain/google-common';
13
- import type {
14
- SummarizationNodeInput,
15
- SummarizeCompleteEvent,
16
- CompactionSemanticIndex,
17
- SummarizationConfig,
18
- SummarizeStartEvent,
19
- SummarizeDeltaEvent,
20
- } from '@/types/summarize';
21
13
  import type {
22
14
  RunStep,
15
+ ModelResponseEvent,
16
+ ModelToolsClaimedEvent,
23
17
  RunStepDeltaEvent,
24
18
  RunStepResumeState,
25
19
  RunStepClosedEvent,
26
20
  MessageDeltaEvent,
27
21
  ReasoningDeltaEvent,
28
22
  } from '@/types/stream';
23
+ import type {
24
+ SummarizationNodeInput,
25
+ SummarizeCompleteEvent,
26
+ CompactionSemanticIndex,
27
+ SummarizationConfig,
28
+ SummarizeStartEvent,
29
+ SummarizeDeltaEvent,
30
+ } from '@/types/summarize';
29
31
  import type {
30
32
  ToolMap,
31
33
  ToolSessionMap,
@@ -183,7 +185,7 @@ export interface ContextUsageEvent {
183
185
  * `RunConfig.fadingTiers[agentId]`.
184
186
  */
185
187
  export interface FadingTier {
186
- v: 1;
188
+ v: 2;
187
189
  /** Token budget the caps derive from, in raw token space. Never grows;
188
190
  * clamped to the current context window when seeded. */
189
191
  budgetTokens: number;
@@ -202,6 +204,8 @@ export interface EventHandler {
202
204
  data:
203
205
  | StreamEventData
204
206
  | ModelEndData
207
+ | ModelResponseEvent
208
+ | ModelToolsClaimedEvent
205
209
  | RunStep
206
210
  | RunStepDeltaEvent
207
211
  | RunStepClosedEvent
@@ -406,6 +410,8 @@ export type StandardGraphInput = {
406
410
  agents: AgentInputs[];
407
411
  /** Execution backend used to resolve the effective tool registry. */
408
412
  toolExecution?: ToolExecutionConfig;
413
+ /** Trusted single-agent client delegation policy; mixed batches fail closed. */
414
+ clientDelegatedToolNames?: readonly string[];
409
415
  langfuse?: LangfuseConfig;
410
416
  tokenCounter?: TokenCounter;
411
417
  indexTokenCountMap?: Record<string, number>;
package/src/types/run.ts CHANGED
@@ -279,6 +279,11 @@ export type RunConfig = {
279
279
  */
280
280
  langfuse?: g.LangfuseConfig;
281
281
  customHandlers?: Record<string, g.EventHandler>;
282
+ /** Explicit client-owned tools for a single-agent graph. A batch mixing
283
+ * client and SDK/provider calls fails closed; omitted means SDK ownership.
284
+ * Hosts must register the corresponding model-facing tool schemas.
285
+ */
286
+ clientDelegatedToolNames?: readonly string[];
282
287
  /**
283
288
  * Receives token usage for every model call made inside subagent child
284
289
  * runs (including nested subagents). Child graphs execute via `invoke()`
@@ -1,5 +1,6 @@
1
1
  // src/types/stream.ts
2
2
  import type {
3
+ AIMessageChunk,
3
4
  MessageContentImageUrl,
4
5
  MessageContentText,
5
6
  ToolMessage,
@@ -15,6 +16,24 @@ import type { SummarizeCompleteEvent } from '@/types/summarize';
15
16
  import type { ToolEndEvent } from '@/types/tools';
16
17
  import { StepTypes, ContentTypes, GraphEvents } from '@/common/enum';
17
18
 
19
+ /** One accepted model result, detached from execution state before host dispatch.
20
+ * Provider chunks, failed attempts and UI run-step events are not this contract. */
21
+ export interface ModelResponseEvent {
22
+ type: 'model_response';
23
+ /** Graph-generated acceptance ID, not a provider ID or run-step index. */
24
+ id: string;
25
+ agentId: string;
26
+ /** Graph-state message identity used to correlate ToolNode ownership. */
27
+ messageId?: string;
28
+ toolCalls: ReadonlyArray<ToolCall>;
29
+ /** Same index as toolCalls. Only a trusted graph decision of 'client'
30
+ * permits this call on the OpenAI client wire; absence fails closed. */
31
+ toolCallDispositions: ReadonlyArray<'sdk' | 'provider' | 'client'>;
32
+ invalidToolCalls: ReadonlyArray<
33
+ NonNullable<AIMessageChunk['invalid_tool_calls']>[number]
34
+ >;
35
+ }
36
+
18
37
  export type HandleLLMEnd = (
19
38
  output: LLMResult,
20
39
  runId: string,
@@ -542,3 +561,10 @@ export type ContentAggregatorResult = {
542
561
  contentParts: Array<MessageContentComplex | undefined>;
543
562
  aggregateContent: ContentAggregator;
544
563
  };
564
+
565
+ /** Ownership, not successful completion. Interrupted/failed batches remain graph-owned. */
566
+ export interface ModelToolsClaimedEvent {
567
+ type: 'model_tools_claimed';
568
+ agentId: string;
569
+ messageId: string;
570
+ }
@@ -313,6 +313,8 @@ export type ToolNodeOptions = {
313
313
  ) => void;
314
314
  /** SDK-owned checkpoint snapshot for open run-step lifecycle state. */
315
315
  createRunStepResumeState?: () => RunStepResumeState;
316
+ /** Internal ownership bridge, awaited before ToolNode can execute a batch. */
317
+ onToolCallsClaimed?: (messageId: string, config: RunnableConfig) => Promise<void>;
316
318
  };
317
319
 
318
320
  export type ToolNodeConstructorParams = ToolRefs & ToolNodeOptions;
@@ -0,0 +1,204 @@
1
+ import { types } from 'node:util';
2
+
3
+ const MAX_ARGUMENT_DEPTH = 64;
4
+ const INVALID = 'Accepted tool call arguments are not JSON serializable';
5
+ const LIMIT = 'Tool projection buffer limit exceeded';
6
+
7
+ function invalid(): never {
8
+ throw new Error(INVALID);
9
+ }
10
+
11
+ /** structuredClone and VM providers may return objects from another realm. */
12
+ function hasJSONPrototype(value: object, array: boolean): boolean {
13
+ const proto: unknown = Object.getPrototypeOf(value);
14
+ if (proto === null) return true;
15
+ if (typeof proto !== 'object' || types.isProxy(proto)) return false;
16
+ const ctor: unknown = Object.getOwnPropertyDescriptor(
17
+ proto,
18
+ 'constructor'
19
+ )?.value;
20
+ if (typeof ctor !== 'function' || types.isProxy(ctor)) return false;
21
+ return (
22
+ Object.getOwnPropertyDescriptor(ctor, 'prototype')?.value === proto &&
23
+ Function.prototype.toString.call(ctor) ===
24
+ Function.prototype.toString.call(array ? Array : Object)
25
+ );
26
+ }
27
+
28
+ /** Encode JSON data, not arbitrary JS values. No getters, proxies or toJSON hooks run.
29
+ * Repeated references expand like JSON (and count against the budget); cycles fail.
30
+ * Limits bound output and traversal, including deeply nested/alias-heavy input.
31
+ */
32
+ export function serializeToolArguments(
33
+ value: unknown,
34
+ maxBytes: number
35
+ ): string {
36
+ if (maxBytes < 2) throw new Error(LIMIT);
37
+ const parts: string[] = [];
38
+ const ancestors = new Set<object>();
39
+ let remaining = maxBytes;
40
+ const append = (text: string): void => {
41
+ remaining -= Buffer.byteLength(text, 'utf8');
42
+ if (remaining < 0) throw new Error(LIMIT);
43
+ parts.push(text);
44
+ };
45
+ const string = (text: string): void => {
46
+ // Every UTF-16 unit needs at least one encoded byte. Check before allocating escapes.
47
+ if (text.length + 2 > remaining) throw new Error(LIMIT);
48
+ append(JSON.stringify(text));
49
+ };
50
+ const encode = (input: unknown, depth: number): void => {
51
+ if (depth > MAX_ARGUMENT_DEPTH) invalid();
52
+ if (input === null) {
53
+ append('null');
54
+ return;
55
+ }
56
+ if (typeof input === 'string') {
57
+ string(input);
58
+ return;
59
+ }
60
+ if (typeof input === 'boolean') {
61
+ append(input ? 'true' : 'false');
62
+ return;
63
+ }
64
+ if (typeof input === 'number') {
65
+ if (!Number.isFinite(input)) invalid();
66
+ append(Object.is(input, -0) ? '-0' : String(input));
67
+ return;
68
+ }
69
+ if (typeof input !== 'object') invalid();
70
+ if (types.isProxy(input) || ancestors.has(input)) invalid();
71
+ const array = Array.isArray(input);
72
+ if (!hasJSONPrototype(input, array)) invalid();
73
+ const keys = Reflect.ownKeys(input);
74
+ // At least one byte per entry, plus punctuation. Bound descriptor work up front.
75
+ if (keys.length > remaining + (array ? 1 : 0)) throw new Error(LIMIT);
76
+ ancestors.add(input);
77
+ if (array) {
78
+ // Dense arrays only, with no symbols, hidden entries or extra named properties.
79
+ if (keys.length !== input.length + 1) invalid();
80
+ append('[');
81
+ for (let i = 0; i < input.length; i++) {
82
+ const descriptor = Object.getOwnPropertyDescriptor(input, String(i));
83
+ if (
84
+ descriptor == null ||
85
+ descriptor.enumerable !== true ||
86
+ !('value' in descriptor)
87
+ )
88
+ invalid();
89
+ if (i !== 0) append(',');
90
+ encode(descriptor.value, depth + 1);
91
+ }
92
+ append(']');
93
+ } else {
94
+ append('{');
95
+ for (let i = 0; i < keys.length; i++) {
96
+ const key = keys[i];
97
+ if (typeof key !== 'string') invalid();
98
+ const descriptor = Object.getOwnPropertyDescriptor(input, key);
99
+ if (
100
+ descriptor == null ||
101
+ descriptor.enumerable !== true ||
102
+ !('value' in descriptor)
103
+ )
104
+ invalid();
105
+ if (i !== 0) append(',');
106
+ string(key);
107
+ append(':');
108
+ encode(descriptor.value, depth + 1);
109
+ }
110
+ append('}');
111
+ }
112
+ ancestors.delete(input);
113
+ };
114
+ if (value != null && typeof value === 'object' && types.isProxy(value))
115
+ invalid();
116
+ if (value == null || typeof value !== 'object' || Array.isArray(value)) {
117
+ throw new Error('Accepted tool call arguments must be an object');
118
+ }
119
+ encode(value, 0);
120
+ return parts.join('');
121
+ }
122
+
123
+ /** Detach JSON data without encoding it. Unlike projection, invocation has no
124
+ * formatting budget: preserve shared references and traverse iteratively so a
125
+ * deep or alias-heavy object cannot expand exponentially during validation.
126
+ */
127
+ export function cloneToolArguments(value: unknown): Record<string, unknown> {
128
+ const copies = new Map<object, object>();
129
+ const ancestors = new Set<object>();
130
+ const stack: Array<{
131
+ input: object;
132
+ output: object;
133
+ keys: (string | symbol)[];
134
+ index: number;
135
+ }> = [];
136
+ const copy = (input: unknown): unknown => {
137
+ if (
138
+ input === null ||
139
+ typeof input === 'string' ||
140
+ typeof input === 'boolean'
141
+ )
142
+ return input;
143
+ if (typeof input === 'number') {
144
+ if (!Number.isFinite(input)) invalid();
145
+ return input;
146
+ }
147
+ if (
148
+ typeof input !== 'object' ||
149
+ types.isProxy(input) ||
150
+ ancestors.has(input)
151
+ )
152
+ invalid();
153
+ const existing = copies.get(input);
154
+ if (existing != null) return existing;
155
+ const array = Array.isArray(input);
156
+ if (!hasJSONPrototype(input, array)) invalid();
157
+ const keys = Reflect.ownKeys(input);
158
+ if (array && keys.length !== input.length + 1) invalid();
159
+ const output: object = array ? [] : {};
160
+ copies.set(input, output);
161
+ ancestors.add(input);
162
+ stack.push({
163
+ input,
164
+ output,
165
+ keys: array ? keys.filter((key) => key !== 'length') : keys,
166
+ index: 0,
167
+ });
168
+ return output;
169
+ };
170
+ if (
171
+ value == null ||
172
+ typeof value !== 'object' ||
173
+ types.isProxy(value) ||
174
+ Array.isArray(value)
175
+ )
176
+ invalid();
177
+ const result = copy(value) as Record<string, unknown>;
178
+ while (stack.length > 0) {
179
+ const frame = stack[stack.length - 1];
180
+ if (frame.index === frame.keys.length) {
181
+ ancestors.delete(frame.input);
182
+ stack.pop();
183
+ continue;
184
+ }
185
+ const key = frame.keys[frame.index++];
186
+ if (typeof key !== 'string') invalid();
187
+ if (Array.isArray(frame.input) && key !== String(frame.index - 1))
188
+ invalid();
189
+ const descriptor = Object.getOwnPropertyDescriptor(frame.input, key);
190
+ if (
191
+ descriptor == null ||
192
+ !('value' in descriptor) ||
193
+ descriptor.enumerable !== true
194
+ )
195
+ invalid();
196
+ Object.defineProperty(frame.output, key, {
197
+ value: copy(descriptor.value),
198
+ enumerable: true,
199
+ writable: true,
200
+ configurable: true,
201
+ });
202
+ }
203
+ return result;
204
+ }