@librechat/agents 3.0.56 → 3.0.61

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.
@@ -1141,251 +1141,4 @@ describe('formatAgentMessages', () => {
1141
1141
  expect(result.messages[1].name).toBe('search');
1142
1142
  expect(result.messages[1].content).toBe('');
1143
1143
  });
1144
-
1145
- describe('targetAgentId filtering', () => {
1146
- it('should filter content parts to only include those from targetAgentId', () => {
1147
- const payload: TPayload = [
1148
- { role: 'user', content: 'Hello' },
1149
- {
1150
- role: 'assistant',
1151
- content: [
1152
- { type: ContentTypes.TEXT, text: 'Response from agent_a' },
1153
- { type: ContentTypes.TEXT, text: 'Response from agent_b' },
1154
- { type: ContentTypes.TEXT, text: 'Another from agent_a' },
1155
- ],
1156
- },
1157
- ];
1158
-
1159
- const contentMetadataMap = new Map([
1160
- [0, { agentId: 'agent_a' }],
1161
- [1, { agentId: 'agent_b' }],
1162
- [2, { agentId: 'agent_a' }],
1163
- ]);
1164
-
1165
- const result = formatAgentMessages(payload, undefined, undefined, {
1166
- targetAgentId: 'agent_a',
1167
- contentMetadataMap,
1168
- });
1169
-
1170
- // Should have user message + filtered assistant message
1171
- expect(result.messages).toHaveLength(2);
1172
- expect(result.messages[0]).toBeInstanceOf(HumanMessage);
1173
- expect(result.messages[1]).toBeInstanceOf(AIMessage);
1174
-
1175
- // The AIMessage should only have agent_a's content parts
1176
- const aiMessage = result.messages[1] as AIMessage;
1177
- expect(Array.isArray(aiMessage.content)).toBe(true);
1178
- expect(aiMessage.content as Array<{ text: string }>).toHaveLength(2);
1179
- expect((aiMessage.content as Array<{ text: string }>)[0].text).toBe(
1180
- 'Response from agent_a'
1181
- );
1182
- expect((aiMessage.content as Array<{ text: string }>)[1].text).toBe(
1183
- 'Another from agent_a'
1184
- );
1185
- });
1186
-
1187
- it('should skip assistant message entirely if no content parts match targetAgentId', () => {
1188
- const payload: TPayload = [
1189
- { role: 'user', content: 'Hello' },
1190
- {
1191
- role: 'assistant',
1192
- content: [{ type: ContentTypes.TEXT, text: 'Response from agent_b' }],
1193
- },
1194
- ];
1195
-
1196
- const contentMetadataMap = new Map([[0, { agentId: 'agent_b' }]]);
1197
-
1198
- const result = formatAgentMessages(payload, undefined, undefined, {
1199
- targetAgentId: 'agent_a',
1200
- contentMetadataMap,
1201
- });
1202
-
1203
- // Should only have the user message, assistant message skipped
1204
- expect(result.messages).toHaveLength(1);
1205
- expect(result.messages[0]).toBeInstanceOf(HumanMessage);
1206
- });
1207
-
1208
- it('should not filter when targetAgentId is not provided', () => {
1209
- const payload: TPayload = [
1210
- {
1211
- role: 'assistant',
1212
- content: [
1213
- { type: ContentTypes.TEXT, text: 'Response from agent_a' },
1214
- { type: ContentTypes.TEXT, text: 'Response from agent_b' },
1215
- ],
1216
- },
1217
- ];
1218
-
1219
- const contentMetadataMap = new Map([
1220
- [0, { agentId: 'agent_a' }],
1221
- [1, { agentId: 'agent_b' }],
1222
- ]);
1223
-
1224
- // No targetAgentId provided - should include all content
1225
- const result = formatAgentMessages(payload, undefined, undefined, {
1226
- contentMetadataMap,
1227
- });
1228
-
1229
- expect(result.messages).toHaveLength(1);
1230
- const aiMessage = result.messages[0] as AIMessage;
1231
- expect(Array.isArray(aiMessage.content)).toBe(true);
1232
- expect(aiMessage.content as Array<{ text: string }>).toHaveLength(2);
1233
- });
1234
-
1235
- it('should not filter when contentMetadataMap is not provided', () => {
1236
- const payload: TPayload = [
1237
- {
1238
- role: 'assistant',
1239
- content: [
1240
- { type: ContentTypes.TEXT, text: 'Response 1' },
1241
- { type: ContentTypes.TEXT, text: 'Response 2' },
1242
- ],
1243
- },
1244
- ];
1245
-
1246
- // targetAgentId provided but no contentMetadataMap - should include all content
1247
- const result = formatAgentMessages(payload, undefined, undefined, {
1248
- targetAgentId: 'agent_a',
1249
- });
1250
-
1251
- expect(result.messages).toHaveLength(1);
1252
- const aiMessage = result.messages[0] as AIMessage;
1253
- expect(Array.isArray(aiMessage.content)).toBe(true);
1254
- expect(aiMessage.content as Array<{ text: string }>).toHaveLength(2);
1255
- });
1256
-
1257
- it('should filter content with groupId metadata (parallel execution)', () => {
1258
- const payload: TPayload = [
1259
- { role: 'user', content: 'Analyze this' },
1260
- {
1261
- role: 'assistant',
1262
- content: [
1263
- { type: ContentTypes.TEXT, text: 'Creative analysis' },
1264
- { type: ContentTypes.TEXT, text: 'Practical analysis' },
1265
- ],
1266
- },
1267
- ];
1268
-
1269
- const contentMetadataMap = new Map([
1270
- [0, { agentId: 'creative_analyst', groupId: 1 }],
1271
- [1, { agentId: 'practical_analyst', groupId: 1 }],
1272
- ]);
1273
-
1274
- const result = formatAgentMessages(payload, undefined, undefined, {
1275
- targetAgentId: 'creative_analyst',
1276
- contentMetadataMap,
1277
- });
1278
-
1279
- expect(result.messages).toHaveLength(2);
1280
- const aiMessage = result.messages[1] as AIMessage;
1281
- expect(Array.isArray(aiMessage.content)).toBe(true);
1282
- expect(aiMessage.content as Array<{ text: string }>).toHaveLength(1);
1283
- expect((aiMessage.content as Array<{ text: string }>)[0].text).toBe(
1284
- 'Creative analysis'
1285
- );
1286
- });
1287
-
1288
- it('should not affect non-assistant messages when filtering', () => {
1289
- const payload: TPayload = [
1290
- { role: 'user', content: 'Hello from user' },
1291
- { role: 'system', content: 'System message' },
1292
- {
1293
- role: 'assistant',
1294
- content: [{ type: ContentTypes.TEXT, text: 'From agent_a' }],
1295
- },
1296
- ];
1297
-
1298
- const contentMetadataMap = new Map([[0, { agentId: 'agent_a' }]]);
1299
-
1300
- const result = formatAgentMessages(payload, undefined, undefined, {
1301
- targetAgentId: 'agent_a',
1302
- contentMetadataMap,
1303
- });
1304
-
1305
- // All three messages should be present
1306
- expect(result.messages).toHaveLength(3);
1307
- expect(result.messages[0]).toBeInstanceOf(HumanMessage);
1308
- expect(result.messages[1]).toBeInstanceOf(SystemMessage);
1309
- expect(result.messages[2]).toBeInstanceOf(AIMessage);
1310
- });
1311
-
1312
- it('should include content parts without metadata (unattributed) when filtering by targetAgentId', () => {
1313
- const payload: TPayload = [
1314
- { role: 'user', content: 'Hello' },
1315
- {
1316
- role: 'assistant',
1317
- content: [
1318
- { type: ContentTypes.TEXT, text: 'Unattributed content' },
1319
- { type: ContentTypes.TEXT, text: 'Response from agent_a' },
1320
- { type: ContentTypes.TEXT, text: 'Response from agent_b' },
1321
- { type: ContentTypes.TEXT, text: 'Another unattributed' },
1322
- ],
1323
- },
1324
- ];
1325
-
1326
- // Only indices 1 and 2 have metadata; 0 and 3 are unattributed
1327
- const contentMetadataMap = new Map([
1328
- [1, { agentId: 'agent_a' }],
1329
- [2, { agentId: 'agent_b' }],
1330
- ]);
1331
-
1332
- const result = formatAgentMessages(payload, undefined, undefined, {
1333
- targetAgentId: 'agent_a',
1334
- contentMetadataMap,
1335
- });
1336
-
1337
- // Should have user message + filtered assistant message
1338
- expect(result.messages).toHaveLength(2);
1339
- expect(result.messages[0]).toBeInstanceOf(HumanMessage);
1340
- expect(result.messages[1]).toBeInstanceOf(AIMessage);
1341
-
1342
- // The AIMessage should have: unattributed (index 0) + agent_a (index 1) + unattributed (index 3)
1343
- const aiMessage = result.messages[1] as AIMessage;
1344
- expect(Array.isArray(aiMessage.content)).toBe(true);
1345
- expect(aiMessage.content as Array<{ text: string }>).toHaveLength(3);
1346
- expect((aiMessage.content as Array<{ text: string }>)[0].text).toBe(
1347
- 'Unattributed content'
1348
- );
1349
- expect((aiMessage.content as Array<{ text: string }>)[1].text).toBe(
1350
- 'Response from agent_a'
1351
- );
1352
- expect((aiMessage.content as Array<{ text: string }>)[2].text).toBe(
1353
- 'Another unattributed'
1354
- );
1355
- });
1356
-
1357
- it('should include all unattributed content even when no parts match targetAgentId', () => {
1358
- const payload: TPayload = [
1359
- {
1360
- role: 'assistant',
1361
- content: [
1362
- { type: ContentTypes.TEXT, text: 'Unattributed content 1' },
1363
- { type: ContentTypes.TEXT, text: 'Response from agent_b' },
1364
- { type: ContentTypes.TEXT, text: 'Unattributed content 2' },
1365
- ],
1366
- },
1367
- ];
1368
-
1369
- // Only index 1 has metadata (agent_b)
1370
- const contentMetadataMap = new Map([[1, { agentId: 'agent_b' }]]);
1371
-
1372
- const result = formatAgentMessages(payload, undefined, undefined, {
1373
- targetAgentId: 'agent_a', // Looking for agent_a, but only agent_b content exists
1374
- contentMetadataMap,
1375
- });
1376
-
1377
- // Should still have the message with unattributed content
1378
- expect(result.messages).toHaveLength(1);
1379
- const aiMessage = result.messages[0] as AIMessage;
1380
- expect(Array.isArray(aiMessage.content)).toBe(true);
1381
- // Should have the 2 unattributed parts (indices 0 and 2), but NOT agent_b's content
1382
- expect(aiMessage.content as Array<{ text: string }>).toHaveLength(2);
1383
- expect((aiMessage.content as Array<{ text: string }>)[0].text).toBe(
1384
- 'Unattributed content 1'
1385
- );
1386
- expect((aiMessage.content as Array<{ text: string }>)[1].text).toBe(
1387
- 'Unattributed content 2'
1388
- );
1389
- });
1390
- });
1391
1144
  });
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
  };