@librechat/agents 3.9.1 → 3.9.3

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 (65) hide show
  1. package/dist/cjs/graphs/Graph.cjs +13 -4
  2. package/dist/cjs/graphs/Graph.cjs.map +1 -1
  3. package/dist/cjs/graphs/MultiAgentGraph.cjs +97 -5
  4. package/dist/cjs/graphs/MultiAgentGraph.cjs.map +1 -1
  5. package/dist/cjs/graphs/handoff.cjs +195 -0
  6. package/dist/cjs/graphs/handoff.cjs.map +1 -0
  7. package/dist/cjs/graphs/index.cjs +1 -0
  8. package/dist/cjs/main.cjs +3 -0
  9. package/dist/cjs/messages/contextPruning.cjs +2 -1
  10. package/dist/cjs/messages/contextPruning.cjs.map +1 -1
  11. package/dist/cjs/messages/prune.cjs +3 -3
  12. package/dist/cjs/messages/prune.cjs.map +1 -1
  13. package/dist/cjs/run.cjs +33 -3
  14. package/dist/cjs/run.cjs.map +1 -1
  15. package/dist/cjs/tools/ToolNode.cjs +4 -1
  16. package/dist/cjs/tools/ToolNode.cjs.map +1 -1
  17. package/dist/cjs/utils/toolContent.cjs +6 -6
  18. package/dist/cjs/utils/toolContent.cjs.map +1 -1
  19. package/dist/cjs/utils/truncation.cjs +14 -5
  20. package/dist/cjs/utils/truncation.cjs.map +1 -1
  21. package/dist/esm/graphs/Graph.mjs +13 -4
  22. package/dist/esm/graphs/Graph.mjs.map +1 -1
  23. package/dist/esm/graphs/MultiAgentGraph.mjs +97 -5
  24. package/dist/esm/graphs/MultiAgentGraph.mjs.map +1 -1
  25. package/dist/esm/graphs/handoff.mjs +193 -0
  26. package/dist/esm/graphs/handoff.mjs.map +1 -0
  27. package/dist/esm/graphs/index.mjs +1 -0
  28. package/dist/esm/main.mjs +3 -2
  29. package/dist/esm/messages/contextPruning.mjs +2 -1
  30. package/dist/esm/messages/contextPruning.mjs.map +1 -1
  31. package/dist/esm/messages/prune.mjs +4 -4
  32. package/dist/esm/messages/prune.mjs.map +1 -1
  33. package/dist/esm/run.mjs +33 -3
  34. package/dist/esm/run.mjs.map +1 -1
  35. package/dist/esm/tools/ToolNode.mjs +4 -1
  36. package/dist/esm/tools/ToolNode.mjs.map +1 -1
  37. package/dist/esm/utils/toolContent.mjs +7 -7
  38. package/dist/esm/utils/toolContent.mjs.map +1 -1
  39. package/dist/esm/utils/truncation.mjs +14 -6
  40. package/dist/esm/utils/truncation.mjs.map +1 -1
  41. package/dist/types/graphs/Graph.d.ts +2 -0
  42. package/dist/types/graphs/MultiAgentGraph.d.ts +3 -0
  43. package/dist/types/graphs/handoff.d.ts +28 -0
  44. package/dist/types/graphs/index.d.ts +1 -0
  45. package/dist/types/run.d.ts +4 -0
  46. package/dist/types/tools/ToolNode.d.ts +2 -1
  47. package/dist/types/types/graph.d.ts +46 -1
  48. package/dist/types/types/run.d.ts +3 -1
  49. package/dist/types/types/tools.d.ts +3 -0
  50. package/dist/types/utils/toolContent.d.ts +1 -1
  51. package/dist/types/utils/truncation.d.ts +2 -0
  52. package/package.json +1 -1
  53. package/src/graphs/Graph.ts +8 -0
  54. package/src/graphs/MultiAgentGraph.ts +142 -4
  55. package/src/graphs/handoff.ts +263 -0
  56. package/src/graphs/index.ts +2 -0
  57. package/src/messages/contextPruning.ts +8 -2
  58. package/src/messages/prune.ts +15 -10
  59. package/src/run.ts +54 -2
  60. package/src/tools/ToolNode.ts +14 -6
  61. package/src/types/graph.ts +44 -1
  62. package/src/types/run.ts +3 -1
  63. package/src/types/tools.ts +3 -0
  64. package/src/utils/toolContent.ts +23 -7
  65. package/src/utils/truncation.ts +31 -6
