anthropics / anthropics/claude-agent-sdk-typescript

feat: interrupt the foreground turn without killing backgrounded subagents/tasks

オープン
#352 コメント 1 件 リアクション 0 件 担当者 0 名 GitHub で見る
enhancement
主要言語
Shell
スター
1.8k
フォーク
226
PR マージ指標
30日以内にマージされた PR はありません

説明

## Problem

`query.interrupt()` is the only way to stop an in-flight turn while keeping the
session alive, but it tears down **all** in-session tasks — including subagents
that were explicitly backgrounded. There is no way to cancel just the foreground
turn (model generation + foreground tools) while letting already-backgrounded
work keep running.

This is the same gap noted in #275 ("interrupt() / abortController to kill
everything"), but from the other side: even after a task is backgrounded, there
is no interrupt that spares it.

The interactive Claude Code CLI preserves background work when you press Esc
(background agents run on the on-demand daemon, outside the interruptible
session). SDK/headless consumers have no equivalent.

## Current behavior (measured on 0.3.177)

| Background work | `interrupt()` |
|---|---|
| `run_in_background` Bash | survives ✅ (detached OS process) |
| Monitor | survives ✅ |
| Subagent (`Agent` tool, `run_in_background:true`) | **killed** ❌ (`task_updated {status:"killed"}` → `task_notification: stopped`) |

Calling `backgroundTasks()` first (sets `is_backgrounded:true`) does **not** help —
a subsequent `interrupt()` still kills the subagent.

I re-checked the newest release (**0.3.178**): the `Query` control methods are
still `interrupt()` / `setPermissionMode` / `setModel` / `stopTask` /
`backgroundTasks` / `setMcpServers` / `close`. `interrupt()` takes no options,
and there is no `interruptForeground()` or per-task `backgroundTask(taskId)`
(the #275 ask), so this is not addressed yet.

### Looks like a behavior change between 0.2.x and 0.3.x

Same repro, same prompt:
- **0.2.140** → backgrounded subagent runs to completion after interrupt
(`task_notification: completed`) — 2/2 runs.
- **0.3.177** → backgrounded subagent is killed (`task_notification: stopped`)
— 4/4 runs (bare interrupt, interrupt-of-a-later-turn, and after
`backgroundTasks()`).

If the intended model is "interrupt kills everything", then 0.2.140's survival
was incidental — but either way there's currently no supported way to get the
"stop foreground, keep background" behavior that the interactive CLI has.

## Use case

We run Claude Code headless via the SDK, one long-lived streaming session per
user. When the user sends a follow-up mid-turn we `interrupt()` and resend a
merged prompt (to coalesce rapid messages). On 0.3.x this now destroys any
in-flight backgrounded subagent — i.e. a normal follow-up message silently kills
the user's long-running background work. We can't tell foreground from
background at interrupt time well enough to avoid it without re-deriving task
state from the message stream.

## Proposed solutions (any one would suffice)

1. An option on interrupt:
`interrupt({ preserveBackgroundTasks: true })` — stop the foreground turn,
leave `is_backgrounded` tasks running.
2. A dedicated control request, e.g. `interruptForeground()`, mirroring the
CLI's Esc semantics (stop generation + foreground tools only).
3. At minimum: guarantee + document that `is_backgrounded:true` tasks survive
`interrupt()` (so `backgroundTasks()` + `interrupt()` becomes a usable
"foreground-only stop" pattern). Today they don't (see above).

This pairs naturally with #275 (detach a running task) — detach + a non-destructive
interrupt would together give SDK consumers the full Ctrl+B / Esc behavior.

## Minimal repro

```ts
import { query } from "@anthropic-ai/claude-agent-sdk";

async function* prompt() {
yield {
type: "user" as const,
parent_tool_use_id: null,
message: { role: "user" as const, content: [{ type: "text" as const, text:
"Use the Agent tool with run_in_background:true and subagent_type:'general-purpose' " +
"to run `sleep 12 && echo SUBDONE` via Bash. The instant it's launched, reply " +
"exactly: STARTED. Do not wait for it." }] },
};
await new Promise(() => {}); // keep streaming input open
}

const session = query({
prompt: prompt(),
options: {
permissionMode: "bypassPermissions",
includePartialMessages: true,
env: { ...process.env, CLAUDE_CODE_EMIT_SESSION_STATE_EVENTS: "1" },
},
});

let fired = false;
for await (const m of session) {
if (m.type === "system" && m.subtype === "task_notification")
console.log("task_notification:", m.status, "—", m.summary);
if (m.type === "assistant") {
const text = m.message.content
.filter((b) => b.type === "text").map((b) => (b as { text: string }).text).join("");
if (text.includes("STARTED") && !fired) {
fired = true;
setTimeout(() => session.interrupt(), 3000); // subagent still sleeping
}
}
}
// 0.2.140 → "task_notification: completed …"
// 0.3.177 → "task_notification: stopped …" (subagent killed)
```

## Environment
- `@anthropic-ai/claude-agent-sdk` 0.3.177 (regression vs 0.2.140); confirmed still absent in 0.3.178 (latest at time of filing)
- Runtime: Bun 1.3.x, macOS (also reproduces headless)

コントリビューションガイド

このリポジトリのコントリビューションガイドは索引されていません

評価

この issue はまだ評価されていません。

新しい issue をメールで受け取る

初心者向けの GitHub issue を短くまとめたダイジェスト。