mirror of
https://github.com/danny-avila/LibreChat.git
synced 2026-10-22 01:53:33 +00:00
🧶 fix: Untangle Parallel Subagent Descriptor Paths (#15492)
* fix: isolate parallel subagent descriptor walks * fix: preserve canonical subagent config identity * fix: separate subagent graph assembly from path validation
This commit is contained in:
parent
6d56d3b6fe
commit
6372cb08ee
3 changed files with 111 additions and 7 deletions
|
|
@ -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);
|
||||
|
|
|
|||
|
|
@ -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
|
||||
|
|
|
|||
|
|
@ -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<typeof buildChildInputs>[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({
|
||||
|
|
|
|||
Loading…
Add table
Add a link
Reference in a new issue