@librechat/agents 3.4.2 → 3.4.4

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 (75) hide show
  1. package/dist/cjs/graphs/Graph.cjs +27 -14
  2. package/dist/cjs/graphs/Graph.cjs.map +1 -1
  3. package/dist/cjs/graphs/MultiAgentGraph.cjs +1 -1
  4. package/dist/cjs/instrumentation.cjs +3 -3
  5. package/dist/cjs/langfuse.cjs +3 -3
  6. package/dist/cjs/langfuseRuntimeScope.cjs +1 -1
  7. package/dist/cjs/langfuseToolOutputTracing.cjs +2 -2
  8. package/dist/cjs/main.cjs +5 -1
  9. package/dist/cjs/messages/assistantPhase.cjs +59 -0
  10. package/dist/cjs/messages/assistantPhase.cjs.map +1 -0
  11. package/dist/cjs/messages/index.cjs +1 -0
  12. package/dist/cjs/prompts/activityLabel.cjs +76 -0
  13. package/dist/cjs/prompts/activityLabel.cjs.map +1 -1
  14. package/dist/cjs/run.cjs +200 -10
  15. package/dist/cjs/run.cjs.map +1 -1
  16. package/dist/cjs/session/AgentSession.cjs +1 -1
  17. package/dist/cjs/stream.cjs +45 -8
  18. package/dist/cjs/stream.cjs.map +1 -1
  19. package/dist/cjs/tools/ToolNode.cjs +3 -3
  20. package/dist/cjs/tools/subagent/SubagentExecutor.cjs +81 -6
  21. package/dist/cjs/tools/subagent/SubagentExecutor.cjs.map +1 -1
  22. package/dist/cjs/utils/callbacks.cjs +8 -0
  23. package/dist/cjs/utils/callbacks.cjs.map +1 -1
  24. package/dist/esm/graphs/Graph.mjs +27 -14
  25. package/dist/esm/graphs/Graph.mjs.map +1 -1
  26. package/dist/esm/graphs/MultiAgentGraph.mjs +1 -1
  27. package/dist/esm/instrumentation.mjs +3 -3
  28. package/dist/esm/langfuse.mjs +3 -3
  29. package/dist/esm/langfuseRuntimeScope.mjs +1 -1
  30. package/dist/esm/langfuseToolOutputTracing.mjs +2 -2
  31. package/dist/esm/main.mjs +3 -2
  32. package/dist/esm/messages/assistantPhase.mjs +57 -0
  33. package/dist/esm/messages/assistantPhase.mjs.map +1 -0
  34. package/dist/esm/messages/index.mjs +1 -0
  35. package/dist/esm/prompts/activityLabel.mjs +74 -1
  36. package/dist/esm/prompts/activityLabel.mjs.map +1 -1
  37. package/dist/esm/run.mjs +202 -12
  38. package/dist/esm/run.mjs.map +1 -1
  39. package/dist/esm/session/AgentSession.mjs +1 -1
  40. package/dist/esm/stream.mjs +45 -8
  41. package/dist/esm/stream.mjs.map +1 -1
  42. package/dist/esm/tools/ToolNode.mjs +3 -3
  43. package/dist/esm/tools/subagent/SubagentExecutor.mjs +81 -6
  44. package/dist/esm/tools/subagent/SubagentExecutor.mjs.map +1 -1
  45. package/dist/esm/utils/callbacks.mjs +8 -1
  46. package/dist/esm/utils/callbacks.mjs.map +1 -1
  47. package/dist/types/messages/assistantPhase.d.ts +22 -0
  48. package/dist/types/messages/index.d.ts +1 -0
  49. package/dist/types/prompts/activityLabel.d.ts +21 -1
  50. package/dist/types/run.d.ts +15 -2
  51. package/dist/types/types/activityLabel.d.ts +63 -0
  52. package/dist/types/types/assistantPhase.d.ts +6 -0
  53. package/dist/types/types/graph.d.ts +8 -1
  54. package/dist/types/types/index.d.ts +1 -0
  55. package/dist/types/types/stream.d.ts +11 -0
  56. package/dist/types/utils/callbacks.d.ts +1 -0
  57. package/package.json +3 -3
  58. package/src/graphs/Graph.ts +33 -9
  59. package/src/graphs/__tests__/Graph.reasoning.test.ts +57 -0
  60. package/src/messages/assistantPhase.test.ts +75 -0
  61. package/src/messages/assistantPhase.ts +91 -0
  62. package/src/messages/index.ts +1 -0
  63. package/src/prompts/activityLabel.ts +177 -1
  64. package/src/run.ts +403 -21
  65. package/src/specs/activity-label-prompt.test.ts +123 -1
  66. package/src/specs/activity-phase-label.test.ts +306 -0
  67. package/src/stream.ts +69 -12
  68. package/src/tools/__tests__/SubagentExecutor.test.ts +436 -0
  69. package/src/tools/subagent/SubagentExecutor.ts +160 -8
  70. package/src/types/activityLabel.ts +65 -0
  71. package/src/types/assistantPhase.ts +6 -0
  72. package/src/types/graph.ts +8 -0
  73. package/src/types/index.ts +1 -0
  74. package/src/types/stream.ts +9 -0
  75. package/src/utils/callbacks.ts +21 -0
