后台任务与通知

后台 Bash、后台子 Agent 与工作流 run 怎样登记进同一张内存任务表,结束后怎样变成一条只给模型看的 task-notification 消息,在回合中途注入或在空闲时自动开一轮;单个任务的停止分派,以及桌面端后台面板怎样与运行时对齐。

作者 David更新于 28 篇(共 47 篇)

ZCode 里能放到后台跑的东西有三类:Bash 命令(显式带 run_in_background,或前台跑过期限后原地转后台,apps/zcode-cli/packages/core/src/tool/handlers/bash.ts:172)、后台运行的子 Agent,以及动态工作流的 run。它们各自怎么启动分别见 Bash子 Agent动态工作流(三)。这一篇讲它们共用的那一层:每个会话一张内存任务表 RuntimeTaskRegistry,任务结束时由运行时铸造一段 <task-notification> XML,作为一条只给模型看的用户消息送回主 Agent;停止走同一个分派函数,桌面端的后台面板读的是同一组事件。

位置职责
apps/zcode-cli/packages/core/src/runtime-task/任务表 registry.ts、通知格式器 notification.ts、工作流专属文案 workflow-notification-copy.ts、子 Agent 封口判定 notification-policy.ts
core/src/tool/executor/background-task*.ts工具返回“已转后台”之后的追踪器:登记、轮询、发事件、认领并投递通知
core/src/runtime/methods/background*.ts运行时侧:通知入队与持久化、停止分派、子 Agent 收尾时的封口与清扫
core/src/runtime/command-queue.tsmethods/runtime-command-*.ts通知搭乘的运行时命令队列(受理与引导的整体机制见输入受理
packages/shared/src/background-*.ts桌面端与服务层共用的控制项解析、合并、通知 XML 解析、输出快照 schema

任务种类与任务表

任务类型是一个五值联合,注释交代了为什么不把两种工作流合成一类(apps/zcode-cli/packages/core/src/runtime-task/registry.ts:10):

// local_dynamic_workflow 与 local_workflow 刻意分开:后者是 legacy `Workflow` 工具(不可取消),
// 前者是 workflow run(经 DynamicWorkflowRunPort.cancel 可取消)。合成一个类型,取消分派就无法区分。
export type RuntimeTaskType =
  | "local_agent"
  | "local_bash"
  | "local_workflow"
  | "local_dynamic_workflow"
  | "monitor_mcp";
类型谁往表里登记当前版本
local_bash追踪器,Bash 返回 backgrounded 时(apps/zcode-cli/packages/core/src/tool/executor/background-task-registry.ts:205在用
local_agent子 Agent runner 自己登记(apps/zcode-cli/packages/core/src/subagent/runner.ts:181448),追踪器只发事件在用
local_dynamic_workflow追踪器,CreateWorkflowAmendWorkflowResumeWorkflowRun 三个工具名(background-task-registry.ts:39在用
local_workflow追踪器,旧 Workflow 工具(background-task-registry.ts:207该工具的导入与注册条目都被注释掉了(apps/zcode-cli/packages/core/src/tool/handlers/index.ts:70136
monitor_mcp只有类型与名字映射,没有找到登记方

条目类型 RuntimeTaskSnapshot 扩展自子 Agent 的任务快照(registry.ts:43)。状态沿用 SubagentTaskStatus 的七个值(apps/zcode-cli/packages/contracts/src/interfaces/subagent.port.ts:87),除 running 外全算终态(registry.ts:103)。几个关键字段:

  • notified:终态结果“已送达”的认领令牌(subagent.port.ts:110),下文细说。
  • isBackgrounded:前台子 Agent 也登记在表里,但只有这一位为真、状态为 running 的条目才算“正在跑的后台任务”(registry.ts:288)。会话常驻池据此判断能不能回收会话(apps/zcode-cli/packages/core/src/runtime/methods/residency.ts:24)。
  • branchGeneration:登记时盖上当前对话分支代数(registry.ts:118)。回退对话会把代数加一(apps/zcode-cli/packages/core/src/runtime/methods/rewind-message.ts:581),旧分支上任务的迟到结果随之作废,回退本身见检查点、回退与分叉
  • pendingMessagesmessageSink:给运行中的子 Agent 转发消息用,归子 Agent
  • resultTextstopInitiator:工作流 run 的产物文本,以及是谁请求的停止("user""model"registry.ts:63)。

实现 InMemoryRuntimeTaskRegistryregistry.ts:112)就是一个 Map 加两组等待者:waitForTerminal 等终态,waitForBackgroundRequest 等“转后台”,都接受 AbortSignal。每个 AgentRuntime 构造时新建一张(apps/zcode-cli/packages/core/src/runtime/agent-runtime.ts:284),不落盘,进程重启即清空。

前台子 Agent 转后台的入口有两个:SubagentPort.backgroundTaskrequestBackgroundrunner.ts:532),或者 subagents.autoBackgroundMs 到时自动转(runner.ts:660)。当前版本里前者没有调用方,后者只在 core 的配置类型里声明(apps/zcode-cli/packages/core/src/runtime/types.ts:142),bootstrap 与桌面端都没有设置,这条路实际处于休眠状态。转入后台的子 Agent 会与父回合的中止信号脱钩(runner.ts:319),所以停掉前台回合不会连带停掉它。

生命周期

工具调用一结束,执行器在发出 ToolCallResult 事件之后调 trackBackgroundTaskapps/zcode-cli/packages/core/src/tool/executor/call-runner.ts:543)。它只认两种输出:statusbackgrounded,或 Agent 工具返回的 async_launchedapps/zcode-cli/packages/core/src/tool/executor/background-tasks.ts:805)。之后的步骤:

  1. 登记进任务表,发 BackgroundTaskStarted 事件(background-tasks.ts:108110)。
  2. 按工具名查一张“生命周期提供者”表,得到快照来源、终态等待者与能否取消(background-tasks.ts:739)。旧 Workflowcancellable 写死为假(background-tasks.ts:776)。两种来源都没有时,直接判 lostbackground-tasks.ts:134)。
  3. 有快照来源就每 1000 毫秒轮询一次(background-tasks.ts:353),有终态等待者的话同时挂上(background-tasks.ts:360)。运行中的更新只在 pid、字节数或输出尾部变化时才发 BackgroundTaskUpdatedbackground-tasks.ts:206437),这是唯一的“节流”,没有按时间合并。
  4. 看到终态:更新任务表,认领并投递通知,发 BackgroundTaskCompletedbackground-tasks.ts:285)。

