feat(gateway): wire safe-point runtime cancellation for agent.cancel
This commit is contained in:
@@ -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.',
|
||||
},
|
||||
};
|
||||
},
|
||||
};
|
||||
}
|
||||
|
||||
@@ -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');
|
||||
});
|
||||
});
|
||||
|
||||
|
||||
@@ -113,6 +113,24 @@ describe('SessionBridge', () => {
|
||||
expect(bridge.isBusy('conn-1')).toBe(false);
|
||||
});
|
||||
|
||||
it('cancel returns false when no active operation exists', () => {
|
||||
const bridge = createBridge();
|
||||
bridge.connect('conn-1');
|
||||
|
||||
expect(bridge.cancel('conn-1')).toBe(false);
|
||||
});
|
||||
|
||||
it('cancel requests cancellation when connection is busy', () => {
|
||||
const bridge = createBridge();
|
||||
bridge.connect('conn-1');
|
||||
const agent = bridge.getAgent('conn-1');
|
||||
const cancelSpy = vi.spyOn(agent!, 'cancel');
|
||||
|
||||
bridge.setBusy('conn-1', true);
|
||||
expect(bridge.cancel('conn-1')).toBe(true);
|
||||
expect(cancelSpy).toHaveBeenCalledTimes(1);
|
||||
});
|
||||
|
||||
it('switchSession changes session for a connection', () => {
|
||||
const bridge = createBridge();
|
||||
bridge.connect('conn-1');
|
||||
|
||||
@@ -97,6 +97,17 @@ export class SessionBridge {
|
||||
if (client) {client.busy = busy;}
|
||||
}
|
||||
|
||||
/** Request cancellation for the current operation on a connection's agent. */
|
||||
cancel(connectionId: string): boolean {
|
||||
const client = this.clients.get(connectionId);
|
||||
if (!client || !client.busy) {
|
||||
return false;
|
||||
}
|
||||
|
||||
client.agent.cancel();
|
||||
return true;
|
||||
}
|
||||
|
||||
/** Set onToolUse callback for a connection's agent. */
|
||||
setOnToolUse(connectionId: string, callback: ((event: ToolUseEvent) => void) | undefined): void {
|
||||
const client = this.clients.get(connectionId);
|
||||
|
||||
Reference in New Issue
Block a user