feat(gateway): wire safe-point runtime cancellation for agent.cancel

This commit is contained in:
William Valentin
2026-02-13 08:51:14 -08:00
parent 9f81c01603
commit 46099664f0
7 changed files with 182 additions and 26 deletions
+16 -5
View File
@@ -182,8 +182,6 @@ export function createAgentHandlers(deps: AgentHandlerDeps) {
},
'agent.cancel': async (request: GatewayRequest): Promise<OutboundMessage> => {
// Cancel is a placeholder — proper cancellation requires abort controller support in NativeAgent.
// For now, just report whether the agent was busy.
const params = request.params as { connectionId?: string } | undefined;
const connectionId = params?.connectionId as string;
@@ -191,9 +189,22 @@ export function createAgentHandlers(deps: AgentHandlerDeps) {
return makeError(request.id, ErrorCode.InvalidRequest, 'connectionId is required');
}
const wasBusy = deps.sessionBridge.isBusy(connectionId);
// TODO: Wire AbortController into NativeAgent for actual cancellation
return { id: request.id, result: { cancelled: wasBusy } };
const sessionId = deps.sessionBridge.getSessionId(connectionId);
const laneId = sessionId ?? connectionId;
// Clear any queued (not-yet-started) work first.
deps.laneQueue.cancel(laneId);
const cancelled = deps.sessionBridge.cancel(connectionId);
return {
id: request.id,
result: {
cancelled,
message: cancelled
? 'Cancellation requested. The active operation will stop at the next safe point.'
: 'No active operation to cancel.',
},
};
},
};
}
+14 -1
View File
@@ -251,6 +251,7 @@ describe('agent handlers', () => {
getAgent: vi.fn(() => mockAgent),
getSessionId: vi.fn(() => 'ws:conn-1'),
isBusy: vi.fn(() => false),
cancel: vi.fn(() => false),
setBusy: vi.fn(),
setOnToolUse: vi.fn(),
};
@@ -265,6 +266,7 @@ describe('agent handlers', () => {
beforeEach(() => {
vi.clearAllMocks();
mockBridge.isBusy.mockReturnValue(false);
mockBridge.cancel.mockReturnValue(false);
mockBridge.getAgent.mockReturnValue(mockAgent);
mockAgent.process.mockResolvedValue('response text');
});
@@ -399,11 +401,22 @@ describe('agent handlers', () => {
});
it('agent.cancel returns cancelled state', async () => {
mockBridge.isBusy.mockReturnValue(true);
mockBridge.cancel.mockReturnValue(true);
const req: GatewayRequest = { id: 7, method: 'agent.cancel', params: { connectionId: 'conn-1' } };
const result = await handlers['agent.cancel'](req) as GatewayResponse;
expect((result.result as any).cancelled).toBe(true);
expect((result.result as any).message).toContain('Cancellation requested');
expect(mockBridge.cancel).toHaveBeenCalledWith('conn-1');
});
it('agent.cancel returns not-cancelled when no active operation exists', async () => {
mockBridge.cancel.mockReturnValue(false);
const req: GatewayRequest = { id: 8, method: 'agent.cancel', params: { connectionId: 'conn-1' } };
const result = await handlers['agent.cancel'](req) as GatewayResponse;
expect((result.result as any).cancelled).toBe(false);
expect((result.result as any).message).toContain('No active operation');
});
});