fix(cli): prevent sub-agent tool calls from leaking into UI (#20580)

This commit is contained in:
Abhi
2026-02-27 14:00:19 -05:00
committed by GitHub
parent 5d24d6a9e1
commit c914fd0700
2 changed files with 40 additions and 27 deletions
@@ -359,14 +359,10 @@ describe('useToolScheduler', () => {
} as ToolCallsUpdateMessage); } as ToolCallsUpdateMessage);
}); });
let [toolCalls] = result.current; const [toolCalls] = result.current;
expect(toolCalls).toHaveLength(2); expect(toolCalls).toHaveLength(1);
expect( expect(toolCalls[0].request.callId).toBe('call-root');
toolCalls.find((t) => t.request.callId === 'call-root')?.schedulerId, expect(toolCalls[0].schedulerId).toBe(ROOT_SCHEDULER_ID);
).toBe(ROOT_SCHEDULER_ID);
expect(
toolCalls.find((t) => t.request.callId === 'call-sub')?.schedulerId,
).toBe('subagent-1');
// 2. Call setToolCallsForDisplay (e.g., simulate a manual update or clear) // 2. Call setToolCallsForDisplay (e.g., simulate a manual update or clear)
act(() => { act(() => {
@@ -377,34 +373,45 @@ describe('useToolScheduler', () => {
}); });
// 3. Verify that tools are still present and maintain their scheduler IDs // 3. Verify that tools are still present and maintain their scheduler IDs
// The internal map should have been re-grouped. const [toolCalls2] = result.current;
[toolCalls] = result.current; expect(toolCalls2).toHaveLength(1);
expect(toolCalls).toHaveLength(2); expect(toolCalls2[0].responseSubmittedToGemini).toBe(true);
expect(toolCalls.every((t) => t.responseSubmittedToGemini)).toBe(true); expect(toolCalls2[0].schedulerId).toBe(ROOT_SCHEDULER_ID);
});
const updatedRoot = toolCalls.find((t) => t.request.callId === 'call-root'); it('ignores TOOL_CALLS_UPDATE from non-root schedulers', () => {
const updatedSub = toolCalls.find((t) => t.request.callId === 'call-sub'); const { result } = renderHook(() =>
useToolScheduler(
vi.fn().mockResolvedValue(undefined),
mockConfig,
() => undefined,
),
);
expect(updatedRoot?.schedulerId).toBe(ROOT_SCHEDULER_ID); const subagentCall = {
expect(updatedSub?.schedulerId).toBe('subagent-1'); status: CoreToolCallStatus.Executing as const,
request: {
callId: 'call-sub',
name: 'test',
args: {},
isClientInitiated: false,
prompt_id: 'p1',
},
tool: createMockTool(),
invocation: createMockInvocation(),
schedulerId: 'subagent-1',
};
// 4. Verify that a subsequent update to ONE scheduler doesn't wipe the other
act(() => { act(() => {
void mockMessageBus.publish({ void mockMessageBus.publish({
type: MessageBusType.TOOL_CALLS_UPDATE, type: MessageBusType.TOOL_CALLS_UPDATE,
toolCalls: [{ ...callRoot, status: CoreToolCallStatus.Executing }], toolCalls: [subagentCall],
schedulerId: ROOT_SCHEDULER_ID, schedulerId: 'subagent-1',
} as ToolCallsUpdateMessage); } as ToolCallsUpdateMessage);
}); });
[toolCalls] = result.current; const [toolCalls] = result.current;
expect(toolCalls).toHaveLength(2); expect(toolCalls).toHaveLength(0);
expect(
toolCalls.find((t) => t.request.callId === 'call-root')?.status,
).toBe(CoreToolCallStatus.Executing);
expect(
toolCalls.find((t) => t.request.callId === 'call-sub')?.schedulerId,
).toBe('subagent-1');
}); });
it('adapts success/error status to executing when a tail call is present', () => { it('adapts success/error status to executing when a tail call is present', () => {
@@ -115,6 +115,12 @@ export function useToolScheduler(
useEffect(() => { useEffect(() => {
const handler = (event: ToolCallsUpdateMessage) => { const handler = (event: ToolCallsUpdateMessage) => {
// Only process updates for the root scheduler.
// Subagent internal tools should not be displayed in the main tool list.
if (event.schedulerId !== ROOT_SCHEDULER_ID) {
return;
}
// Update output timer for UI spinners (Side Effect) // Update output timer for UI spinners (Side Effect)
const hasExecuting = event.toolCalls.some( const hasExecuting = event.toolCalls.some(
(tc) => (tc) =>