Skip to content

feat: add _session/stop_tasks to stop background sub-agents - #995

Open
jstrunk wants to merge 1 commit into
agentclientprotocol:mainfrom
jstrunk:feat/session-stop-tasks
Open

feat: add _session/stop_tasks to stop background sub-agents#995
jstrunk wants to merge 1 commit into
agentclientprotocol:mainfrom
jstrunk:feat/session-stop-tasks

Conversation

@jstrunk

@jstrunk jstrunk commented Aug 13, 2026

Copy link
Copy Markdown

Closes #994

What

Adds a request extension method that lets a client stop the background tasks (sub-agents) a session has dispatched. Mirrors the existing _session/steering ext-method exactly (underscore-prefixed method const + param validator, handler on the agent class, .onRequest registration beside steering, capability advertised in the initialize _meta).

_session/stop_tasks  { sessionId: string, toolUseIds?: string[] }
                  ->  { stopped: string[], notFound: string[] }

(both arrays are parent tool_use ids — the card ids a client already renders.)

Why

session/cancel aborts the turn, but the SDK's background tasks survive it by design. In the settled-dispatch shape there is no turn left to cancel at all, so a client's stop button is a full no-op while the sub-agents run on invisibly and unkillably. There was previously no ACP surface to stop them (no stopTask reference in the adapter).

Semantics

  • With toolUseIds: stop the live tasks whose spawning Agent/Task tool_use id matches.
  • Without toolUseIds: stop every live task marked isSubagent. Background Bash jobs are deliberately spared — they are not isSubagent, matching the discriminator used across the liveBackgroundTasks registry (a shell can outlive every turn; its contract is a wake-on-exit notification, not a turn-scoped stop).
  • Each stop is session.query.stopTask(taskId), awaited so the response reports what the SDK actually accepted. A per-task failure (the task settled between snapshot and stop, or the SDK rejects an unknown id) degrades that id to notFound rather than failing the batch.
  • Reading liveBackgroundTasks is safe here: a stoppable task has not settled, so its entry is live by definition. A task that finishes in the snapshot→stop window simply lands in notFound — the honest answer.
  • Capability advertised at initialize via _meta.stopTasks.supported: true, mirroring _meta.steering.supported. This lets a client gate the call; an unpatched agent answering -32601 would otherwise be silently swallowed by fire-and-forget senders.

Downstream

stopTask causes the SDK to emit a task_notification with status 'stopped', which flows through the existing task_notification handling — no new terminal path is introduced here.

Testing

npm run build (tsc) is clean; npm run lint and prettier --check are clean. New unit tests in src/tests/acp-agent.test.ts under describe("stop background tasks (_session/stop_tasks)"), all passing:

  • stops the tasks named by explicit toolUseIds and returns them in stopped``
  • reports an unmatched explicit toolUseId in notFound``
  • without toolUseIds stops every live sub-agent and spares background Bash
  • a stopTask that throws lands the id in notFound and does not fail the batch
  • advertises the stopTasks capability at initialize
  • rejects when the session is unknown
  • rejects when the query stream has already closed

Full suite: 771 passed, 2 failed — both failures are pre-existing on clean main (context-window-seeding assertions in create-session-options.test.ts), unrelated to this change and reproduced with these changes stashed.

Behavioral probing against a running adapter (in a downstream integration) showed the expected early settle + tool card close + no result streamed for the stopped sub-agent. This does not claim OS process-level kill verification — only the observable ACP-level outcome.

Closes agentclientprotocol#994

session/cancel aborts the turn, but the SDK's background tasks (sub-agents)
survive it by design — and in the settled-dispatch shape there is no turn to
cancel at all, so a client's stop button is otherwise a no-op while the
sub-agents run on invisibly. There was no ACP surface to stop them.

Adds a request extension method mirroring _session/steering:

  _session/stop_tasks { sessionId, toolUseIds?: string[] }
    -> { stopped: string[], notFound: string[] }   (parent tool_use ids)

With toolUseIds, stop the live tasks whose spawning Agent/Task call id
matches; without, stop every live isSubagent task (background Bash spared,
matching the discriminator used across the liveBackgroundTasks registry).
Each stop is session.query.stopTask(taskId); a per-task failure degrades to
notFound rather than failing the batch. The SDK emits a task_notification
with status 'stopped', which the existing terminal path turns into a card
close. Capability advertised at initialize via _meta.stopTasks.supported.

Adds unit tests covering: stop by explicit toolUseIds, stop-all sub-agents
with Bash spared, per-task stopTask failure degrading to notFound without
failing the batch, and the capability advertisement at initialize.
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.

Client stop cancels the turn but leaves background sub-agents running — expose a stop primitive

1 participant