后台任务系统

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

作者 David更新于 第 15 篇(共 33 篇)

上一篇提到 Bash 的前台执行本身就是一个前台任务。v0.29 的 BackgroundManager(agent/background/index.ts,1102 行)随 v1 引擎一起删除,现役的是 agent-core-v2/src/agent/task/ 下的 AgentTaskService。

目录结构

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 一千六百多行——后台任务的生命周期管理并不像看上去那么简单。具体的任务类型不再集中在这里,而是跟着各自的工具走。

开放的任务类型

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实现代表什么
processagent/tools/os/bash/process-task.ts后台 shell 进程
agentagent/tools/agent/subagent-task.ts后台运行的子代理
questionagent/tools/ask-user-question/question-background-task.ts等待用户回答的提问

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

把「等用户回答」也建模成后台任务,仍是这个模块最巧妙的决定。

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

既然形状一样,就没有理由为它单开一套机制:通知、TaskOutput、WaitFor 都不必区分「在等机器」还是「在等人」。

状态、超时与两阶段 kill

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

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

    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=...) 续跑的写法。

这和会话持久化是同一个诉求在不同层面的体现:用户很可能在任务跑到一半时关掉终端,重开后至少要知道发生过什么。

完成通知

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

  • 通知按「任务 ID + 状态」去重,已经送达的不会重复送。
  • TaskStop 停任务前先调 suppressTerminalNotification(),不再额外通知。
  • 上下文压缩后,下一轮注入一段提醒,列出仍在运行的任务,防止模型重复启动。

四个后台工具

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 工具。

本页目录