@librechat/agents 3.1.74 → 3.1.75-dev.0

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 (137) hide show
  1. package/README.md +66 -0
  2. package/dist/cjs/agents/AgentContext.cjs +84 -37
  3. package/dist/cjs/agents/AgentContext.cjs.map +1 -1
  4. package/dist/cjs/graphs/Graph.cjs +13 -3
  5. package/dist/cjs/graphs/Graph.cjs.map +1 -1
  6. package/dist/cjs/llm/anthropic/index.cjs +145 -52
  7. package/dist/cjs/llm/anthropic/index.cjs.map +1 -1
  8. package/dist/cjs/llm/anthropic/types.cjs.map +1 -1
  9. package/dist/cjs/llm/anthropic/utils/message_inputs.cjs +25 -15
  10. package/dist/cjs/llm/anthropic/utils/message_inputs.cjs.map +1 -1
  11. package/dist/cjs/llm/anthropic/utils/message_outputs.cjs +84 -70
  12. package/dist/cjs/llm/anthropic/utils/message_outputs.cjs.map +1 -1
  13. package/dist/cjs/llm/bedrock/index.cjs +1 -1
  14. package/dist/cjs/llm/bedrock/index.cjs.map +1 -1
  15. package/dist/cjs/llm/bedrock/utils/message_inputs.cjs +213 -3
  16. package/dist/cjs/llm/bedrock/utils/message_inputs.cjs.map +1 -1
  17. package/dist/cjs/llm/bedrock/utils/message_outputs.cjs +2 -1
  18. package/dist/cjs/llm/bedrock/utils/message_outputs.cjs.map +1 -1
  19. package/dist/cjs/llm/google/utils/common.cjs +5 -4
  20. package/dist/cjs/llm/google/utils/common.cjs.map +1 -1
  21. package/dist/cjs/llm/openai/index.cjs +468 -647
  22. package/dist/cjs/llm/openai/index.cjs.map +1 -1
  23. package/dist/cjs/llm/openai/utils/index.cjs +1 -448
  24. package/dist/cjs/llm/openai/utils/index.cjs.map +1 -1
  25. package/dist/cjs/llm/openrouter/index.cjs +57 -175
  26. package/dist/cjs/llm/openrouter/index.cjs.map +1 -1
  27. package/dist/cjs/llm/vertexai/index.cjs +5 -3
  28. package/dist/cjs/llm/vertexai/index.cjs.map +1 -1
  29. package/dist/cjs/messages/cache.cjs +39 -4
  30. package/dist/cjs/messages/cache.cjs.map +1 -1
  31. package/dist/cjs/messages/core.cjs +7 -6
  32. package/dist/cjs/messages/core.cjs.map +1 -1
  33. package/dist/cjs/messages/format.cjs +7 -6
  34. package/dist/cjs/messages/format.cjs.map +1 -1
  35. package/dist/cjs/messages/langchain.cjs +26 -0
  36. package/dist/cjs/messages/langchain.cjs.map +1 -0
  37. package/dist/cjs/messages/prune.cjs +7 -6
  38. package/dist/cjs/messages/prune.cjs.map +1 -1
  39. package/dist/cjs/tools/ToolNode.cjs +5 -1
  40. package/dist/cjs/tools/ToolNode.cjs.map +1 -1
  41. package/dist/esm/agents/AgentContext.mjs +85 -38
  42. package/dist/esm/agents/AgentContext.mjs.map +1 -1
  43. package/dist/esm/graphs/Graph.mjs +13 -3
  44. package/dist/esm/graphs/Graph.mjs.map +1 -1
  45. package/dist/esm/llm/anthropic/index.mjs +146 -54
  46. package/dist/esm/llm/anthropic/index.mjs.map +1 -1
  47. package/dist/esm/llm/anthropic/types.mjs.map +1 -1
  48. package/dist/esm/llm/anthropic/utils/message_inputs.mjs +25 -15
  49. package/dist/esm/llm/anthropic/utils/message_inputs.mjs.map +1 -1
  50. package/dist/esm/llm/anthropic/utils/message_outputs.mjs +84 -71
  51. package/dist/esm/llm/anthropic/utils/message_outputs.mjs.map +1 -1
  52. package/dist/esm/llm/bedrock/index.mjs +1 -1
  53. package/dist/esm/llm/bedrock/index.mjs.map +1 -1
  54. package/dist/esm/llm/bedrock/utils/message_inputs.mjs +214 -4
  55. package/dist/esm/llm/bedrock/utils/message_inputs.mjs.map +1 -1
  56. package/dist/esm/llm/bedrock/utils/message_outputs.mjs +2 -1
  57. package/dist/esm/llm/bedrock/utils/message_outputs.mjs.map +1 -1
  58. package/dist/esm/llm/google/utils/common.mjs +5 -4
  59. package/dist/esm/llm/google/utils/common.mjs.map +1 -1
  60. package/dist/esm/llm/openai/index.mjs +469 -648
  61. package/dist/esm/llm/openai/index.mjs.map +1 -1
  62. package/dist/esm/llm/openai/utils/index.mjs +4 -449
  63. package/dist/esm/llm/openai/utils/index.mjs.map +1 -1
  64. package/dist/esm/llm/openrouter/index.mjs +57 -175
  65. package/dist/esm/llm/openrouter/index.mjs.map +1 -1
  66. package/dist/esm/llm/vertexai/index.mjs +5 -3
  67. package/dist/esm/llm/vertexai/index.mjs.map +1 -1
  68. package/dist/esm/messages/cache.mjs +39 -4
  69. package/dist/esm/messages/cache.mjs.map +1 -1
  70. package/dist/esm/messages/core.mjs +7 -6
  71. package/dist/esm/messages/core.mjs.map +1 -1
  72. package/dist/esm/messages/format.mjs +7 -6
  73. package/dist/esm/messages/format.mjs.map +1 -1
  74. package/dist/esm/messages/langchain.mjs +23 -0
  75. package/dist/esm/messages/langchain.mjs.map +1 -0
  76. package/dist/esm/messages/prune.mjs +7 -6
  77. package/dist/esm/messages/prune.mjs.map +1 -1
  78. package/dist/esm/tools/ToolNode.mjs +5 -1
  79. package/dist/esm/tools/ToolNode.mjs.map +1 -1
  80. package/dist/types/agents/AgentContext.d.ts +14 -4
  81. package/dist/types/agents/__tests__/promptCacheLiveHelpers.d.ts +46 -0
  82. package/dist/types/llm/anthropic/index.d.ts +22 -9
  83. package/dist/types/llm/anthropic/types.d.ts +5 -1
  84. package/dist/types/llm/anthropic/utils/message_outputs.d.ts +13 -6
  85. package/dist/types/llm/anthropic/utils/output_parsers.d.ts +1 -1
  86. package/dist/types/llm/openai/index.d.ts +21 -24
  87. package/dist/types/llm/openrouter/index.d.ts +11 -9
  88. package/dist/types/llm/vertexai/index.d.ts +1 -0
  89. package/dist/types/messages/cache.d.ts +4 -1
  90. package/dist/types/messages/langchain.d.ts +27 -0
  91. package/dist/types/types/graph.d.ts +26 -38
  92. package/dist/types/types/llm.d.ts +3 -3
  93. package/dist/types/types/run.d.ts +2 -0
  94. package/dist/types/types/stream.d.ts +1 -1
  95. package/package.json +17 -16
  96. package/src/agents/AgentContext.ts +123 -44
  97. package/src/agents/__tests__/AgentContext.anthropic.live.test.ts +116 -0
  98. package/src/agents/__tests__/AgentContext.bedrock.live.test.ts +149 -0
  99. package/src/agents/__tests__/AgentContext.test.ts +155 -2
  100. package/src/agents/__tests__/promptCacheLiveHelpers.ts +165 -0
  101. package/src/graphs/Graph.ts +24 -4
  102. package/src/graphs/__tests__/composition.smoke.test.ts +188 -0
  103. package/src/llm/anthropic/index.ts +252 -84
  104. package/src/llm/anthropic/llm.spec.ts +751 -102
  105. package/src/llm/anthropic/types.ts +9 -1
  106. package/src/llm/anthropic/utils/message_inputs.ts +43 -20
  107. package/src/llm/anthropic/utils/message_outputs.ts +119 -101
  108. package/src/llm/anthropic/utils/server-tool-inputs.test.ts +77 -0
  109. package/src/llm/bedrock/index.ts +2 -2
  110. package/src/llm/bedrock/llm.spec.ts +341 -0
  111. package/src/llm/bedrock/utils/message_inputs.ts +303 -4
  112. package/src/llm/bedrock/utils/message_outputs.ts +2 -1
  113. package/src/llm/custom-chat-models.smoke.test.ts +662 -0
  114. package/src/llm/google/llm.spec.ts +339 -57
  115. package/src/llm/google/utils/common.ts +53 -48
  116. package/src/llm/openai/contentBlocks.test.ts +346 -0
  117. package/src/llm/openai/index.ts +736 -837
  118. package/src/llm/openai/utils/index.ts +84 -64
  119. package/src/llm/openrouter/index.ts +124 -247
  120. package/src/llm/openrouter/reasoning.test.ts +8 -1
  121. package/src/llm/vertexai/index.ts +11 -5
  122. package/src/llm/vertexai/llm.spec.ts +28 -1
  123. package/src/messages/cache.test.ts +106 -4
  124. package/src/messages/cache.ts +57 -5
  125. package/src/messages/core.ts +16 -9
  126. package/src/messages/format.ts +9 -6
  127. package/src/messages/langchain.ts +39 -0
  128. package/src/messages/prune.ts +12 -8
  129. package/src/scripts/caching.ts +2 -3
  130. package/src/specs/anthropic.simple.test.ts +61 -0
  131. package/src/specs/summarization.test.ts +58 -61
  132. package/src/tools/ToolNode.ts +5 -1
  133. package/src/types/graph.ts +35 -88
  134. package/src/types/llm.ts +3 -3
  135. package/src/types/run.ts +2 -0
  136. package/src/types/stream.ts +1 -1
  137. package/src/utils/llmConfig.ts +1 -6