执行端口的状态词在写进任务表时被折算:cancelledtimed_out 记为 killedspawn_error 等其余值记为 failedbackground-task-registry.ts:233)。子 Agent 运行时里的后台 Bash 另有一道上限,默认 3600000 毫秒(1 小时)到点就取消(apps/zcode-cli/packages/core/src/runtime/helpers/runtime-tools.ts:25,计时器在 background-tasks.ts:177)。工作流 run 用 ResumeWorkflowRun 恢复时沿用同一个 runId,登记函数会把上一轮的 notified、完成时间、错误等“结算面”整体复位,并重新盖分支代数,否则恢复后的终态通知会被当成已认领或旧分支残留吞掉(background-task-registry.ts:78)。

图表加载中…

同一个任务在各层用的是不同的状态词,读代码时容易混:

取值出处
任务表runningcompletedfailedcancelledkilledstoppedlostsubagent.port.ts:87
执行端口与事件载荷runningcompletedfailedtimed_outcancelledspawn_errorlostapps/zcode-cli/packages/contracts/src/interfaces/session.port.ts:163
通知的 <status>Bash 与工作流为 completedfailedkilled,子 Agent 为 completedfailedstopped,动态工作流另有真实终态词 completederroredstoppedbackground-tasks.ts:813apps/zcode-cli/packages/core/src/subagent/completion-notification.ts:4apps/zcode-cli/packages/core/src/runtime-task/notification.ts:53
桌面 v4 快照runningresultPendingfailedcancelledpackages/shared/src/zcode-protocol-v4/snapshot.ts:371
旧投影栈控制项pendingrunningcompletedfailedkilledlostpackages/shared/src/background-task-controls.ts:1

