@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.
- package/dist/cjs/graphs/Graph.cjs +13 -4
- package/dist/cjs/graphs/Graph.cjs.map +1 -1
- package/dist/cjs/graphs/MultiAgentGraph.cjs +97 -5
- package/dist/cjs/graphs/MultiAgentGraph.cjs.map +1 -1
- package/dist/cjs/graphs/handoff.cjs +195 -0
- package/dist/cjs/graphs/handoff.cjs.map +1 -0
- package/dist/cjs/graphs/index.cjs +1 -0
- package/dist/cjs/main.cjs +3 -0
- package/dist/cjs/messages/contextPruning.cjs +2 -1
- package/dist/cjs/messages/contextPruning.cjs.map +1 -1
- package/dist/cjs/messages/prune.cjs +3 -3
- package/dist/cjs/messages/prune.cjs.map +1 -1
- package/dist/cjs/run.cjs +33 -3
- package/dist/cjs/run.cjs.map +1 -1
- package/dist/cjs/tools/ToolNode.cjs +4 -1
- package/dist/cjs/tools/ToolNode.cjs.map +1 -1
- package/dist/cjs/utils/toolContent.cjs +6 -6
- package/dist/cjs/utils/toolContent.cjs.map +1 -1
- package/dist/cjs/utils/truncation.cjs +14 -5
- package/dist/cjs/utils/truncation.cjs.map +1 -1
- package/dist/esm/graphs/Graph.mjs +13 -4
- package/dist/esm/graphs/Graph.mjs.map +1 -1
- package/dist/esm/graphs/MultiAgentGraph.mjs +97 -5
- package/dist/esm/graphs/MultiAgentGraph.mjs.map +1 -1
- package/dist/esm/graphs/handoff.mjs +193 -0
- package/dist/esm/graphs/handoff.mjs.map +1 -0
- package/dist/esm/graphs/index.mjs +1 -0
- package/dist/esm/main.mjs +3 -2
- package/dist/esm/messages/contextPruning.mjs +2 -1
- package/dist/esm/messages/contextPruning.mjs.map +1 -1
- package/dist/esm/messages/prune.mjs +4 -4
- package/dist/esm/messages/prune.mjs.map +1 -1
- package/dist/esm/run.mjs +33 -3
- package/dist/esm/run.mjs.map +1 -1
- package/dist/esm/tools/ToolNode.mjs +4 -1
- package/dist/esm/tools/ToolNode.mjs.map +1 -1
- package/dist/esm/utils/toolContent.mjs +7 -7
- package/dist/esm/utils/toolContent.mjs.map +1 -1
- package/dist/esm/utils/truncation.mjs +14 -6
- package/dist/esm/utils/truncation.mjs.map +1 -1
- package/dist/types/graphs/Graph.d.ts +2 -0
- package/dist/types/graphs/MultiAgentGraph.d.ts +3 -0
- package/dist/types/graphs/handoff.d.ts +28 -0
- package/dist/types/graphs/index.d.ts +1 -0
- package/dist/types/run.d.ts +4 -0
- package/dist/types/tools/ToolNode.d.ts +2 -1
- package/dist/types/types/graph.d.ts +46 -1
- package/dist/types/types/run.d.ts +3 -1
- package/dist/types/types/tools.d.ts +3 -0
- package/dist/types/utils/toolContent.d.ts +1 -1
- package/dist/types/utils/truncation.d.ts +2 -0
- package/package.json +1 -1
- package/src/graphs/Graph.ts +8 -0
- package/src/graphs/MultiAgentGraph.ts +142 -4
- package/src/graphs/handoff.ts +263 -0
- package/src/graphs/index.ts +2 -0
- package/src/messages/contextPruning.ts +8 -2
- package/src/messages/prune.ts +15 -10
- package/src/run.ts +54 -2
- package/src/tools/ToolNode.ts +14 -6
- package/src/types/graph.ts +44 -1
- package/src/types/run.ts +3 -1
- package/src/types/tools.ts +3 -0
- package/src/utils/toolContent.ts +23 -7
- 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
|
+
}
|
package/dist/types/run.d.ts
CHANGED
|
@@ -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
|
-
|
|
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
|
-
/**
|
|
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
package/src/graphs/Graph.ts
CHANGED
|
@@ -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: {
|
|
905
|
+
update: {
|
|
906
|
+
messages: state.messages.concat(toolMessage),
|
|
907
|
+
handoffRequest: {
|
|
908
|
+
sourceAgentId,
|
|
909
|
+
targetAgentId: destination,
|
|
910
|
+
toolCallId,
|
|
911
|
+
scope: edge.handoffScope ?? 'turn',
|
|
912
|
+
},
|
|
913
|
+
},
|
|
793
914
|
graph: Command.PARENT,
|
|
794
915
|
});
|
|
795
916
|
},
|
|
@@ -926,7 +1047,15 @@ export class MultiAgentGraph extends StandardGraph {
|
|
|
926
1047
|
|
|
927
1048
|
return new Command({
|
|
928
1049
|
goto: destination,
|
|
929
|
-
update: {
|
|
1050
|
+
update: {
|
|
1051
|
+
messages: filteredMessages,
|
|
1052
|
+
handoffRequest: {
|
|
1053
|
+
sourceAgentId,
|
|
1054
|
+
targetAgentId: destination,
|
|
1055
|
+
toolCallId,
|
|
1056
|
+
scope: edge.handoffScope ?? 'turn',
|
|
1057
|
+
},
|
|
1058
|
+
},
|
|
930
1059
|
graph: Command.PARENT,
|
|
931
1060
|
});
|
|
932
1061
|
},
|
|
@@ -1274,6 +1403,7 @@ export class MultiAgentGraph extends StandardGraph {
|
|
|
1274
1403
|
default: () => undefined,
|
|
1275
1404
|
}),
|
|
1276
1405
|
runStepState: this.createRunStepStateAnnotation(),
|
|
1406
|
+
handoffState: handoffStateAnnotation(),
|
|
1277
1407
|
});
|
|
1278
1408
|
|
|
1279
1409
|
const builder = new StateGraph(StateAnnotation);
|
|
@@ -1298,9 +1428,16 @@ export class MultiAgentGraph extends StandardGraph {
|
|
|
1298
1428
|
const agentIds =
|
|
1299
1429
|
summarizeOnlyAgentId != null
|
|
1300
1430
|
? [summarizeOnlyAgentId]
|
|
1301
|
-
: [...this.agentContexts.keys()];
|
|
1431
|
+
: [...(this.reachableAgentIds ?? this.agentContexts.keys())];
|
|
1302
1432
|
const handoffEdges = summarizeOnlyAgentId != null ? [] : this.handoffEdges;
|
|
1303
|
-
const directEdges =
|
|
1433
|
+
const directEdges =
|
|
1434
|
+
summarizeOnlyAgentId != null
|
|
1435
|
+
? []
|
|
1436
|
+
: this.directEdges.filter((edge) => {
|
|
1437
|
+
if (this.reachableAgentIds == null) return true;
|
|
1438
|
+
const sources = Array.isArray(edge.from) ? edge.from : [edge.from];
|
|
1439
|
+
return sources.every((id) => this.reachableAgentIds!.has(id));
|
|
1440
|
+
});
|
|
1304
1441
|
|
|
1305
1442
|
// Add all agents as complete subgraphs
|
|
1306
1443
|
for (const agentId of agentIds) {
|
|
@@ -1348,6 +1485,7 @@ export class MultiAgentGraph extends StandardGraph {
|
|
|
1348
1485
|
state: t.MultiAgentGraphState,
|
|
1349
1486
|
config?: LangGraphRunnableConfig
|
|
1350
1487
|
): Promise<t.MultiAgentGraphState | Command> => {
|
|
1488
|
+
this.handoffRouting?.restore(state.handoffState);
|
|
1351
1489
|
let result: t.MultiAgentGraphState;
|
|
1352
1490
|
let inputMessages = state.messages;
|
|
1353
1491
|
const agentContext = this.agentContexts.get(agentId);
|