feat(opencode): interrupt a running subagent — steer / cancel / abort#32425
Open
iceteaSA wants to merge 1 commit into
Open
feat(opencode): interrupt a running subagent — steer / cancel / abort#32425iceteaSA wants to merge 1 commit into
iceteaSA wants to merge 1 commit into
Conversation
Experimental capability for a parent agent or human operator to steer, gracefully cancel, or hard-abort a specific running Task subagent mid-run, without affecting the parent or sibling subagents. Core: - Interrupt service (session/interrupt.ts): process-local registry holding one pending interrupt per child plus a terminal record; steer/cancel frame renderers and a visible-marker renderer, both with origin attribution (user vs parent); reason length-capped and XML-escaped at every sink (frames AND the visible marker). - The child consumes pending interrupts at the runLoop turn boundary: steer injects a <steer> frame and a visible "Steered by ..." marker and continues; cancel injects <cancel> + a visible marker, records a terminal, and force-breaks within a grace window. abortChild writes a visible "Aborted by ..." marker (model/agent derived from the child's latest user message), records a terminal, and cancels the BackgroundJob. Agent tools (gated by permission.interrupt): - task_steer / task_cancel / task_abort (origin=parent). Human paths: - POST /session/:id/interrupt (intent steer|cancel|abort, origin=user), restricted to subagent sessions, gated by the experimental flag, and rejecting non-running children. - TUI: esc on a subagent opens a Steer/Cancel/Abort menu, then a reason prompt; markers render as "... by user". Bound at the session route via a uniquely-named gather bucket (the keymap gather() caches by name). Visible interrupt markers render as a distinct "Interrupt" line (tagged via part.metadata.interrupt), not as user prose. Whole feature gated by OPENCODE_EXPERIMENTAL_SUBAGENT_INTERRUPT (off by default): agent tools, HTTP endpoint, and TUI affordance. Limitations: agent-driven steer/cancel applies to background children only (a foreground child blocks the parent turn); cancel is boundary-soft (use task_abort / Abort for a child stuck in a long tool call).
0603d35 to
579eef5
Compare
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.
Issue for this PR
Related to #21458 (a running subagent can't be steered/guided), #23534 ([sub-agents] not killed on cancel), and #28738 (interrupting the main agent doesn't stop background subagents). This adds the small experimental primitive those depend on rather than fully resolving each.
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).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 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: 365 pass / 0 fail; fullbun testpassesbun 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