通知长什么样

formatTaskNotification 按任务类型分到三个格式器,其余类型走一个带缩进的通用格式(notification.ts:91)。Bash 的最精简(notification.ts:129):

function formatLocalBashTaskNotification(input: TaskNotificationInput): string {
  const lines = ["<task-notification>", `<task-id>${escapeLocalBashXml(input.taskId)}</task-id>`];
  if (input.toolUseId) {
    lines.push(`<tool-use-id>${escapeLocalBashXml(input.toolUseId)}</tool-use-id>`);
  }
  if (input.outputFile) {
    lines.push(`<output-file>${escapeLocalBashXml(input.outputFile)}</output-file>`);
  }
  lines.push(`<status>${escapeLocalBashXml(input.status)}</status>`);
  lines.push(`<summary>${escapeLocalBashXml(input.summary)}</summary>`);
  lines.push("</task-notification>");

  return lines.join("\n");
}

Bash 通知不带输出正文,只给输出文件路径,摘要形如 Background command "<描述>" completed (exit code 0)background-tasks.ts:843),模型要看输出得自己调 TaskOutput 或读文件。三种格式的差别:

类型额外字段转义与截断
Bash只转义 <>&,不截断(notification.ts:404
子 Agent<result>(子 Agent 的最终回答)、<error><usage>(token、工具调用次数、耗时)(notification.ts:144全量转义,总长超过 120000 字符截断并追加 [truncated]notification.ts:18380
工作流 run<stop-reason><result><reports><artifacts>,XML 之后再追加一段“怎样把结果呈现给用户”的指引(notification.ts:160235同上;指引、产物清单排在最后,截断时先被砍掉

工作流指引按终态分支写死了下一步:用户停的 run 明确要求“不要自行恢复”(notification.ts:264),模型自己 TaskStop 的 run 提示用 AmendWorkflow 修订,出错的 run 指向修改脚本。动态工作流运行中的“停滞”与“升级提问”也走同一条通知管线(apps/zcode-cli/packages/core/src/runtime/methods/dynamic-workflow-run-progress.ts:73143),细节见动态工作流(三)

认领:一个终态只送达一次

notified 是一个先到先得的令牌。追踪器必须先认领成功才入队(background-tasks.ts:536),入队抛错就归还令牌(background-tasks.ts:573);TaskOutput 读到终态结果时也会把它置真(apps/zcode-cli/packages/core/src/tool/handlers/task-output.ts:60),所以模型主动读过的任务不会再收到重复通知。子 Agent 的通知由 runner 自己投递并认领(runner.ts:1848),追踪器看到已认领的子 Agent 快照就只收尾不再通知(background-tasks.ts:264)。

即使认领成功,下面几种情况也不送达:

  • AmendWorkflow 替代的 run:发起修订的那次工具结果已经说明了一切,再来一条“你停了 run,去修订它”会把模型送进循环(background-tasks.ts:487)。
  • 运行时正在关闭:直接丢弃,不再启动模型轮次(apps/zcode-cli/packages/core/src/runtime/methods/background-notifications.ts:20runtime-tools.ts:242)。
  • 分支代数对不上:入队时查一次(background-notifications.ts:33),出队、持久化前再查一次(apps/zcode-cli/packages/core/src/runtime/methods/runtime-command-generation.ts:17)。
  • 子 Agent 已经结束:子 Agent 那一轮跑完(正常结束或被取消)后,其运行时把后台通知“封口”(apps/zcode-cli/packages/core/src/runtime/methods/subagent.ts:409),此后该子运行时里后台 Bash 的通知一律压下(apps/zcode-cli/packages/core/src/runtime-task/notification-policy.ts:9);如果子 Agent 是被取消的,还会顺手取消它名下仍在跑的后台 Bash(subagent.ts:414)。

送回主 Agent

通知以一条运行时命令入队(background-notifications.ts:44):

  const commandId = createRuntimeCommandId();
  this.enqueueRuntimeCommand({
    branchGeneration,
    createdAt: new Date(),
    id: commandId,
    mode: "task-notification",
    priority: "next",
    source: "background_task",
    originMeta: notification.originMeta,
    taskId: notification.taskId,
    text: notification.text,
    toolName: notification.toolName,
    traceContext: notification.traceContext,
  });

队列有 nownextlater 三档优先级(apps/zcode-cli/packages/core/src/runtime/command-queue.ts:10),但 core 里所有入队点写的都是 next(例如 apps/zcode-cli/packages/core/src/runtime/methods/prompt-admission.ts:116),实际就是先进先出。队列只在内存里,所以入队的同时还往会话输入账本写一条 backgroundNotificationbackground-notifications.ts:61);进程崩溃后重启恢复时,残留的这类记录被收口为 discarded,原因 session_resumedapps/zcode-cli/packages/core/src/runtime/methods/steering.ts:1340),因为后台子进程已随 CLI 一起退出。

之后分两条路:

回合进行中。回合循环每一轮开头、且已经完成过至少一次模型请求时,先把队列里的通知与子 Agent 回复取进本轮请求(apps/zcode-cli/packages/core/src/runtime/methods/turn-loop.ts:55,这段代码在输入受理里有全文)。也就是在一批工具结果之后、下一次模型请求之前注入;输出 token 截断后的续写期间不注入。每条通知各自持久化成一条消息,展示意图是 task_notification_steerapps/zcode-cli/packages/core/src/runtime/methods/runtime-command-active-loop.ts:76)。遇到排队中的“配置”控制轮就停下,把后面的通知留给外层按序处理(runtime-command-active-loop.ts:37)。注入后重复工具调用检测清零,回合循环本身见回合循环

