Skip to content

feat: cancel in-flight subagent tasks by id#40

Merged
ZhiXiao-Lin merged 1 commit into
mainfrom
feat/subagent-cancel
May 23, 2026
Merged

feat: cancel in-flight subagent tasks by id#40
ZhiXiao-Lin merged 1 commit into
mainfrom
feat/subagent-cancel

Conversation

@ZhiXiao-Lin
Copy link
Copy Markdown
Contributor

Stacked on #39#38#37#36#35. Base retargets to `main` automatically as upstream PRs land.

Summary

Closes the last gap in the "subagent task as task dashboard" story: a parent session can now interrupt a delegated child run without canceling the whole parent.

```rust
let fired = session.cancel_subagent_task("task-abc").await; // bool
```

```ts
await session.cancelSubagentTask('task-abc') // Promise
```

```python
session.cancel_subagent_task('task-abc') # bool
```

Returns `true` when a registered cancellation token was found and fired; `false` for unknown ids or already-finished tasks.

Design notes

Where the token lives. Cancellation tokens are stored on the same `InMemorySubagentTaskTracker` that backs the query API. The executor registers a token on entry, clears it on exit. The parent session calls `cancel(id)` which atomically (a) fires the token and (b) flips the snapshot status to `Cancelled`. Race-safe: a late `SubagentEnd` from the cancelled child does not downgrade `Cancelled` → `Failed`.

How cancel propagates into the child. `TaskExecutor::execute_with_task_id` now runs the child loop through `AgentLoop::execute_with_session(... Some(&token))` (was the no-token `execute()` shortcut). The token propagates into `llm_turn` which already honors it, so cancellation hits at the next LLM streaming yield or tool boundary.

Why a new `Cancelled` variant. `SubagentStatus` was `Running | Completed | Failed` — collapsing cancellation into `Failed` would lose information dashboards care about ("did the user stop this?" vs "did the child agent error out?"). The Node/Python SDKs serialize the enum directly, so consumers get the new value automatically.

Test plan

  • 4 new unit tests on the tracker (cancel fires token + status, unknown id → false, late End doesn't downgrade, clear disarms).
  • 1 new integration test in `agent_api/tests.rs` drives a synthetic subagent through `RuntimeEventSink`, registers a canceller, fires `session.cancel_subagent_task`, verifies the terminal state survives a late SubagentEnd.
  • `cargo test --lib -p a3s-code-core`: 1661 passed / 0 failed (5 new tests beyond feat(core): expose subagent task tracker for delegated runs #35-feat(core): emit SubagentProgress for child tool/turn milestones #36).
  • `cargo test --tests -p a3s-code-core`: all integration binaries green.
  • `cargo clippy --lib --tests -p a3s-code-core`: no new warnings (3 pre-existing in untouched files).
  • `npm test` + `npm run test:types`: pass with new cancellation smoke assertion.
  • `maturin build --release` produces a wheel; `tests/test_subagent_query_api.py` passes with new cancellation smoke assertion.

What this completes

After PRs #35#39, callers can:

Operation API
Look up a task by id `session.subagent_task(id)`
List all subagent tasks (this session) `session.subagent_tasks()`
List in-flight subagent tasks `session.pending_subagent_tasks()`
Observe mid-task milestones `SubagentProgress` events in `run_events()`
Cancel an in-flight task `session.cancel_subagent_task(id)`

Every operation works in Rust, Node, and Python with identical semantics.

@ZhiXiao-Lin ZhiXiao-Lin changed the base branch from feat/subagent-python-sdk to main May 23, 2026 15:53
@ZhiXiao-Lin ZhiXiao-Lin force-pushed the feat/subagent-cancel branch from 9571f61 to e6c49ce Compare May 23, 2026 16:01
Closes the last gap in the "subagent task as task dashboard" story: a
parent session can now interrupt a delegated child run without canceling
the whole parent run.

Core
- `SubagentStatus` gains a `Cancelled` variant. Late `SubagentEnd`
  events from the cancelled child do not downgrade it back to
  `Failed`.
- `InMemorySubagentTaskTracker` now also stores `CancellationToken`s
  per task. `register_canceller` / `clear_canceller` / `cancel(id)`
  bracket the in-flight token lifetime; `cancel` fires the token and
  flips the snapshot status atomically.
- `TaskExecutor` gains `with_subagent_tracker(...)`. When set, each
  task registers its token, then runs the child loop through
  `AgentLoop::execute_with_session(... Some(&token))` so the
  cancellation propagates into LLM streaming and tool execution.
- `register_task_with_mcp` grows an optional tracker parameter so the
  session bootstrap path can share a single Arc with the executor and
  the live `AgentSession`.
- `AgentSession::cancel_subagent_task(task_id)` exposes the operation
  to callers.

SDKs
- Node: `Session.cancelSubagentTask(taskId): Promise<boolean>` via the
  same get_runtime().spawn pattern used by other run-control methods.
- Python: `Session.cancel_subagent_task(task_id) -> bool` via the
  py.allow_threads / tokio block_on pattern.

Tests
- Tracker-level unit tests for the four interesting cases: cancel fires
  the token + flips status, cancel returns False on unknown ids, late
  SubagentEnd doesn't downgrade Cancelled, and clear_canceller disarms
  future cancel calls.
- Integration test in agent_api/tests.rs drives a synthetic subagent
  lifecycle through `RuntimeEventSink`, registers a canceller, and
  asserts the public `cancel_subagent_task` API + the Cancelled
  terminal state survive a late SubagentEnd.
- Node + Python smoke tests assert cancelling an unknown task id
  resolves to false / False.
@ZhiXiao-Lin ZhiXiao-Lin force-pushed the feat/subagent-cancel branch from e6c49ce to 200bb11 Compare May 23, 2026 16:13
@ZhiXiao-Lin ZhiXiao-Lin merged commit be9d45b into main May 23, 2026
1 check passed
@ZhiXiao-Lin ZhiXiao-Lin deleted the feat/subagent-cancel branch May 23, 2026 16:14
@ZhiXiao-Lin ZhiXiao-Lin mentioned this pull request May 23, 2026
8 tasks
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

2 participants