@@ -24,6 +24,7 @@ export declare class MultiAgentGraph extends StandardGraph {
24
24
  private handoffSourceIds;
25
25
  private readonly resultAgentId?;
26
26
  private readonly memberRecursionLimit?;
27
+ private reachableAgentIds?;
27
28
  private handoffPromptLabels;
28
29
  /**
29
30
  * Map of agentId to parallel group info.
@@ -37,6 +38,8 @@ export declare class MultiAgentGraph extends StandardGraph {
37
38
  */
38
39
  private agentParallelGroups;
39
40
  constructor(input: t.MultiAgentGraphInput, dependencies?: GraphFactoryDependencies);
41
+ private validateHandoffScopes;
42
+ private resolveEntryReachability;
40
43
  /**
41
44
  * Fails fast when an edge references an agent that is not in
42
45
  * `agentContexts`. Without this check, the underlying LangGraph
@@ -0,0 +1,28 @@
1
+ import { Command } from '@langchain/langgraph';
2
+ import type { BaseChannel, OverwriteValue } from '@langchain/langgraph';
3
+ import type { RunnableConfig } from '@langchain/core/runnables';
4
+ import type { BaseGraphState, HandoffOutcome, HandoffState, HandoffTransition } from '@/types/graph';
5
+ export type HandoffRequest = Pick<HandoffTransition, 'sourceAgentId' | 'targetAgentId' | 'toolCallId' | 'scope'>;
6
+ /** Union by identity makes checkpoint replay idempotent and parallel merges commutative. */
7
+ export declare function mergeHandoffState(current: HandoffState | undefined, update: HandoffState | undefined): HandoffState | undefined;
8
+ export declare function handoffStateAnnotation(): BaseChannel<HandoffState | undefined, HandoffState | OverwriteValue<HandoffState | undefined> | undefined>;
9
+ export declare class HandoffLimitError extends Error {
10
+ readonly limit: number;
11
+ constructor(limit: number);
12
+ }
13
+ /** One owner per graph, never shared with isolated child executions. */
14
+ export declare class HandoffRouting {
15
+ private readonly entryAgentId;
16
+ private readonly maxHandoffs;
17
+ private readonly parallel;
18
+ private state;
19
+ constructor(entryAgentId: string, maxHandoffs: number | undefined, parallel: boolean);
20
+ private freshState;
21
+ start(): HandoffState;
22
+ resume(state: HandoffState | undefined): void;
23
+ restore(state: HandoffState | undefined): void;
24
+ snapshot(): HandoffState;
25
+ /** Called after tools settle but before Commands can schedule recipients. */
26
+ finalize(commands: Command[], input: BaseGraphState, config: RunnableConfig): void;
27
+ outcome(reason?: string): HandoffOutcome;
28
+ }
@@ -2,3 +2,4 @@ export * from './Graph';
2
2
  export * from './MultiAgentGraph';
3
3
  export * from './createGraph';
4
4
  export type * from './graphFactory';
5
+ export { HandoffLimitError } from './handoff';
@@ -60,6 +60,7 @@ export declare class Run<_T extends t.BaseGraphState> {
60
60
  /** Distinguishes sibling forks started from the same explicit checkpoint. */
61
61
  private checkpointForkSeq;
62
62
  private _haltedReason;
63
+ private _handoffOutcome?;
63
64
  private constructor();
64
65
  private createLegacyGraph;
65
66
  private createMultiAgentGraph;
@@ -216,6 +217,9 @@ export declare class Run<_T extends t.BaseGraphState> {
216
217
  * no halt reason.
217
218
  */
218
219
  getHaltReason(): string | undefined;
220
+ private getHandoffIncompleteReason;
221
+ /** Execution evidence only. The host must authorize and durably commit a candidate. */
222
+ getHandoffOutcome(): t.HandoffOutcome | undefined;
219
223
  /**
220
224
  * Resume a paused HITL run with the value the user (or whatever
221
225
  * decided the interrupt) supplied. The default `TResume` covers the
@@ -228,7 +228,8 @@ export declare class ToolNode<T = any> extends RunnableCallable<T, T> {
228
228
  * other's in-flight state.
229
229
  */
230
230
  private anonBatchCounter;
231
- constructor({ tools, toolMap, name, tags, trace, runLangfuse, agentLangfuse, errorHandler, toolCallStepIds, handleToolErrors, loadRuntimeTools, toolRegistry, toolDefinitions, getDiscoveredToolNames, sessions, codeSessionKey, eventDrivenMode, eagerEventToolExecution, eagerEventToolExecutions, eagerEventToolUsageCount, eagerEventToolSuppressions, agentId, executingAgentId, executionContext, executingAgentName, rootAgentId, rootAgentName, directToolNames, interruptingToolNames, codeSessionToolNames, maxContextTokens, maxToolResultChars, hookRegistry, humanInTheLoop, toolOutputReferences, toolOutputRegistry, toolExecution, fileCheckpointer, getBreakerSignal, getRunScope, preparedSubagents, restoreRunStepResumeState, createRunStepResumeState, }: t.ToolNodeConstructorParams);
231
+ private handoffRouting?;
232
+ constructor({ handoffRouting, tools, toolMap, name, tags, trace, runLangfuse, agentLangfuse, errorHandler, toolCallStepIds, handleToolErrors, loadRuntimeTools, toolRegistry, toolDefinitions, getDiscoveredToolNames, sessions, codeSessionKey, eventDrivenMode, eagerEventToolExecution, eagerEventToolExecutions, eagerEventToolUsageCount, eagerEventToolSuppressions, agentId, executingAgentId, executionContext, executingAgentName, rootAgentId, rootAgentName, directToolNames, interruptingToolNames, codeSessionToolNames, maxContextTokens, maxToolResultChars, hookRegistry, humanInTheLoop, toolOutputReferences, toolOutputRegistry, toolExecution, fileCheckpointer, getBreakerSignal, getRunScope, preparedSubagents, restoreRunStepResumeState, createRunStepResumeState, }: t.ToolNodeConstructorParams);
232
233
  invoke(input: any, options?: Partial<RunnableConfig>): Promise<any>;
233
234
  private withToolScope;
234
235
  /**
@@ -27,9 +27,47 @@ export type ClientCallbacks = {
27
27
  export type SystemCallbacks = {
28
28
  [K in keyof ClientCallbacks]: ClientCallbacks[K] extends ClientCallback<infer Args> ? (...args: Args) => void : never;
29
29
  };
30
+ /** An executed, SDK-generated handoff, identified independently of stream events. */
31
+ export interface HandoffTransition {
32
+ id: string;
33
+ sourceAgentId: string;
34
+ targetAgentId: string;
35
+ toolCallId: string;
36
+ scope: 'turn' | 'conversation';
37
+ /** Causal order: number of handoffs visible at this batch's input. */
38
+ depth: number;
39
+ }
40
+ /** Versioned graph checkpoint payload, scoped to one logical user turn. */
41
+ export interface HandoffState {
42
+ version: 1;
43
+ executionId: string;
44
+ entryAgentId: string;
45
+ maxHandoffs?: number;
46
+ transitions: HandoffTransition[];
47
+ parallel: boolean;
48
+ /** False when resuming an older checkpoint without complete routing provenance. */
49
+ historyComplete?: boolean;
50
+ }
51
+ /** Only a candidate from a completed top-level run may be promoted by a host. */
52
+ export type HandoffOutcome = {
53
+ executionId: string;
54
+ entryAgentId: string;
55
+ transitions: readonly HandoffTransition[];
56
+ } & ({
57
+ status: 'candidate';
58
+ agentId: string;
59
+ transitionId: string;
60
+ } | {
61
+ status: 'unchanged' | 'ambiguous';
62
+ } | {
63
+ status: 'incomplete';
64
+ reason: string;
65
+ });
30
66
  export type BaseGraphState = {
31
67
  messages: BaseMessage[];
32
68
  runStepState?: RunStepResumeState;
69
+ /** SDK-owned routing state; hosts must not synthesize it from messages. */
70
+ handoffState?: HandoffState;
33
71
  /**
34
72
  * The summary a summarize-only run produced. Kept in state because such a
35
73
  * run has no assistant reply: trace roots report it as the run's output
@@ -316,6 +354,8 @@ export type GraphEdge = {
316
354
  condition?: (state: BaseGraphState) => boolean | string | string[];
317
355
  /** 'handoff' creates tools for dynamic routing, 'direct' creates direct edges, which also allow parallel execution */
318
356
  edgeType?: 'handoff' | 'direct';
357
+ /** Host may promote the destination after successful top-level completion. */
358
+ handoffScope?: 'turn' | 'conversation';
319
359
  /**
320
360
  * For direct edges: Optional prompt to add when transitioning through this edge.
321
361
  * String prompts can include variables like {results} which will be replaced with
@@ -337,13 +377,18 @@ export type GraphEdge = {
337
377
  */
338
378
  promptKey?: string;
339
379
  };
340
- export type GraphSubagentEdge = Omit<GraphEdge, 'edgeType' | 'condition' | 'promptKey'> & {
380
+ export type GraphSubagentEdge = Omit<GraphEdge, 'edgeType' | 'condition' | 'promptKey' | 'handoffScope'> & {
341
381
  edgeType: 'direct';
342
382
  condition?: never;
343
383
  promptKey?: never;
384
+ handoffScope?: never;
344
385
  };
345
386
  export type MultiAgentGraphInput = StandardGraphInput & {
346
387
  edges: GraphEdge[];
388
+ /** Explicit fresh-turn entry; absent preserves topology-inferred entry points. */
389
+ entryAgentId?: string;
390
+ /** Shared logical-turn handoff cap. Zero forbids handoffs; absent uses only recursion limits. */
391
+ maxHandoffs?: number;
347
392
  /** Captures the designated member's final AI turn in graph state. */
348
393
  resultAgentId?: string;
349
394
  /** Optional per-member Pregel budget when the outer graph has its own topology budget. */
@@ -101,8 +101,10 @@ export type MultiAgentGraphConfig = {
101
101
  compileOptions?: g.CompileOptions;
102
102
  agents: g.AgentInputs[];
103
103
  edges: g.GraphEdge[];
104
+ entryAgentId?: string;
105
+ maxHandoffs?: number;
104
106
  };
105
- export type StandardGraphConfig = Omit<MultiAgentGraphConfig, 'edges' | 'type'> & {
107
+ export type StandardGraphConfig = Omit<MultiAgentGraphConfig, 'edges' | 'type' | 'entryAgentId' | 'maxHandoffs'> & {
106
108
  type?: 'standard';
107
109
  signal?: AbortSignal;
108
110
  };
@@ -6,6 +6,7 @@ import type { ToolOutputReferenceRegistry } from '@/tools/toolOutputReferences';
6
6
  import type { LangfuseConfig, SubagentExecutionContext } from './graph';
7
7
  import type { PreparedSubagents } from '@/tools/preparedSubagents';
8
8
  import type { RunBreakerScope } from '@/llm/streamLimits';
9
+ import type { HandoffRouting } from '@/graphs/handoff';
9
10
  import type { HumanInTheLoopConfig } from './hitl';
10
11
  import type { HookRegistry } from '@/hooks';
11
12
  /** Replacement type for `import type { ToolCall } from '@langchain/core/messages/tool'` in order to have stringified args typed */
@@ -103,6 +104,8 @@ export type EagerEventToolCallChunkState = {
103
104
  sealedArgsFragment?: string;
104
105
  };
105
106
  export type ToolNodeOptions = {
107
+ /** @internal Admission shared by tool nodes belonging to one multi-agent graph. */
108
+ handoffRouting?: Pick<HandoffRouting, 'finalize'>;
106
109
  name?: string;
107
110
  tags?: string[];
108
111
  /** Enables LangChain/LangGraph tracing for this ToolNode. Defaults to false. */
@@ -16,7 +16,7 @@ type StructuredWorkState = {
16
16
  export type BoundedStructuredSerialization = {
17
17
  /** Provider-facing head/tail preview, bounded by `maxChars`. */
18
18
  content: string;
19
- /** Exact serialized prefix up to the defensive traversal-work ceiling. */
19
+ /** Code-point-aligned serialized prefix up to the traversal-work ceiling. */
20
20
  prefix: string;
21
21
  /** Exact length, or `Number.MAX_SAFE_INTEGER` when traversal was capped. */
22
22
  originalChars: number;
@@ -4,6 +4,8 @@
4
4
  * Prevents oversized tool outputs from entering the message array and
5
5
  * consuming the entire context window.
6
6
  */
7
+ /** Slices by UTF-16 code units, dropping orphaned surrogate halves at either edge. */
8
+ export declare function sliceWithoutSplittingSurrogates(value: string, start: number, end?: number): string;
7
9
  /**
8
10
  * Absolute hard cap on tool result length (characters).
9
11
  * Even if the model has a 1M-token context, a single tool result
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@librechat/agents",
3
- "version": "3.9.1",
3
+ "version": "3.9.3",
4
4
  "reova": {
5
5
  "enabled": true,
6
6
  "endpoint": "https://telemetry.reo.dev/data"
@@ -38,6 +38,7 @@ import type {
38
38
  } from '@/graphs/graphFactory';
39
39
  import type { OverflowRecoveryPlan } from '@/llm/contextOverflowRecovery';
40
40
  import type { FallbackErrorContext } from '@/llm/invoke';
41
+ import type { HandoffRouting } from './handoff';
41
42
  import type { HookRegistry } from '@/hooks';
42
43
  import type * as t from '@/types';
43
44
  import {
@@ -185,6 +186,7 @@ import { getTruncationStopReason } from '@/llm/truncation';
185
186
  import { createSchemaOnlyTools } from '@/tools/schema';
186
187
  import { AgentContext } from '@/agents/AgentContext';
187
188
  import { createFakeStreamingLLM } from '@/llm/fake';
189
+ import { handoffStateAnnotation } from './handoff';
188
190
  import { handleToolCalls } from '@/tools/handlers';
189
191
  import { isThinkingEnabled } from '@/llm/request';
190
192
  import { resolveMaxSeals } from '@/llm/preempt';
@@ -1354,6 +1356,7 @@ export class StandardGraph extends Graph<t.BaseGraphState, t.GraphNode> {
1354
1356
  subagentUsageSink?: t.SubagentUsageSink;
1355
1357
  /** See {@link t.StandardGraphInput.subagentScope}. */
1356
1358
  subagentScope: boolean;
1359
+ handoffRouting?: HandoffRouting;
1357
1360
  /** See {@link t.StandardGraphInput.subagentTasks}. */
1358
1361
  subagentTasks: t.SubagentTaskConfig | undefined;
1359
1362
  /** See {@link t.StandardGraphInput.subagentExecutionContext}. */
@@ -2871,6 +2874,7 @@ export class StandardGraph extends Graph<t.BaseGraphState, t.GraphNode> {
2871
2874
  }
2872
2875
 
2873
2876
  const node = new CustomToolNode<t.BaseGraphState>({
2877
+ handoffRouting: this.handoffRouting,
2874
2878
  tools: allTools,
2875
2879
  toolMap: allToolMap,
2876
2880
  trace: traceToolNode,
@@ -2960,6 +2964,7 @@ export class StandardGraph extends Graph<t.BaseGraphState, t.GraphNode> {
2960
2964
  : currentToolMap;
2961
2965
 
2962
2966
  const node = new CustomToolNode<t.BaseGraphState>({
2967
+ handoffRouting: this.handoffRouting,
2963
2968
  tools: allTraditionalTools,
2964
2969
  toolMap: traditionalToolMap,
2965
2970
  trace: traceToolNode,
@@ -5434,6 +5439,7 @@ export class StandardGraph extends Graph<t.BaseGraphState, t.GraphNode> {
5434
5439
  ): Promise<Partial<t.AgentSubgraphState>> => {
5435
5440
  this.config = config;
5436
5441
  this.restoreRunStepResumeState(state.runStepState);
5442
+ this.handoffRouting?.restore(state.handoffState);
5437
5443
  const result = await invoke();
5438
5444
  /** An ordinary run on a checkpointed thread inherits the last
5439
5445
  * compaction's summary in state; it is not this run's output. */
@@ -5512,6 +5518,7 @@ export class StandardGraph extends Graph<t.BaseGraphState, t.GraphNode> {
5512
5518
  default: () => undefined,
5513
5519
  }),
5514
5520
  runStepState: this.createRunStepStateAnnotation(),
5521
+ handoffState: handoffStateAnnotation(),
5515
5522
  });
5516
5523
 
5517
5524
  const readChargeCredits = ():
@@ -5727,6 +5734,7 @@ export class StandardGraph extends Graph<t.BaseGraphState, t.GraphNode> {
5727
5734
  default: () => undefined,
5728
5735
  }),
5729
5736
  runStepState: this.createRunStepStateAnnotation(),
5737
+ handoffState: handoffStateAnnotation(),
5730
5738
  });
5731
5739
  const compactingAgentNode = async (
5732
5740
  state: t.AgentSubgraphState,
@@ -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);