7.2 KiB
Cancel/Stop Agent Turn Implementation
This document describes how to implement end-to-end functionality for cancelling the current agent turn for both Codex and Auggie agents.
Overview
Users need the ability to stop an agent mid-execution without killing the entire agent process. This allows for quick iteration—cancelling a wrong path and sending a new prompt—without waiting for the agent to finish or restarting the session.
Protocol Summary
Auggie (ACP Protocol)
- Method:
session/cancel(notification) - Transport: JSON-RPC 2.0 over stdin/stdout
- Message format:
{"jsonrpc":"2.0","method":"session/cancel","params":{"sessionId":"<session-id>"}} - Key characteristics:
- It's a notification (no
idfield, no response expected) - Only requires
sessionId - The pending
session/promptrequest returns with{"stopReason":"cancelled"} - Works immediately—agent stops current operation
- It's a notification (no
Codex (App-Server Protocol)
- Method:
turn/interrupt(request) - Transport: JSON-RPC variant over stdin/stdout (omits
jsonrpcfield) - Message format:
{"id":N,"method":"turn/interrupt","params":{"threadId":"<thread-id>","turnId":"<turn-id>"}} - Key characteristics:
- It's a request (has
idfield, expects response) - Requires both
threadIdANDturnId threadId= session ID (fromthread/startresponse)turnId= operation ID (fromturn/startresponse)
- It's a request (has
Current State
Already Implemented ✅
- AgentAdapter interface has
Cancel(ctx context.Context) errormethod - ACPAdapter.Cancel() is fully implemented using
acpConn.Cancel() - CodexAdapter.Cancel() is fully implemented using
turn/interrupt - AgentAdapter.GetOperationID() returns
turnIDfor Codex (needed for cancel) - WebSocket action constant
ActionAgentCancel = "agent.cancel"exists
Not Implemented ❌
- agentctl HTTP endpoint for cancel (
POST /api/v1/acp/cancel) - agentctl Client method to call the cancel endpoint
- Backend orchestrator/lifecycle integration to route cancel requests
- WebSocket handler for
agent.cancelaction - Frontend UI cancel button and hook
High-Level Flow
Frontend (Cancel Button)
|
| agent.cancel (WS request)
v
Backend (WS Gateway → Dispatcher)
|
| orchestrator handler
v
Backend (Executor → AgentManager)
|
| CancelAgent(ctx, sessionID)
v
Lifecycle Manager
|
| HTTP POST /api/v1/acp/cancel
v
agentctl (api server)
|
| adapter.Cancel(ctx)
v
Agent subprocess (ACP or Codex protocol)
|
v
Agent stops current turn
|
| Returns stopReason=cancelled (ACP) or turn/completed with cancelled (Codex)
v
Prompt() returns → lifecycle emits session.cancelled event → UI updates
Implementation Plan
Layer 1: agentctl Server (HTTP API)
File: apps/backend/internal/agentctl/server/api/acp.go
Add a new endpoint:
POST /api/v1/acp/cancel→handleACPCancel
Handler implementation:
- Get the adapter from process manager
- Call
adapter.Cancel(ctx) - Return success/error response
File: apps/backend/internal/agentctl/server/api/server.go
Register the new route in the router group.
Layer 2: agentctl Client
File: apps/backend/internal/agentctl/client/acp.go
Add Cancel(ctx context.Context) error method:
- Make HTTP POST to
/api/v1/acp/cancel - Parse response and return error if failed
Layer 3: Lifecycle Manager
File: apps/backend/internal/agent/lifecycle/manager.go
Add CancelAgent(ctx context.Context, executionID string) error:
- Look up execution by ID
- Validate execution is in running state
- Call
execution.agentctl.Cancel(ctx) - The running Prompt() call will return with cancelled stop reason
File: apps/backend/internal/agent/lifecycle/session.go
Handle cancelled stop reason in SendPrompt():
- When prompt returns with
stopReason=cancelled, don't treat as error - Emit appropriate event for UI notification
Layer 4: Executor & Orchestrator
File: apps/backend/internal/orchestrator/executor/executor.go
Add interface method to AgentManagerClient:
CancelAgent(ctx context.Context, sessionID string) error
Add Cancel(ctx, taskID, sessionID string) error:
- Look up session by ID
- Get agent execution ID
- Call
agentManager.CancelAgent(ctx, executionID)
File: apps/backend/cmd/kandev/adapters.go
Implement CancelAgent on lifecycleAdapter:
- Delegate to
mgr.CancelAgent()
Layer 5: WebSocket Handler
File: apps/backend/internal/orchestrator/handlers/handlers.go
Add handler for agent.cancel:
func (h *Handlers) wsCancelAgent(ctx context.Context, msg *ws.Message) (*ws.Message, error)
Request payload:
type wsCancelAgentRequest struct {
TaskID string `json:"task_id"`
SessionID string `json:"session_id"`
}
Register in RegisterHandlers():
d.RegisterFunc(ws.ActionAgentCancel, h.wsCancelAgent)
File: apps/backend/internal/orchestrator/controller/controller.go
Add controller method:
func (c *Controller) CancelAgent(ctx context.Context, req dto.CancelAgentRequest) (dto.CancelAgentResponse, error)
File: apps/backend/internal/orchestrator/controller/dto/dto.go
Add request/response types:
type CancelAgentRequest struct {
TaskID string `json:"task_id"`
SessionID string `json:"session_id"`
}
type CancelAgentResponse struct {
Success bool `json:"success"`
Error string `json:"error,omitempty"`
}
Layer 6: Frontend
File: apps/web/hooks/use-session-agent.ts
Add handleCancelTurn callback:
const handleCancelTurn = useCallback(async () => {
if (!task?.id || !agentSessionId) return;
const client = getWebSocketClient();
if (!client) return;
await client.request('agent.cancel', {
task_id: task.id,
session_id: agentSessionId
});
}, [task?.id, agentSessionId]);
Return from hook: { ..., handleCancelTurn }
UI Integration (location TBD—likely task chat panel or top bar):
- Add "Stop" button visible when agent is running
- Wire to
handleCancelTurn() - Show loading state during cancel
- Button disabled when no active turn
Event Flow After Cancel
adapter.Cancel()sends protocol message- Agent stops and responds with cancelled status
Prompt()returns withstopReason = "cancelled"- Lifecycle manager:
- Updates execution status to READY (not COMPLETED)
- Emits
session.agent.cancelledor similar event
- Frontend receives event and updates UI:
- Stops spinner
- Shows "Cancelled" indicator
- Re-enables input for new prompt
Error Handling
| Scenario | Behavior |
|---|---|
| No active turn | Return success (no-op) |
| Agent not running | Return error "agent not running" |
| Session not found | Return error "session not found" |
| Cancel timeout | Return error, but cancel still in flight |
| Cancel during permission request | Cancel the permission, return cancelled |
Testing
- Unit tests for each layer (adapter, client, handler)
- Integration test: Start agent, send prompt, cancel mid-execution
- E2E test: UI cancel button flow
- Protocol tests: Verify correct JSON-RPC messages for ACP and Codex