mirror of
https://github.com/google-gemini/gemini-cli.git
synced 2026-07-22 15:51:18 -07:00
refactor(core): adopt CoreToolCallStatus enum for type safety (#18998)
This commit is contained in:
@@ -17,7 +17,11 @@ import {
|
||||
type ToolConfirmationPayload,
|
||||
type ToolCallConfirmationDetails,
|
||||
} from '../tools/tools.js';
|
||||
import type { ValidatingToolCall, WaitingToolCall } from './types.js';
|
||||
import {
|
||||
type ValidatingToolCall,
|
||||
type WaitingToolCall,
|
||||
CoreToolCallStatus,
|
||||
} from './types.js';
|
||||
import type { Config } from '../config/config.js';
|
||||
import type { SchedulerStateManager } from './state-manager.js';
|
||||
import type { ToolModificationHandler } from './tool-modifier.js';
|
||||
@@ -145,7 +149,7 @@ export async function resolveConfirmation(
|
||||
const ideConfirmation =
|
||||
'ideConfirmation' in details ? details.ideConfirmation : undefined;
|
||||
|
||||
state.updateStatus(callId, 'awaiting_approval', {
|
||||
state.updateStatus(callId, CoreToolCallStatus.AwaitingApproval, {
|
||||
confirmationDetails: serializableDetails,
|
||||
correlationId,
|
||||
});
|
||||
|
||||
@@ -77,7 +77,7 @@ import type {
|
||||
CompletedToolCall,
|
||||
ToolCallResponseInfo,
|
||||
} from './types.js';
|
||||
import { ROOT_SCHEDULER_ID } from './types.js';
|
||||
import { CoreToolCallStatus, 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';
|
||||
@@ -276,7 +276,7 @@ describe('Scheduler (Orchestrator)', () => {
|
||||
expect(mockStateManager.enqueue).toHaveBeenCalledWith(
|
||||
expect.arrayContaining([
|
||||
expect.objectContaining({
|
||||
status: 'error',
|
||||
status: CoreToolCallStatus.Error,
|
||||
response: expect.objectContaining({
|
||||
errorType: ToolErrorType.TOOL_NOT_REGISTERED,
|
||||
}),
|
||||
@@ -295,7 +295,7 @@ describe('Scheduler (Orchestrator)', () => {
|
||||
expect(mockStateManager.enqueue).toHaveBeenCalledWith(
|
||||
expect.arrayContaining([
|
||||
expect.objectContaining({
|
||||
status: 'error',
|
||||
status: CoreToolCallStatus.Error,
|
||||
response: expect.objectContaining({
|
||||
errorType: ToolErrorType.INVALID_TOOL_PARAMS,
|
||||
}),
|
||||
@@ -310,7 +310,7 @@ describe('Scheduler (Orchestrator)', () => {
|
||||
expect(mockStateManager.enqueue).toHaveBeenCalledWith(
|
||||
expect.arrayContaining([
|
||||
expect.objectContaining({
|
||||
status: 'validating',
|
||||
status: CoreToolCallStatus.Validating,
|
||||
request: req1,
|
||||
tool: mockTool,
|
||||
invocation: mockInvocation,
|
||||
@@ -325,7 +325,7 @@ describe('Scheduler (Orchestrator)', () => {
|
||||
describe('Phase 2: Queue Management', () => {
|
||||
it('should drain the queue if multiple calls are scheduled', async () => {
|
||||
const validatingCall: ValidatingToolCall = {
|
||||
status: 'validating',
|
||||
status: CoreToolCallStatus.Validating,
|
||||
request: req1,
|
||||
tool: mockTool,
|
||||
invocation: mockInvocation as unknown as AnyToolInvocation,
|
||||
@@ -355,7 +355,7 @@ describe('Scheduler (Orchestrator)', () => {
|
||||
|
||||
// Execute is the end of the loop, stub it
|
||||
mockExecutor.execute.mockResolvedValue({
|
||||
status: 'success',
|
||||
status: CoreToolCallStatus.Success,
|
||||
} as unknown as SuccessfulToolCall);
|
||||
|
||||
await scheduler.schedule(req1, signal);
|
||||
@@ -382,14 +382,14 @@ describe('Scheduler (Orchestrator)', () => {
|
||||
});
|
||||
|
||||
const validatingCall1: ValidatingToolCall = {
|
||||
status: 'validating',
|
||||
status: CoreToolCallStatus.Validating,
|
||||
request: req1,
|
||||
tool: mockTool,
|
||||
invocation: mockInvocation as unknown as AnyToolInvocation,
|
||||
};
|
||||
|
||||
const validatingCall2: ValidatingToolCall = {
|
||||
status: 'validating',
|
||||
status: CoreToolCallStatus.Validating,
|
||||
request: req2,
|
||||
tool: mockTool,
|
||||
invocation: mockInvocation as unknown as AnyToolInvocation,
|
||||
@@ -419,7 +419,9 @@ describe('Scheduler (Orchestrator)', () => {
|
||||
// Yield to the event loop deterministically using queueMicrotask
|
||||
await new Promise<void>((resolve) => queueMicrotask(resolve));
|
||||
executionLog.push(`end-${id}`);
|
||||
return { status: 'success' } as unknown as SuccessfulToolCall;
|
||||
return {
|
||||
status: CoreToolCallStatus.Success,
|
||||
} as unknown as SuccessfulToolCall;
|
||||
});
|
||||
|
||||
// Action: Schedule batch of 2 tools
|
||||
@@ -436,14 +438,14 @@ describe('Scheduler (Orchestrator)', () => {
|
||||
|
||||
it('should queue and process multiple schedule() calls made synchronously', async () => {
|
||||
const validatingCall1: ValidatingToolCall = {
|
||||
status: 'validating',
|
||||
status: CoreToolCallStatus.Validating,
|
||||
request: req1,
|
||||
tool: mockTool,
|
||||
invocation: mockInvocation as unknown as AnyToolInvocation,
|
||||
};
|
||||
|
||||
const validatingCall2: ValidatingToolCall = {
|
||||
status: 'validating',
|
||||
status: CoreToolCallStatus.Validating,
|
||||
request: req2, // Second request
|
||||
tool: mockTool,
|
||||
invocation: mockInvocation as unknown as AnyToolInvocation,
|
||||
@@ -483,7 +485,7 @@ describe('Scheduler (Orchestrator)', () => {
|
||||
|
||||
// Executor succeeds instantly
|
||||
mockExecutor.execute.mockResolvedValue({
|
||||
status: 'success',
|
||||
status: CoreToolCallStatus.Success,
|
||||
} as unknown as SuccessfulToolCall);
|
||||
|
||||
// ACT: Call schedule twice synchronously (without awaiting the first)
|
||||
@@ -500,14 +502,14 @@ describe('Scheduler (Orchestrator)', () => {
|
||||
|
||||
it('should queue requests when scheduler is busy (overlapping batches)', async () => {
|
||||
const validatingCall1: ValidatingToolCall = {
|
||||
status: 'validating',
|
||||
status: CoreToolCallStatus.Validating,
|
||||
request: req1,
|
||||
tool: mockTool,
|
||||
invocation: mockInvocation as unknown as AnyToolInvocation,
|
||||
};
|
||||
|
||||
const validatingCall2: ValidatingToolCall = {
|
||||
status: 'validating',
|
||||
status: CoreToolCallStatus.Validating,
|
||||
request: req2, // Second request
|
||||
tool: mockTool,
|
||||
invocation: mockInvocation as unknown as AnyToolInvocation,
|
||||
@@ -554,13 +556,17 @@ describe('Scheduler (Orchestrator)', () => {
|
||||
executionLog.push('start-batch-1');
|
||||
await firstBatchPromise; // Simulating long-running tool execution
|
||||
executionLog.push('end-batch-1');
|
||||
return { status: 'success' } as unknown as SuccessfulToolCall;
|
||||
return {
|
||||
status: CoreToolCallStatus.Success,
|
||||
} as unknown as SuccessfulToolCall;
|
||||
});
|
||||
|
||||
mockExecutor.execute.mockImplementationOnce(async () => {
|
||||
executionLog.push('start-batch-2');
|
||||
executionLog.push('end-batch-2');
|
||||
return { status: 'success' } as unknown as SuccessfulToolCall;
|
||||
return {
|
||||
status: CoreToolCallStatus.Success,
|
||||
} as unknown as SuccessfulToolCall;
|
||||
});
|
||||
|
||||
// 3. ACTIONS
|
||||
@@ -608,7 +614,7 @@ describe('Scheduler (Orchestrator)', () => {
|
||||
|
||||
it('cancelAll() should cancel active call and clear queue', () => {
|
||||
const activeCall: ValidatingToolCall = {
|
||||
status: 'validating',
|
||||
status: CoreToolCallStatus.Validating,
|
||||
request: req1,
|
||||
tool: mockTool,
|
||||
invocation: mockInvocation as unknown as AnyToolInvocation,
|
||||
@@ -623,7 +629,7 @@ describe('Scheduler (Orchestrator)', () => {
|
||||
|
||||
expect(mockStateManager.updateStatus).toHaveBeenCalledWith(
|
||||
'call-1',
|
||||
'cancelled',
|
||||
CoreToolCallStatus.Cancelled,
|
||||
'Operation cancelled by user',
|
||||
);
|
||||
// finalizeCall is handled by the processing loop, not synchronously by cancelAll
|
||||
@@ -656,7 +662,7 @@ describe('Scheduler (Orchestrator)', () => {
|
||||
|
||||
describe('Phase 3: Policy & Confirmation Loop', () => {
|
||||
const validatingCall: ValidatingToolCall = {
|
||||
status: 'validating',
|
||||
status: CoreToolCallStatus.Validating,
|
||||
request: req1,
|
||||
tool: mockTool,
|
||||
invocation: mockInvocation as unknown as AnyToolInvocation,
|
||||
@@ -684,7 +690,7 @@ describe('Scheduler (Orchestrator)', () => {
|
||||
|
||||
expect(mockStateManager.updateStatus).toHaveBeenCalledWith(
|
||||
'call-1',
|
||||
'error',
|
||||
CoreToolCallStatus.Error,
|
||||
expect.objectContaining({
|
||||
errorType: ToolErrorType.POLICY_VIOLATION,
|
||||
}),
|
||||
@@ -706,7 +712,7 @@ describe('Scheduler (Orchestrator)', () => {
|
||||
|
||||
expect(mockStateManager.updateStatus).toHaveBeenCalledWith(
|
||||
'call-1',
|
||||
'error',
|
||||
CoreToolCallStatus.Error,
|
||||
expect.objectContaining({
|
||||
errorType: ToolErrorType.POLICY_VIOLATION,
|
||||
responseParts: expect.arrayContaining([
|
||||
@@ -731,7 +737,7 @@ describe('Scheduler (Orchestrator)', () => {
|
||||
|
||||
expect(mockStateManager.updateStatus).toHaveBeenCalledWith(
|
||||
'call-1',
|
||||
'error',
|
||||
CoreToolCallStatus.Error,
|
||||
expect.objectContaining({
|
||||
errorType: ToolErrorType.UNHANDLED_EXCEPTION,
|
||||
responseParts: expect.arrayContaining([
|
||||
@@ -757,7 +763,7 @@ describe('Scheduler (Orchestrator)', () => {
|
||||
|
||||
expect(mockStateManager.updateStatus).toHaveBeenCalledWith(
|
||||
'call-1',
|
||||
'error',
|
||||
CoreToolCallStatus.Error,
|
||||
expect.objectContaining({
|
||||
errorType: ToolErrorType.POLICY_VIOLATION,
|
||||
responseParts: expect.arrayContaining([
|
||||
@@ -786,7 +792,7 @@ describe('Scheduler (Orchestrator)', () => {
|
||||
|
||||
expect(mockStateManager.updateStatus).toHaveBeenCalledWith(
|
||||
'call-1',
|
||||
'error',
|
||||
CoreToolCallStatus.Error,
|
||||
expect.objectContaining({
|
||||
errorType: ToolErrorType.POLICY_VIOLATION,
|
||||
responseParts: expect.arrayContaining([
|
||||
@@ -810,7 +816,7 @@ describe('Scheduler (Orchestrator)', () => {
|
||||
|
||||
// Provide a mock execute to finish the loop
|
||||
mockExecutor.execute.mockResolvedValue({
|
||||
status: 'success',
|
||||
status: CoreToolCallStatus.Success,
|
||||
} as unknown as SuccessfulToolCall);
|
||||
|
||||
await scheduler.schedule(req1, signal);
|
||||
@@ -827,7 +833,7 @@ describe('Scheduler (Orchestrator)', () => {
|
||||
// Triggered execution
|
||||
expect(mockStateManager.updateStatus).toHaveBeenCalledWith(
|
||||
'call-1',
|
||||
'executing',
|
||||
CoreToolCallStatus.Executing,
|
||||
);
|
||||
expect(mockExecutor.execute).toHaveBeenCalled();
|
||||
});
|
||||
@@ -835,13 +841,13 @@ describe('Scheduler (Orchestrator)', () => {
|
||||
it('should auto-approve remaining identical tools in batch after ProceedAlways', async () => {
|
||||
// Setup: two identical tools
|
||||
const validatingCall1: ValidatingToolCall = {
|
||||
status: 'validating',
|
||||
status: CoreToolCallStatus.Validating,
|
||||
request: req1,
|
||||
tool: mockTool,
|
||||
invocation: mockInvocation as unknown as AnyToolInvocation,
|
||||
};
|
||||
const validatingCall2: ValidatingToolCall = {
|
||||
status: 'validating',
|
||||
status: CoreToolCallStatus.Validating,
|
||||
request: req2,
|
||||
tool: mockTool,
|
||||
invocation: mockInvocation as unknown as AnyToolInvocation,
|
||||
@@ -874,7 +880,7 @@ describe('Scheduler (Orchestrator)', () => {
|
||||
});
|
||||
|
||||
mockExecutor.execute.mockResolvedValue({
|
||||
status: 'success',
|
||||
status: CoreToolCallStatus.Success,
|
||||
} as unknown as SuccessfulToolCall);
|
||||
|
||||
await scheduler.schedule([req1, req2], signal);
|
||||
@@ -904,7 +910,7 @@ describe('Scheduler (Orchestrator)', () => {
|
||||
vi.mocked(resolveConfirmation).mockResolvedValue(resolution);
|
||||
|
||||
mockExecutor.execute.mockResolvedValue({
|
||||
status: 'success',
|
||||
status: CoreToolCallStatus.Success,
|
||||
} as unknown as SuccessfulToolCall);
|
||||
|
||||
await scheduler.schedule(req1, signal);
|
||||
@@ -949,7 +955,7 @@ describe('Scheduler (Orchestrator)', () => {
|
||||
|
||||
expect(mockStateManager.updateStatus).toHaveBeenCalledWith(
|
||||
'call-1',
|
||||
'cancelled',
|
||||
CoreToolCallStatus.Cancelled,
|
||||
'User denied execution.',
|
||||
);
|
||||
expect(mockStateManager.cancelAllQueued).toHaveBeenCalledWith(
|
||||
@@ -979,7 +985,7 @@ describe('Scheduler (Orchestrator)', () => {
|
||||
// Because the signal is aborted, the catch block should convert the error to a cancellation
|
||||
expect(mockStateManager.updateStatus).toHaveBeenCalledWith(
|
||||
'call-1',
|
||||
'cancelled',
|
||||
CoreToolCallStatus.Cancelled,
|
||||
'Operation cancelled',
|
||||
);
|
||||
});
|
||||
@@ -1010,7 +1016,7 @@ describe('Scheduler (Orchestrator)', () => {
|
||||
|
||||
expect(mockStateManager.updateStatus).toHaveBeenCalledWith(
|
||||
'call-1',
|
||||
'cancelled',
|
||||
CoreToolCallStatus.Cancelled,
|
||||
'User denied execution.',
|
||||
);
|
||||
// We assume the state manager stores these details.
|
||||
@@ -1021,7 +1027,7 @@ describe('Scheduler (Orchestrator)', () => {
|
||||
|
||||
describe('Phase 4: Execution Outcomes', () => {
|
||||
const validatingCall: ValidatingToolCall = {
|
||||
status: 'validating',
|
||||
status: CoreToolCallStatus.Validating,
|
||||
request: req1,
|
||||
tool: mockTool,
|
||||
invocation: mockInvocation as unknown as AnyToolInvocation,
|
||||
@@ -1047,7 +1053,7 @@ describe('Scheduler (Orchestrator)', () => {
|
||||
} as unknown as ToolCallResponseInfo;
|
||||
|
||||
mockExecutor.execute.mockResolvedValue({
|
||||
status: 'success',
|
||||
status: CoreToolCallStatus.Success,
|
||||
response: mockResponse,
|
||||
} as unknown as SuccessfulToolCall);
|
||||
|
||||
@@ -1055,14 +1061,14 @@ describe('Scheduler (Orchestrator)', () => {
|
||||
|
||||
expect(mockStateManager.updateStatus).toHaveBeenCalledWith(
|
||||
'call-1',
|
||||
'success',
|
||||
CoreToolCallStatus.Success,
|
||||
mockResponse,
|
||||
);
|
||||
});
|
||||
|
||||
it('should update state to cancelled when executor returns cancelled status', async () => {
|
||||
mockExecutor.execute.mockResolvedValue({
|
||||
status: 'cancelled',
|
||||
status: CoreToolCallStatus.Cancelled,
|
||||
response: { callId: 'call-1', responseParts: [] },
|
||||
} as unknown as CancelledToolCall);
|
||||
|
||||
@@ -1070,7 +1076,7 @@ describe('Scheduler (Orchestrator)', () => {
|
||||
|
||||
expect(mockStateManager.updateStatus).toHaveBeenCalledWith(
|
||||
'call-1',
|
||||
'cancelled',
|
||||
CoreToolCallStatus.Cancelled,
|
||||
'Operation cancelled',
|
||||
);
|
||||
});
|
||||
@@ -1082,7 +1088,7 @@ describe('Scheduler (Orchestrator)', () => {
|
||||
} as unknown as ToolCallResponseInfo;
|
||||
|
||||
mockExecutor.execute.mockResolvedValue({
|
||||
status: 'error',
|
||||
status: CoreToolCallStatus.Error,
|
||||
response: mockResponse,
|
||||
} as unknown as ErroredToolCall);
|
||||
|
||||
@@ -1090,7 +1096,7 @@ describe('Scheduler (Orchestrator)', () => {
|
||||
|
||||
expect(mockStateManager.updateStatus).toHaveBeenCalledWith(
|
||||
'call-1',
|
||||
'error',
|
||||
CoreToolCallStatus.Error,
|
||||
mockResponse,
|
||||
);
|
||||
});
|
||||
@@ -1103,14 +1109,14 @@ describe('Scheduler (Orchestrator)', () => {
|
||||
|
||||
// Mock the execution so the state advances
|
||||
mockExecutor.execute.mockResolvedValue({
|
||||
status: 'success',
|
||||
status: CoreToolCallStatus.Success,
|
||||
response: mockResponse,
|
||||
} as unknown as SuccessfulToolCall);
|
||||
|
||||
// Mock the state manager to return a SUCCESS state when getToolCall is
|
||||
// called
|
||||
const successfulCall: SuccessfulToolCall = {
|
||||
status: 'success',
|
||||
status: CoreToolCallStatus.Success,
|
||||
request: req1,
|
||||
response: mockResponse,
|
||||
tool: mockTool,
|
||||
@@ -1145,7 +1151,7 @@ describe('Scheduler (Orchestrator)', () => {
|
||||
};
|
||||
|
||||
mockExecutor.execute.mockResolvedValue({
|
||||
status: 'success',
|
||||
status: CoreToolCallStatus.Success,
|
||||
response,
|
||||
} as unknown as SuccessfulToolCall);
|
||||
|
||||
@@ -1172,7 +1178,7 @@ describe('Scheduler (Orchestrator)', () => {
|
||||
});
|
||||
|
||||
const validatingCall: ValidatingToolCall = {
|
||||
status: 'validating',
|
||||
status: CoreToolCallStatus.Validating,
|
||||
request: req1,
|
||||
tool: mockTool,
|
||||
invocation: mockInvocation as unknown as AnyToolInvocation,
|
||||
@@ -1200,7 +1206,7 @@ describe('Scheduler (Orchestrator)', () => {
|
||||
mockExecutor.execute.mockImplementation(async () => {
|
||||
capturedContext = getToolCallContext();
|
||||
return {
|
||||
status: 'success',
|
||||
status: CoreToolCallStatus.Success,
|
||||
request: req1,
|
||||
tool: mockTool,
|
||||
invocation: mockInvocation as unknown as AnyToolInvocation,
|
||||
|
||||
@@ -19,6 +19,7 @@ import {
|
||||
type ExecutingToolCall,
|
||||
type ValidatingToolCall,
|
||||
type ErroredToolCall,
|
||||
CoreToolCallStatus,
|
||||
} from './types.js';
|
||||
import { ToolErrorType } from '../tools/tool-error.js';
|
||||
import { PolicyDecision } from '../policy/types.js';
|
||||
@@ -212,7 +213,7 @@ export class Scheduler {
|
||||
if (activeCall && !this.isTerminal(activeCall.status)) {
|
||||
this.state.updateStatus(
|
||||
activeCall.request.callId,
|
||||
'cancelled',
|
||||
CoreToolCallStatus.Cancelled,
|
||||
'Operation cancelled by user',
|
||||
);
|
||||
}
|
||||
@@ -226,7 +227,11 @@ export class Scheduler {
|
||||
}
|
||||
|
||||
private isTerminal(status: string) {
|
||||
return status === 'success' || status === 'error' || status === 'cancelled';
|
||||
return (
|
||||
status === CoreToolCallStatus.Success ||
|
||||
status === CoreToolCallStatus.Error ||
|
||||
status === CoreToolCallStatus.Cancelled
|
||||
);
|
||||
}
|
||||
|
||||
// --- Phase 1: Ingestion & Resolution ---
|
||||
@@ -250,10 +255,12 @@ export class Scheduler {
|
||||
const tool = toolRegistry.getTool(request.name);
|
||||
|
||||
if (!tool) {
|
||||
return this._createToolNotFoundErroredToolCall(
|
||||
enrichedRequest,
|
||||
toolRegistry.getAllToolNames(),
|
||||
);
|
||||
return {
|
||||
...this._createToolNotFoundErroredToolCall(
|
||||
enrichedRequest,
|
||||
toolRegistry.getAllToolNames(),
|
||||
),
|
||||
};
|
||||
}
|
||||
|
||||
return this._validateAndCreateToolCall(enrichedRequest, tool);
|
||||
@@ -275,7 +282,7 @@ export class Scheduler {
|
||||
): ErroredToolCall {
|
||||
const suggestion = getToolSuggestion(request.name, toolNames);
|
||||
return {
|
||||
status: 'error',
|
||||
status: CoreToolCallStatus.Error,
|
||||
request,
|
||||
response: createErrorResponse(
|
||||
request,
|
||||
@@ -301,7 +308,7 @@ export class Scheduler {
|
||||
try {
|
||||
const invocation = tool.build(request.args);
|
||||
return {
|
||||
status: 'validating',
|
||||
status: CoreToolCallStatus.Validating,
|
||||
request,
|
||||
tool,
|
||||
invocation,
|
||||
@@ -310,7 +317,7 @@ export class Scheduler {
|
||||
};
|
||||
} catch (e) {
|
||||
return {
|
||||
status: 'error',
|
||||
status: CoreToolCallStatus.Error,
|
||||
request,
|
||||
tool,
|
||||
response: createErrorResponse(
|
||||
@@ -349,8 +356,12 @@ export class Scheduler {
|
||||
const next = this.state.dequeue();
|
||||
if (!next) return false;
|
||||
|
||||
if (next.status === 'error') {
|
||||
this.state.updateStatus(next.request.callId, 'error', next.response);
|
||||
if (next.status === CoreToolCallStatus.Error) {
|
||||
this.state.updateStatus(
|
||||
next.request.callId,
|
||||
CoreToolCallStatus.Error,
|
||||
next.response,
|
||||
);
|
||||
this.state.finalizeCall(next.request.callId);
|
||||
return true;
|
||||
}
|
||||
@@ -359,7 +370,7 @@ export class Scheduler {
|
||||
const active = this.state.firstActiveCall;
|
||||
if (!active) return false;
|
||||
|
||||
if (active.status === 'validating') {
|
||||
if (active.status === CoreToolCallStatus.Validating) {
|
||||
await this._processValidatingCall(active, signal);
|
||||
}
|
||||
|
||||
@@ -379,13 +390,13 @@ export class Scheduler {
|
||||
if (signal.aborted || err.name === 'AbortError') {
|
||||
this.state.updateStatus(
|
||||
active.request.callId,
|
||||
'cancelled',
|
||||
CoreToolCallStatus.Cancelled,
|
||||
'Operation cancelled',
|
||||
);
|
||||
} else {
|
||||
this.state.updateStatus(
|
||||
active.request.callId,
|
||||
'error',
|
||||
CoreToolCallStatus.Error,
|
||||
createErrorResponse(
|
||||
active.request,
|
||||
err,
|
||||
@@ -417,7 +428,7 @@ export class Scheduler {
|
||||
|
||||
this.state.updateStatus(
|
||||
callId,
|
||||
'error',
|
||||
CoreToolCallStatus.Error,
|
||||
createErrorResponse(
|
||||
toolCall.request,
|
||||
new Error(errorMessage),
|
||||
@@ -456,7 +467,11 @@ export class Scheduler {
|
||||
|
||||
// Handle cancellation (cascades to entire batch)
|
||||
if (outcome === ToolConfirmationOutcome.Cancel) {
|
||||
this.state.updateStatus(callId, 'cancelled', 'User denied execution.');
|
||||
this.state.updateStatus(
|
||||
callId,
|
||||
CoreToolCallStatus.Cancelled,
|
||||
'User denied execution.',
|
||||
);
|
||||
this.state.finalizeCall(callId);
|
||||
this.state.cancelAllQueued('User cancelled operation');
|
||||
return; // Skip execution
|
||||
@@ -472,9 +487,9 @@ export class Scheduler {
|
||||
* Executes the tool and records the result.
|
||||
*/
|
||||
private async _execute(callId: string, signal: AbortSignal): Promise<void> {
|
||||
this.state.updateStatus(callId, 'scheduled');
|
||||
this.state.updateStatus(callId, CoreToolCallStatus.Scheduled);
|
||||
if (signal.aborted) throw new Error('Operation cancelled');
|
||||
this.state.updateStatus(callId, 'executing');
|
||||
this.state.updateStatus(callId, CoreToolCallStatus.Executing);
|
||||
|
||||
// eslint-disable-next-line @typescript-eslint/no-unsafe-type-assertion
|
||||
const activeCall = this.state.firstActiveCall as ExecutingToolCall;
|
||||
@@ -490,10 +505,15 @@ export class Scheduler {
|
||||
call: activeCall,
|
||||
signal,
|
||||
outputUpdateHandler: (id, out) =>
|
||||
this.state.updateStatus(id, 'executing', { liveOutput: out }),
|
||||
this.state.updateStatus(id, CoreToolCallStatus.Executing, {
|
||||
liveOutput: out,
|
||||
}),
|
||||
onUpdateToolCall: (updated) => {
|
||||
if (updated.status === 'executing' && updated.pid) {
|
||||
this.state.updateStatus(callId, 'executing', {
|
||||
if (
|
||||
updated.status === CoreToolCallStatus.Executing &&
|
||||
updated.pid
|
||||
) {
|
||||
this.state.updateStatus(callId, CoreToolCallStatus.Executing, {
|
||||
pid: updated.pid,
|
||||
});
|
||||
}
|
||||
@@ -501,12 +521,24 @@ export class Scheduler {
|
||||
}),
|
||||
);
|
||||
|
||||
if (result.status === 'success') {
|
||||
this.state.updateStatus(callId, 'success', result.response);
|
||||
} else if (result.status === 'cancelled') {
|
||||
this.state.updateStatus(callId, 'cancelled', 'Operation cancelled');
|
||||
if (result.status === CoreToolCallStatus.Success) {
|
||||
this.state.updateStatus(
|
||||
callId,
|
||||
CoreToolCallStatus.Success,
|
||||
result.response,
|
||||
);
|
||||
} else if (result.status === CoreToolCallStatus.Cancelled) {
|
||||
this.state.updateStatus(
|
||||
callId,
|
||||
CoreToolCallStatus.Cancelled,
|
||||
'Operation cancelled',
|
||||
);
|
||||
} else {
|
||||
this.state.updateStatus(callId, 'error', result.response);
|
||||
this.state.updateStatus(
|
||||
callId,
|
||||
CoreToolCallStatus.Error,
|
||||
result.response,
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -16,6 +16,7 @@ import type {
|
||||
ToolCallRequestInfo,
|
||||
ToolCallResponseInfo,
|
||||
} from './types.js';
|
||||
import { CoreToolCallStatus, ROOT_SCHEDULER_ID } from './types.js';
|
||||
import {
|
||||
ToolConfirmationOutcome,
|
||||
type AnyDeclarativeTool,
|
||||
@@ -23,7 +24,6 @@ import {
|
||||
} from '../tools/tools.js';
|
||||
import { MessageBusType } from '../confirmation-bus/types.js';
|
||||
import type { MessageBus } from '../confirmation-bus/message-bus.js';
|
||||
import { ROOT_SCHEDULER_ID } from './types.js';
|
||||
|
||||
describe('SchedulerStateManager', () => {
|
||||
const mockRequest: ToolCallRequestInfo = {
|
||||
@@ -44,7 +44,7 @@ describe('SchedulerStateManager', () => {
|
||||
} as unknown as AnyToolInvocation;
|
||||
|
||||
const createValidatingCall = (id = 'call-1'): ValidatingToolCall => ({
|
||||
status: 'validating',
|
||||
status: CoreToolCallStatus.Validating,
|
||||
request: { ...mockRequest, callId: id },
|
||||
tool: mockTool,
|
||||
invocation: mockInvocation,
|
||||
@@ -97,7 +97,7 @@ describe('SchedulerStateManager', () => {
|
||||
manager.dequeue();
|
||||
manager.updateStatus(
|
||||
call.request.callId,
|
||||
'success',
|
||||
CoreToolCallStatus.Success,
|
||||
createMockResponse(call.request.callId),
|
||||
);
|
||||
manager.finalizeCall(call.request.callId);
|
||||
@@ -105,7 +105,7 @@ describe('SchedulerStateManager', () => {
|
||||
expect(onTerminalCall).toHaveBeenCalledTimes(1);
|
||||
expect(onTerminalCall).toHaveBeenCalledWith(
|
||||
expect.objectContaining({
|
||||
status: 'success',
|
||||
status: CoreToolCallStatus.Success,
|
||||
request: expect.objectContaining({ callId: call.request.callId }),
|
||||
}),
|
||||
);
|
||||
@@ -125,13 +125,13 @@ describe('SchedulerStateManager', () => {
|
||||
expect(onTerminalCall).toHaveBeenCalledTimes(2);
|
||||
expect(onTerminalCall).toHaveBeenCalledWith(
|
||||
expect.objectContaining({
|
||||
status: 'cancelled',
|
||||
status: CoreToolCallStatus.Cancelled,
|
||||
request: expect.objectContaining({ callId: '1' }),
|
||||
}),
|
||||
);
|
||||
expect(onTerminalCall).toHaveBeenCalledWith(
|
||||
expect.objectContaining({
|
||||
status: 'cancelled',
|
||||
status: CoreToolCallStatus.Cancelled,
|
||||
request: expect.objectContaining({ callId: '2' }),
|
||||
}),
|
||||
);
|
||||
@@ -167,7 +167,7 @@ describe('SchedulerStateManager', () => {
|
||||
stateManager.dequeue();
|
||||
stateManager.updateStatus(
|
||||
'completed-1',
|
||||
'success',
|
||||
CoreToolCallStatus.Success,
|
||||
createMockResponse('completed-1'),
|
||||
);
|
||||
stateManager.finalizeCall('completed-1');
|
||||
@@ -212,10 +212,13 @@ describe('SchedulerStateManager', () => {
|
||||
stateManager.enqueue([call]);
|
||||
stateManager.dequeue();
|
||||
|
||||
stateManager.updateStatus(call.request.callId, 'scheduled');
|
||||
stateManager.updateStatus(
|
||||
call.request.callId,
|
||||
CoreToolCallStatus.Scheduled,
|
||||
);
|
||||
|
||||
const snapshot = stateManager.getSnapshot();
|
||||
expect(snapshot[0].status).toBe('scheduled');
|
||||
expect(snapshot[0].status).toBe(CoreToolCallStatus.Scheduled);
|
||||
expect(snapshot[0].request.callId).toBe(call.request.callId);
|
||||
});
|
||||
|
||||
@@ -223,11 +226,19 @@ describe('SchedulerStateManager', () => {
|
||||
const call = createValidatingCall();
|
||||
stateManager.enqueue([call]);
|
||||
stateManager.dequeue();
|
||||
stateManager.updateStatus(call.request.callId, 'scheduled');
|
||||
stateManager.updateStatus(
|
||||
call.request.callId,
|
||||
CoreToolCallStatus.Scheduled,
|
||||
);
|
||||
|
||||
stateManager.updateStatus(call.request.callId, 'executing');
|
||||
stateManager.updateStatus(
|
||||
call.request.callId,
|
||||
CoreToolCallStatus.Executing,
|
||||
);
|
||||
|
||||
expect(stateManager.firstActiveCall?.status).toBe('executing');
|
||||
expect(stateManager.firstActiveCall?.status).toBe(
|
||||
CoreToolCallStatus.Executing,
|
||||
);
|
||||
});
|
||||
|
||||
it('should transition to success and move to completed batch', () => {
|
||||
@@ -244,7 +255,11 @@ describe('SchedulerStateManager', () => {
|
||||
};
|
||||
|
||||
vi.mocked(onUpdate).mockClear();
|
||||
stateManager.updateStatus(call.request.callId, 'success', response);
|
||||
stateManager.updateStatus(
|
||||
call.request.callId,
|
||||
CoreToolCallStatus.Success,
|
||||
response,
|
||||
);
|
||||
expect(onUpdate).toHaveBeenCalledTimes(1);
|
||||
|
||||
vi.mocked(onUpdate).mockClear();
|
||||
@@ -254,7 +269,7 @@ describe('SchedulerStateManager', () => {
|
||||
expect(stateManager.isActive).toBe(false);
|
||||
expect(stateManager.completedBatch).toHaveLength(1);
|
||||
const completed = stateManager.completedBatch[0] as SuccessfulToolCall;
|
||||
expect(completed.status).toBe('success');
|
||||
expect(completed.status).toBe(CoreToolCallStatus.Success);
|
||||
expect(completed.response).toEqual(response);
|
||||
expect(completed.durationMs).toBeDefined();
|
||||
});
|
||||
@@ -272,13 +287,17 @@ describe('SchedulerStateManager', () => {
|
||||
errorType: undefined,
|
||||
};
|
||||
|
||||
stateManager.updateStatus(call.request.callId, 'error', response);
|
||||
stateManager.updateStatus(
|
||||
call.request.callId,
|
||||
CoreToolCallStatus.Error,
|
||||
response,
|
||||
);
|
||||
stateManager.finalizeCall(call.request.callId);
|
||||
|
||||
expect(stateManager.isActive).toBe(false);
|
||||
expect(stateManager.completedBatch).toHaveLength(1);
|
||||
const completed = stateManager.completedBatch[0] as ErroredToolCall;
|
||||
expect(completed.status).toBe('error');
|
||||
expect(completed.status).toBe(CoreToolCallStatus.Error);
|
||||
expect(completed.response).toEqual(response);
|
||||
});
|
||||
|
||||
@@ -296,12 +315,12 @@ describe('SchedulerStateManager', () => {
|
||||
|
||||
stateManager.updateStatus(
|
||||
call.request.callId,
|
||||
'awaiting_approval',
|
||||
CoreToolCallStatus.AwaitingApproval,
|
||||
details,
|
||||
);
|
||||
|
||||
const active = stateManager.firstActiveCall as WaitingToolCall;
|
||||
expect(active.status).toBe('awaiting_approval');
|
||||
expect(active.status).toBe(CoreToolCallStatus.AwaitingApproval);
|
||||
expect(active.confirmationDetails).toEqual(details);
|
||||
});
|
||||
|
||||
@@ -322,12 +341,12 @@ describe('SchedulerStateManager', () => {
|
||||
|
||||
stateManager.updateStatus(
|
||||
call.request.callId,
|
||||
'awaiting_approval',
|
||||
CoreToolCallStatus.AwaitingApproval,
|
||||
eventDrivenData,
|
||||
);
|
||||
|
||||
const active = stateManager.firstActiveCall as WaitingToolCall;
|
||||
expect(active.status).toBe('awaiting_approval');
|
||||
expect(active.status).toBe(CoreToolCallStatus.AwaitingApproval);
|
||||
expect(active.correlationId).toBe('corr-123');
|
||||
expect(active.confirmationDetails).toEqual(details);
|
||||
});
|
||||
@@ -350,18 +369,18 @@ describe('SchedulerStateManager', () => {
|
||||
|
||||
stateManager.updateStatus(
|
||||
call.request.callId,
|
||||
'awaiting_approval',
|
||||
CoreToolCallStatus.AwaitingApproval,
|
||||
details,
|
||||
);
|
||||
stateManager.updateStatus(
|
||||
call.request.callId,
|
||||
'cancelled',
|
||||
CoreToolCallStatus.Cancelled,
|
||||
'User said no',
|
||||
);
|
||||
stateManager.finalizeCall(call.request.callId);
|
||||
|
||||
const completed = stateManager.completedBatch[0] as CancelledToolCall;
|
||||
expect(completed.status).toBe('cancelled');
|
||||
expect(completed.status).toBe(CoreToolCallStatus.Cancelled);
|
||||
expect(completed.response.resultDisplay).toEqual({
|
||||
fileDiff: 'diff',
|
||||
fileName: 'test.txt',
|
||||
@@ -372,7 +391,7 @@ describe('SchedulerStateManager', () => {
|
||||
});
|
||||
|
||||
it('should ignore status updates for non-existent callIds', () => {
|
||||
stateManager.updateStatus('unknown', 'scheduled');
|
||||
stateManager.updateStatus('unknown', CoreToolCallStatus.Scheduled);
|
||||
expect(onUpdate).not.toHaveBeenCalled();
|
||||
});
|
||||
|
||||
@@ -382,13 +401,16 @@ describe('SchedulerStateManager', () => {
|
||||
stateManager.dequeue();
|
||||
stateManager.updateStatus(
|
||||
call.request.callId,
|
||||
'success',
|
||||
CoreToolCallStatus.Success,
|
||||
createMockResponse(call.request.callId),
|
||||
);
|
||||
stateManager.finalizeCall(call.request.callId);
|
||||
|
||||
vi.mocked(onUpdate).mockClear();
|
||||
stateManager.updateStatus(call.request.callId, 'scheduled');
|
||||
stateManager.updateStatus(
|
||||
call.request.callId,
|
||||
CoreToolCallStatus.Scheduled,
|
||||
);
|
||||
expect(onUpdate).not.toHaveBeenCalled();
|
||||
});
|
||||
|
||||
@@ -397,7 +419,10 @@ describe('SchedulerStateManager', () => {
|
||||
stateManager.enqueue([call]);
|
||||
stateManager.dequeue();
|
||||
|
||||
stateManager.updateStatus(call.request.callId, 'executing');
|
||||
stateManager.updateStatus(
|
||||
call.request.callId,
|
||||
CoreToolCallStatus.Executing,
|
||||
);
|
||||
stateManager.finalizeCall(call.request.callId);
|
||||
|
||||
expect(stateManager.isActive).toBe(true);
|
||||
@@ -405,7 +430,7 @@ describe('SchedulerStateManager', () => {
|
||||
|
||||
stateManager.updateStatus(
|
||||
call.request.callId,
|
||||
'success',
|
||||
CoreToolCallStatus.Success,
|
||||
createMockResponse(call.request.callId),
|
||||
);
|
||||
stateManager.finalizeCall(call.request.callId);
|
||||
@@ -420,30 +445,45 @@ describe('SchedulerStateManager', () => {
|
||||
stateManager.dequeue();
|
||||
|
||||
// Start executing
|
||||
stateManager.updateStatus(call.request.callId, 'executing');
|
||||
stateManager.updateStatus(
|
||||
call.request.callId,
|
||||
CoreToolCallStatus.Executing,
|
||||
);
|
||||
let active = stateManager.firstActiveCall as ExecutingToolCall;
|
||||
expect(active.status).toBe('executing');
|
||||
expect(active.status).toBe(CoreToolCallStatus.Executing);
|
||||
expect(active.liveOutput).toBeUndefined();
|
||||
|
||||
// Update with live output
|
||||
stateManager.updateStatus(call.request.callId, 'executing', {
|
||||
liveOutput: 'chunk 1',
|
||||
});
|
||||
stateManager.updateStatus(
|
||||
call.request.callId,
|
||||
CoreToolCallStatus.Executing,
|
||||
{
|
||||
liveOutput: 'chunk 1',
|
||||
},
|
||||
);
|
||||
active = stateManager.firstActiveCall as ExecutingToolCall;
|
||||
expect(active.liveOutput).toBe('chunk 1');
|
||||
|
||||
// Update with pid (should preserve liveOutput)
|
||||
stateManager.updateStatus(call.request.callId, 'executing', {
|
||||
pid: 1234,
|
||||
});
|
||||
stateManager.updateStatus(
|
||||
call.request.callId,
|
||||
CoreToolCallStatus.Executing,
|
||||
{
|
||||
pid: 1234,
|
||||
},
|
||||
);
|
||||
active = stateManager.firstActiveCall as ExecutingToolCall;
|
||||
expect(active.liveOutput).toBe('chunk 1');
|
||||
expect(active.pid).toBe(1234);
|
||||
|
||||
// Update live output again (should preserve pid)
|
||||
stateManager.updateStatus(call.request.callId, 'executing', {
|
||||
liveOutput: 'chunk 2',
|
||||
});
|
||||
stateManager.updateStatus(
|
||||
call.request.callId,
|
||||
CoreToolCallStatus.Executing,
|
||||
{
|
||||
liveOutput: 'chunk 2',
|
||||
},
|
||||
);
|
||||
active = stateManager.firstActiveCall as ExecutingToolCall;
|
||||
expect(active.liveOutput).toBe('chunk 2');
|
||||
expect(active.pid).toBe(1234);
|
||||
@@ -475,7 +515,7 @@ describe('SchedulerStateManager', () => {
|
||||
stateManager.dequeue();
|
||||
stateManager.updateStatus(
|
||||
call.request.callId,
|
||||
'error',
|
||||
CoreToolCallStatus.Error,
|
||||
createMockResponse(call.request.callId),
|
||||
);
|
||||
stateManager.finalizeCall(call.request.callId);
|
||||
@@ -521,7 +561,9 @@ describe('SchedulerStateManager', () => {
|
||||
expect(stateManager.queueLength).toBe(0);
|
||||
expect(stateManager.completedBatch).toHaveLength(2);
|
||||
expect(
|
||||
stateManager.completedBatch.every((c) => c.status === 'cancelled'),
|
||||
stateManager.completedBatch.every(
|
||||
(c) => c.status === CoreToolCallStatus.Cancelled,
|
||||
),
|
||||
).toBe(true);
|
||||
expect(onUpdate).toHaveBeenCalledTimes(1);
|
||||
});
|
||||
@@ -538,7 +580,7 @@ describe('SchedulerStateManager', () => {
|
||||
stateManager.dequeue();
|
||||
stateManager.updateStatus(
|
||||
call.request.callId,
|
||||
'success',
|
||||
CoreToolCallStatus.Success,
|
||||
createMockResponse(call.request.callId),
|
||||
);
|
||||
stateManager.finalizeCall(call.request.callId);
|
||||
@@ -555,7 +597,7 @@ describe('SchedulerStateManager', () => {
|
||||
stateManager.dequeue();
|
||||
stateManager.updateStatus(
|
||||
call.request.callId,
|
||||
'success',
|
||||
CoreToolCallStatus.Success,
|
||||
createMockResponse(call.request.callId),
|
||||
);
|
||||
stateManager.finalizeCall(call.request.callId);
|
||||
@@ -578,7 +620,11 @@ describe('SchedulerStateManager', () => {
|
||||
const call1 = createValidatingCall('1');
|
||||
stateManager.enqueue([call1]);
|
||||
stateManager.dequeue();
|
||||
stateManager.updateStatus('1', 'success', createMockResponse('1'));
|
||||
stateManager.updateStatus(
|
||||
'1',
|
||||
CoreToolCallStatus.Success,
|
||||
createMockResponse('1'),
|
||||
);
|
||||
stateManager.finalizeCall('1');
|
||||
|
||||
// 2. Active
|
||||
|
||||
@@ -17,6 +17,7 @@ import type {
|
||||
ExecutingToolCall,
|
||||
ToolCallResponseInfo,
|
||||
} from './types.js';
|
||||
import { CoreToolCallStatus } from './types.js';
|
||||
import { ROOT_SCHEDULER_ID } from './types.js';
|
||||
import type {
|
||||
ToolConfirmationOutcome,
|
||||
@@ -98,17 +99,17 @@ export class SchedulerStateManager {
|
||||
*/
|
||||
updateStatus(
|
||||
callId: string,
|
||||
status: 'success',
|
||||
status: CoreToolCallStatus.Success,
|
||||
data: ToolCallResponseInfo,
|
||||
): void;
|
||||
updateStatus(
|
||||
callId: string,
|
||||
status: 'error',
|
||||
status: CoreToolCallStatus.Error,
|
||||
data: ToolCallResponseInfo,
|
||||
): void;
|
||||
updateStatus(
|
||||
callId: string,
|
||||
status: 'awaiting_approval',
|
||||
status: CoreToolCallStatus.AwaitingApproval,
|
||||
data:
|
||||
| ToolCallConfirmationDetails
|
||||
| {
|
||||
@@ -116,13 +117,20 @@ export class SchedulerStateManager {
|
||||
confirmationDetails: SerializableConfirmationDetails;
|
||||
},
|
||||
): void;
|
||||
updateStatus(callId: string, status: 'cancelled', data: string): void;
|
||||
updateStatus(
|
||||
callId: string,
|
||||
status: 'executing',
|
||||
status: CoreToolCallStatus.Cancelled,
|
||||
data: string,
|
||||
): void;
|
||||
updateStatus(
|
||||
callId: string,
|
||||
status: CoreToolCallStatus.Executing,
|
||||
data?: Partial<ExecutingToolCall>,
|
||||
): void;
|
||||
updateStatus(callId: string, status: 'scheduled' | 'validating'): void;
|
||||
updateStatus(
|
||||
callId: string,
|
||||
status: CoreToolCallStatus.Scheduled | CoreToolCallStatus.Validating,
|
||||
): void;
|
||||
updateStatus(callId: string, status: Status, auxiliaryData?: unknown): void {
|
||||
const call = this.activeCalls.get(callId);
|
||||
if (!call) return;
|
||||
@@ -152,7 +160,7 @@ export class SchedulerStateManager {
|
||||
newInvocation: AnyToolInvocation,
|
||||
): void {
|
||||
const call = this.activeCalls.get(callId);
|
||||
if (!call || call.status === 'error') return;
|
||||
if (!call || call.status === CoreToolCallStatus.Error) return;
|
||||
|
||||
this.activeCalls.set(
|
||||
callId,
|
||||
@@ -179,7 +187,7 @@ export class SchedulerStateManager {
|
||||
|
||||
while (this.queue.length > 0) {
|
||||
const queuedCall = this.queue.shift()!;
|
||||
if (queuedCall.status === 'error') {
|
||||
if (queuedCall.status === CoreToolCallStatus.Error) {
|
||||
this._completedBatch.push(queuedCall);
|
||||
this.onTerminalCall?.(queuedCall);
|
||||
continue;
|
||||
@@ -222,7 +230,11 @@ export class SchedulerStateManager {
|
||||
|
||||
private isTerminalCall(call: ToolCall): call is CompletedToolCall {
|
||||
const { status } = call;
|
||||
return status === 'success' || status === 'error' || status === 'cancelled';
|
||||
return (
|
||||
status === CoreToolCallStatus.Success ||
|
||||
status === CoreToolCallStatus.Error ||
|
||||
status === CoreToolCallStatus.Cancelled
|
||||
);
|
||||
}
|
||||
|
||||
private transitionCall(
|
||||
@@ -231,7 +243,7 @@ export class SchedulerStateManager {
|
||||
auxiliaryData?: unknown,
|
||||
): ToolCall {
|
||||
switch (newStatus) {
|
||||
case 'success': {
|
||||
case CoreToolCallStatus.Success: {
|
||||
if (!this.isToolCallResponseInfo(auxiliaryData)) {
|
||||
throw new Error(
|
||||
`Invalid data for 'success' transition (callId: ${call.request.callId})`,
|
||||
@@ -239,7 +251,7 @@ export class SchedulerStateManager {
|
||||
}
|
||||
return this.toSuccess(call, auxiliaryData);
|
||||
}
|
||||
case 'error': {
|
||||
case CoreToolCallStatus.Error: {
|
||||
if (!this.isToolCallResponseInfo(auxiliaryData)) {
|
||||
throw new Error(
|
||||
`Invalid data for 'error' transition (callId: ${call.request.callId})`,
|
||||
@@ -247,7 +259,7 @@ export class SchedulerStateManager {
|
||||
}
|
||||
return this.toError(call, auxiliaryData);
|
||||
}
|
||||
case 'awaiting_approval': {
|
||||
case CoreToolCallStatus.AwaitingApproval: {
|
||||
if (!auxiliaryData) {
|
||||
throw new Error(
|
||||
`Missing data for 'awaiting_approval' transition (callId: ${call.request.callId})`,
|
||||
@@ -255,9 +267,9 @@ export class SchedulerStateManager {
|
||||
}
|
||||
return this.toAwaitingApproval(call, auxiliaryData);
|
||||
}
|
||||
case 'scheduled':
|
||||
case CoreToolCallStatus.Scheduled:
|
||||
return this.toScheduled(call);
|
||||
case 'cancelled': {
|
||||
case CoreToolCallStatus.Cancelled: {
|
||||
if (typeof auxiliaryData !== 'string') {
|
||||
throw new Error(
|
||||
`Invalid reason (string) for 'cancelled' transition (callId: ${call.request.callId})`,
|
||||
@@ -265,9 +277,9 @@ export class SchedulerStateManager {
|
||||
}
|
||||
return this.toCancelled(call, auxiliaryData);
|
||||
}
|
||||
case 'validating':
|
||||
case CoreToolCallStatus.Validating:
|
||||
return this.toValidating(call);
|
||||
case 'executing': {
|
||||
case CoreToolCallStatus.Executing: {
|
||||
if (
|
||||
auxiliaryData !== undefined &&
|
||||
!this.isExecutingToolCallPatch(auxiliaryData)
|
||||
@@ -327,13 +339,13 @@ export class SchedulerStateManager {
|
||||
call: ToolCall,
|
||||
response: ToolCallResponseInfo,
|
||||
): SuccessfulToolCall {
|
||||
this.validateHasToolAndInvocation(call, 'success');
|
||||
this.validateHasToolAndInvocation(call, CoreToolCallStatus.Success);
|
||||
const startTime = 'startTime' in call ? call.startTime : undefined;
|
||||
return {
|
||||
request: call.request,
|
||||
tool: call.tool,
|
||||
invocation: call.invocation,
|
||||
status: 'success',
|
||||
status: CoreToolCallStatus.Success,
|
||||
response,
|
||||
durationMs: startTime ? Date.now() - startTime : undefined,
|
||||
outcome: call.outcome,
|
||||
@@ -348,7 +360,7 @@ export class SchedulerStateManager {
|
||||
const startTime = 'startTime' in call ? call.startTime : undefined;
|
||||
return {
|
||||
request: call.request,
|
||||
status: 'error',
|
||||
status: CoreToolCallStatus.Error,
|
||||
tool: 'tool' in call ? call.tool : undefined,
|
||||
response,
|
||||
durationMs: startTime ? Date.now() - startTime : undefined,
|
||||
@@ -358,7 +370,10 @@ export class SchedulerStateManager {
|
||||
}
|
||||
|
||||
private toAwaitingApproval(call: ToolCall, data: unknown): WaitingToolCall {
|
||||
this.validateHasToolAndInvocation(call, 'awaiting_approval');
|
||||
this.validateHasToolAndInvocation(
|
||||
call,
|
||||
CoreToolCallStatus.AwaitingApproval,
|
||||
);
|
||||
|
||||
let confirmationDetails:
|
||||
| ToolCallConfirmationDetails
|
||||
@@ -377,7 +392,7 @@ export class SchedulerStateManager {
|
||||
return {
|
||||
request: call.request,
|
||||
tool: call.tool,
|
||||
status: 'awaiting_approval',
|
||||
status: CoreToolCallStatus.AwaitingApproval,
|
||||
correlationId,
|
||||
confirmationDetails,
|
||||
startTime: 'startTime' in call ? call.startTime : undefined,
|
||||
@@ -400,11 +415,11 @@ export class SchedulerStateManager {
|
||||
}
|
||||
|
||||
private toScheduled(call: ToolCall): ScheduledToolCall {
|
||||
this.validateHasToolAndInvocation(call, 'scheduled');
|
||||
this.validateHasToolAndInvocation(call, CoreToolCallStatus.Scheduled);
|
||||
return {
|
||||
request: call.request,
|
||||
tool: call.tool,
|
||||
status: 'scheduled',
|
||||
status: CoreToolCallStatus.Scheduled,
|
||||
startTime: 'startTime' in call ? call.startTime : undefined,
|
||||
outcome: call.outcome,
|
||||
invocation: call.invocation,
|
||||
@@ -413,7 +428,7 @@ export class SchedulerStateManager {
|
||||
}
|
||||
|
||||
private toCancelled(call: ToolCall, reason: string): CancelledToolCall {
|
||||
this.validateHasToolAndInvocation(call, 'cancelled');
|
||||
this.validateHasToolAndInvocation(call, CoreToolCallStatus.Cancelled);
|
||||
const startTime = 'startTime' in call ? call.startTime : undefined;
|
||||
|
||||
// TODO: Refactor this tool-specific logic into the confirmation details payload.
|
||||
@@ -444,7 +459,7 @@ export class SchedulerStateManager {
|
||||
request: call.request,
|
||||
tool: call.tool,
|
||||
invocation: call.invocation,
|
||||
status: 'cancelled',
|
||||
status: CoreToolCallStatus.Cancelled,
|
||||
response: {
|
||||
callId: call.request.callId,
|
||||
responseParts: [
|
||||
@@ -468,7 +483,7 @@ export class SchedulerStateManager {
|
||||
}
|
||||
|
||||
private isWaitingToolCall(call: ToolCall): call is WaitingToolCall {
|
||||
return call.status === 'awaiting_approval';
|
||||
return call.status === CoreToolCallStatus.AwaitingApproval;
|
||||
}
|
||||
|
||||
private patchCall<T extends ToolCall>(call: T, patch: Partial<T>): T {
|
||||
@@ -476,11 +491,11 @@ export class SchedulerStateManager {
|
||||
}
|
||||
|
||||
private toValidating(call: ToolCall): ValidatingToolCall {
|
||||
this.validateHasToolAndInvocation(call, 'validating');
|
||||
this.validateHasToolAndInvocation(call, CoreToolCallStatus.Validating);
|
||||
return {
|
||||
request: call.request,
|
||||
tool: call.tool,
|
||||
status: 'validating',
|
||||
status: CoreToolCallStatus.Validating,
|
||||
startTime: 'startTime' in call ? call.startTime : undefined,
|
||||
outcome: call.outcome,
|
||||
invocation: call.invocation,
|
||||
@@ -489,7 +504,7 @@ export class SchedulerStateManager {
|
||||
}
|
||||
|
||||
private toExecuting(call: ToolCall, data?: unknown): ExecutingToolCall {
|
||||
this.validateHasToolAndInvocation(call, 'executing');
|
||||
this.validateHasToolAndInvocation(call, CoreToolCallStatus.Executing);
|
||||
// eslint-disable-next-line @typescript-eslint/no-unsafe-type-assertion
|
||||
const execData = data as Partial<ExecutingToolCall> | undefined;
|
||||
const liveOutput =
|
||||
@@ -500,7 +515,7 @@ export class SchedulerStateManager {
|
||||
return {
|
||||
request: call.request,
|
||||
tool: call.tool,
|
||||
status: 'executing',
|
||||
status: CoreToolCallStatus.Executing,
|
||||
startTime: 'startTime' in call ? call.startTime : undefined,
|
||||
outcome: call.outcome,
|
||||
invocation: call.invocation,
|
||||
|
||||
@@ -11,6 +11,7 @@ import type { ToolResult } from '../tools/tools.js';
|
||||
import { makeFakeConfig } from '../test-utils/config.js';
|
||||
import { MockTool } from '../test-utils/mock-tool.js';
|
||||
import type { ScheduledToolCall } from './types.js';
|
||||
import { CoreToolCallStatus } from './types.js';
|
||||
import type { AnyToolInvocation } from '../index.js';
|
||||
import { SHELL_TOOL_NAME } from '../tools/tool-names.js';
|
||||
import * as fileUtils from '../utils/fileUtils.js';
|
||||
@@ -71,7 +72,7 @@ describe('ToolExecutor', () => {
|
||||
} as ToolResult);
|
||||
|
||||
const scheduledCall: ScheduledToolCall = {
|
||||
status: 'scheduled',
|
||||
status: CoreToolCallStatus.Scheduled,
|
||||
request: {
|
||||
callId: 'call-1',
|
||||
name: 'testTool',
|
||||
@@ -91,8 +92,8 @@ describe('ToolExecutor', () => {
|
||||
onUpdateToolCall,
|
||||
});
|
||||
|
||||
expect(result.status).toBe('success');
|
||||
if (result.status === 'success') {
|
||||
expect(result.status).toBe(CoreToolCallStatus.Success);
|
||||
if (result.status === CoreToolCallStatus.Success) {
|
||||
const response = result.response.responseParts[0]?.functionResponse
|
||||
?.response as Record<string, unknown>;
|
||||
expect(response).toEqual({ output: 'Tool output' });
|
||||
@@ -111,7 +112,7 @@ describe('ToolExecutor', () => {
|
||||
);
|
||||
|
||||
const scheduledCall: ScheduledToolCall = {
|
||||
status: 'scheduled',
|
||||
status: CoreToolCallStatus.Scheduled,
|
||||
request: {
|
||||
callId: 'call-2',
|
||||
name: 'failTool',
|
||||
@@ -130,8 +131,8 @@ describe('ToolExecutor', () => {
|
||||
onUpdateToolCall: vi.fn(),
|
||||
});
|
||||
|
||||
expect(result.status).toBe('error');
|
||||
if (result.status === 'error') {
|
||||
expect(result.status).toBe(CoreToolCallStatus.Error);
|
||||
if (result.status === CoreToolCallStatus.Error) {
|
||||
expect(result.response.error?.message).toBe('Tool Failed');
|
||||
}
|
||||
});
|
||||
@@ -151,7 +152,7 @@ describe('ToolExecutor', () => {
|
||||
);
|
||||
|
||||
const scheduledCall: ScheduledToolCall = {
|
||||
status: 'scheduled',
|
||||
status: CoreToolCallStatus.Scheduled,
|
||||
request: {
|
||||
callId: 'call-3',
|
||||
name: 'slowTool',
|
||||
@@ -174,7 +175,7 @@ describe('ToolExecutor', () => {
|
||||
controller.abort();
|
||||
const result = await promise;
|
||||
|
||||
expect(result.status).toBe('cancelled');
|
||||
expect(result.status).toBe(CoreToolCallStatus.Cancelled);
|
||||
});
|
||||
|
||||
it('should truncate large shell output', async () => {
|
||||
@@ -193,7 +194,7 @@ describe('ToolExecutor', () => {
|
||||
});
|
||||
|
||||
const scheduledCall: ScheduledToolCall = {
|
||||
status: 'scheduled',
|
||||
status: CoreToolCallStatus.Scheduled,
|
||||
request: {
|
||||
callId: 'call-trunc',
|
||||
name: SHELL_TOOL_NAME,
|
||||
@@ -228,8 +229,8 @@ describe('ToolExecutor', () => {
|
||||
10, // threshold (maxChars)
|
||||
);
|
||||
|
||||
expect(result.status).toBe('success');
|
||||
if (result.status === 'success') {
|
||||
expect(result.status).toBe(CoreToolCallStatus.Success);
|
||||
if (result.status === CoreToolCallStatus.Success) {
|
||||
const response = result.response.responseParts[0]?.functionResponse
|
||||
?.response as Record<string, unknown>;
|
||||
// The content should be the *truncated* version returned by the mock formatTruncatedToolOutput
|
||||
@@ -262,7 +263,7 @@ describe('ToolExecutor', () => {
|
||||
);
|
||||
|
||||
const scheduledCall: ScheduledToolCall = {
|
||||
status: 'scheduled',
|
||||
status: CoreToolCallStatus.Scheduled,
|
||||
request: {
|
||||
callId: 'call-pid',
|
||||
name: SHELL_TOOL_NAME,
|
||||
@@ -287,7 +288,7 @@ describe('ToolExecutor', () => {
|
||||
// 4. Verify PID was reported
|
||||
expect(onUpdateToolCall).toHaveBeenCalledWith(
|
||||
expect.objectContaining({
|
||||
status: 'executing',
|
||||
status: CoreToolCallStatus.Executing,
|
||||
pid: testPid,
|
||||
}),
|
||||
);
|
||||
|
||||
@@ -33,6 +33,7 @@ import type {
|
||||
SuccessfulToolCall,
|
||||
CancelledToolCall,
|
||||
} from './types.js';
|
||||
import { CoreToolCallStatus } from './types.js';
|
||||
|
||||
export interface ToolExecutionContext {
|
||||
call: ToolCall;
|
||||
@@ -81,7 +82,7 @@ export class ToolExecutor {
|
||||
const setPidCallback = (pid: number) => {
|
||||
const executingCall: ExecutingToolCall = {
|
||||
...call,
|
||||
status: 'executing',
|
||||
status: CoreToolCallStatus.Executing,
|
||||
tool,
|
||||
invocation,
|
||||
pid,
|
||||
@@ -170,7 +171,7 @@ export class ToolExecutor {
|
||||
}
|
||||
|
||||
return {
|
||||
status: 'cancelled',
|
||||
status: CoreToolCallStatus.Cancelled,
|
||||
request: call.request,
|
||||
response: {
|
||||
callId: call.request.callId,
|
||||
@@ -256,7 +257,7 @@ export class ToolExecutor {
|
||||
}
|
||||
|
||||
return {
|
||||
status: 'success',
|
||||
status: CoreToolCallStatus.Success,
|
||||
request: call.request,
|
||||
tool: call.tool,
|
||||
response: successResponse,
|
||||
@@ -281,7 +282,7 @@ export class ToolExecutor {
|
||||
const startTime = 'startTime' in call ? call.startTime : undefined;
|
||||
|
||||
return {
|
||||
status: 'error',
|
||||
status: CoreToolCallStatus.Error,
|
||||
request: call.request,
|
||||
response,
|
||||
tool: call.tool,
|
||||
|
||||
@@ -7,6 +7,7 @@
|
||||
import { describe, it, expect, vi, beforeEach } from 'vitest';
|
||||
import { ToolModificationHandler } from './tool-modifier.js';
|
||||
import type { WaitingToolCall, ToolCallRequestInfo } from './types.js';
|
||||
import { CoreToolCallStatus } from './types.js';
|
||||
import * as modifiableToolModule from '../tools/modifiable-tool.js';
|
||||
import * as Diff from 'diff';
|
||||
import { MockModifiableTool, MockTool } from '../test-utils/mock-tool.js';
|
||||
@@ -37,7 +38,7 @@ function createMockWaitingToolCall(
|
||||
overrides: Partial<WaitingToolCall> = {},
|
||||
): WaitingToolCall {
|
||||
return {
|
||||
status: 'awaiting_approval',
|
||||
status: CoreToolCallStatus.AwaitingApproval,
|
||||
request: {
|
||||
callId: 'test-call-id',
|
||||
name: 'test-tool',
|
||||
|
||||
@@ -18,6 +18,19 @@ import type { SerializableConfirmationDetails } from '../confirmation-bus/types.
|
||||
|
||||
export const ROOT_SCHEDULER_ID = 'root';
|
||||
|
||||
/**
|
||||
* Internal core statuses for the tool call state machine.
|
||||
*/
|
||||
export enum CoreToolCallStatus {
|
||||
Validating = 'validating',
|
||||
Scheduled = 'scheduled',
|
||||
Error = 'error',
|
||||
Success = 'success',
|
||||
Executing = 'executing',
|
||||
Cancelled = 'cancelled',
|
||||
AwaitingApproval = 'awaiting_approval',
|
||||
}
|
||||
|
||||
export interface ToolCallRequestInfo {
|
||||
callId: string;
|
||||
name: string;
|
||||
@@ -45,7 +58,7 @@ export interface ToolCallResponseInfo {
|
||||
}
|
||||
|
||||
export type ValidatingToolCall = {
|
||||
status: 'validating';
|
||||
status: CoreToolCallStatus.Validating;
|
||||
request: ToolCallRequestInfo;
|
||||
tool: AnyDeclarativeTool;
|
||||
invocation: AnyToolInvocation;
|
||||
@@ -55,7 +68,7 @@ export type ValidatingToolCall = {
|
||||
};
|
||||
|
||||
export type ScheduledToolCall = {
|
||||
status: 'scheduled';
|
||||
status: CoreToolCallStatus.Scheduled;
|
||||
request: ToolCallRequestInfo;
|
||||
tool: AnyDeclarativeTool;
|
||||
invocation: AnyToolInvocation;
|
||||
@@ -65,7 +78,7 @@ export type ScheduledToolCall = {
|
||||
};
|
||||
|
||||
export type ErroredToolCall = {
|
||||
status: 'error';
|
||||
status: CoreToolCallStatus.Error;
|
||||
request: ToolCallRequestInfo;
|
||||
response: ToolCallResponseInfo;
|
||||
tool?: AnyDeclarativeTool;
|
||||
@@ -75,7 +88,7 @@ export type ErroredToolCall = {
|
||||
};
|
||||
|
||||
export type SuccessfulToolCall = {
|
||||
status: 'success';
|
||||
status: CoreToolCallStatus.Success;
|
||||
request: ToolCallRequestInfo;
|
||||
tool: AnyDeclarativeTool;
|
||||
response: ToolCallResponseInfo;
|
||||
@@ -86,7 +99,7 @@ export type SuccessfulToolCall = {
|
||||
};
|
||||
|
||||
export type ExecutingToolCall = {
|
||||
status: 'executing';
|
||||
status: CoreToolCallStatus.Executing;
|
||||
request: ToolCallRequestInfo;
|
||||
tool: AnyDeclarativeTool;
|
||||
invocation: AnyToolInvocation;
|
||||
@@ -98,7 +111,7 @@ export type ExecutingToolCall = {
|
||||
};
|
||||
|
||||
export type CancelledToolCall = {
|
||||
status: 'cancelled';
|
||||
status: CoreToolCallStatus.Cancelled;
|
||||
request: ToolCallRequestInfo;
|
||||
response: ToolCallResponseInfo;
|
||||
tool: AnyDeclarativeTool;
|
||||
@@ -109,7 +122,7 @@ export type CancelledToolCall = {
|
||||
};
|
||||
|
||||
export type WaitingToolCall = {
|
||||
status: 'awaiting_approval';
|
||||
status: CoreToolCallStatus.AwaitingApproval;
|
||||
request: ToolCallRequestInfo;
|
||||
tool: AnyDeclarativeTool;
|
||||
invocation: AnyToolInvocation;
|
||||
|
||||
Reference in New Issue
Block a user