@librechat/agents 3.0.60 → 3.0.62

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.
@@ -2,6 +2,7 @@ import { z } from 'zod';
2
2
  import { tool } from '@langchain/core/tools';
3
3
  import { PromptTemplate } from '@langchain/core/prompts';
4
4
  import {
5
+ AIMessage,
5
6
  ToolMessage,
6
7
  HumanMessage,
7
8
  getBufferString,
@@ -15,12 +16,15 @@ import {
15
16
  getCurrentTaskInput,
16
17
  messagesStateReducer,
17
18
  } from '@langchain/langgraph';
19
+ import type { BaseMessage, AIMessageChunk } from '@langchain/core/messages';
18
20
  import type { ToolRunnableConfig } from '@langchain/core/tools';
19
- import type { BaseMessage } from '@langchain/core/messages';
20
21
  import type * as t from '@/types';
21
22
  import { StandardGraph } from './Graph';
22
23
  import { Constants } from '@/common';
23
24
 
25
+ /** Pattern to extract instructions from transfer ToolMessage content */
26
+ const HANDOFF_INSTRUCTIONS_PATTERN = /(?:Instructions?|Context):\s*(.+)/is;
27
+
24
28
  /**
25
29
  * MultiAgentGraph extends StandardGraph to support dynamic multi-agent workflows
26
30
  * with handoffs, fan-in/fan-out, and other composable patterns.
@@ -312,6 +316,10 @@ export class MultiAgentGraph extends StandardGraph {
312
316
  content,
313
317
  name: toolName,
314
318
  tool_call_id: toolCallId,
319
+ additional_kwargs: {
320
+ /** Store destination for programmatic access in handoff detection */
321
+ handoff_destination: destination,
322
+ },
315
323
  });
316
324
 
