@librechat/agents 3.4.3 → 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.
- package/dist/cjs/graphs/Graph.cjs +27 -14
- package/dist/cjs/graphs/Graph.cjs.map +1 -1
- package/dist/cjs/graphs/MultiAgentGraph.cjs +1 -1
- package/dist/cjs/instrumentation.cjs +3 -3
- package/dist/cjs/langfuse.cjs +3 -3
- package/dist/cjs/langfuseRuntimeScope.cjs +1 -1
- package/dist/cjs/langfuseToolOutputTracing.cjs +2 -2
- package/dist/cjs/main.cjs +5 -1
- package/dist/cjs/messages/assistantPhase.cjs +59 -0
- package/dist/cjs/messages/assistantPhase.cjs.map +1 -0
- package/dist/cjs/messages/index.cjs +1 -0
- package/dist/cjs/prompts/activityLabel.cjs +76 -0
- package/dist/cjs/prompts/activityLabel.cjs.map +1 -1
- package/dist/cjs/run.cjs +200 -10
- package/dist/cjs/run.cjs.map +1 -1
- package/dist/cjs/session/AgentSession.cjs +1 -1
- package/dist/cjs/stream.cjs +45 -8
- package/dist/cjs/stream.cjs.map +1 -1
- package/dist/cjs/tools/ToolNode.cjs +3 -3
- package/dist/cjs/tools/subagent/SubagentExecutor.cjs +81 -6
- package/dist/cjs/tools/subagent/SubagentExecutor.cjs.map +1 -1
- package/dist/cjs/utils/callbacks.cjs +8 -0
- package/dist/cjs/utils/callbacks.cjs.map +1 -1
- package/dist/esm/graphs/Graph.mjs +27 -14
- package/dist/esm/graphs/Graph.mjs.map +1 -1
- package/dist/esm/graphs/MultiAgentGraph.mjs +1 -1
- package/dist/esm/instrumentation.mjs +3 -3
- package/dist/esm/langfuse.mjs +3 -3
- package/dist/esm/langfuseRuntimeScope.mjs +1 -1
- package/dist/esm/langfuseToolOutputTracing.mjs +2 -2
- package/dist/esm/main.mjs +3 -2
- package/dist/esm/messages/assistantPhase.mjs +57 -0
- package/dist/esm/messages/assistantPhase.mjs.map +1 -0
- package/dist/esm/messages/index.mjs +1 -0
- package/dist/esm/prompts/activityLabel.mjs +74 -1
- package/dist/esm/prompts/activityLabel.mjs.map +1 -1
- package/dist/esm/run.mjs +202 -12
- package/dist/esm/run.mjs.map +1 -1
- package/dist/esm/session/AgentSession.mjs +1 -1
- package/dist/esm/stream.mjs +45 -8
- package/dist/esm/stream.mjs.map +1 -1
- package/dist/esm/tools/ToolNode.mjs +3 -3
- package/dist/esm/tools/subagent/SubagentExecutor.mjs +81 -6
- package/dist/esm/tools/subagent/SubagentExecutor.mjs.map +1 -1
- package/dist/esm/utils/callbacks.mjs +8 -1
- package/dist/esm/utils/callbacks.mjs.map +1 -1
- package/dist/types/messages/assistantPhase.d.ts +22 -0
- package/dist/types/messages/index.d.ts +1 -0
- package/dist/types/prompts/activityLabel.d.ts +21 -1
- package/dist/types/run.d.ts +15 -2
- package/dist/types/types/activityLabel.d.ts +63 -0
- package/dist/types/types/assistantPhase.d.ts +6 -0
- package/dist/types/types/graph.d.ts +8 -1
- package/dist/types/types/index.d.ts +1 -0
- package/dist/types/types/stream.d.ts +11 -0
- package/dist/types/utils/callbacks.d.ts +1 -0
- package/package.json +1 -1
- package/src/graphs/Graph.ts +33 -9
- package/src/graphs/__tests__/Graph.reasoning.test.ts +57 -0
- package/src/messages/assistantPhase.test.ts +75 -0
- package/src/messages/assistantPhase.ts +91 -0
- package/src/messages/index.ts +1 -0
- package/src/prompts/activityLabel.ts +177 -1
- package/src/run.ts +403 -21
- package/src/specs/activity-label-prompt.test.ts +123 -1
- package/src/specs/activity-phase-label.test.ts +306 -0
- package/src/stream.ts +69 -12
- package/src/tools/__tests__/SubagentExecutor.test.ts +436 -0
- package/src/tools/subagent/SubagentExecutor.ts +160 -8
- package/src/types/activityLabel.ts +65 -0
- package/src/types/assistantPhase.ts +6 -0
- package/src/types/graph.ts +8 -0
- package/src/types/index.ts +1 -0
- package/src/types/stream.ts +9 -0
- 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
|
-
|
|
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?: {
|
|
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
|
|
3194
|
-
|
|
3195
|
-
|
|
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
|
}
|