@@ -10,6 +10,7 @@ import type {
10
10
  StandardGraphInput,
11
11
  SubagentUpdateEvent,
12
12
  SubagentUsageEvent,
13
+ ToolSessionMap,
13
14
  ToolExecuteBatchRequest,
14
15
  ToolExecuteResult,
15
16
  } from '@/types';
@@ -1536,6 +1537,253 @@ describe('SubagentExecutor', () => {
1536
1537
  expect(observedChildInputs!.maxSubagentDepth).toBeUndefined();
1537
1538
  });
1538
1539
 
1540
+ it('seeds only the selected single-agent child before its first invocation', async () => {
1541
+ const selectedSession = {
1542
+ session_id: 'selected-storage',
1543
+ files: [
1544
+ {
1545
+ id: 'selected-file',
1546
+ name: 'selected.txt',
1547
+ storage_session_id: 'selected-storage',
1548
+ },
1549
+ ],
1550
+ lastUpdated: 1,
1551
+ };
1552
+ const siblingSession = {
1553
+ session_id: 'sibling-storage',
1554
+ files: [
1555
+ {
1556
+ id: 'sibling-file',
1557
+ name: 'sibling.txt',
1558
+ storage_session_id: 'sibling-storage',
1559
+ },
1560
+ ],
1561
+ lastUpdated: 1,
1562
+ };
1563
+ const selected = makeConfig('selected', {
1564
+ agentInputs: {
1565
+ ...makeChildInputs('selected-agent'),
1566
+ initialSessions: new Map([
1567
+ [Constants.EXECUTE_CODE, selectedSession],
1568
+ ]),
1569
+ },
1570
+ });
1571
+ const sibling = makeConfig('sibling', {
1572
+ agentInputs: {
1573
+ ...makeChildInputs('sibling-agent'),
1574
+ initialSessions: new Map([
1575
+ [Constants.EXECUTE_CODE, siblingSession],
1576
+ ]),
1577
+ },
1578
+ });
1579
+ const childSessions: ToolSessionMap = new Map();
1580
+ const invoke = jest.fn(async () => {
1581
+ expect(childSessions.get(Constants.EXECUTE_CODE)).toEqual(
1582
+ selectedSession
1583
+ );
1584
+ expect(childSessions.get(Constants.EXECUTE_CODE)).not.toBe(
1585
+ selectedSession
1586
+ );
1587
+ return { messages: [new AIMessage('done')] };
1588
+ });
1589
+ const executor = createExecutor({
1590
+ configs: new Map([
1591
+ [selected.type, selected],
1592
+ [sibling.type, sibling],
1593
+ ]),
1594
+ createChildGraph: (): StandardGraph =>
1595
+ ({
1596
+ sessions: childSessions,
1597
+ createWorkflow: () => ({ invoke }),
1598
+ clearHeavyState: jest.fn(),
1599
+ }) as unknown as StandardGraph,
1600
+ });
1601
+
1602
+ await executor.execute({
1603
+ description: 'Use the selected file.',
1604
+ subagentType: selected.type,
1605
+ });
1606
+
1607
+ expect(invoke).toHaveBeenCalledTimes(1);
1608
+ expect(
1609
+ childSessions
1610
+ .get(Constants.EXECUTE_CODE)
1611
+ ?.files?.some((file) => file.id === 'sibling-file')
1612
+ ).toBe(false);
1613
+ });
1614
+
1615
+ it('combines member session seeds inside a selected graph child', async () => {
1616
+ const makeSession = (storageSessionId: string, includeFileSession = true) => ({
1617
+ session_id: storageSessionId,
1618
+ files: [
1619
+ {
1620
+ id: 'shared-file-id',
1621
+ name: `${storageSessionId}.txt`,
1622
+ ...(includeFileSession
1623
+ ? { storage_session_id: storageSessionId }
1624
+ : {}),
1625
+ },
1626
+ ],
1627
+ lastUpdated: 1,
1628
+ });
1629
+ const first = {
1630
+ ...makeChildInputs('first'),
1631
+ initialSessions: new Map([
1632
+ [Constants.EXECUTE_CODE, makeSession('first-storage')],
1633
+ ]),
1634
+ };
1635
+ const second = {
1636
+ ...makeChildInputs('second'),
1637
+ initialSessions: new Map([
1638
+ [Constants.EXECUTE_CODE, makeSession('second-storage', false)],
1639
+ ]),
1640
+ };
1641
+ const graphConfig: GraphSubagentConfig = {
1642
+ kind: 'graph',
1643
+ type: 'graph-team',
1644
+ name: 'Graph Team',
1645
+ description: 'Runs a graph.',
1646
+ agents: [first, second],
1647
+ edges: [{ from: 'first', to: 'second', edgeType: 'direct' }],
1648
+ entryAgentId: 'first',
1649
+ resultAgentId: 'second',
1650
+ };
1651
+ const childSessions: ToolSessionMap = new Map();
1652
+ const invoke = jest.fn(async () => {
1653
+ expect(
1654
+ childSessions
1655
+ .get(Constants.EXECUTE_CODE)
1656
+ ?.files?.map((file) => [file.id, file.storage_session_id])
1657
+ ).toEqual([
1658
+ ['shared-file-id', 'first-storage'],
1659
+ ['shared-file-id', 'second-storage'],
1660
+ ]);
1661
+ return {
1662
+ messages: [new AIMessage('done')],
1663
+ subagentResult: { agentId: 'second' },
1664
+ };
1665
+ });
1666
+ const childGraph = {
1667
+ sessions: childSessions,
1668
+ createWorkflow: () => ({ invoke }),
1669
+ clearHeavyState: jest.fn(),
1670
+ } as unknown as StandardGraph;
1671
+ const executor = createExecutor({
1672
+ configs: new Map([[graphConfig.type, graphConfig]]),
1673
+ createChildGraphByKind: () => childGraph,
1674
+ });
1675
+
1676
+ await executor.execute({
1677
+ description: 'Use both files.',
1678
+ subagentType: graphConfig.type,
1679
+ });
1680
+
1681
+ expect(invoke).toHaveBeenCalledTimes(1);
1682
+ });
1683
+
1684
+ it('preserves the child session across an in-memory HITL resume', async () => {
1685
+ const seededSession = {
1686
+ session_id: 'seed-storage',
1687
+ files: [
1688
+ {
1689
+ id: 'seed-file',
1690
+ name: 'seed.txt',
1691
+ storage_session_id: 'seed-storage',
1692
+ },
1693
+ ],
1694
+ lastUpdated: 1,
1695
+ };
1696
+ const hitlConfig = makeConfig('hitl-child', {
1697
+ agentInputs: {
1698
+ ...makeChildInputs('hitl-child'),
1699
+ initialSessions: new Map([
1700
+ [Constants.EXECUTE_CODE, seededSession],
1701
+ ]),
1702
+ },
1703
+ });
1704
+ const childSessions: ToolSessionMap = new Map();
1705
+ const childInterrupt = new GraphInterrupt([
1706
+ {
1707
+ id: 'approval-1',
1708
+ value: {
1709
+ type: 'tool_approval',
1710
+ action_requests: [],
1711
+ review_configs: [],
1712
+ },
1713
+ },
1714
+ ]);
1715
+ const invoke = jest
1716
+ .fn()
1717
+ .mockImplementationOnce(async () => {
1718
+ const session = childSessions.get(Constants.EXECUTE_CODE)!;
1719
+ childSessions.set(Constants.EXECUTE_CODE, {
1720
+ ...session,
1721
+ session_id: 'live-execution',
1722
+ files: [
1723
+ ...(session.files ?? []),
1724
+ {
1725
+ id: 'generated-file',
1726
+ name: 'generated.txt',
1727
+ storage_session_id: 'live-execution',
1728
+ },
1729
+ ],
1730
+ });
1731
+ throw childInterrupt;
1732
+ })
1733
+ .mockImplementationOnce(async () => {
1734
+ expect(childSessions.get(Constants.EXECUTE_CODE)).toMatchObject({
1735
+ session_id: 'live-execution',
1736
+ files: [{ id: 'seed-file' }, { id: 'generated-file' }],
1737
+ });
1738
+ return { messages: [new AIMessage('resumed')] };
1739
+ });
1740
+ const executor = createExecutor({
1741
+ configs: new Map([[hitlConfig.type, hitlConfig]]),
1742
+ humanInTheLoop: { enabled: true },
1743
+ checkpointer: new MemorySaver(),
1744
+ createChildGraph: (): StandardGraph =>
1745
+ ({
1746
+ sessions: childSessions,
1747
+ createWorkflow: () => ({
1748
+ getState: jest.fn().mockResolvedValue({
1749
+ values: {},
1750
+ next: [],
1751
+ tasks: [],
1752
+ }),
1753
+ invoke,
1754
+ }),
1755
+ getChildCheckpointThreadIds: jest.fn(() => []),
1756
+ createSubagentResumeState: jest.fn(() => ({
1757
+ toolCallSteps: [],
1758
+ toolSessions: [],
1759
+ toolNodes: [],
1760
+ eagerToolUsage: [],
1761
+ eagerToolSuppressions: [],
1762
+ })),
1763
+ clearHeavyState: jest.fn(),
1764
+ }) as unknown as StandardGraph,
1765
+ });
1766
+ const params = {
1767
+ description: 'Pause and resume.',
1768
+ subagentType: hitlConfig.type,
1769
+ threadId: 'durable-parent',
1770
+ parentToolCallId: 'call-hitl',
1771
+ };
1772
+
1773
+ await expect(executor.execute(params)).rejects.toBeInstanceOf(
1774
+ GraphInterrupt
1775
+ );
1776
+ const result = await executor.execute({
1777
+ ...params,
1778
+ parentConfigurable: {
1779
+ __pregel_resume_map: { 'approval-1': [] },
1780
+ },
1781
+ });
1782
+
1783
+ expect(result.content).toBe('resumed');
1784
+ expect(invoke).toHaveBeenCalledTimes(2);
1785
+ });
1786
+
1539
1787
  describe('parentConfigurable inheritance', () => {
1540
1788
  type CapturingGraphFactory = {
1541
1789
  factory: () => StandardGraph;
@@ -2154,6 +2402,146 @@ describe('SubagentExecutor', () => {
2154
2402
  expect(phases[phases.length - 1]).toBe('stop');
2155
2403
  });
2156
2404
 
2405
+ it('attributes graph updates to validated payload and step-local member identities', async () => {
2406
+ const updates: SubagentUpdateEvent[] = [];
2407
+ const registry = new HandlerRegistry();
2408
+ registry.register(GraphEvents.ON_SUBAGENT_UPDATE, {
2409
+ handle: (_event, data): void => {
2410
+ updates.push(data as SubagentUpdateEvent);
2411
+ },
2412
+ });
2413
+ const graphConfig: GraphSubagentConfig = {
2414
+ kind: 'graph',
2415
+ type: 'graph-team',
2416
+ name: 'Graph Team',
2417
+ description: 'Runs a graph.',
2418
+ agents: [
2419
+ makeChildInputs('entry'),
2420
+ makeChildInputs('worker'),
2421
+ makeChildInputs('result'),
2422
+ ],
2423
+ edges: [
2424
+ { from: 'entry', to: 'worker', edgeType: 'direct' },
2425
+ { from: 'worker', to: 'result', edgeType: 'direct' },
2426
+ ],
2427
+ entryAgentId: 'entry',
2428
+ resultAgentId: 'result',
2429
+ };
2430
+ const graphFactory = (): StandardGraph =>
2431
+ ({
2432
+ createWorkflow: (): { invoke: jest.Mock } => ({
2433
+ invoke: jest.fn().mockImplementation(async (_state, options) => {
2434
+ const opts = options as { callbacks?: unknown[] };
2435
+ const forwarder = (opts.callbacks ?? [])[0] as {
2436
+ handleCustomEvent?: (
2437
+ eventName: string,
2438
+ data: unknown,
2439
+ runId: string,
2440
+ tags?: string[],
2441
+ metadata?: Record<string, unknown>
2442
+ ) => Promise<void> | void;
2443
+ };
2444
+ const emit = async (
2445
+ eventName: string,
2446
+ data: unknown,
2447
+ agentId: string
2448
+ ): Promise<void> => {
2449
+ await forwarder.handleCustomEvent?.(
2450
+ eventName,
2451
+ data,
2452
+ 'child-run',
2453
+ [],
2454
+ { agentId }
2455
+ );
2456
+ };
2457
+
2458
+ await emit(
2459
+ GraphEvents.ON_RUN_STEP,
2460
+ {
2461
+ id: 'step_entry',
2462
+ type: StepTypes.MESSAGE_CREATION,
2463
+ agentId: 'entry',
2464
+ },
2465
+ 'parent-agent'
2466
+ );
2467
+ await emit(
2468
+ GraphEvents.ON_MESSAGE_DELTA,
2469
+ { id: 'step_entry', delta: { content: 'entry' } },
2470
+ 'parent-agent'
2471
+ );
2472
+ await emit(
2473
+ GraphEvents.ON_REASONING_DELTA,
2474
+ { id: 'step_entry', delta: { content: [] } },
2475
+ 'parent-agent'
2476
+ );
2477
+ await emit(
2478
+ GraphEvents.ON_RUN_STEP,
2479
+ {
2480
+ id: 'step_worker',
2481
+ type: StepTypes.MESSAGE_CREATION,
2482
+ agentId: 'worker',
2483
+ },
2484
+ 'entry'
2485
+ );
2486
+ await emit(
2487
+ GraphEvents.ON_RUN_STEP,
2488
+ {
2489
+ id: 'step_spoofed',
2490
+ type: StepTypes.MESSAGE_CREATION,
2491
+ agentId: 'unknown-member',
2492
+ },
2493
+ 'worker'
2494
+ );
2495
+ await emit(
2496
+ GraphEvents.ON_RUN_STEP_COMPLETED,
2497
+ { result: { id: 'step_result', agentId: 'result' } },
2498
+ 'parent-agent'
2499
+ );
2500
+ await emit(
2501
+ GraphEvents.ON_MESSAGE_DELTA,
2502
+ { id: 'step_metadata', delta: { content: 'worker' } },
2503
+ 'worker'
2504
+ );
2505
+ return {
2506
+ messages: [new AIMessage('ok')],
2507
+ subagentResult: { agentId: 'result' },
2508
+ };
2509
+ }),
2510
+ }),
2511
+ clearHeavyState: jest.fn(),
2512
+ }) as unknown as StandardGraph;
2513
+ const executor = createExecutor({
2514
+ configs: new Map([[graphConfig.type, graphConfig]]),
2515
+ createChildGraphByKind: graphFactory,
2516
+ parentHandlerRegistry: registry,
2517
+ });
2518
+
2519
+ await executor.execute({
2520
+ description: 'Run the graph.',
2521
+ subagentType: graphConfig.type,
2522
+ });
2523
+
2524
+ const activity = updates.filter(
2525
+ (update) => update.phase !== 'start' && update.phase !== 'stop'
2526
+ );
2527
+ expect(
2528
+ activity.map(({ phase, memberAgentId }) => ({
2529
+ phase,
2530
+ memberAgentId,
2531
+ }))
2532
+ ).toEqual([
2533
+ { phase: 'run_step', memberAgentId: 'entry' },
2534
+ { phase: 'message_delta', memberAgentId: 'entry' },
2535
+ { phase: 'reasoning_delta', memberAgentId: 'entry' },
2536
+ { phase: 'run_step', memberAgentId: 'worker' },
2537
+ { phase: 'run_step', memberAgentId: undefined },
2538
+ { phase: 'run_step_completed', memberAgentId: 'result' },
2539
+ { phase: 'message_delta', memberAgentId: 'worker' },
2540
+ ]);
2541
+ expect(updates[0].memberAgentId).toBeUndefined();
2542
+ expect(updates[updates.length - 1].memberAgentId).toBeUndefined();
2543
+ });
2544
+
2157
2545
  it('keeps toolDefinitions on child when registry has ON_TOOL_EXECUTE handler', async () => {
2158
2546
  const registry = new HandlerRegistry();
2159
2547
  registry.register(GraphEvents.ON_TOOL_EXECUTE, {
@@ -3138,6 +3526,54 @@ describe('summarizeEvent', () => {
3138
3526
  });
3139
3527
 
3140
3528
  describe('sanitizeForwardedSubagentUpdateData', () => {
3529
+ it('preserves bounded assistant phase metadata on message-creation steps', () => {
3530
+ const sanitized = sanitizeForwardedSubagentUpdateData(
3531
+ GraphEvents.ON_RUN_STEP,
3532
+ {
3533
+ id: 'step_1',
3534
+ stepDetails: {
3535
+ type: StepTypes.MESSAGE_CREATION,
3536
+ message_creation: {
3537
+ message_id: 'message_1',
3538
+ content_type: 'text',
3539
+ phase: 'commentary',
3540
+ futureSecret: 'nested-secret',
3541
+ },
3542
+ },
3543
+ }
3544
+ );
3545
+
3546
+ expect(sanitized).toEqual({
3547
+ id: 'step_1',
3548
+ stepDetails: {
3549
+ type: StepTypes.MESSAGE_CREATION,
3550
+ message_creation: {
3551
+ message_id: 'message_1',
3552
+ content_type: 'text',
3553
+ phase: 'commentary',
3554
+ },
3555
+ },
3556
+ });
3557
+
3558
+ expect(
3559
+ sanitizeForwardedSubagentUpdateData(GraphEvents.ON_RUN_STEP, {
3560
+ stepDetails: {
3561
+ type: StepTypes.MESSAGE_CREATION,
3562
+ message_creation: {
3563
+ message_id: 'message_2',
3564
+ content_type: 'image',
3565
+ phase: 'internal',
3566
+ },
3567
+ },
3568
+ })
3569
+ ).toEqual({
3570
+ stepDetails: {
3571
+ type: StepTypes.MESSAGE_CREATION,
3572
+ message_creation: { message_id: 'message_2' },
3573
+ },
3574
+ });
3575
+ });
3576
+
3141
3577
  it('uses an allowlist for run step payloads', () => {
3142
3578
  const sanitized = sanitizeForwardedSubagentUpdateData(
3143
3579
  GraphEvents.ON_RUN_STEP,
@@ -40,6 +40,7 @@ import type {
40
40
  HumanInTheLoopConfig,
41
41
  InjectedMessage,
42
42
  MessageDeltaEvent,
43
+ MessageCreationDetails,
43
44
  ProcessedToolCall,
44
45
  ReasoningDeltaEvent,
45
46
  RunStep,
@@ -59,6 +60,7 @@ import type {
59
60
  ToolApprovalInterruptPayload,
60
61
  ToolExecuteBatchRequest,
61
62
  ToolCallDelta,
63
+ ToolSessionContext,
62
64
  TokenCounter,
63
65
  ToolApprovalDecision,
64
66
  ToolApprovalDecisionMap,
@@ -144,6 +146,75 @@ const SUBAGENT_CONFIG_CHANGED_MESSAGE =
144
146
  const SUBAGENT_INVOCATION_CHANGED_MESSAGE =
145
147
  'Subagent error: Subagent invocation changed for this execution.';
146
148
 
149
+ function cloneToolSessionContext(
150
+ context: ToolSessionContext
151
+ ): ToolSessionContext {
152
+ return {
153
+ ...context,
154
+ ...(context.files == null
155
+ ? {}
156
+ : {
157
+ files: context.files.map((file) => ({
158
+ ...file,
159
+ storage_session_id:
160
+ file.storage_session_id ?? context.session_id,
161
+ })),
162
+ }),
163
+ };
164
+ }
165
+
166
+ function seedChildGraphSessions(
167
+ childGraph: StandardGraph,
168
+ agents: AgentInputs[]
169
+ ): void {
170
+ const seenFilesByTool = new Map<string, Set<string>>();
171
+ for (const agent of agents) {
172
+ if (agent.initialSessions == null) {
173
+ continue;
174
+ }
175
+ for (const [toolName, context] of agent.initialSessions) {
176
+ const existing = childGraph.sessions.get(toolName);
177
+ if (existing == null) {
178
+ const cloned = cloneToolSessionContext(context);
179
+ childGraph.sessions.set(toolName, cloned);
180
+ seenFilesByTool.set(
181
+ toolName,
182
+ new Set(
183
+ cloned.files?.map(
184
+ (file) => `${file.storage_session_id ?? ''}\0${file.id}`
185
+ ) ?? []
186
+ )
187
+ );
188
+ continue;
189
+ }
190
+ if (context.files == null || context.files.length === 0) {
191
+ continue;
192
+ }
193
+ const seenFiles =
194
+ seenFilesByTool.get(toolName) ??
195
+ new Set(
196
+ existing.files?.map(
197
+ (file) =>
198
+ `${file.storage_session_id ?? existing.session_id}\0${file.id}`
199
+ ) ?? []
200
+ );
201
+ const files = existing.files == null ? [] : [...existing.files];
202
+ for (const file of context.files) {
203
+ const storageSessionId =
204
+ file.storage_session_id ?? context.session_id;
205
+ const key = `${storageSessionId}\0${file.id}`;
206
+ if (seenFiles.has(key)) {
207
+ continue;
208
+ }
209
+ seenFiles.add(key);
210
+ files.push({ ...file, storage_session_id: storageSessionId });
211
+ }
212
+ seenFilesByTool.set(toolName, seenFiles);
213
+ childGraph.sessions.set(toolName, { ...existing, files });
214
+ }
215
+ }
216
+ }
217
+
147
218
  async function dispatchObservationalSubagentUpdate(
148
219
  handler: EventHandler,
149
220
  event: SubagentUpdateEvent
@@ -208,9 +279,7 @@ type SanitizedRunStep = Partial<
208
279
  type SanitizedStepDetails =
209
280
  | {
210
281
  type: StepTypes.MESSAGE_CREATION;
211
- message_creation?: {
212
- message_id?: string;
213
- };
282
+ message_creation?: Partial<MessageCreationDetails['message_creation']>;
214
283
  }
215
284
  | {
216
285
  type: StepTypes.TOOL_CALLS;
@@ -2132,6 +2201,9 @@ export class SubagentExecutor {
2132
2201
  });
2133
2202
  }
2134
2203
  childGraph ??= this.createChildGraph(childGraphInput);
2204
+ if (cachedChildRun == null) {
2205
+ seedChildGraphSessions(childGraph, childPlan.agents);
2206
+ }
2135
2207
  let forwarding: ForwarderCallback | undefined;
2136
2208
  if (forwardingEnabled) {
2137
2209
  forwarding = this.createForwarderCallback({
@@ -2139,6 +2211,8 @@ export class SubagentExecutor {
2139
2211
  subagentType,
2140
2212
  subagentKind: childPlan.kind,
2141
2213
  subagentAgentId: childAgentId,
2214
+ graphMemberInputs:
2215
+ childPlan.kind === 'graph' ? childPlan.memberInputs : undefined,
2142
2216
  childRunId,
2143
2217
  parentToolCallId,
2144
2218
  executionContext: childExecutionContext,
@@ -2589,6 +2663,7 @@ export class SubagentExecutor {
2589
2663
  subagentType: string;
2590
2664
  subagentKind: 'agent' | 'graph';
2591
2665
  subagentAgentId: string;
2666
+ graphMemberInputs?: ReadonlyMap<string, AgentInputs>;
2592
2667
  childRunId: string;
2593
2668
  parentToolCallId?: string;
2594
2669
  executionContext: SubagentExecutionContext;
@@ -2598,12 +2673,14 @@ export class SubagentExecutor {
2598
2673
  subagentType,
2599
2674
  subagentKind,
2600
2675
  subagentAgentId,
2676
+ graphMemberInputs,
2601
2677
  childRunId,
2602
2678
  parentToolCallId,
2603
2679
  executionContext,
2604
2680
  } = args;
2605
2681
  const immediateParentRunId = this.parentRunId;
2606
2682
  const parentAgentId = this.parentAgentId;
2683
+ const memberAgentIdByStepId = new Map<string, string>();
2607
2684
 
2608
2685
  const wrap = async (
2609
2686
  eventName: string,
@@ -2710,7 +2787,14 @@ export class SubagentExecutor {
2710
2787
  metadata?: Record<string, unknown>
2711
2788
  ): Promise<void> => {
2712
2789
  const memberAgentId =
2713
- asNonEmptyString(metadata?.agentId) ?? getEventAgentId(data);
2790
+ graphMemberInputs == null
2791
+ ? (asNonEmptyString(metadata?.agentId) ?? getEventAgentId(data))
2792
+ : getGraphEventMemberAgentId({
2793
+ data,
2794
+ metadata,
2795
+ memberInputs: graphMemberInputs,
2796
+ memberAgentIdByStepId,
2797
+ });
2714
2798
  if (eventName === GraphEvents.ON_TOOL_EXECUTE) {
2715
2799
  const toolHandler = parentRegistry.getHandler(
2716
2800
  GraphEvents.ON_TOOL_EXECUTE
@@ -2928,6 +3012,52 @@ function getEventAgentId(data: unknown): string | undefined {
2928
3012
  );
2929
3013
  }
2930
3014
 
3015
+ function getEventStepId(data: unknown): string | undefined {
3016
+ if (data == null || typeof data !== 'object') {
3017
+ return undefined;
3018
+ }
3019
+ const event = data as {
3020
+ id?: unknown;
3021
+ result?: { id?: unknown };
3022
+ };
3023
+ return asNonEmptyString(event.id) ?? asNonEmptyString(event.result?.id);
3024
+ }
3025
+
3026
+ function getGraphEventMemberAgentId({
3027
+ data,
3028
+ metadata,
3029
+ memberInputs,
3030
+ memberAgentIdByStepId,
3031
+ }: {
3032
+ data: unknown;
3033
+ metadata?: Record<string, unknown>;
3034
+ memberInputs: ReadonlyMap<string, AgentInputs>;
3035
+ memberAgentIdByStepId: Map<string, string>;
3036
+ }): string | undefined {
3037
+ const eventAgentId = getEventAgentId(data);
3038
+ const stepId = getEventStepId(data);
3039
+ if (eventAgentId != null) {
3040
+ if (!memberInputs.has(eventAgentId)) {
3041
+ return undefined;
3042
+ }
3043
+ if (stepId != null) {
3044
+ memberAgentIdByStepId.set(stepId, eventAgentId);
3045
+ }
3046
+ return eventAgentId;
3047
+ }
3048
+
3049
+ const stepMemberAgentId =
3050
+ stepId == null ? undefined : memberAgentIdByStepId.get(stepId);
3051
+ if (stepMemberAgentId != null) {
3052
+ return stepMemberAgentId;
3053
+ }
3054
+
3055
+ const metadataAgentId = asNonEmptyString(metadata?.agentId);
3056
+ return metadataAgentId != null && memberInputs.has(metadataAgentId)
3057
+ ? metadataAgentId
3058
+ : undefined;
3059
+ }
3060
+
2931
3061
  /**
2932
3062
  * Best-effort read of the configured model from a subagent's client
2933
3063
  * options. Providers disagree on the key (`model` vs `modelName`), and the
@@ -3182,7 +3312,11 @@ function sanitizeStepDetails(
3182
3312
  return undefined;
3183
3313
  }
3184
3314
  const rawDetails = stepDetails as {
3185
- message_creation?: { message_id?: unknown };
3315
+ message_creation?: {
3316
+ message_id?: unknown;
3317
+ content_type?: unknown;
3318
+ phase?: unknown;
3319
+ };
3186
3320
  tool_calls?: unknown[];
3187
3321
  type?: unknown;
3188
3322
  };
@@ -3190,9 +3324,27 @@ function sanitizeStepDetails(
3190
3324
  const sanitized: SanitizedStepDetails = {
3191
3325
  type: StepTypes.MESSAGE_CREATION,
3192
3326
  };
3193
- const messageId = rawDetails.message_creation?.message_id;
3194
- if (typeof messageId === 'string') {
3195
- sanitized.message_creation = { message_id: messageId };
3327
+ const rawMessageCreation = rawDetails.message_creation;
3328
+ const messageId = rawMessageCreation?.message_id;
3329
+ const contentType = rawMessageCreation?.content_type;
3330
+ const phase = rawMessageCreation?.phase;
3331
+ if (
3332
+ typeof messageId === 'string' ||
3333
+ contentType === ContentTypes.TEXT ||
3334
+ contentType === ContentTypes.THINK ||
3335
+ phase === 'commentary' ||
3336
+ phase === 'final_answer'
3337
+ ) {
3338
+ sanitized.message_creation = {
3339
+ ...(typeof messageId === 'string' ? { message_id: messageId } : {}),
3340
+ ...(contentType === ContentTypes.TEXT ||
3341
+ contentType === ContentTypes.THINK
3342
+ ? { content_type: contentType }
3343
+ : {}),
3344
+ ...(phase === 'commentary' || phase === 'final_answer'
3345
+ ? { phase }
3346
+ : {}),
3347
+ };
3196
3348
  }
3197
3349
  return sanitized;
3198
3350
  }