回合之外。入队后立即尝试排空队列(apps/zcode-cli/packages/core/src/runtime/methods/runtime-command-queue.ts:34)。出队时如果队首是通知,队列会把同优先级的所有通知一起带走,而不是一条一条出(command-queue.ts:184)。整批只写一条消息,各段 XML 以空行拼接(background-notifications.ts:174),展示标题取前 3 个任务名加“+N”(background-notifications.ts:116149);注释说过去逐条启动模型轮,请求数会随积压线性放大(background-notifications.ts:192)。然后以 inputSource: "background_task"、仅模型可见、跳过 UserPromptSubmit 钩子的方式开一轮(runtime-command-queue.ts:345)。

所以主会话空闲时,后台任务一结束就会自动起一轮,没有开关;只有“立即发送排队消息”持有的前台提升租约会让它稍等(runtime-command-queue.ts:72)。这一轮结束后如果会话有活动目标,还会接着跑目标续跑循环(runtime-command-queue.ts:167,见目标模式)。

这条消息的角色是 user:运行时把它当作直接注入的用户消息,而不是包成系统提醒(apps/zcode-cli/packages/core/src/runtime/helpers/runtime-reminders.ts:99),落库时标 visibility: "model-only"background-notifications.ts:184)。桌面端把这样开出的一轮画成来源为 backgroundResult 的回合头(apps/zcode-cli/packages/bootstrap/src/zcode-protocol-v4/event-normalizer.ts:403)。

图表加载中…