@@ -0,0 +1,165 @@
1
+ import { expect } from '@jest/globals';
2
+ import { HumanMessage } from '@langchain/core/messages';
3
+ import type { UsageMetadata } from '@langchain/core/messages';
4
+ import type * as t from '@/types';
5
+ import { GraphEvents, Providers } from '@/common';
6
+ import { AgentContext } from '../AgentContext';
7
+ import { ModelEndHandler } from '@/events';
8
+ import { Run } from '@/run';
9
+
10
+ type LivePromptCacheProvider = Providers.ANTHROPIC | Providers.BEDROCK;
11
+
12
+ type PromptCacheExpectedSystemBlock =
13
+ | { type: 'text'; text: string; cache_control?: { type: 'ephemeral' } }
14
+ | { cachePoint: { type: 'default' } };
15
+
16
+ type LivePromptCacheClientOptions =
17
+ | t.ClientOptions
18
+ | t.BedrockAnthropicClientOptions;
19
+
20
+ export function buildStableInstructions({
21
+ nonce,
22
+ providerLabel,
23
+ }: {
24
+ nonce: string;
25
+ providerLabel: string;
26
+ }): string {
27
+ const records = Array.from(
28
+ { length: 360 },
29
+ (_, index) =>
30
+ `Stable ${providerLabel} cache record ${index}: nonce ${nonce}; keep this reference in the cacheable prefix and do not use it as the dynamic marker.`
31
+ );
32
+ return [
33
+ `You are a ${providerLabel} prompt-cache verification assistant.`,
34
+ 'When asked for the dynamic marker, answer with only the marker value from the Dynamic Marker line.',
35
+ ...records,
36
+ ].join('\n');
37
+ }
38
+
39
+ export function buildDynamicInstructions({
40
+ marker,
41
+ tailDescription,
42
+ }: {
43
+ marker: string;
44
+ tailDescription: string;
45
+ }): string {
46
+ return [`Dynamic Marker: ${marker}`, tailDescription].join('\n');
47
+ }
48
+
49
+ export function waitForCachePropagation(): Promise<void> {
50
+ return new Promise((resolve) => setTimeout(resolve, 2000));
51
+ }
52
+
53
+ export async function assertSystemPayloadShape({
54
+ agentId,
55
+ provider,
56
+ clientOptions,
57
+ stableInstructions,
58
+ dynamicInstructions,
59
+ expectedContent,
60
+ }: {
61
+ agentId: string;
62
+ provider: LivePromptCacheProvider;
63
+ clientOptions: LivePromptCacheClientOptions;
64
+ stableInstructions: string;
65
+ dynamicInstructions: string;
66
+ expectedContent: PromptCacheExpectedSystemBlock[];
67
+ }): Promise<void> {
68
+ const ctx = AgentContext.fromConfig({
69
+ agentId,
70
+ provider,
71
+ clientOptions,
72
+ instructions: stableInstructions,
73
+ additional_instructions: dynamicInstructions,
74
+ });
75
+
76
+ const messages = await ctx.systemRunnable!.invoke([
77
+ new HumanMessage('What is the dynamic marker?'),
78
+ ]);
79
+
80
+ expect(messages[0].content).toEqual(expectedContent);
81
+ }
82
+
83
+ function latestUsage({
84
+ collectedUsage,
85
+ label,
86
+ providerLabel,
87
+ }: {
88
+ collectedUsage: UsageMetadata[];
89
+ label: string;
90
+ providerLabel: string;
91
+ }): UsageMetadata {
92
+ if (collectedUsage.length === 0) {
93
+ throw new Error(`Missing ${providerLabel} usage metadata for ${label}`);
94
+ }
95
+ return collectedUsage[collectedUsage.length - 1];
96
+ }
97
+
98
+ function collectText(parts: t.MessageContentComplex[] | undefined): string {
99
+ return (parts ?? []).reduce((text, part) => {
100
+ if (part.type === 'text') {
101
+ return text + part.text;
102
+ }
103
+ return text;
104
+ }, '');
105
+ }
106
+
107
+ export async function runLiveTurn({
108
+ provider,
109
+ providerLabel,
110
+ clientOptions,
111
+ runId,
112
+ threadId,
113
+ stableInstructions,
114
+ dynamicInstructions,
115
+ }: {
116
+ provider: LivePromptCacheProvider;
117
+ providerLabel: string;
118
+ clientOptions: LivePromptCacheClientOptions;
119
+ runId: string;
120
+ threadId: string;
121
+ stableInstructions: string;
122
+ dynamicInstructions: string;
123
+ }): Promise<{
124
+ text: string;
125
+ usage: UsageMetadata;
126
+ }> {
127
+ const collectedUsage: UsageMetadata[] = [];
128
+ const run = await Run.create<t.IState>({
129
+ runId,
130
+ graphConfig: {
131
+ type: 'standard',
132
+ llmConfig: {
133
+ provider,
134
+ ...clientOptions,
135
+ } as t.LLMConfig,
136
+ instructions: stableInstructions,
137
+ additional_instructions: dynamicInstructions,
138
+ },
139
+ returnContent: true,
140
+ skipCleanup: true,
141
+ customHandlers: {
142
+ [GraphEvents.CHAT_MODEL_END]: new ModelEndHandler(collectedUsage),
143
+ },
144
+ });
145
+
146
+ const config = {
147
+ configurable: { thread_id: threadId },
148
+ streamMode: 'values',
149
+ version: 'v2' as const,
150
+ };
151
+
152
+ const contentParts = await run.processStream(
153
+ {
154
+ messages: [
155
+ new HumanMessage('What is the dynamic marker? Reply with only it.'),
156
+ ],
157
+ },
158
+ config
159
+ );
160
+
161
+ return {
162
+ text: collectText(contentParts),
163
+ usage: latestUsage({ collectedUsage, label: runId, providerLabel }),
164
+ };
165
+ }
@@ -399,12 +399,25 @@ export class StandardGraph extends Graph<t.BaseGraphState, t.GraphNode> {
399
399
  ): (string | number | undefined)[] {
400
400
  if (!metadata) return [];
401
401
 
402
+ const configurable = this.config?.configurable;
403
+ const runId =
404
+ (metadata.run_id as string | undefined) ??
405
+ (configurable?.run_id as string | undefined) ??
406
+ this.runId;
407
+ const threadId =
408
+ (metadata.thread_id as string | undefined) ??
409
+ (configurable?.thread_id as string | undefined) ??
410
+ runId;
411
+ const checkpointNs =
412
+ (metadata.checkpoint_ns as string | undefined) ??
413
+ (metadata.langgraph_checkpoint_ns as string | undefined) ??
414
+ '';
402
415
  const keyList = [
403
- metadata.run_id as string,
404
- metadata.thread_id as string,
416
+ runId,
417
+ threadId,
405
418
  metadata.langgraph_node as string,
406
419
  metadata.langgraph_step as number,
407
- metadata.checkpoint_ns as string,
420
+ checkpointNs,
408
421
  ];
409
422
 
410
423
  const agentContext = this.getAgentContext(metadata);
@@ -1461,7 +1474,14 @@ export class StandardGraph extends Graph<t.BaseGraphState, t.GraphNode> {
1461
1474
  }),
