From d745d86af15971073f319778570c00d6e73139d8 Mon Sep 17 00:00:00 2001 From: Abhi <43648792+abhipatel12@users.noreply.github.com> Date: Mon, 26 Jan 2026 13:38:11 -0500 Subject: [PATCH] feat(scheduler): support multi-scheduler tool aggregation and nested call IDs (#17429) --- .../hooks/useToolExecutionScheduler.test.ts | 110 ++++++++++++++++++ .../src/ui/hooks/useToolExecutionScheduler.ts | 87 +++++++++++--- packages/core/src/confirmation-bus/types.ts | 1 + .../core/src/scheduler/confirmation.test.ts | 10 +- packages/core/src/scheduler/confirmation.ts | 1 + packages/core/src/scheduler/scheduler.test.ts | 9 ++ packages/core/src/scheduler/scheduler.ts | 21 +++- packages/core/src/scheduler/state-manager.ts | 14 ++- packages/core/src/scheduler/types.ts | 11 ++ 9 files changed, 241 insertions(+), 23 deletions(-) diff --git a/packages/cli/src/ui/hooks/useToolExecutionScheduler.test.ts b/packages/cli/src/ui/hooks/useToolExecutionScheduler.test.ts index 2a526150c3..797109499b 100644 --- a/packages/cli/src/ui/hooks/useToolExecutionScheduler.test.ts +++ b/packages/cli/src/ui/hooks/useToolExecutionScheduler.test.ts @@ -19,6 +19,7 @@ import { type ToolCallsUpdateMessage, type AnyDeclarativeTool, type AnyToolInvocation, + ROOT_SCHEDULER_ID, } from '@google/gemini-cli-core'; import { createMockMessageBus } from '@google/gemini-cli-core/src/test-utils/mock-message-bus.js'; @@ -73,6 +74,10 @@ describe('useToolExecutionScheduler', () => { } as unknown as Config; }); + afterEach(() => { + vi.clearAllMocks(); + }); + it('initializes with empty tool calls', () => { const { result } = renderHook(() => useToolExecutionScheduler( @@ -112,6 +117,7 @@ describe('useToolExecutionScheduler', () => { void mockMessageBus.publish({ type: MessageBusType.TOOL_CALLS_UPDATE, toolCalls: [mockToolCall], + schedulerId: ROOT_SCHEDULER_ID, } as ToolCallsUpdateMessage); }); @@ -156,6 +162,7 @@ describe('useToolExecutionScheduler', () => { void mockMessageBus.publish({ type: MessageBusType.TOOL_CALLS_UPDATE, toolCalls: [mockToolCall], + schedulerId: ROOT_SCHEDULER_ID, } as ToolCallsUpdateMessage); }); @@ -212,6 +219,7 @@ describe('useToolExecutionScheduler', () => { void mockMessageBus.publish({ type: MessageBusType.TOOL_CALLS_UPDATE, toolCalls: [mockToolCall], + schedulerId: ROOT_SCHEDULER_ID, } as ToolCallsUpdateMessage); }); @@ -274,6 +282,7 @@ describe('useToolExecutionScheduler', () => { void mockMessageBus.publish({ type: MessageBusType.TOOL_CALLS_UPDATE, toolCalls: [mockToolCall], + schedulerId: ROOT_SCHEDULER_ID, } as ToolCallsUpdateMessage); }); @@ -290,6 +299,7 @@ describe('useToolExecutionScheduler', () => { void mockMessageBus.publish({ type: MessageBusType.TOOL_CALLS_UPDATE, toolCalls: [mockToolCall], + schedulerId: ROOT_SCHEDULER_ID, } as ToolCallsUpdateMessage); }); @@ -326,6 +336,7 @@ describe('useToolExecutionScheduler', () => { invocation: createMockInvocation(), }, ], + schedulerId: ROOT_SCHEDULER_ID, } as ToolCallsUpdateMessage); }); @@ -412,4 +423,103 @@ describe('useToolExecutionScheduler', () => { expect(completedResult).toEqual([completedToolCall]); expect(onComplete).toHaveBeenCalledWith([completedToolCall]); }); + + it('setToolCallsForDisplay re-groups tools by schedulerId (Multi-Scheduler support)', () => { + const { result } = renderHook(() => + useToolExecutionScheduler( + vi.fn().mockResolvedValue(undefined), + mockConfig, + () => undefined, + ), + ); + + const callRoot = { + status: 'success' as const, + request: { + callId: 'call-root', + name: 'test', + args: {}, + isClientInitiated: false, + prompt_id: 'p1', + }, + tool: createMockTool(), + invocation: createMockInvocation(), + response: { + callId: 'call-root', + responseParts: [], + resultDisplay: 'OK', + error: undefined, + errorType: undefined, + }, + schedulerId: ROOT_SCHEDULER_ID, + }; + + const callSub = { + ...callRoot, + request: { ...callRoot.request, callId: 'call-sub' }, + schedulerId: 'subagent-1', + }; + + // 1. Populate state with multiple schedulers + act(() => { + void mockMessageBus.publish({ + type: MessageBusType.TOOL_CALLS_UPDATE, + toolCalls: [callRoot], + schedulerId: ROOT_SCHEDULER_ID, + } as ToolCallsUpdateMessage); + + void mockMessageBus.publish({ + type: MessageBusType.TOOL_CALLS_UPDATE, + toolCalls: [callSub], + schedulerId: 'subagent-1', + } as ToolCallsUpdateMessage); + }); + + let [toolCalls] = result.current; + expect(toolCalls).toHaveLength(2); + expect( + toolCalls.find((t) => t.request.callId === 'call-root')?.schedulerId, + ).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) + act(() => { + const [, , , setToolCalls] = result.current; + setToolCalls((prev) => + prev.map((t) => ({ ...t, responseSubmittedToGemini: true })), + ); + }); + + // 3. Verify that tools are still present and maintain their scheduler IDs + // The internal map should have been re-grouped. + [toolCalls] = result.current; + expect(toolCalls).toHaveLength(2); + expect(toolCalls.every((t) => t.responseSubmittedToGemini)).toBe(true); + + const updatedRoot = toolCalls.find((t) => t.request.callId === 'call-root'); + const updatedSub = toolCalls.find((t) => t.request.callId === 'call-sub'); + + expect(updatedRoot?.schedulerId).toBe(ROOT_SCHEDULER_ID); + expect(updatedSub?.schedulerId).toBe('subagent-1'); + + // 4. Verify that a subsequent update to ONE scheduler doesn't wipe the other + act(() => { + void mockMessageBus.publish({ + type: MessageBusType.TOOL_CALLS_UPDATE, + toolCalls: [{ ...callRoot, status: 'executing' }], + schedulerId: ROOT_SCHEDULER_ID, + } as ToolCallsUpdateMessage); + }); + + [toolCalls] = result.current; + expect(toolCalls).toHaveLength(2); + expect( + toolCalls.find((t) => t.request.callId === 'call-root')?.status, + ).toBe('executing'); + expect( + toolCalls.find((t) => t.request.callId === 'call-sub')?.schedulerId, + ).toBe('subagent-1'); + }); }); diff --git a/packages/cli/src/ui/hooks/useToolExecutionScheduler.ts b/packages/cli/src/ui/hooks/useToolExecutionScheduler.ts index c68e414e9b..0c58e7fc41 100644 --- a/packages/cli/src/ui/hooks/useToolExecutionScheduler.ts +++ b/packages/cli/src/ui/hooks/useToolExecutionScheduler.ts @@ -16,6 +16,7 @@ import { Scheduler, type EditorType, type ToolCallsUpdateMessage, + ROOT_SCHEDULER_ID, } from '@google/gemini-cli-core'; import { useCallback, useState, useMemo, useEffect, useRef } from 'react'; @@ -54,8 +55,10 @@ export function useToolExecutionScheduler( CancelAllFn, number, ] { - // State stores Core objects, not Display objects - const [toolCalls, setToolCalls] = useState([]); + // State stores tool calls organized by their originating schedulerId + const [toolCallsMap, setToolCallsMap] = useState< + Record + >({}); const [lastToolOutputTime, setLastToolOutputTime] = useState(0); const messageBus = useMemo(() => config.getMessageBus(), [config]); @@ -76,6 +79,7 @@ export function useToolExecutionScheduler( config, messageBus, getPreferredEditor: () => getPreferredEditorRef.current(), + schedulerId: ROOT_SCHEDULER_ID, }), [config, messageBus], ); @@ -88,15 +92,21 @@ export function useToolExecutionScheduler( useEffect(() => { const handler = (event: ToolCallsUpdateMessage) => { - setToolCalls((prev) => { - const adapted = internalAdaptToolCalls(event.toolCalls, prev); + // Update output timer for UI spinners (Side Effect) + if (event.toolCalls.some((tc) => tc.status === 'executing')) { + setLastToolOutputTime(Date.now()); + } - // Update output timer for UI spinners - if (event.toolCalls.some((tc) => tc.status === 'executing')) { - setLastToolOutputTime(Date.now()); - } + setToolCallsMap((prev) => { + const adapted = internalAdaptToolCalls( + event.toolCalls, + prev[event.schedulerId] ?? [], + ); - return adapted; + return { + ...prev, + [event.schedulerId]: adapted, + }; }); }; @@ -109,12 +119,14 @@ export function useToolExecutionScheduler( const schedule: ScheduleFn = useCallback( async (request, signal) => { // Clear state for new run - setToolCalls([]); + setToolCallsMap({}); // 1. Await Core Scheduler directly const results = await scheduler.schedule(request, signal); // 2. Trigger legacy reinjection logic (useGeminiStream loop) + // Since this hook instance owns the "root" scheduler, we always trigger + // onComplete when it finishes its batch. await onCompleteRef.current(results); return results; @@ -131,13 +143,52 @@ export function useToolExecutionScheduler( const markToolsAsSubmitted: MarkToolsAsSubmittedFn = useCallback( (callIdsToMark: string[]) => { - setToolCalls((prevCalls) => - prevCalls.map((tc) => - callIdsToMark.includes(tc.request.callId) - ? { ...tc, responseSubmittedToGemini: true } - : tc, - ), - ); + setToolCallsMap((prevMap) => { + const nextMap = { ...prevMap }; + for (const [sid, calls] of Object.entries(nextMap)) { + nextMap[sid] = calls.map((tc) => + callIdsToMark.includes(tc.request.callId) + ? { ...tc, responseSubmittedToGemini: true } + : tc, + ); + } + return nextMap; + }); + }, + [], + ); + + // Flatten the map for the UI components that expect a single list of tools. + const toolCalls = useMemo( + () => Object.values(toolCallsMap).flat(), + [toolCallsMap], + ); + + // Provide a setter that maintains compatibility with legacy []. + const setToolCallsForDisplay = useCallback( + (action: React.SetStateAction) => { + setToolCallsMap((prev) => { + const currentFlattened = Object.values(prev).flat(); + const nextFlattened = + typeof action === 'function' ? action(currentFlattened) : action; + + if (nextFlattened.length === 0) { + return {}; + } + + // Re-group by schedulerId to preserve multi-scheduler state + const nextMap: Record = {}; + for (const call of nextFlattened) { + // All tool calls should have a schedulerId from the core. + // Default to ROOT_SCHEDULER_ID as a safeguard. + const sid = call.schedulerId ?? ROOT_SCHEDULER_ID; + if (!nextMap[sid]) { + nextMap[sid] = []; + } + nextMap[sid].push(call); + } + return nextMap; + }); }, [], ); @@ -146,7 +197,7 @@ export function useToolExecutionScheduler( toolCalls, schedule, markToolsAsSubmitted, - setToolCalls, + setToolCallsForDisplay, cancelAll, lastToolOutputTime, ]; diff --git a/packages/core/src/confirmation-bus/types.ts b/packages/core/src/confirmation-bus/types.ts index aeecf73b3e..9279485986 100644 --- a/packages/core/src/confirmation-bus/types.ts +++ b/packages/core/src/confirmation-bus/types.ts @@ -26,6 +26,7 @@ export enum MessageBusType { export interface ToolCallsUpdateMessage { type: MessageBusType.TOOL_CALLS_UPDATE; toolCalls: ToolCall[]; + schedulerId: string; } export interface ToolConfirmationRequest { diff --git a/packages/core/src/scheduler/confirmation.test.ts b/packages/core/src/scheduler/confirmation.test.ts index 7162af9d46..9bfdba2184 100644 --- a/packages/core/src/scheduler/confirmation.test.ts +++ b/packages/core/src/scheduler/confirmation.test.ts @@ -29,6 +29,7 @@ import { import type { SchedulerStateManager } from './state-manager.js'; import type { ToolModificationHandler } from './tool-modifier.js'; import type { ValidatingToolCall, WaitingToolCall } from './types.js'; +import { ROOT_SCHEDULER_ID } from './types.js'; import type { Config } from '../config/config.js'; import type { EditorType } from '../utils/editor.js'; import { randomUUID } from 'node:crypto'; @@ -52,7 +53,7 @@ describe('confirmation.ts', () => { }); afterEach(() => { - vi.clearAllMocks(); + vi.restoreAllMocks(); }); const emitResponse = (response: ToolConfirmationResponse) => { @@ -188,6 +189,7 @@ describe('confirmation.ts', () => { state: mockState, modifier: mockModifier, getPreferredEditor, + schedulerId: ROOT_SCHEDULER_ID, }); expect(result.outcome).toBe(ToolConfirmationOutcome.ProceedOnce); @@ -217,6 +219,7 @@ describe('confirmation.ts', () => { state: mockState, modifier: mockModifier, getPreferredEditor, + schedulerId: ROOT_SCHEDULER_ID, }); await listenerPromise; @@ -252,6 +255,7 @@ describe('confirmation.ts', () => { state: mockState, modifier: mockModifier, getPreferredEditor, + schedulerId: ROOT_SCHEDULER_ID, }); await waitForListener(MessageBusType.TOOL_CONFIRMATION_RESPONSE); @@ -293,6 +297,7 @@ describe('confirmation.ts', () => { state: mockState, modifier: mockModifier, getPreferredEditor, + schedulerId: ROOT_SCHEDULER_ID, }); await listenerPromise1; @@ -351,6 +356,7 @@ describe('confirmation.ts', () => { state: mockState, modifier: mockModifier, getPreferredEditor, + schedulerId: ROOT_SCHEDULER_ID, }); await listenerPromise; @@ -397,6 +403,7 @@ describe('confirmation.ts', () => { state: mockState, modifier: mockModifier, getPreferredEditor, + schedulerId: ROOT_SCHEDULER_ID, }); const result = await promise; @@ -420,6 +427,7 @@ describe('confirmation.ts', () => { state: mockState, modifier: mockModifier, getPreferredEditor, + schedulerId: ROOT_SCHEDULER_ID, }), ).rejects.toThrow(/lost during confirmation loop/); }); diff --git a/packages/core/src/scheduler/confirmation.ts b/packages/core/src/scheduler/confirmation.ts index c6aa541508..73958815d0 100644 --- a/packages/core/src/scheduler/confirmation.ts +++ b/packages/core/src/scheduler/confirmation.ts @@ -103,6 +103,7 @@ export async function resolveConfirmation( state: SchedulerStateManager; modifier: ToolModificationHandler; getPreferredEditor: () => EditorType | undefined; + schedulerId: string; }, ): Promise { const { state } = deps; diff --git a/packages/core/src/scheduler/scheduler.test.ts b/packages/core/src/scheduler/scheduler.test.ts index 96340e4d5e..95b6470d1b 100644 --- a/packages/core/src/scheduler/scheduler.test.ts +++ b/packages/core/src/scheduler/scheduler.test.ts @@ -66,6 +66,7 @@ import type { CancelledToolCall, ToolCallResponseInfo, } from './types.js'; +import { ROOT_SCHEDULER_ID } from './types.js'; import { ToolErrorType } from '../tools/tool-error.js'; import * as ToolUtils from '../utils/tool-utils.js'; import type { EditorType } from '../utils/editor.js'; @@ -94,6 +95,8 @@ describe('Scheduler (Orchestrator)', () => { args: { foo: 'bar' }, isClientInitiated: false, prompt_id: 'prompt-1', + schedulerId: ROOT_SCHEDULER_ID, + parentCallId: undefined, }; const req2: ToolCallRequestInfo = { @@ -102,6 +105,8 @@ describe('Scheduler (Orchestrator)', () => { args: { foo: 'baz' }, isClientInitiated: false, prompt_id: 'prompt-1', + schedulerId: ROOT_SCHEDULER_ID, + parentCallId: undefined, }; const mockTool = { @@ -208,6 +213,7 @@ describe('Scheduler (Orchestrator)', () => { config: mockConfig, messageBus: mockMessageBus, getPreferredEditor, + schedulerId: 'root', }); // Reset Tool build behavior @@ -271,6 +277,8 @@ describe('Scheduler (Orchestrator)', () => { request: req1, tool: mockTool, invocation: mockInvocation, + schedulerId: ROOT_SCHEDULER_ID, + startTime: expect.any(Number), }), ]), ); @@ -769,6 +777,7 @@ describe('Scheduler (Orchestrator)', () => { config: mockConfig, messageBus: mockMessageBus, state: mockStateManager, + schedulerId: ROOT_SCHEDULER_ID, }), ); diff --git a/packages/core/src/scheduler/scheduler.ts b/packages/core/src/scheduler/scheduler.ts index b4021faa0b..a8d295b1f9 100644 --- a/packages/core/src/scheduler/scheduler.ts +++ b/packages/core/src/scheduler/scheduler.ts @@ -48,6 +48,8 @@ export interface SchedulerOptions { config: Config; messageBus: MessageBus; getPreferredEditor: () => EditorType | undefined; + schedulerId: string; + parentCallId?: string; } const createErrorResponse = ( @@ -85,6 +87,8 @@ export class Scheduler { private readonly config: Config; private readonly messageBus: MessageBus; private readonly getPreferredEditor: () => EditorType | undefined; + private readonly schedulerId: string; + private readonly parentCallId?: string; private isProcessing = false; private isCancelling = false; @@ -94,7 +98,9 @@ export class Scheduler { this.config = options.config; this.messageBus = options.messageBus; this.getPreferredEditor = options.getPreferredEditor; - this.state = new SchedulerStateManager(this.messageBus); + this.schedulerId = options.schedulerId; + this.parentCallId = options.parentCallId; + this.state = new SchedulerStateManager(this.messageBus, this.schedulerId); this.executor = new ToolExecutor(this.config); this.modifier = new ToolModificationHandler(); @@ -228,16 +234,21 @@ export class Scheduler { try { const toolRegistry = this.config.getToolRegistry(); const newCalls: ToolCall[] = requests.map((request) => { + const enrichedRequest: ToolCallRequestInfo = { + ...request, + schedulerId: this.schedulerId, + parentCallId: this.parentCallId, + }; const tool = toolRegistry.getTool(request.name); if (!tool) { return this._createToolNotFoundErroredToolCall( - request, + enrichedRequest, toolRegistry.getAllToolNames(), ); } - return this._validateAndCreateToolCall(request, tool); + return this._validateAndCreateToolCall(enrichedRequest, tool); }); this.state.enqueue(newCalls); @@ -263,6 +274,7 @@ export class Scheduler { ToolErrorType.TOOL_NOT_REGISTERED, ), durationMs: 0, + schedulerId: this.schedulerId, }; } @@ -278,6 +290,7 @@ export class Scheduler { tool, invocation, startTime: Date.now(), + schedulerId: this.schedulerId, }; } catch (e) { return { @@ -290,6 +303,7 @@ export class Scheduler { ToolErrorType.INVALID_TOOL_PARAMS, ), durationMs: 0, + schedulerId: this.schedulerId, }; } } @@ -411,6 +425,7 @@ export class Scheduler { state: this.state, modifier: this.modifier, getPreferredEditor: this.getPreferredEditor, + schedulerId: this.schedulerId, }); outcome = result.outcome; lastDetails = result.lastDetails; diff --git a/packages/core/src/scheduler/state-manager.ts b/packages/core/src/scheduler/state-manager.ts index dd05556590..519bdb3ee3 100644 --- a/packages/core/src/scheduler/state-manager.ts +++ b/packages/core/src/scheduler/state-manager.ts @@ -17,6 +17,7 @@ import type { ExecutingToolCall, ToolCallResponseInfo, } from './types.js'; +import { ROOT_SCHEDULER_ID } from './types.js'; import type { ToolConfirmationOutcome, ToolResultDisplay, @@ -39,7 +40,10 @@ export class SchedulerStateManager { private readonly queue: ToolCall[] = []; private _completedBatch: CompletedToolCall[] = []; - constructor(private readonly messageBus: MessageBus) {} + constructor( + private readonly messageBus: MessageBus, + private readonly schedulerId: string = ROOT_SCHEDULER_ID, + ) {} addToolCalls(calls: ToolCall[]): void { this.enqueue(calls); @@ -201,6 +205,7 @@ export class SchedulerStateManager { void this.messageBus.publish({ type: MessageBusType.TOOL_CALLS_UPDATE, toolCalls: snapshot, + schedulerId: this.schedulerId, }); } @@ -321,6 +326,7 @@ export class SchedulerStateManager { response, durationMs: startTime ? Date.now() - startTime : undefined, outcome: call.outcome, + schedulerId: call.schedulerId, }; } @@ -336,6 +342,7 @@ export class SchedulerStateManager { response, durationMs: startTime ? Date.now() - startTime : undefined, outcome: call.outcome, + schedulerId: call.schedulerId, }; } @@ -364,6 +371,7 @@ export class SchedulerStateManager { startTime: 'startTime' in call ? call.startTime : undefined, outcome: call.outcome, invocation: call.invocation, + schedulerId: call.schedulerId, }; } @@ -388,6 +396,7 @@ export class SchedulerStateManager { startTime: 'startTime' in call ? call.startTime : undefined, outcome: call.outcome, invocation: call.invocation, + schedulerId: call.schedulerId, }; } @@ -442,6 +451,7 @@ export class SchedulerStateManager { }, durationMs: startTime ? Date.now() - startTime : undefined, outcome: call.outcome, + schedulerId: call.schedulerId, }; } @@ -462,6 +472,7 @@ export class SchedulerStateManager { startTime: 'startTime' in call ? call.startTime : undefined, outcome: call.outcome, invocation: call.invocation, + schedulerId: call.schedulerId, }; } @@ -482,6 +493,7 @@ export class SchedulerStateManager { invocation: call.invocation, liveOutput, pid, + schedulerId: call.schedulerId, }; } } diff --git a/packages/core/src/scheduler/types.ts b/packages/core/src/scheduler/types.ts index 2f2baf77e3..7c0bbe07bd 100644 --- a/packages/core/src/scheduler/types.ts +++ b/packages/core/src/scheduler/types.ts @@ -16,6 +16,8 @@ import type { AnsiOutput } from '../utils/terminalSerializer.js'; import type { ToolErrorType } from '../tools/tool-error.js'; import type { SerializableConfirmationDetails } from '../confirmation-bus/types.js'; +export const ROOT_SCHEDULER_ID = 'root'; + export interface ToolCallRequestInfo { callId: string; name: string; @@ -24,6 +26,8 @@ export interface ToolCallRequestInfo { prompt_id: string; checkpoint?: string; traceId?: string; + parentCallId?: string; + schedulerId?: string; } export interface ToolCallResponseInfo { @@ -43,6 +47,7 @@ export type ValidatingToolCall = { invocation: AnyToolInvocation; startTime?: number; outcome?: ToolConfirmationOutcome; + schedulerId?: string; }; export type ScheduledToolCall = { @@ -52,6 +57,7 @@ export type ScheduledToolCall = { invocation: AnyToolInvocation; startTime?: number; outcome?: ToolConfirmationOutcome; + schedulerId?: string; }; export type ErroredToolCall = { @@ -61,6 +67,7 @@ export type ErroredToolCall = { tool?: AnyDeclarativeTool; durationMs?: number; outcome?: ToolConfirmationOutcome; + schedulerId?: string; }; export type SuccessfulToolCall = { @@ -71,6 +78,7 @@ export type SuccessfulToolCall = { invocation: AnyToolInvocation; durationMs?: number; outcome?: ToolConfirmationOutcome; + schedulerId?: string; }; export type ExecutingToolCall = { @@ -82,6 +90,7 @@ export type ExecutingToolCall = { startTime?: number; outcome?: ToolConfirmationOutcome; pid?: number; + schedulerId?: string; }; export type CancelledToolCall = { @@ -92,6 +101,7 @@ export type CancelledToolCall = { invocation: AnyToolInvocation; durationMs?: number; outcome?: ToolConfirmationOutcome; + schedulerId?: string; }; export type WaitingToolCall = { @@ -113,6 +123,7 @@ export type WaitingToolCall = { correlationId?: string; startTime?: number; outcome?: ToolConfirmationOutcome; + schedulerId?: string; }; export type Status = ToolCall['status'];