anthropics / anthropics/claude-agent-sdk-typescript

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

Abierto
#352 1 comentario 0 reacciones 0 asignados Ver en GitHub
enhancement
Lenguaje dominante
Shell
Estrellas
1.8k
Forks
226
Métricas de merge de PR
Sin PR fusionados en 30 d

Descripción

## 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)

Guía de contribución

No hay ninguna guía de contribución indexada para este repositorio

Evaluación

Este issue todavía no se ha evaluado.

Recibe los nuevos issues en tu correo

Un resumen breve de issues de GitHub para principiantes.