无头模式 zcode -p 只在观察到动态工作流活动时才等后台工作结算(apps/zcode-cli/packages/cli/src/prompt-command.ts:315)。只有后台 Bash 或后台子 Agent 时,第一回合一结束就进入收尾;会话关闭时执行端口会把仍在跑的后台 Bash 记为取消(apps/zcode-cli/packages/adapters/src/exec/node-execution-adapter-base.ts:259),它们的通知也不会再送达。

停止:一条分派,三个入口

入口调用发起者已终结的任务
模型的 TaskStopbackgroundTaskControlPort.stopBackgroundTask,绑定到运行时的同名方法(runtime-tools.ts:170modelstrict: trueapps/zcode-cli/packages/core/src/tool/handlers/task-stop.ts:50工具报错“is not running”
桌面端 Stop 按钮v4 cancelBackgroundWork,经 cancelBackgroundTaskuserapps/zcode-cli/packages/core/src/runtime/methods/background.ts:50分派层宽松成功,cancelBackgroundTask 再补上 background_task_not_runningbackground.ts:67),网关据此回拒绝
TUI 的 /dwf cancel同上user回一句“Could not cancel”,设计上只针对工作流 run

三个入口最终都进 stopBackgroundTask:先从任务表与事件投影两处解析目标(background.ts:132),再按类型分派(background.ts:112):

  if (target.taskType === "local_agent") {
    return stopLocalAgentBackgroundTask.call(this, target);
  }

  if (target.taskType === "local_bash") {
    return stopLocalBashBackgroundTask.call(this, target, traceContext);
  }

  if (target.taskType === "local_dynamic_workflow") {
    return stopDynamicWorkflowBackgroundTask.call(
      this,
      target,
      unsupportedBackgroundStopResult,
      options.initiator,
    );
  }

  return unsupportedBackgroundStopResult(target);
  • 子 Agent:调 subagentPort.stopTask,任务表记 killed,通知里的状态写 stopped,后台事件写 cancelled,子 Agent 事件写 stopped(这组词集中定义在 runner.ts:1655);停止通知没能入队就把任务表恢复原状并报错(runner.ts:1713)。
  • Bash:先补发一条带 cancelRequestedAtcancellable 为假的 BackgroundTaskUpdated,让面板先把它标成不可再取消(background.ts:215),再调执行端口取消;端口已查不到这个任务就判 lostbackground.ts:231)。
  • 动态工作流:先把发起者写进任务表的 stopInitiator,再调 DynamicWorkflowRunPort.cancel,顺序不能反,否则终态通知可能读到空值(apps/zcode-cli/packages/core/src/runtime/methods/background-stop-dynamic-workflow.ts:45)。
  • Workflowmonitor_mcp:返回 background_task_cancel_not_supported

失败原因只有三个:background_task_not_foundbackground_task_not_runningbackground_task_cancel_not_supportedapps/zcode-cli/packages/core/src/runtime/methods/background-stop-types.ts:15)。

面向用户的“全部停止”在代码里没有找到,只有几处隐式清扫:子 Agent 被取消时停掉它名下在跑的后台 Bash(background.ts:294);会话关闭时先 beginShutdownshuttingDown 挡住通知(agent-runtime.ts:327),再停下本会话拥有的动态工作流 run,最后由执行端口把在跑的后台 Bash 记为取消(apps/zcode-cli/packages/bootstrap/src/app/session-facade.ts:267274297node-execution-adapter-base.ts:259)。

桌面端怎样与运行时对齐

事实只从运行时的三个 BackgroundTask* 事件出。展示类别 taskKindbashsubagentworkflow)在发事件处一次裁决(background-tasks.ts:3971157),cancellable 取自生命周期提供者(background-tasks.ts:402)。v4 投影把事件归约成快照里的 backgroundWorksapps/zcode-cli/packages/bootstrap/src/zcode-protocol-v4/product-projection.ts:4213):状态压成四个值,completed 记为 resultPendingtimed_outspawn_errorlost 都记为 failedproduct-projection.ts:4248);内容无变化不产出增量(product-projection.ts:4288)。workId 就是 taskId,对工作流 run 也等于 runId,全程不做身份翻译(background-stop-dynamic-workflow.ts:24)。

