# 后台任务系统

> AgentTaskService 的开放任务类型、把「等用户回答」也建模成任务、落盘与 lost 状态、两阶段 kill、完成通知，以及四个后台工具。

- 作者：David（道雾轩）
- 专栏：Kimi Code 源码解读（https://daiw.org/manual/kimi-code.md）
- 最后更新：2026-09-19
- 原文：https://daiw.org/manual/kimi-code/background
- 转载与引用：请注明出处并附原文链接（https://daiw.org/about/copyright）

[上一篇](https://daiw.org/manual/kimi-code/shell-tools)提到 Bash 的前台执行本身就是一个前台任务。v0.29 的 `BackgroundManager`（`agent/background/index.ts`，1102 行）随 v1 引擎一起删除，现役的是 `agent-core-v2/src/agent/task/` 下的 `AgentTaskService`。

## 目录结构

```text
packages/agent-core-v2/src/agent/task/
├── task.ts             IAgentTaskService 接口
├── taskService.ts      AgentTaskService（1632 行）
├── types.ts            AgentTask / AgentTaskSink / 任务状态
├── persist.ts          AgentTaskPersistence（落盘）
├── configSection.ts    task 配置段
└── notificationXml.ts  完成通知的渲染
```

`taskService.ts` 一千六百多行——后台任务的生命周期管理并不像看上去那么简单。具体的任务类型不再集中在这里，而是跟着各自的工具走。

## 开放的任务类型

```ts
export interface AgentTask {
  readonly idPrefix: string;
  readonly kind: AgentTaskKind;
  readonly description: string;
  readonly timeoutMs?: number;

  start(sink: AgentTaskSink): void | Promise<void>;
  onDetach?(): void;
  forceStop?(): Promise<void>;
  toInfo(base: AgentTaskInfoBase): AgentTaskInfo;
}
```

任务只需实现 `start(sink)`：往 `sink.appendOutput()` 写输出，结束时用 `sink.settle()` 报告结局，`sink.signal` 被中止就该收手。`AgentTaskKind` 由 `AgentTaskInfoByKind` 的键推导，各任务类型用 `declare module` 给它补键——新增类型不必改管理器。

| `kind` | 实现 | 代表什么 |
| --- | --- | --- |
| `process` | `agent/tools/os/bash/process-task.ts` | 后台 shell 进程 |
| `agent` | `agent/tools/agent/subagent-task.ts` | 后台运行的[子代理](https://daiw.org/manual/kimi-code/subagents) |
| `question` | `agent/tools/ask-user-question/question-background-task.ts` | 等待用户回答的提问 |

`track()` 还能把 App 层 `ITaskService` 的通用任务句柄纳入同一套管理。

<Callout type="info">
  **把「等用户回答」也建模成后台任务，仍是这个模块最巧妙的决定。**

  `AskUserQuestion(background=true)` 把提问包成 `QuestionBackgroundTask`：它和进程、子代理一样处于 `running` 状态、可以被停止、结局以通知送回模型；回答不超过 16000 字节时直接内联在通知的 `<answer>` 块里。

  既然形状一样，就没有理由为它单开一套机制：通知、`TaskOutput`、`WaitFor` 都**不必区分「在等机器」还是「在等人」**。
</Callout>

## 状态、超时与两阶段 kill

```ts
export type AgentTaskStatus =
  | 'running'
  | 'completed'
  | 'failed'
  | 'timed_out'
  | 'killed'
  | 'lost';
```

超时由管理器计时：到点后，允许自动转后台的前台 Bash 被 `detach`，其余一律以 `timed_out` 收场。停止走 `terminateWithGrace()`：先中止任务（`ProcessTask` 据此发 `SIGTERM`），等待宽限期，仍未收场再调任务自己的 `forceStop()`（进程任务发 `SIGKILL`）：

```ts
    const graceMs = resolveAgentTaskConfig(this.config)?.killGracePeriodMs ?? SIGTERM_GRACE_MS;
```

`SIGTERM_GRACE_MS` 是 5 秒。注意杀进程的逻辑**归管理器所有**：工具只负责把任务注册进来，生死由同一个地方管，不会出现游离在管理之外的进程。

其他闸门：`maxRunningTasks` 限制同时运行的后台任务数（超出报 `task.limit_exceeded`）；`keepAliveOnExit` 决定退出时要不要保留任务。

## 落盘：记录跨会话，进程不跨

`AgentTaskPersistence` 把任务信息按 `<taskId>.json` 存进文档存储的 `tasks` 作用域，输出追加到会话目录下的 `agents/<agentId>/tasks/<taskId>/output.log`。输出在内存里只保留最近 1 MiB；后台任务一开始就写日志文件，前台任务在积压超过 1 MiB 或转入后台时开始写（前台 Bash 输出超过 5 万字符时也会提前写）。

会话恢复时，磁盘上的任务先以「幽灵」（`ghosts`）身份载入，其中仍标着 `running` 的一律改为 **`lost`**——新进程已经无法观察或控制它们。所以准确的说法是：**任务的记录和输出活过进程，进程本身不跨。**默认退出时 `stopAllOnExit()` 会停掉所有任务（打开 `keepAliveOnExit` 例外）；恢复后模型会收到一条提醒，列出上次会话留下的任务，子代理还附带用 `Agent(resume=...)` 续跑的写法。

这和[会话持久化](https://daiw.org/manual/kimi-code/session)是同一个诉求在不同层面的体现：用户很可能在任务跑到一半时关掉终端，重开后至少要知道发生过什么。

## 完成通知

后台任务结束时，管理器用 `loop.notify()` 把一条通知消息排进主循环：`<notification>` 块里是状态摘要，外加输出预览或日志路径——模型不用轮询。配套机制：

- 通知按「任务 ID + 状态」去重，已经送达的不会重复送。
- `TaskStop` 停任务前先调 `suppressTerminalNotification()`，不再额外通知。
- 上下文[压缩](https://daiw.org/manual/kimi-code/compaction)后，下一轮注入一段提醒，列出仍在运行的任务，防止模型重复启动。

## 四个后台工具

`agent/tools/task/` 把管理器的能力暴露给模型：

| 工具 | 作用 |
| --- | --- |
| `TaskList` | 列出任务，默认只列未结束的（`active_only=true`），`limit` 默认 20、最多 100 |
| `TaskOutput` | 非阻塞快照：结构化元数据、最后 32 KB 的输出预览、完整日志的 `output_path` |
| `TaskStop` | 停止任务；已结束的直接返回当前状态 |
| `WaitFor` | 在**当前回合内**等后台任务结束，`timeout` 最长 600 秒 |

`WaitFor` 是 v0.38.0 加入的（实验开关 `wait_for`，默认开）：模型真正依赖某个结果时可以原地等待，不必结束回合、等通知再被唤醒；被 `WaitFor` 报告过的结果不会再发一次通知。v0.29 的 `ExecutableToolResult.message` 侧信道在 v2 契约里已经没有了，`TaskOutput` 返回的就是普通文本。

## 输出去哪了

| | 去向 | 为什么 |
| --- | --- | --- |
| 前台 | 回调实时收集，同时写进任务输出记录 | 模型在等，需要流式反馈（`ToolUpdate` 的 `kind: 'stdout'`） |
| 后台 | 任务输出记录，落盘为 `output.log` | 没人在等，需要**可回查的存档**（供 `TaskOutput` 与 Read） |

因为前台输出从一开始就同步写进任务记录，「跑着跑着转后台」时输出不会丢，只是换了一个消费者。

下一篇：[Web 工具](https://daiw.org/manual/kimi-code/web-tools)。
