@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
package/src/run.ts CHANGED
@@ -111,6 +111,7 @@ import { initializeLangfuseTracing } from './instrumentation';
111
111
  import { seedRunInitialSessions } from '@/utils/toolSessions';
112
112
  import { getTraceIdSeed } from '@/langfuseRuntimeContext';
113
113
  import { resolveClientOptionsModel } from '@/llm/request';
114
+ import { HandoffLimitError } from '@/graphs/handoff';
114
115
  import { createGraph } from '@/graphs/createGraph';
115
116
  import { isFadingTier } from '@/messages/fading';
116
117
  import { resolveMaxSeals } from '@/llm/preempt';
@@ -291,6 +292,7 @@ function getInterruptHookSessionId(payload: unknown): string | undefined {
291
292
  type InterruptStateSnapshot = {
292
293
  config?: RunnableConfig;
293
294
  values?: {
295
+ handoffState?: t.HandoffState;
294
296
  messages?: BaseMessage[];
295
297
  runStepState?: t.RunStepResumeState;
296
298
  };
@@ -466,6 +468,7 @@ export class Run<_T extends t.BaseGraphState> {
466
468
  private langfuse?: t.LangfuseConfig;
467
469
  private toolOutputReferences?: t.ToolOutputReferencesConfig;
468
470
  private eagerEventToolExecution?: t.EagerEventToolExecutionConfig;
471
+ private clientDelegatedToolNames?: readonly string[];
469
472
  private codeSessionToolNames?: string[];
470
473
  private interruptingToolNames?: string[];
471
474
  private toolExecution?: t.ToolExecutionConfig;
@@ -512,6 +515,7 @@ export class Run<_T extends t.BaseGraphState> {
512
515
  /** Distinguishes sibling forks started from the same explicit checkpoint. */
513
516
  private checkpointForkSeq = 0;
514
517
  private _haltedReason: string | undefined;
518
+ private _handoffOutcome?: t.HandoffOutcome;
515
519
 
516
520
  private constructor(config: Partial<t.RunConfig>) {
517
521
  const runId = config.runId ?? '';
@@ -546,12 +550,19 @@ export class Run<_T extends t.BaseGraphState> {
546
550
  }
547
551
  }
548
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
+ }
549
559
  this.handlerRegistry = handlerRegistry;
550
560
  this.hookRegistry = config.hooks;
551
561
  this.humanInTheLoop = config.humanInTheLoop;
552
562
  this.langfuse = config.langfuse;
553
563
  this.toolOutputReferences = config.toolOutputReferences;
554
564
  this.eagerEventToolExecution = config.eagerEventToolExecution;
565
+ this.clientDelegatedToolNames = config.clientDelegatedToolNames;
555
566
  this.codeSessionToolNames = config.codeSessionToolNames;
556
567
  this.interruptingToolNames = config.interruptingToolNames;
557
568
  this.toolExecution = config.toolExecution;
@@ -570,6 +581,9 @@ export class Run<_T extends t.BaseGraphState> {
570
581
 
571
582
  /** Handle different graph types */
572
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
+ }
573
587
  this.graphRunnable = this.createMultiAgentGraph(config.graphConfig);
574
588
  if (this.Graph) {
575
589
  this.Graph.handlerRegistry = handlerRegistry;
@@ -660,6 +674,7 @@ export class Run<_T extends t.BaseGraphState> {
660
674
  preemption: this.preemption,
661
675
  streamLimits: this.streamLimits,
662
676
  toolExecution: this.toolExecution,
677
+ clientDelegatedToolNames: this.clientDelegatedToolNames,
663
678
  },
664
679
  });
665
680
  /** Propagate compile options from graph config */
@@ -683,7 +698,7 @@ export class Run<_T extends t.BaseGraphState> {
683
698
  private createMultiAgentGraph(
684
699
  config: t.MultiAgentGraphConfig
685
700
  ): t.CompiledStateWorkflow {
686
- const { agents, edges, compileOptions } = config;
701
+ const { agents, edges, compileOptions, entryAgentId, maxHandoffs } = config;
687
702
 
688
703
  const multiAgentGraph = createGraph({
689
704
  kind: 'multi-agent',
@@ -691,6 +706,8 @@ export class Run<_T extends t.BaseGraphState> {
691
706
  runId: this.id,
692
707
  agents,
693
708
  edges,
709
+ entryAgentId,
710
+ maxHandoffs,
694
711
  langfuse: this.langfuse,
695
712
  tokenCounter: this.tokenCounter,
696
713
  indexTokenCountMap: this.indexTokenCountMap,
@@ -1053,6 +1070,11 @@ export class Run<_T extends t.BaseGraphState> {
1053
1070
  ) {
1054
1071
  return;
1055
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;
1056
1078
  const handler = this.handlerRegistry?.getHandler(eventName);
1057
1079
  /**
1058
1080
  * Tool completions arriving over the custom-event channel are the only
@@ -1237,6 +1259,7 @@ export class Run<_T extends t.BaseGraphState> {
1237
1259
  }
1238
1260
  const graphRunnable = this.graphRunnable;
1239
1261
  const graph = this.Graph;
1262
+ this._handoffOutcome = undefined;
1240
1263
 
1241
1264
  /**
1242
1265
  * `Command` inputs (`Command({ resume, update?, goto? })`) are
@@ -1347,6 +1370,7 @@ export class Run<_T extends t.BaseGraphState> {
1347
1370
  checkpointId === '' ? 0 : ++this.checkpointForkSeq,
1348
1371
  ]);
1349
1372
  graph.resetValues(streamOptions?.keepContent, checkpointScope);
1373
+ graph.handoffRouting?.start();
1350
1374
  graph.startStopContinuationExecution(nanoid());
1351
1375
  }
1352
1376
  this._interrupt = undefined;
@@ -1482,9 +1506,18 @@ export class Run<_T extends t.BaseGraphState> {
1482
1506
 
1483
1507
  const consumeStream = async (): Promise<void> => {
1484
1508
  let streamInputs: t.IState | Command = inputs;
1485
- if (!isResume && this.hasCheckpointer) {
1509
+ if (!isResume && graph.handoffRouting != null) {
1486
1510
  streamInputs = {
1487
1511
  ...(inputs as t.IState),
1512
+ handoffState: graph.handoffRouting.snapshot(),
1513
+ };
1514
+ }
1515
+ if (!isResume && this.hasCheckpointer) {
1516
+ streamInputs = {
1517
+ ...(streamInputs as t.IState),
1518
+ ...(graph.handoffRouting == null
1519
+ ? {}
1520
+ : { handoffState: new Overwrite(graph.handoffRouting.snapshot()) }),
1488
1521
  runStepState: new Overwrite(graph.createRunStepResumeState()),
1489
1522
  } as unknown as t.IState;
1490
1523
  } else if (overwriteLegacyResumeState) {
@@ -1538,6 +1571,10 @@ export class Run<_T extends t.BaseGraphState> {
1538
1571
 
1539
1572
  const modelEndAt =
1540
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;
1541
1578
  const handler = this.handlerRegistry?.getHandler(eventName);
1542
1579
  if (handler) {
1543
1580
  await handler.handle(eventName, data, metadata, this.Graph);
@@ -1679,6 +1716,7 @@ export class Run<_T extends t.BaseGraphState> {
1679
1716
  ? injected
1680
1717
  : [...graph.messages, ...injected],
1681
1718
  runStepState: graph.createRunStepResumeState(),
1719
+ handoffState: graph.handoffRouting?.snapshot(),
1682
1720
  };
1683
1721
  streamConfig = completedSegmentConfig;
1684
1722
  continue;
@@ -1716,6 +1754,8 @@ export class Run<_T extends t.BaseGraphState> {
1716
1754
  } catch (err) {
1717
1755
  terminalAt = Date.now();
1718
1756
  streamThrew = true;
1757
+ if (err instanceof HandoffLimitError)
1758
+ this._haltedReason = 'handoff_limit';
1719
1759
  await langfuseHandler?.handleChainError(
1720
1760
  err instanceof Error ? err : new Error(String(err)),
1721
1761
  this.id
@@ -1860,6 +1900,13 @@ export class Run<_T extends t.BaseGraphState> {
1860
1900
  * `HumanInTheLoopConfig` JSDoc.
1861
1901
  */
1862
1902
  const awaitingResume = this.isAwaitingResume(streamThrew);
1903
+ this._handoffOutcome = graph.handoffRouting?.outcome(
1904
+ this.getHandoffIncompleteReason(
1905
+ streamThrew,
1906
+ awaitingResume,
1907
+ config.signal
1908
+ )
1909
+ );
1863
1910
  if (!this.skipCleanup && !awaitingResume) {
1864
1911
  this.Graph.clearHeavyState();
1865
1912
  }
@@ -1921,6 +1968,31 @@ export class Run<_T extends t.BaseGraphState> {
1921
1968
  return this._haltedReason;
1922
1969
  }
1923
1970
 
1971
+ private getHandoffIncompleteReason(
1972
+ streamThrew: boolean,
1973
+ awaitingResume: boolean,
1974
+ signal?: AbortSignal
1975
+ ): string | undefined {
1976
+ if (this._haltedReason != null) return this._haltedReason;
1977
+ if (signal?.aborted === true || this.Graph?.signal?.aborted === true) {
1978
+ return 'aborted';
1979
+ }
1980
+ if (streamThrew) return 'error';
1981
+ if (awaitingResume) return 'interrupted';
1982
+ if (this.Graph?.subagentScope === true) return 'subagent';
1983
+ return undefined;
1984
+ }
1985
+
1986
+ /** Execution evidence only. The host must authorize and durably commit a candidate. */
1987
+ getHandoffOutcome(): t.HandoffOutcome | undefined {
1988
+ const outcome = this._handoffOutcome;
1989
+ if (outcome == null) return undefined;
1990
+ return {
1991
+ ...outcome,
1992
+ transitions: outcome.transitions.map((item) => ({ ...item })),
1993
+ };
1994
+ }
1995
+
1924
1996
  /**
1925
1997
  * Resume a paused HITL run with the value the user (or whatever
1926
1998
  * decided the interrupt) supplied. The default `TResume` covers the
@@ -2134,6 +2206,7 @@ export class Run<_T extends t.BaseGraphState> {
2134
2206
  const snapshot = await workflow.getState(callerConfig as RunnableConfig, {
2135
2207
  subgraphs: true,
2136
2208
  });
2209
+ this.Graph?.handoffRouting?.resume(snapshot.values?.handoffState);
2137
2210
  const persistedInterrupt = getFirstPersistedInterrupt(snapshot);
2138
2211
  if (persistedInterrupt == null) {
2139
2212
  return;
@@ -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 });
@@ -974,8 +974,10 @@ export class ToolNode<T = any> extends RunnableCallable<T, T> {
974
974
  * other's in-flight state.
975
975
  */
976
976
  private anonBatchCounter: number = 0;
977
+ private handoffRouting?: t.ToolNodeOptions['handoffRouting'];
977
978
 
978
979
  constructor({
980
+ handoffRouting,
979
981
  tools,
980
982
  toolMap,
981
983
  name,
@@ -1019,6 +1021,7 @@ export class ToolNode<T = any> extends RunnableCallable<T, T> {
1019
1021
  preparedSubagents,
1020
1022
  restoreRunStepResumeState,
1021
1023
  createRunStepResumeState,
1024
+ onToolCallsClaimed,
1022
1025
  }: t.ToolNodeConstructorParams) {
1023
1026
  super({
1024
1027
  name: name ?? TOOL_NODE_RUN_NAME,
@@ -1153,6 +1156,10 @@ export class ToolNode<T = any> extends RunnableCallable<T, T> {
1153
1156
  }
1154
1157
  const state = input as T & Pick<t.BaseGraphState, 'runStepState'>;
1155
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
+ }
1156
1163
  let result: T;
1157
1164
  try {
1158
1165
  result = await this.run(input, config, referenceReplay);
@@ -1219,6 +1226,7 @@ export class ToolNode<T = any> extends RunnableCallable<T, T> {
1219
1226
  this.runLangfuse = runLangfuse;
1220
1227
  this.agentLangfuse = agentLangfuse;
1221
1228
  this.toolMap = toolMap ?? new Map(tools.map((tool) => [tool.name, tool]));
1229
+ this.handoffRouting = handoffRouting;
1222
1230
  this.toolCallStepIds = toolCallStepIds;
1223
1231
  this.handleToolErrors = handleToolErrors ?? this.handleToolErrors;
1224
1232
  this.loadRuntimeTools = loadRuntimeTools;
@@ -3221,10 +3229,7 @@ export class ToolNode<T = any> extends RunnableCallable<T, T> {
3221
3229
 
3222
3230
  private retainCodeSessionInputsFromRequests(
3223
3231
  requests: Iterable<t.ToolCallRequest>,
3224
- baselineByRequestId: ReadonlyMap<
3225
- string,
3226
- ReadonlyMap<string, string>
3227
- >
3232
+ baselineByRequestId: ReadonlyMap<string, ReadonlyMap<string, string>>
3228
3233
  ): void {
3229
3234
  if (!this.sessions) {
3230
3235
  return;
@@ -5140,8 +5145,9 @@ export class ToolNode<T = any> extends RunnableCallable<T, T> {
5140
5145
  call.id == null
5141
5146
  ? undefined
5142
5147
  : baseContext.resolvedArgsByCallId?.get(call.id);
5143
- const codeSessionBaseline =
5144
- baseContext.codeSessionBaselineByCallId?.get(call.id ?? '');
5148
+ const codeSessionBaseline = baseContext.codeSessionBaselineByCallId?.get(
5149
+ call.id ?? ''
5150
+ );
5145
5151
  const result: SettledDirectToolResult = {
5146
5152
  proposal: structuredClone({
5147
5153
  name: call.name,
@@ -6073,6 +6079,13 @@ export class ToolNode<T = any> extends RunnableCallable<T, T> {
6073
6079
  if (replayBatchKey != null) {
6074
6080
  this.settledDirectResultsByBatch.delete(replayBatchKey);
6075
6081
  }
6082
+ if (!Array.isArray(input) && !this.isSendInput(input)) {
6083
+ this.handoffRouting?.finalize(
6084
+ combinedOutputs.filter(isCommand),
6085
+ input as t.BaseGraphState,
6086
+ config
6087
+ );
6088
+ }
6076
6089
  return combinedOutputs as T;
6077
6090
  }
6078
6091
 
@@ -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,
@@ -78,9 +80,45 @@ export type SystemCallbacks = {
78
80
  : never;
79
81
  };
80
82
 
83
+ /** An executed, SDK-generated handoff, identified independently of stream events. */
84
+ export interface HandoffTransition {
85
+ id: string;
86
+ sourceAgentId: string;
87
+ targetAgentId: string;
88
+ toolCallId: string;
89
+ scope: 'turn' | 'conversation';
90
+ /** Causal order: number of handoffs visible at this batch's input. */
91
+ depth: number;
92
+ }
93
+
94
+ /** Versioned graph checkpoint payload, scoped to one logical user turn. */
95
+ export interface HandoffState {
96
+ version: 1;
97
+ executionId: string;
98
+ entryAgentId: string;
99
+ maxHandoffs?: number;
100
+ transitions: HandoffTransition[];
101
+ parallel: boolean;
102
+ /** False when resuming an older checkpoint without complete routing provenance. */
103
+ historyComplete?: boolean;
104
+ }
105
+
106
+ /** Only a candidate from a completed top-level run may be promoted by a host. */
107
+ export type HandoffOutcome = {
108
+ executionId: string;
109
+ entryAgentId: string;
110
+ transitions: readonly HandoffTransition[];
111
+ } & (
112
+ | { status: 'candidate'; agentId: string; transitionId: string }
113
+ | { status: 'unchanged' | 'ambiguous' }
114
+ | { status: 'incomplete'; reason: string }
115
+ );
116
+
81
117
  export type BaseGraphState = {
82
118
  messages: BaseMessage[];
83
119
  runStepState?: RunStepResumeState;
120
+ /** SDK-owned routing state; hosts must not synthesize it from messages. */
121
+ handoffState?: HandoffState;
84
122
  /**
85
123
  * The summary a summarize-only run produced. Kept in state because such a
86
124
  * run has no assistant reply: trace roots report it as the run's output
@@ -147,7 +185,7 @@ export interface ContextUsageEvent {
147
185
  * `RunConfig.fadingTiers[agentId]`.
148
186
  */
149
187
  export interface FadingTier {
150
- v: 1;
188
+ v: 2;
151
189
  /** Token budget the caps derive from, in raw token space. Never grows;
152
190
  * clamped to the current context window when seeded. */
153
191
  budgetTokens: number;
@@ -166,6 +204,8 @@ export interface EventHandler {
166
204
  data:
167
205
  | StreamEventData
168
206
  | ModelEndData
207
+ | ModelResponseEvent
208
+ | ModelToolsClaimedEvent
169
209
  | RunStep
170
210
  | RunStepDeltaEvent
171
211
  | RunStepClosedEvent
@@ -370,6 +410,8 @@ export type StandardGraphInput = {
370
410
  agents: AgentInputs[];
371
411
  /** Execution backend used to resolve the effective tool registry. */
372
412
  toolExecution?: ToolExecutionConfig;
413
+ /** Trusted single-agent client delegation policy; mixed batches fail closed. */
414
+ clientDelegatedToolNames?: readonly string[];
373
415
  langfuse?: LangfuseConfig;
374
416
  tokenCounter?: TokenCounter;
375
417
  indexTokenCountMap?: Record<string, number>;
@@ -434,6 +476,8 @@ export type GraphEdge = {
434
476
  condition?: (state: BaseGraphState) => boolean | string | string[];
435
477
  /** 'handoff' creates tools for dynamic routing, 'direct' creates direct edges, which also allow parallel execution */
436
478
  edgeType?: 'handoff' | 'direct';
479
+ /** Host may promote the destination after successful top-level completion. */
480
+ handoffScope?: 'turn' | 'conversation';
437
481
  /**
438
482
  * For direct edges: Optional prompt to add when transitioning through this edge.
439
483
  * String prompts can include variables like {results} which will be replaced with
@@ -463,15 +507,20 @@ export type GraphEdge = {
463
507
 
464
508
  export type GraphSubagentEdge = Omit<
465
509
  GraphEdge,
466
- 'edgeType' | 'condition' | 'promptKey'
510
+ 'edgeType' | 'condition' | 'promptKey' | 'handoffScope'
467
511
  > & {
468
512
  edgeType: 'direct';
469
513
  condition?: never;
470
514
  promptKey?: never;
515
+ handoffScope?: never;
471
516
  };
472
517
 
473
518
  export type MultiAgentGraphInput = StandardGraphInput & {
474
519
  edges: GraphEdge[];
520
+ /** Explicit fresh-turn entry; absent preserves topology-inferred entry points. */
521
+ entryAgentId?: string;
522
+ /** Shared logical-turn handoff cap. Zero forbids handoffs; absent uses only recursion limits. */
523
+ maxHandoffs?: number;
475
524
  /** Captures the designated member's final AI turn in graph state. */
476
525
  resultAgentId?: string;
477
526
  /** Optional per-member Pregel budget when the outer graph has its own topology budget. */
package/src/types/run.ts CHANGED
@@ -115,11 +115,13 @@ export type MultiAgentGraphConfig = {
115
115
  compileOptions?: g.CompileOptions;
116
116
  agents: g.AgentInputs[];
117
117
  edges: g.GraphEdge[];
118
+ entryAgentId?: string;
119
+ maxHandoffs?: number;
118
120
  };
119
121
 
120
122
  export type StandardGraphConfig = Omit<
121
123
  MultiAgentGraphConfig,
122
- 'edges' | 'type'
124
+ 'edges' | 'type' | 'entryAgentId' | 'maxHandoffs'
123
125
  > & { type?: 'standard'; signal?: AbortSignal };
124
126
 
125
127
  /**
@@ -277,6 +279,11 @@ export type RunConfig = {
277
279
  */
278
280
  langfuse?: g.LangfuseConfig;
279
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[];
280
287
  /**
281
288
  * Receives token usage for every model call made inside subagent child
282
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
+ }
@@ -14,6 +14,7 @@ import type { ToolOutputReferenceRegistry } from '@/tools/toolOutputReferences';
14
14
  import type { LangfuseConfig, SubagentExecutionContext } from './graph';
15
15
  import type { PreparedSubagents } from '@/tools/preparedSubagents';
16
16
  import type { RunBreakerScope } from '@/llm/streamLimits';
17
+ import type { HandoffRouting } from '@/graphs/handoff';
17
18
  import type { HumanInTheLoopConfig } from './hitl';
18
19
  import type { HookRegistry } from '@/hooks';
19
20
 
@@ -117,6 +118,8 @@ export type EagerEventToolCallChunkState = {
117
118
  };
118
119
 
119
120
  export type ToolNodeOptions = {
121
+ /** @internal Admission shared by tool nodes belonging to one multi-agent graph. */
122
+ handoffRouting?: Pick<HandoffRouting, 'finalize'>;
120
123
  name?: string;
121
124
  tags?: string[];
122
125
  /** Enables LangChain/LangGraph tracing for this ToolNode. Defaults to false. */
@@ -310,6 +313,8 @@ export type ToolNodeOptions = {
310
313
  ) => void;
311
314
  /** SDK-owned checkpoint snapshot for open run-step lifecycle state. */
312
315
  createRunStepResumeState?: () => RunStepResumeState;
316
+ /** Internal ownership bridge, awaited before ToolNode can execute a batch. */
317
+ onToolCallsClaimed?: (messageId: string, config: RunnableConfig) => Promise<void>;
313
318
  };
314
319
 
315
320
  export type ToolNodeConstructorParams = ToolRefs & ToolNodeOptions;