UI 这一侧:

  • 输入框旁的徽标统计正在跑的 Bash、工作流 run 与子 Agent 数量,三类合计大于零才显示(packages/ui/src/v4/composer/ConversationBackgroundWorkTrigger.tsx:1665);点开的是状态面板还是工作流详情,由宿主决定(ConversationBackgroundWorkTrigger.tsx:50)。
  • 状态面板只给仍在 running 的条目 Stop 按钮,已结束的条目没有可取消的东西(packages/ui/src/v4/conversationStatusPanelModel.ts:226)。
  • Stop 发出 cancelBackgroundWork {workId}packages/ui/src/v4/SessionPane.tsx:2118)。运行时明确回答“什么也没取消”时,网关以 fault.command.backgroundWorkCancelRejected. 加去掉 background_task_ 前缀的原因作为 ACK 返回,而不是假装成功(apps/zcode-cli/packages/bootstrap/src/zcode-protocol-v4/commands/handlers/interaction-background.ts:187packages/shared/src/zcode-protocol-v4/command.ts:289)。
  • 后台 Bash 的详情页经 v4/conversation/backgroundBashOutput 读一次文件快照(packages/shared/src/zcode-protocol-v4/transport.ts:320),最多 8192 字节(packages/shared/src/background-bash-output.ts:3);找执行器时沿父会话链往上走,绝不为了看输出去恢复运行时(apps/zcode-cli/packages/bootstrap/src/zcode-protocol/background-work-owner.ts:5)。
  • 侧栏的“有后台活动”标记也由 backgroundWorks 里有无 running 条目决定(apps/zcode-cli/packages/bootstrap/src/zcode-protocol-v4/sessions-index-projection.ts:43)。

snapshot.ts:369 的注释说 resultPending 条目在结果投递后消失,但投影里只看到把 completed 映射成 resultPending,没有找到移除条目的代码,从代码看条目会一直留在快照里。

packages/shared 下几个 background-* 文件服务的是旧投影栈:core 的事件归约维护 projection.backgroundTasks,旧协议把它映射成 backgroundJobsapps/zcode-cli/packages/bootstrap/src/zcode-protocol/session-mapper.ts:538),服务层再用 parseZCodeBackgroundTaskControlItems 解析、按 jobId 合并(packages/services/src/zcode-agent/zcodeTaskServiceAdapter.ts:1550packages/shared/src/background-task-control-merge.ts:3)。解析时 jobId 必须优先读 taskId,注释说落到 toolCallId 会让取消请求查无此任务(background-task-controls.ts:300)。同一层还会从用户消息里解析 <task-notification>,按 tool-use-id 回写原工具卡的状态(packages/shared/src/background-task-notifications.ts:1457);旧栈推导会话状态时只看活动回合、待审批与在跑的工具调用,后台任务不算在内(packages/shared/src/zcode-session-task-status.ts:20)。按运行 30 秒过滤可见任务的 collectVisibleZCodeBackgroundTaskControlItemsbackground-task-controls.ts:391)没有找到调用方,旧的 session/cancelBackgroundTask 协议方法也已标为废弃(packages/shared/src/zcode-protocol/index.ts:3579)。

TUI 没有后台任务面板。apps/zcode-cli/packages/tui/src/types.ts:317 声明了 cancelBackgroundTask,但没有调用方;唯一直接取消的入口是 /dwf cancel,缺省只从 pendingrunning 的 run 里挑(apps/zcode-cli/packages/cli/src/command-center/handlers/dwf.ts:1988)。其余后台任务只能让模型调 TaskStop

下一篇:定时任务与闲时任务——谁来调度定时任务与闲时任务,任务定义存在哪里,自动化回合为什么要禁掉一部分工具。

本页目录