The standard A2A chat UI already has a Cancel control, but it currently appears to abort only the local browser stream.
In ui/src/components/chat/ChatInterface.tsx, handleCancel aborts the local AbortController, clears streaming UI state, sets the chat back to ready, and shows “Request cancelled.” However, ui/src/lib/a2aClient.ts currently exposes only:
message/stream
tasks/resubscribe
It does not expose a tasks/cancel call.
The backend/controller path appears to support A2A task cancellation at the passthrough layer:
go/core/internal/a2a/passthrough_handler.go implements CancelTask(...) by delegating to h.client.CancelTask(...).
Runtime support appears mixed:
- Go ADK executor implements
Cancel(...) and emits a final TaskStateCanceled event.
- Python ADK executor currently has
cancel(...) implemented as raise NotImplementedError("Cancellation is not supported").
So the current UI Cancel behavior can be misleading: the user may see “Request cancelled” while the underlying task continues, completes, or later reappears after reload/reconciliation.
Current behavior
Current standard chat cancel behavior is local-only:
const handleCancel = (e: React.FormEvent) => {
e.preventDefault();
if (abortControllerRef.current) {
abortControllerRef.current.abort();
}
setIsStreaming(false);
setStreamingContent("");
setChatStatus("ready");
toast.error("Request cancelled");
};
This makes the UI responsive, but it does not appear to tell the backend/runtime to cancel the active task.
Problem
This creates ambiguous task state:
- User clicks Cancel.
- UI says the request was cancelled.
- The local SSE stream is aborted.
- Backend task may still be running.
- The task may later complete and be persisted.
- On refresh/reload/resubscribe, the “cancelled” turn may reappear.
- A subsequent send may be affected by the actual backend task state.
The user-facing Cancel control should either actually cancel the active turn or clearly report that cancellation was not supported and reconcile with the backend’s final task state.
Desired behavior
Cancel should have clear semantics for standard A2A chat:
- The UI should call A2A
tasks/cancel for the active task where possible.
- The UI should still abort the local stream promptly.
- After cancel, the UI should reload/reconcile latest task/session state.
- If the task is cancelled, show a clear cancelled terminal state.
- If the task completed or failed before cancellation took effect, show that final state.
- If cancellation is unsupported by the runtime, surface that clearly instead of presenting the turn as successfully cancelled.
- The next send should not be blocked by stale local state from a turn the UI abandoned.
Acceptance criteria
a2aClient exposes a standard A2A tasks/cancel request.
- Standard chat Cancel calls
tasks/cancel for the active task ID, in addition to aborting the local stream.
- Post-cancel reconciliation reloads the latest task/session state.
- Reconciliation refreshes or clears local state even when the backend returns an empty task list.
- If backend/runtime reports
canceled, the UI shows a clear cancelled state.
- If backend/runtime reports completed/failed before cancellation took effect, the UI shows that final state.
- If cancellation is unsupported or fails, the UI surfaces an actionable error and keeps the session recoverable.
- A locally cancelled turn that backend confirms terminal/cancelled should not incorrectly block the next send.
- Existing stale-send, resubscribe, HITL, and stream behavior continue to work.
- UI tests cover local abort + backend cancel + reconciliation behavior.
Implementation notes / known backend state
Observed from current code:
-
Controller passthrough supports cancellation:
go/core/internal/a2a/passthrough_handler.go
CancelTask(ctx, req) delegates to h.client.CancelTask(ctx, req).
-
Go ADK executor supports cancellation:
go/adk/pkg/a2a/executor.go
Cancel(...) emits final TaskStateCanceled.
-
Python ADK executor does not currently support cancellation:
python/packages/kagent-adk/src/kagent/adk/_agent_executor.py
cancel(...) raises NotImplementedError("Cancellation is not supported").
So this may require both:
- UI wiring for
tasks/cancel.
- Runtime behavior decision for Python-backed agents: implement cancellation or surface unsupported cancellation clearly.
Ordering footgun
handleCancel currently sets chatStatus to "ready" synchronously, while streamA2AMessage also handles AbortError by setting status back to ready.
If cancel becomes async, make sure post-cancel reconciliation is not clobbered by these local ready-state updates. The final UI state should come from reconciled backend task/session state where available.
Scope
This issue is focused on standard A2A chat Cancel.
Relevant areas:
ui/src/components/chat/ChatInterface.tsx
ui/src/lib/a2aClient.ts
ui/src/app/a2a/[namespace]/[agentName]/route.ts
ui/src/app/actions/sessions.ts
- A2A runtime cancellation support where needed, especially Python ADK
Non-goals
This is not asking to add a Cancel button; one already exists.
This is not asking to add general chat lifecycle handling, resubscribe support, stale-send guards, or HITL support. Those already exist in various forms.
This issue is also not covering AgentHarness/WebSocket chat Cancel behavior. That path has different mechanics and should be tracked separately if needed.
Open questions
- Should Python ADK-backed agents implement real cancellation, or should the UI surface “cancellation unsupported” for those runtimes?
- What exact final task state/message should be persisted/displayed for a cancelled turn?
- Should cancelling the local stream wait for
tasks/cancel response, or optimistically abort local streaming first and reconcile afterward?
The standard A2A chat UI already has a Cancel control, but it currently appears to abort only the local browser stream.
In
ui/src/components/chat/ChatInterface.tsx,handleCancelaborts the localAbortController, clears streaming UI state, sets the chat back to ready, and shows “Request cancelled.” However,ui/src/lib/a2aClient.tscurrently exposes only:message/streamtasks/resubscribeIt does not expose a
tasks/cancelcall.The backend/controller path appears to support A2A task cancellation at the passthrough layer:
go/core/internal/a2a/passthrough_handler.goimplementsCancelTask(...)by delegating toh.client.CancelTask(...).Runtime support appears mixed:
Cancel(...)and emits a finalTaskStateCanceledevent.cancel(...)implemented asraise NotImplementedError("Cancellation is not supported").So the current UI Cancel behavior can be misleading: the user may see “Request cancelled” while the underlying task continues, completes, or later reappears after reload/reconciliation.
Current behavior
Current standard chat cancel behavior is local-only:
This makes the UI responsive, but it does not appear to tell the backend/runtime to cancel the active task.
Problem
This creates ambiguous task state:
The user-facing Cancel control should either actually cancel the active turn or clearly report that cancellation was not supported and reconcile with the backend’s final task state.
Desired behavior
Cancel should have clear semantics for standard A2A chat:
tasks/cancelfor the active task where possible.Acceptance criteria
a2aClientexposes a standard A2Atasks/cancelrequest.tasks/cancelfor the active task ID, in addition to aborting the local stream.canceled, the UI shows a clear cancelled state.Implementation notes / known backend state
Observed from current code:
Controller passthrough supports cancellation:
go/core/internal/a2a/passthrough_handler.goCancelTask(ctx, req)delegates toh.client.CancelTask(ctx, req).Go ADK executor supports cancellation:
go/adk/pkg/a2a/executor.goCancel(...)emits finalTaskStateCanceled.Python ADK executor does not currently support cancellation:
python/packages/kagent-adk/src/kagent/adk/_agent_executor.pycancel(...)raisesNotImplementedError("Cancellation is not supported").So this may require both:
tasks/cancel.Ordering footgun
handleCancelcurrently setschatStatusto"ready"synchronously, whilestreamA2AMessagealso handlesAbortErrorby setting status back to ready.If cancel becomes async, make sure post-cancel reconciliation is not clobbered by these local ready-state updates. The final UI state should come from reconciled backend task/session state where available.
Scope
This issue is focused on standard A2A chat Cancel.
Relevant areas:
ui/src/components/chat/ChatInterface.tsxui/src/lib/a2aClient.tsui/src/app/a2a/[namespace]/[agentName]/route.tsui/src/app/actions/sessions.tsNon-goals
This is not asking to add a Cancel button; one already exists.
This is not asking to add general chat lifecycle handling, resubscribe support, stale-send guards, or HITL support. Those already exist in various forms.
This issue is also not covering AgentHarness/WebSocket chat Cancel behavior. That path has different mechanics and should be tracked separately if needed.
Open questions
tasks/cancelresponse, or optimistically abort local streaming first and reconcile afterward?