diff --git a/api/server/services/Endpoints/agents/initialize.js b/api/server/services/Endpoints/agents/initialize.js index 1c0f71c5e0..350e0fd4a1 100644 --- a/api/server/services/Endpoints/agents/initialize.js +++ b/api/server/services/Endpoints/agents/initialize.js @@ -1199,13 +1199,10 @@ const initializeClient = async ({ subagentGraphIds.add(subagentId); } countExpandedSubagentDescriptor(subagentId); - const existingChildren = await buildLazySubagentDescriptors( - existing, - depth + 1, - nextAncestors, - ); - existing.lazySubagentConfigs = existingChildren.filter((child) => child.configId); - existing.subagentAgentConfigs = existingChildren.filter((child) => !child.configId); + /** Every initialized config is expanded independently in `rootSubagentConfigs`. + * Descend here only to enforce path-sensitive depth and size limits; mutating the + * shared object from this ancestor-specific walk would race its root expansion. */ + await buildLazySubagentDescriptors(existing, depth + 1, nextAncestors); return existing; } const metadata = await loadSubagentMetadata(subagentId); diff --git a/api/server/services/Endpoints/agents/initialize.spec.js b/api/server/services/Endpoints/agents/initialize.spec.js index cb80c3807b..c6c4b23c1e 100644 --- a/api/server/services/Endpoints/agents/initialize.spec.js +++ b/api/server/services/Endpoints/agents/initialize.spec.js @@ -2479,6 +2479,45 @@ describe('initializeClient — subagent loading', () => { expect(agentClientArgs.agentConfigs.has(HANDOFF_AND_SUB_ID)).toBe(true); }); + it('preserves shared initialized agent children across parallel walkers', async () => { + const leftId = 'agent_parallel_left'; + const rightId = 'agent_parallel_right'; + const sharedId = 'agent_parallel_shared'; + const primaryConfig = makePrimaryConfig({ + subagents: { enabled: true, allowSelf: false, agent_ids: [leftId, rightId] }, + }); + const leftConfig = makeNestedSubagentConfig(leftId, [sharedId]); + const rightConfig = makeNestedSubagentConfig(rightId, [sharedId]); + const sharedConfig = makeNestedSubagentConfig(sharedId, [leftId]); + mockInitializeAgent.mockResolvedValue(primaryConfig); + processAddedConvo.mockImplementationOnce(async ({ agentConfigs }) => { + agentConfigs.set(leftId, leftConfig); + agentConfigs.set(rightId, rightConfig); + agentConfigs.set(sharedId, sharedConfig); + return { userMCPAuthMap: undefined }; + }); + + await initializeClient({ + req: makeSubagentReq(), + res: {}, + signal: new AbortController().signal, + endpointOption: makeEndpointOption(), + }); + + const subagentsById = new Map( + agentClientArgs.agent.subagentAgentConfigs.map((config) => [config.id, config]), + ); + const left = subagentsById.get(leftId); + const right = subagentsById.get(rightId); + const leftShared = left.subagentAgentConfigs[0]; + const rightShared = right.subagentAgentConfigs[0]; + expect(leftShared).toBe(sharedConfig); + expect(rightShared).toBe(sharedConfig); + expect(leftShared.subagentAgentConfigs.map((child) => child.id)).toEqual([leftId]); + expect(rightShared.subagentAgentConfigs.map((child) => child.id)).toEqual([leftId]); + expect(agentClientArgs.agentConfigs.get(sharedId)).toBe(sharedConfig); + }); + it('clears subagents config on primary when the capability is disabled', async () => { /** Admin can turn subagents off at the endpoint level even if an agent was * configured for them. The primary's `subagents` field should be diff --git a/packages/api/src/agents/__tests__/run-summarization.test.ts b/packages/api/src/agents/__tests__/run-summarization.test.ts index 5132ba889b..26412970fe 100644 --- a/packages/api/src/agents/__tests__/run-summarization.test.ts +++ b/packages/api/src/agents/__tests__/run-summarization.test.ts @@ -2022,6 +2022,74 @@ describe('subagentConfigs', () => { }); }); + it('prunes shared-agent cycles per traversal path without dropping valid edges', async () => { + const left = makeAgent({ + id: 'agent_left', + name: 'Left', + subagents: { enabled: true, allowSelf: false, agent_ids: ['agent_shared'] }, + }) as TestRunAgent; + const right = makeAgent({ + id: 'agent_right', + name: 'Right', + subagents: { enabled: true, allowSelf: false, agent_ids: ['agent_shared'] }, + }) as TestRunAgent; + const shared = makeAgent({ + id: 'agent_shared', + name: 'Shared', + subagents: { enabled: true, allowSelf: false, agent_ids: ['agent_left'] }, + }) as TestRunAgent; + left.subagentAgentConfigs = [shared]; + right.subagentAgentConfigs = [shared]; + shared.subagentAgentConfigs = [left]; + + const agents = await callAndCapture({ + agents: [ + makeAgent({ + subagents: { + enabled: true, + allowSelf: false, + agent_ids: ['agent_left', 'agent_right'], + }, + subagentAgentConfigs: [left, right], + }), + ], + }); + + const rootConfigs = agents[0].subagentConfigs as Parameters[0][]; + const rootConfigsByType = new Map(rootConfigs.map((config) => [config.type, config])); + const leftConfig = rootConfigsByType.get('agent_left'); + const rightConfig = rootConfigsByType.get('agent_right'); + if (!leftConfig || !rightConfig) { + throw new Error('Expected both root subagent configs'); + } + const leftInputs = buildChildInputs(leftConfig, 'agent_left', MAX_SUBAGENT_DEPTH); + const rightInputs = buildChildInputs(rightConfig, 'agent_right', MAX_SUBAGENT_DEPTH); + const leftShared = leftInputs.subagentConfigs?.[0]; + const rightShared = rightInputs.subagentConfigs?.[0]; + if ( + !leftShared || + !rightShared || + leftInputs.maxSubagentDepth == null || + rightInputs.maxSubagentDepth == null + ) { + throw new Error('Expected both shared subagent configs'); + } + const leftSharedInputs = buildChildInputs( + leftShared, + 'agent_shared', + leftInputs.maxSubagentDepth, + ); + const rightSharedInputs = buildChildInputs( + rightShared, + 'agent_shared', + rightInputs.maxSubagentDepth, + ); + + expect(leftSharedInputs.subagentConfigs).toBeUndefined(); + expect(rightSharedInputs.subagentConfigs).toHaveLength(1); + expect(rightSharedInputs.subagentConfigs?.[0]).toMatchObject({ type: 'agent_left' }); + }); + it('combines self-spawn and explicit subagents when both enabled', async () => { const child = makeAgent({ id: 'agent_child', name: 'Helper' }); const agents = await callAndCapture({