1462
1475
  });
1463
1476
  const workflow = new StateGraph(StateAnnotation)
1464
- .addNode(this.defaultAgentId, agentNode, { ends: [END] })
1477
+ .addNode(
1478
+ this.defaultAgentId,
1479
+ agentNode as Runnable<
1480
+ t.AgentSubgraphState,
1481
+ Partial<t.AgentSubgraphState>
1482
+ >,
1483
+ { ends: [END] }
1484
+ )
1465
1485
  .addEdge(START, this.defaultAgentId)
1466
1486
  // LangGraph compile() types are overly strict for opt-in options
1467
1487
  .compile(this.compileOptions as unknown as never);
@@ -0,0 +1,188 @@
1
+ import { HumanMessage } from '@langchain/core/messages';
2
+ import type { ToolCall } from '@langchain/core/messages/tool';
3
+ import type { RunnableConfig } from '@langchain/core/runnables';
4
+ import type * as t from '@/types';
5
+ import { MultiAgentGraph } from '../MultiAgentGraph';
6
+ import { Constants, Providers } from '@/common';
7
+ import { StandardGraph } from '../Graph';
8
+
9
+ const makeAgent = (agentId: string): t.AgentInputs => ({
10
+ agentId,
11
+ provider: Providers.OPENAI,
12
+ instructions: `You are ${agentId}.`,
13
+ });
14
+
15
+ const makeConfig = (threadId: string): RunnableConfig => ({
16
+ configurable: {
17
+ thread_id: threadId,
18
+ },
19
+ });
20
+
21
+ const makeStreamConfig = (threadId: string): t.WorkflowValuesStreamConfig => ({
22
+ ...makeConfig(threadId),
23
+ streamMode: 'values' as const,
24
+ });
25
+
26
+ const getAiContents = (messages: t.BaseGraphState['messages']): string[] =>
27
+ messages
28
+ .filter((message) => message.getType() === 'ai')
29
+ .map((message) => message.content)
30
+ .filter((content): content is string => typeof content === 'string');
31
+
32
+ const expectCompiledWorkflow = (
33
+ workflow: t.CompiledWorkflow | t.CompiledMultiAgentWorkflow
34
+ ): void => {
35
+ expect(typeof workflow.invoke).toBe('function');
36
+ expect(typeof workflow.stream).toBe('function');
37
+ };
38
+
39
+ describe('LangGraph composition smoke tests', () => {
40
+ it('compiles and invokes the standard single-agent graph', async () => {
41
+ const graph = new StandardGraph({
42
+ runId: 'standard-smoke',
43
+ agents: [makeAgent('agent')],
44
+ });
45
+ graph.overrideTestModel(['standard ok']);
46
+
47
+ const workflow = graph.createWorkflow();
48
+ expectCompiledWorkflow(workflow);
49
+
50
+ const result = await workflow.invoke(
51
+ { messages: [new HumanMessage('hello')] },
52
+ makeConfig('standard-smoke')
53
+ );
54
+
55
+ expect(getAiContents(result.messages)).toEqual(['standard ok']);
56
+ });
57
+
58
+ it('streams values from the standard single-agent graph', async () => {
59
+ const graph = new StandardGraph({
60
+ runId: 'standard-stream-smoke',
61
+ agents: [makeAgent('agent')],
62
+ });
63
+ graph.overrideTestModel(['standard stream ok']);
64
+
65
+ const workflow = graph.createWorkflow();
66
+ const stream = (await workflow.stream(
67
+ { messages: [new HumanMessage('hello')] },
68
+ makeStreamConfig('standard-stream-smoke')
69
+ )) as AsyncIterable<t.BaseGraphState>;
70
+ const chunks: t.BaseGraphState[] = [];
71
+
72
+ for await (const chunk of stream) {
73
+ chunks.push(chunk);
74
+ }
75
+
76
+ expect(chunks.length).toBeGreaterThan(0);
77
+ expect(
78
+ chunks.some((chunk) =>
79
+ getAiContents(chunk.messages).includes('standard stream ok')
80
+ )
81
+ ).toBe(true);
82
+ });
83
+
84
+ it('compiles and invokes a multi-agent graph with one agent and no edges', async () => {
85
+ const graph = new MultiAgentGraph({
86
+ runId: 'multi-single-smoke',
87
+ agents: [makeAgent('A')],
88
+ edges: [],
89
+ });
90
+ graph.overrideTestModel(['multi ok']);
91
+
92
+ const workflow = graph.createWorkflow();
93
+ expectCompiledWorkflow(workflow);
94
+
95
+ const result = await workflow.invoke(
96
+ { messages: [new HumanMessage('hello')] },
97
+ makeConfig('multi-single-smoke')
98
+ );
99
+
100
+ expect(getAiContents(result.messages)).toEqual(['multi ok']);
101
+ });
102
+
103
+ it('compiles and invokes direct sequential edges', async () => {
104
+ const graph = new MultiAgentGraph({
105
+ runId: 'direct-chain-smoke',
106
+ agents: [makeAgent('A'), makeAgent('B')],
107
+ edges: [{ from: 'A', to: 'B', edgeType: 'direct' }],
108
+ });
109
+ graph.overrideTestModel(['from A', 'from B']);
110
+
111
+ const workflow = graph.createWorkflow();
112
+ expectCompiledWorkflow(workflow);
113
+
114
+ const result = await workflow.invoke(
115
+ { messages: [new HumanMessage('start')] },
116
+ makeConfig('direct-chain-smoke')
117
+ );
118
+
119
+ expect(getAiContents(result.messages)).toEqual(['from A', 'from B']);
120
+ });
121
+
122
+ it('compiles and invokes a handoff edge using graph-managed transfer tools', async () => {
123
+ const transferToolCall: ToolCall = {
124
+ id: 'call_transfer_to_B',
125
+ name: `${Constants.LC_TRANSFER_TO_}B`,
126
+ args: { instructions: 'Take over from here.' },
127
+ type: 'tool_call',
128
+ };
129
+ const graph = new MultiAgentGraph({
130
+ runId: 'handoff-smoke',
131
+ agents: [makeAgent('A'), makeAgent('B')],
132
+ edges: [{ from: 'A', to: 'B', edgeType: 'handoff' }],
133
+ });
134
+ graph.overrideTestModel(['routing to B', 'handoff complete'], undefined, [
135
+ transferToolCall,
136
+ ]);
137
+
138
+ const workflow = graph.createWorkflow();
139
+ expectCompiledWorkflow(workflow);
140
+
141
+ const result = await workflow.invoke(
142
+ { messages: [new HumanMessage('start')] },
143
+ makeConfig('handoff-smoke')
144
+ );
145
+
146
+ expect(getAiContents(result.messages)).toContain('handoff complete');
147
+ });
148
+
149
+ it('compiles fan-out/fan-in direct composition with prompt wrapping', () => {
150
+ const graph = new MultiAgentGraph({
151
+ runId: 'fan-in-smoke',
152
+ agents: [
153
+ makeAgent('root'),
154
+ makeAgent('left'),
155
+ makeAgent('right'),
156
+ makeAgent('final'),
157
+ ],
158
+ edges: [
159
+ { from: 'root', to: ['left', 'right'], edgeType: 'direct' },
160
+ {
161
+ from: ['left', 'right'],
162
+ to: 'final',
163
+ edgeType: 'direct',
164
+ prompt: 'Summarize these results:\n{results}',
165
+ },
166
+ ],
167
+ });
168
+
169
+ expectCompiledWorkflow(graph.createWorkflow());
170
+ expect(graph.getParallelGroupId('root')).toBeUndefined();
171
+ expect(graph.getParallelGroupId('left')).toBe(1);
172
+ expect(graph.getParallelGroupId('right')).toBe(1);
173
+ expect(graph.getParallelGroupId('final')).toBeUndefined();
174
+ });
175
+
176
+ it('compiles mixed handoff and direct routing from the same agent', () => {
177
+ const graph = new MultiAgentGraph({
178
+ runId: 'mixed-routing-smoke',
179
+ agents: [makeAgent('router'), makeAgent('handoff'), makeAgent('direct')],
180
+ edges: [
181
+ { from: 'router', to: 'handoff', edgeType: 'handoff' },
182
+ { from: 'router', to: 'direct', edgeType: 'direct' },
183
+ ],
184
+ });
185
+
186
+ expectCompiledWorkflow(graph.createWorkflow());
187
+ });
188
+ });