feat(opencode): interrupt a running subagent — steer / cancel / abort - #32425
feat(opencode): interrupt a running subagent — steer / cancel / abort#32425iceteaSA wants to merge 1 commit into
Conversation
0603d35 to
579eef5
Compare
579eef5 to
1ee67f1
Compare
…anomalyco#32425) Ports the subagent interrupt mechanism from the upstream anomalyco/opencode PR anomalyco#32425. Allows a parent session to steer, cancel, or abort a running subagent session mid-turn. Three interrupt modes: - task_steer: injects a <steer> frame into the subagent's context and continues execution. The subagent reads the guidance and can adjust course. - task_cancel: injects a <cancel> frame and starts a grace countdown (CANCEL_GRACE_TURNS turns). After grace expires the loop breaks cleanly. - task_abort: hard-stops the session via SessionRunState.cancel. Implementation: - interrupt.ts: Interrupt service with consume(), recordTerminal(), renderMarker(), renderCancel(), renderSteer(), CANCEL_GRACE_TURNS. Stores pending interrupts in a per-session in-memory queue. - task-interrupt.ts: TaskAbortTool, TaskCancelTool, TaskSteerTool definitions that parent sessions can call to interrupt child sessions. - prompt.ts: added interrupt consume loop at runLoop turn boundary. Only fires when session.parentID is set AND OPENCODE_EXPERIMENTAL_SUBAGENT_INTERRUPT flag is enabled. Tracks cancelGrace countdown. - registry.ts: registers interrupt tools in tool catalog, conditionally adds steer/cancel/abort to builtin tool list behind the experimental flag. Added SessionRunState, SessionStatus, Permission, and Interrupt to the defaultLayer dependency chain. - flag.ts: added OPENCODE_EXPERIMENTAL_SUBAGENT_INTERRUPT feature flag. Enable with: OPENCODE_EXPERIMENTAL_SUBAGENT_INTERRUPT=1 or OPENCODE_EXPERIMENTAL=1. Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
…anomalyco#32425) Ports the subagent interrupt mechanism from the upstream anomalyco/opencode PR anomalyco#32425. Allows a parent session to steer, cancel, or abort a running subagent session mid-turn. Three interrupt modes: - task_steer: injects a <steer> frame into the subagent's context and continues execution. The subagent reads the guidance and can adjust course. - task_cancel: injects a <cancel> frame and starts a grace countdown (CANCEL_GRACE_TURNS turns). After grace expires the loop breaks cleanly. - task_abort: hard-stops the session via SessionRunState.cancel. Implementation: - interrupt.ts: Interrupt service with consume(), recordTerminal(), renderMarker(), renderCancel(), renderSteer(), CANCEL_GRACE_TURNS. Stores pending interrupts in a per-session in-memory queue. - task-interrupt.ts: TaskAbortTool, TaskCancelTool, TaskSteerTool definitions that parent sessions can call to interrupt child sessions. - prompt.ts: added interrupt consume loop at runLoop turn boundary. Only fires when session.parentID is set AND OPENCODE_EXPERIMENTAL_SUBAGENT_INTERRUPT flag is enabled. Tracks cancelGrace countdown. - registry.ts: registers interrupt tools in tool catalog, conditionally adds steer/cancel/abort to builtin tool list behind the experimental flag. Added SessionRunState, SessionStatus, Permission, and Interrupt to the defaultLayer dependency chain. - flag.ts: added OPENCODE_EXPERIMENTAL_SUBAGENT_INTERRUPT feature flag. Enable with: OPENCODE_EXPERIMENTAL_SUBAGENT_INTERRUPT=1 or OPENCODE_EXPERIMENTAL=1. Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
Add an experimental subagent-interrupt capability (behind OPENCODE_EXPERIMENTAL_SUBAGENT_INTERRUPT): an Interrupt service with pending/terminal state, task_steer/task_cancel/task_abort tools (gated by a new `interrupt` permission), a POST /session/:id/interrupt endpoint, and a turn-boundary consume in the prompt loop that injects an escaped steer/cancel frame plus a visible transcript marker. Cancel records a terminal reason and force-breaks after a grace window; abort writes a marker, records the terminal, and cancels the BackgroundJob immediately. The TUI gains an esc-with-reason flow, footer button, and distinct interrupt markers. Ported from anomalyco#32425 (adapted for the Effect layer; merged with the prior task.ts/registry.ts/prompt.ts changes from anomalyco#29447/anomalyco#32122/anomalyco#12520/anomalyco#32192/anomalyco#19961). Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
Add an experimental subagent-interrupt capability (behind OPENCODE_EXPERIMENTAL_SUBAGENT_INTERRUPT): an Interrupt service with pending/terminal state, task_steer/task_cancel/task_abort tools (gated by a new `interrupt` permission), a POST /session/:id/interrupt endpoint, and a turn-boundary consume in the prompt loop that injects an escaped steer/cancel frame plus a visible transcript marker. Cancel records a terminal reason and force-breaks after a grace window; abort writes a marker, records the terminal, and cancels the BackgroundJob immediately. The TUI gains an esc-with-reason flow, footer button, and distinct interrupt markers. Ported from anomalyco#32425 (adapted for the Effect layer; merged with the prior task.ts/registry.ts/prompt.ts changes from anomalyco#29447/anomalyco#32122/anomalyco#12520/anomalyco#32192/anomalyco#19961). Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
|
@iceteaSA I don't think your very nice PR or my slimmer one will ever get any attention :( |
Yeah, I have a couple of other nice ones as well, won't hold my breath. |
1ee67f1 to
7129abe
Compare
Add an experimental subagent-interrupt capability (behind OPENCODE_EXPERIMENTAL_SUBAGENT_INTERRUPT): an Interrupt service with pending/terminal state, task_steer/task_cancel/task_abort tools (gated by a new `interrupt` permission), a POST /session/:id/interrupt endpoint, and a turn-boundary consume in the prompt loop that injects an escaped steer/cancel frame plus a visible transcript marker. Cancel records a terminal reason and force-breaks after a grace window; abort writes a marker, records the terminal, and cancels the BackgroundJob immediately. The TUI gains an esc-with-reason flow, footer button, and distinct interrupt markers. Ported from anomalyco#32425 (adapted for the Effect layer; merged with the prior task.ts/registry.ts/prompt.ts changes from anomalyco#29447/anomalyco#32122/anomalyco#12520/anomalyco#32192/anomalyco#19961). Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
7129abe to
7cb9635
Compare
7cb9635 to
7d3915b
Compare
Add an experimental subagent-interrupt capability (behind OPENCODE_EXPERIMENTAL_SUBAGENT_INTERRUPT): an Interrupt service with pending/terminal state, task_steer/task_cancel/task_abort tools (gated by a new `interrupt` permission), a POST /session/:id/interrupt endpoint, and a turn-boundary consume in the prompt loop that injects an escaped steer/cancel frame plus a visible transcript marker. Cancel records a terminal reason and force-breaks after a grace window; abort writes a marker, records the terminal, and cancels the BackgroundJob immediately. The TUI gains an esc-with-reason flow, footer button, and distinct interrupt markers. Ported from anomalyco#32425 (adapted for the Effect layer; merged with the prior task.ts/registry.ts/prompt.ts changes from anomalyco#29447/anomalyco#32122/anomalyco#12520/anomalyco#32192/anomalyco#19961). Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
7d3915b to
e110d4a
Compare
Add an experimental subagent-interrupt capability (behind OPENCODE_EXPERIMENTAL_SUBAGENT_INTERRUPT): an Interrupt service with pending/terminal state, task_steer/task_cancel/task_abort tools (gated by a new `interrupt` permission), a POST /session/:id/interrupt endpoint, and a turn-boundary consume in the prompt loop that injects an escaped steer/cancel frame plus a visible transcript marker. Cancel records a terminal reason and force-breaks after a grace window; abort writes a marker, records the terminal, and cancels the BackgroundJob immediately. The TUI gains an esc-with-reason flow, footer button, and distinct interrupt markers. Ported from anomalyco#32425 (adapted for the Effect layer; merged with the prior task.ts/registry.ts/prompt.ts changes from anomalyco#29447/anomalyco#32122/anomalyco#12520/anomalyco#32192/anomalyco#19961). Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
e110d4a to
0291a5e
Compare
9fbffb2 to
885e2f8
Compare
885e2f8 to
091f26a
Compare
|
A hard stop is what we need for the subagent session, manually by user or by the lead agent. |
|
@bukit-kronik that's what this does — three levels, since "stop it" turns out to mean different things depending on how stuck the subagent is:
All three are parentage-checked, so an agent can only interrupt its own descendants. The distinction that took a while to get right: On the lead-agent-vs-user split you raise: the tools are available to whichever agent spawned the subagent, so a lead agent can stop its own children. A user-facing hard stop is a separate surface — the TUI would need to expose it — and isn't in this PR. Running in a fork for a while now; the abort path is the one that gets used, usually on a subagent looping without progress. @tobwen on attention — no movement since June, but the PR's been rebased onto current |
Lets a parent agent (via tools) or the human (via TUI esc) steer, gracefully cancel, or hard-abort a single running Task subagent mid-run, without affecting the parent or sibling subagents. A per-instance Interrupt service holds one pending interrupt per child plus a terminal record; the child consumes it at its own runLoop turn boundary. Steer injects a frame and the child adapts and continues; cancel injects a frame, records a terminal, and force-breaks within a grace window; abort cancels the BackgroundJob immediately. Interrupt tools reach any descendant through a bounded ancestry walk that fails closed off the caller's subtree. Off by default behind OPENCODE_EXPERIMENTAL_SUBAGENT_INTERRUPT.
091f26a to
54e4249
Compare
Add an experimental subagent-interrupt capability (behind OPENCODE_EXPERIMENTAL_SUBAGENT_INTERRUPT): an Interrupt service with pending/terminal state, task_steer/task_cancel/task_abort tools (gated by a new `interrupt` permission), a POST /session/:id/interrupt endpoint, and a turn-boundary consume in the prompt loop that injects an escaped steer/cancel frame plus a visible transcript marker. Cancel records a terminal reason and force-breaks after a grace window; abort writes a marker, records the terminal, and cancels the BackgroundJob immediately. The TUI gains an esc-with-reason flow, footer button, and distinct interrupt markers. Ported from anomalyco#32425 (adapted for the Effect layer; merged with the prior task.ts/registry.ts/prompt.ts changes from anomalyco#29447/anomalyco#32122/anomalyco#12520/anomalyco#32192/anomalyco#19961). Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
Issue for this PR
Closes #38966
Related: #32431 (stuck subagents can't be nudged — the steer case, but this PR does not claim to close it without demonstrating stuck-session recovery), #27511 (suspend/resume — overlapping on graceful stop, but resumable suspension is a different guarantee than cancel).
Type of change
What does this PR do?
Lets a parent agent (via tools) or the human (via TUI esc) steer, gracefully cancel, or hard-abort a single running Task subagent mid-run, without affecting the parent or sibling subagents. Off by default behind
OPENCODE_EXPERIMENTAL_SUBAGENT_INTERRUPT.A small per-instance
Interruptservice holds one pending interrupt per child plus a terminal record. The child consumes it at its own runLoop turn boundary (session/prompt.ts): steer injects a <steer> frame and the child adapts and continues; cancel injects a <cancel> frame, records a terminal, and force-breaks within a small grace window; abort records a terminal and cancels the BackgroundJob immediately. Cancel is turn-boundary-soft — a child inside a long-running tool call only breaks once that tool returns, sotask_abortis the hard fallback.Surfaces:
task_steer/task_cancel/task_abort, gated bypermission.interrupt.POST /session/:id/interrupt(subagent sessions only; rejects non-running children).Interrupt tools reach any descendant, not just direct children, through a bounded ancestry walk that fails closed off the caller's subtree — otherwise a runaway grandchild is unreachable from the only session that can see it. They also accept a slug
task_id; a resolved slug still passes the same ancestry check, so a process-global handle cannot be used to reach outside the subtree.Every interrupt drops a distinct visible marker into the subagent transcript — ⊘ Steered / Cancelled / Aborted by user or by parent, with the reason — attributed to its origin (a human TUI action shows "by user"; an agent tool shows "by parent"). Untrusted reasons are XML-escaped at every sink and length-capped; both the tool
task_idpath and the HTTP endpoint verify the target is the caller's own subagent before doing anything.Why it can't hang or deadlock: the child only reads a pending interrupt at a turn boundary and never blocks on one, so there's no new wait. Cancel always terminates (grace window → force-break, or
task_abortas the hard fallback). With the flag off the tools are unregistered, the endpoint rejects, and TUI esc falls back to its existing behavior.How did you verify your code works?
New tests (no mocks): the Interrupt registry (request/consume/terminal/clear, cancel-overrides-steer, reason escaping, length cap), the runLoop consume + frame injection, the three tools (not_found / already_finished / running, permission routing, origin attribution), the ancestry walk including its exact hop bound, slug resolution still passing the descendant check, the abort marker for a model-less subagent (model derived from the latest user message), and a frame-breakout reason that must reach the model only escaped.
packages/opencodetargeted interrupt/tool/prompt/server suites: 3242 pass / 0 fail across the fullpackages/opencodesuite (dev baseline 3207)bun typecheckclean inpackages/opencodeandpackages/tui;packages/core's pre-existing dev-baseline failures are unchanged by this branch./packages/sdk/js/script/build.tsScreenshots / recordings
Subagent controls + interrupt menu
Steer — course-correct, then the subagent continues
Cancel — graceful wrap-up within the grace window
Abort — immediate hard stop
Checklist