feat: cancel in-flight subagent tasks by id#40
Merged
Conversation
9571f61 to
e6c49ce
Compare
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.
e6c49ce to
200bb11
Compare
8 tasks
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Add this suggestion to a batch that can be applied as a single commit.This suggestion is invalid because no changes were made to the code.Suggestions cannot be applied while the pull request is closed.Suggestions cannot be applied while viewing a subset of changes.Only one suggestion per line can be applied in a batch.Add this suggestion to a batch that can be applied as a single commit.Applying suggestions on deleted lines is not supported.You must change the existing code in this line in order to create a valid suggestion.Outdated suggestions cannot be applied.This suggestion has been applied or marked resolved.Suggestions cannot be applied from pending reviews.Suggestions cannot be applied on multi-line comments.Suggestions cannot be applied while the pull request is queued to merge.Suggestion cannot be applied right now. Please check back later.
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
What this completes
After PRs #35 → #39, callers can:
Every operation works in Rust, Node, and Python with identical semantics.