317
325
  return new Command({
@@ -407,6 +415,112 @@ export class MultiAgentGraph extends StandardGraph {
407
415
  return this.createAgentNode(agentId);
408
416
  }
409
417
 
418
+ /**
419
+ * Detects if the current agent is receiving a handoff and processes the messages accordingly.
420
+ * Returns filtered messages with the transfer tool call/message removed, plus any instructions
421
+ * extracted from the transfer to be injected as a HumanMessage preamble.
422
+ *
423
+ * @param messages - Current state messages
424
+ * @param agentId - The agent ID to check for handoff reception
425
+ * @returns Object with filtered messages and extracted instructions, or null if not a handoff
426
+ */
427
+ private processHandoffReception(
428
+ messages: BaseMessage[],
429
+ agentId: string
430
+ ): { filteredMessages: BaseMessage[]; instructions: string | null } | null {
431
+ if (messages.length === 0) return null;
432
+
433
+ const lastMessage = messages[messages.length - 1];
434
+
435
+ /** Check if last message is a transfer ToolMessage targeting this agent */
436
+ if (lastMessage.getType() !== 'tool') return null;
437
+
438
+ const toolMessage = lastMessage as ToolMessage;
439
+ const toolName = toolMessage.name;
440
+
441
+ if (typeof toolName !== 'string') return null;
442
+
443
+ /** Check for standard transfer pattern */
444
+ const isTransferMessage = toolName.startsWith(Constants.LC_TRANSFER_TO_);
445
+ const isConditionalTransfer = toolName === 'conditional_transfer';
446
+
447
+ if (!isTransferMessage && !isConditionalTransfer) return null;
448
+
449
+ /** Extract destination from tool name or additional_kwargs */
450
+ let destinationAgent: string | null = null;
451
+
452
+ if (isTransferMessage) {
453
+ destinationAgent = toolName.replace(Constants.LC_TRANSFER_TO_, '');
454
+ } else if (isConditionalTransfer) {
455
+ /** For conditional transfers, read destination from additional_kwargs */
456
+ const handoffDest = toolMessage.additional_kwargs.handoff_destination;
457
+ destinationAgent = typeof handoffDest === 'string' ? handoffDest : null;
458
+ }
459
+
460
+ /** Verify this agent is the intended destination */
461
+ if (destinationAgent !== agentId) return null;
462
+
463
+ /** Extract instructions from the ToolMessage content */
464
+ const contentStr =
465
+ typeof toolMessage.content === 'string'
466
+ ? toolMessage.content
467
+ : JSON.stringify(toolMessage.content);
468
+
469
+ const instructionsMatch = contentStr.match(HANDOFF_INSTRUCTIONS_PATTERN);
470
+ const instructions = instructionsMatch?.[1]?.trim() ?? null;
471
+
472
+ /** Get the tool_call_id to find and filter the AI message's tool call */
473
+ const toolCallId = toolMessage.tool_call_id;
474
+
475
+ /** Filter out the transfer messages */
476
+ const filteredMessages: BaseMessage[] = [];
477
+
478
+ for (let i = 0; i < messages.length - 1; i++) {
479
+ /** Exclude the last message (ToolMessage) by iterating to length - 1 */
480
+ const msg = messages[i];
481
+ const msgType = msg.getType();
482
+
483
+ if (msgType === 'ai') {
484
+ /** Check if this AI message contains the transfer tool call */
485
+ const aiMsg = msg as AIMessage | AIMessageChunk;
486
+ const toolCalls = aiMsg.tool_calls;
487
+
488
+ if (toolCalls && toolCalls.length > 0) {
489
+ const transferCallIndex = toolCalls.findIndex(
490
+ (tc) => tc.id === toolCallId
491
+ );
492
+
493
+ if (transferCallIndex >= 0) {
494
+ /** This AI message has the transfer tool call - filter it out */
495
+ const remainingToolCalls = toolCalls.filter(
496
+ (tc) => tc.id !== toolCallId
497
+ );
498
+
499
+ if (
500
+ remainingToolCalls.length > 0 ||
501
+ (typeof aiMsg.content === 'string' && aiMsg.content.trim())
502
+ ) {
503
+ /** Keep the message but without the transfer tool call */
504
+ const filteredAiMsg = new AIMessage({
505
+ content: aiMsg.content,
506
+ tool_calls: remainingToolCalls,
507
+ id: aiMsg.id,
508
+ });
509
+ filteredMessages.push(filteredAiMsg);
510
+ }
511
+ /** If no remaining content or tool calls, skip this message entirely */
512
+ continue;
513
+ }
514
+ }
515
+ }
516
+
517
+ /** Keep all other messages */
518
+ filteredMessages.push(msg);
519
+ }
520
+
521
+ return { filteredMessages, instructions };
522
+ }
523
+
410
524
  /**
411
525
  * Create the multi-agent workflow with dynamic handoffs
412
526
  */
@@ -474,26 +588,85 @@ export class MultiAgentGraph extends StandardGraph {
474
588
  /** Agent subgraph (includes agent + tools) */
475
589
  const agentSubgraph = this.createAgentSubgraph(agentId);
476
590
 
477
- /** Wrapper function that handles agentMessages channel and conditional routing */
591
+ /** Wrapper function that handles agentMessages channel, handoff reception, and conditional routing */
478
592
  const agentWrapper = async (
479
593
  state: t.MultiAgentGraphState
480
594
  ): Promise<t.MultiAgentGraphState | Command> => {
481
595
  let result: t.MultiAgentGraphState;
482
596
 
483
- if (state.agentMessages != null && state.agentMessages.length > 0) {
597
+ /**
598
+ * Check if this agent is receiving a handoff.
599
+ * If so, filter out the transfer messages and inject instructions as preamble.
600
+ * This prevents the receiving agent from seeing the transfer as "completed work"
601
+ * and prematurely producing an end token.
602
+ */
603
+ const handoffContext = this.processHandoffReception(
604
+ state.messages,
605
+ agentId
606
+ );
607
+
608
+ if (handoffContext !== null) {
609
+ const { filteredMessages, instructions } = handoffContext;
610
+
611
+ /** Build messages for the receiving agent */
612
+ let messagesForAgent = filteredMessages;
613
+
614
+ /** If there are instructions, inject them as a HumanMessage to ground the agent */
615
+ const hasInstructions = instructions !== null && instructions !== '';
616
+ if (hasInstructions) {
617
+ messagesForAgent = [
618
+ ...filteredMessages,
619
+ new HumanMessage(instructions),
620
+ ];
621
+ }
622
+
623
+ /** Update token map if we have a token counter */
624
+ const agentContext = this.agentContexts.get(agentId);
625
+ if (agentContext?.tokenCounter && hasInstructions) {
626
+ const freshTokenMap: Record<string, number> = {};
627
+ for (
628
+ let i = 0;
629
+ i < Math.min(filteredMessages.length, this.startIndex);
630
+ i++
631
+ ) {
632
+ const tokenCount = agentContext.indexTokenCountMap[i];
633
+ if (tokenCount !== undefined) {
634
+ freshTokenMap[i] = tokenCount;
635
+ }
636
+ }
637
+ /** Add tokens for the instructions message */
638
+ const instructionsMsg = new HumanMessage(instructions);
639
+ freshTokenMap[messagesForAgent.length - 1] =
640
+ agentContext.tokenCounter(instructionsMsg);
641
+ agentContext.updateTokenMapWithInstructions(freshTokenMap);
642
+ }
643
+
644
+ const transformedState: t.MultiAgentGraphState = {
645
+ ...state,
646
+ messages: messagesForAgent,
647
+ };
648
+ result = await agentSubgraph.invoke(transformedState);
649
+ result = {
650
+ ...result,
651
+ agentMessages: [],
652
+ };
653
+ } else if (
654
+ state.agentMessages != null &&
655
+ state.agentMessages.length > 0
656
+ ) {
484
657
  /**
485
658
  * When using agentMessages (excludeResults=true), we need to update
486
659
  * the token map to account for the new prompt message
487
660
  */
488
661
  const agentContext = this.agentContexts.get(agentId);
489
662
  if (agentContext && agentContext.tokenCounter) {
490
- // The agentMessages contains:
491
- // 1. Filtered messages (0 to startIndex) - already have token counts
492
- // 2. New prompt message - needs token counting
493
-
663
+ /** The agentMessages contains:
664
+ * 1. Filtered messages (0 to startIndex) - already have token counts
665
+ * 2. New prompt message - needs token counting
666
+ */
494
667
  const freshTokenMap: Record<string, number> = {};
495
668
 
496
- // Copy existing token counts for filtered messages (0 to startIndex)
669
+ /** Copy existing token counts for filtered messages (0 to startIndex) */
497
670
  for (let i = 0; i < this.startIndex; i++) {
498
671
  const tokenCount = agentContext.indexTokenCountMap[i];
499
672
  if (tokenCount !== undefined) {
@@ -501,7 +674,7 @@ export class MultiAgentGraph extends StandardGraph {
501
674
  }
502
675
  }
503
676
 
504
- // Calculate tokens only for the new prompt message (last message)
677
+ /** Calculate tokens only for the new prompt message (last message) */
505
678
  const promptMessageIndex = state.agentMessages.length - 1;
506
679
  if (promptMessageIndex >= this.startIndex) {
507
680
  const promptMessage = state.agentMessages[promptMessageIndex];
@@ -509,7 +682,7 @@ export class MultiAgentGraph extends StandardGraph {
509
682
  agentContext.tokenCounter(promptMessage);
510
683
  }
511
684
 
512
- // Update the agent's token map with instructions added
685
+ /** Update the agent's token map with instructions added */
513
686
  agentContext.updateTokenMapWithInstructions(freshTokenMap);
514
687
  }
515
688
 
@@ -210,6 +210,7 @@ async function testConditionalMultiAgent() {
210
210
  console.log(`\n\nExpert used: ${selectedExpert}`);
211
211
  console.log('Content parts:', contentParts.length);
212
212
  console.log('---');
213
+ console.dir(contentParts, { depth: null });
213
214
  }
214
215
  } catch (error) {
215
216
  console.error('Error in conditional multi-agent test:', error);
@@ -353,6 +353,7 @@ async function testSupervisorMultiAgent() {
353
353
  );
354
354
  console.log(` transfer_to_legal_advisor`);
355
355
  console.log('─'.repeat(60));
356
+ console.dir(contentParts, { depth: null });
356
357
  }
357
358
  await sleep(3000);
358
359
  } catch (error) {
@@ -0,0 +1,275 @@
1
+ /**
2
+ * Test script for multi-turn handoff behavior.
3
+ *
4
+ * This tests the fix for the issue where receiving agents would see transfer messages
5
+ * and prematurely produce end tokens, thinking the work was already done.
6
+ *
7
+ * The fix:
8
+ * 1. Filters out transfer tool calls and ToolMessages from the receiving agent's context
9
+ * 2. Injects any passthrough instructions as a HumanMessage to ground the receiving agent
10
+ */
11
+ import { config } from 'dotenv';
12
+ config();
13
+
14
+ import { HumanMessage, BaseMessage } from '@langchain/core/messages';
15
+ import type { RunnableConfig } from '@langchain/core/runnables';
16
+ import type * as t from '@/types';
17
+ import { ChatModelStreamHandler, createContentAggregator } from '@/stream';
18
+ import { ToolEndHandler, ModelEndHandler } from '@/events';
19
+ import { getLLMConfig } from '@/utils/llmConfig';
20
+ import { GraphEvents, Providers } from '@/common';
21
+ import { Run } from '@/run';
22
+
23
+ const conversationHistory: BaseMessage[] = [];
24
+
25
+ /**
26
+ * Test multi-turn handoff between a coordinator and a specialist
27
+ */
28
+ async function testHandoffPreamble(): Promise<void> {
29
+ console.log('='.repeat(60));
30
+ console.log('Testing Multi-Turn Handoff with Preamble Injection');
31
+ console.log('='.repeat(60));
32
+ console.log('\nThis test verifies that:');
33
+ console.log('1. Transfer messages are filtered from receiving agent context');
34
+ console.log('2. Passthrough instructions are injected as a HumanMessage');
35
+ console.log('3. Multi-turn conversations work correctly after handoffs\n');
36
+
37
+ const { contentParts, aggregateContent } = createContentAggregator();
38
+
39
+ /** Track which agent is responding */
40
+ let currentAgent = '';
41
+
42
+ const customHandlers = {
43
+ [GraphEvents.TOOL_END]: new ToolEndHandler(),
44
+ [GraphEvents.CHAT_MODEL_END]: new ModelEndHandler(),
45
+ [GraphEvents.CHAT_MODEL_STREAM]: new ChatModelStreamHandler(),
46
+ [GraphEvents.ON_RUN_STEP]: {
47
+ handle: (
48
+ event: GraphEvents.ON_RUN_STEP,
49
+ data: t.StreamEventData
50
+ ): void => {
51
+ const runStep = data as t.RunStep;
52
+ if (runStep.agentId) {
53
+ currentAgent = runStep.agentId;
54
+ console.log(`\n[Agent: ${currentAgent}] Processing...`);
55
+ }
56
+ aggregateContent({ event, data: runStep });
57
+ },
58
+ },
59
+ [GraphEvents.ON_RUN_STEP_COMPLETED]: {
60
+ handle: (
61
+ event: GraphEvents.ON_RUN_STEP_COMPLETED,
62
+ data: t.StreamEventData
63
+ ): void => {
64
+ aggregateContent({
65
+ event,
66
+ data: data as unknown as { result: t.ToolEndEvent },
67
+ });
68
+ },
69
+ },
70
+ [GraphEvents.ON_MESSAGE_DELTA]: {
71
+ handle: (
72
+ event: GraphEvents.ON_MESSAGE_DELTA,
73
+ data: t.StreamEventData
74
+ ): void => {
75
+ console.log('====== ON_MESSAGE_DELTA ======');
76
+ console.dir(data, { depth: null });
77
+ aggregateContent({ event, data: data as t.MessageDeltaEvent });
78
+ },
79
+ },
80
+ [GraphEvents.TOOL_START]: {
81
+ handle: (
82
+ _event: string,
83
+ data: t.StreamEventData,
84
+ _metadata?: Record<string, unknown>
85
+ ): void => {
86
+ const toolData = data as { name?: string };
87
+ if (toolData?.name?.includes('transfer_to_')) {
88
+ const specialist = toolData.name.replace('lc_transfer_to_', '');
89
+ console.log(`\n🔀 Handing off to: ${specialist}`);
90
+ }
91
+ },
92
+ },
93
+ };
94
+
95
+ /**
96
+ * Create agents:
97
+ * - coordinator: Decides when to hand off to specialist
98
+ * - specialist: Handles specific tasks delegated by coordinator
99
+ */
100
+ const agents: t.AgentInputs[] = [
101
+ {
102
+ agentId: 'coordinator',
103
+ provider: Providers.OPENAI,
104
+ clientOptions: {
105
+ modelName: 'gpt-4.1-mini',
106
+ apiKey: process.env.OPENAI_API_KEY,
107
+ },
108
+ instructions: `You are a Task Coordinator. Your role is to:
109
+ 1. Understand user requests
110
+ 2. If the request involves technical analysis, use the transfer_to_specialist tool to hand off
111
+ 3. When handing off, provide clear instructions about what needs to be done
112
+
113
+ IMPORTANT: When using the handoff tool, include specific instructions for the specialist.`,
114
+ maxContextTokens: 8000,
115
+ },
116
+ {
117
+ agentId: 'specialist',
118
+ provider: Providers.OPENAI,
119
+ clientOptions: {
120
+ modelName: 'gpt-4.1-mini',
121
+ apiKey: process.env.OPENAI_API_KEY,
122
+ },
123
+ instructions: `You are a Technical Specialist. When you receive a request:
124
+ 1. Carefully read any instructions provided
125
+ 2. Provide a detailed technical response
126
+ 3. Do NOT just acknowledge - provide substantive help
127
+
128
+ IMPORTANT: You are the specialist - provide a complete, helpful response to the task.`,
129
+ maxContextTokens: 8000,
130
+ },
131
+ ];
132
+
133
+ /** Create handoff edge with passthrough instructions */
134
+ const edges: t.GraphEdge[] = [
135
+ {
136
+ from: 'coordinator',
137
+ to: 'specialist',
138
+ description: 'Transfer to technical specialist for analysis',
139
+ edgeType: 'handoff',
140
+ prompt: 'Specific instructions for the specialist about what to analyze',
141
+ promptKey: 'instructions',
142
+ },
143
+ ];
144
+
145
+ const runConfig: t.RunConfig = {
146
+ runId: `handoff-test-${Date.now()}`,
147
+ graphConfig: {
148
+ type: 'multi-agent',
149
+ agents,
150
+ edges,
151
+ },
152
+ customHandlers,
153
+ returnContent: true,
154
+ };
155
+
156
+ const run = await Run.create(runConfig);
157
+
158
+ const config: Partial<RunnableConfig> & {
159
+ version: 'v1' | 'v2';
160
+ streamMode: string;
161
+ } = {
162
+ configurable: {
163
+ thread_id: 'handoff-test-conversation-1',
164
+ },
165
+ streamMode: 'values',
166
+ version: 'v2' as const,
167
+ };
168
+
169
+ /** TURN 1: Initial request that triggers handoff */
170
+ console.log('\n' + '─'.repeat(60));
171
+ console.log('TURN 1: Initial request (should trigger handoff)');
172
+ console.log('─'.repeat(60));
173
+
174
+ const userMessage1 = `
175
+ Hi! Can you help me understand the time complexity of quicksort?
176
+ I need a technical explanation.
177
+ `;
178
+
179
+ conversationHistory.push(new HumanMessage(userMessage1));
180
+ console.log('\nUser:', userMessage1.trim());
181
+ console.log('\nResponse:');
182
+
183
+ let inputs = { messages: conversationHistory };
184
+ await run.processStream(inputs, config);
185
+ const messages1 = run.getRunMessages();
186
+ if (messages1) {
187
+ conversationHistory.push(...messages1);
188
+ }
189
+
190
+ console.log('\n');
191
+
192
+ /** TURN 2: Follow-up question to test multi-turn after handoff */
193
+ console.log('\n' + '─'.repeat(60));
194
+ console.log('TURN 2: Follow-up question (tests context after handoff)');
195
+ console.log('─'.repeat(60));
196
+
197
+ const userMessage2 = `
198
+ Thanks! Can you also explain the space complexity and when quicksort
199
+ might not be the best choice?
200
+ `;
201
+
202
+ conversationHistory.push(new HumanMessage(userMessage2));
203
+ console.log('\nUser:', userMessage2.trim());
204
+ console.log('\nResponse:');
205
+
206
+ inputs = { messages: conversationHistory };
207
+ await run.processStream(inputs, config);
208
+ const messages2 = run.getRunMessages();
209
+ if (messages2) {
210
+ conversationHistory.push(...messages2);
211
+ }
212
+
213
+ console.log('\n');
214
+
215
+ /** TURN 3: Another follow-up to verify sustained conversation */
216
+ console.log('\n' + '─'.repeat(60));
217
+ console.log('TURN 3: Third turn (tests sustained multi-turn)');
218
+ console.log('─'.repeat(60));
219
+
220
+ const userMessage3 = `
221
+ Great explanation! One more question - how does quicksort compare
222
+ to mergesort in practice?
223
+ `;
224
+
225
+ conversationHistory.push(new HumanMessage(userMessage3));
226
+ console.log('\nUser:', userMessage3.trim());
227
+ console.log('\nResponse:');
228
+
229
+ inputs = { messages: conversationHistory };
230
+ await run.processStream(inputs, config);
231
+ const messages3 = run.getRunMessages();
232
+ if (messages3) {
233
+ conversationHistory.push(...messages3);
234
+ }
235
+
236
+ /** Summary */
237
+ console.log('\n\n' + '='.repeat(60));
238
+ console.log('TEST SUMMARY');
239
+ console.log('='.repeat(60));
240
+ console.log('\nTotal messages in conversation:', conversationHistory.length);
241
+ console.log('\nMessage types:');
242
+
243
+ for (let i = 0; i < conversationHistory.length; i++) {
244
+ const msg = conversationHistory[i];
245
+ const type = msg.getType();
246
+ const preview =
247
+ typeof msg.content === 'string'
248
+ ? msg.content.slice(0, 50).replace(/\n/g, ' ')
249
+ : '[complex content]';
250
+ console.log(` ${i + 1}. [${type}] ${preview}...`);
251
+ }
252
+
253
+ console.log('\n✅ Test completed. Review the output above to verify:');
254
+ console.log(' - Specialist received and acted on instructions');
255
+ console.log(' - No premature end tokens after handoff');
256
+ console.log(' - Multi-turn conversation continued smoothly');
257
+ }
258
+
259
+ process.on('unhandledRejection', (reason, promise) => {
260
+ console.error('Unhandled Rejection at:', promise, 'reason:', reason);
261
+ console.log('\nConversation history at failure:');
262
+ console.dir(conversationHistory, { depth: null });
263
+ process.exit(1);
264
+ });
265
+
266
+ process.on('uncaughtException', (err) => {
267
+ console.error('Uncaught Exception:', err);
268
+ });
269
+
270
+ testHandoffPreamble().catch((err) => {
271
+ console.error('Test failed:', err);
272
+ console.log('\nConversation history at failure:');
273
+ console.dir(conversationHistory, { depth: null });
274
+ process.exit(1);
275
+ });
package/src/stream.ts CHANGED
@@ -583,6 +583,17 @@ export function createContentAggregator(): t.ContentAggregatorResult {
583
583
  tool_call: newToolCall,
584
584
  };
585
585
  }
586
+
587
+ // Apply agentId (for MultiAgentGraph) and groupId (for parallel execution) to content parts
588
+ // - agentId present → MultiAgentGraph (show agent labels)
589
+ // - groupId present → parallel execution (render columns)
590
+ const meta = contentMetaMap.get(index);
591
+ if (meta?.agentId != null) {
592
+ (contentParts[index] as t.MessageContentComplex).agentId = meta.agentId;
593
+ }
594
+ if (meta?.groupId != null) {
595
+ (contentParts[index] as t.MessageContentComplex).groupId = meta.groupId;
596
+ }
586
597
  };
587
598
 
588
599
  const aggregateContent = ({
@@ -727,10 +738,5 @@ export function createContentAggregator(): t.ContentAggregatorResult {
727
738
  }
728
739
  };
729
740
 
730
- return {
731
- contentParts,
732
- aggregateContent,
733
- stepMap,
734
- contentMetadataMap: contentMetaMap,
735
- };
741
+ return { contentParts, aggregateContent, stepMap };
736
742
  }
@@ -343,6 +343,10 @@ export type MessageContentComplex = (
343
343
  })
344
344
  ) & {
345
345
  tool_call_ids?: string[];
346
+ // Optional agentId for parallel execution attribution
347
+ agentId?: string;
348
+ // Optional groupId for parallel group attribution
349
+ groupId?: number;
346
350
  };
347
351
 
348
352
  export interface TMessage {
@@ -406,20 +410,8 @@ export type ContentAggregator = ({
406
410
  result: ToolEndEvent;
407
411
  };
408
412
  }) => void;
409
- /**
410
- * Metadata for content parts in multi-agent runs.
411
- * - agentId: present for all MultiAgentGraph runs (enables agent labels in UI)
412
- * - groupId: present only for parallel execution (enables column rendering)
413
- */
414
- export type ContentMetadata = {
415
- agentId?: string;
416
- groupId?: number;
417
- };
418
-
419
413
  export type ContentAggregatorResult = {
420
414
  stepMap: Map<string, RunStep | undefined>;
421
415
  contentParts: Array<MessageContentComplex | undefined>;
422
- /** Map of content index to metadata (agentId, groupId). Only populated for MultiAgentGraph runs. */
423
- contentMetadataMap: Map<number, ContentMetadata>;
424
416
  aggregateContent: ContentAggregator;
425
417
  };