Background Jobs
慢工作在别处完成时,Agent 可以继续推理。
job_start / job_output / job_list。
问题
s06 的子代理是阻塞的:父循环等它完成才继续。如果子代理要跑 5 分钟,父循环就干等 5 分钟——期间模型在烧 token 空转,什么也做不了。
同样的问题出现在长命令上:npm test 跑 3 分钟,git push 等 CI……慢工作不该阻塞推理。你需要:把工作丢到后台,拿到一个 id,继续推理,需要时再查结果。
解决方案
后台任务协议:
job_start启动任务,立即返回job id;job_output按 id 读取(部分)输出;job_list列出所有任务与状态;- 任务完成时,harness 给模型一条完成通知,模型决定是否继续。
const jobs = new Map(); // id -> { kind, status, output, startedAt }
let nextJob = 1;
define("job_start", {
type: "object", properties: { kind: { type: "string", enum: ["bash", "subagent"] }, label: { type: "string" }, command: { type: "string" } },
required: ["kind", "label", "command"],
}, ({ kind, command, label }) => {
const id = `${kind}-${nextJob++}`;
const job = { id, kind, label, status: "running", output: "" };
jobs.set(id, job);
runInBackground(job, kind, command); // 见下:不 await!
return JSON.stringify({ id, status: "running" });
});
工作原理
第 1 步:后台执行——启动任务后立即返回,任务完成时回调更新状态:
function runInBackground(job, kind, command) {
if (kind === "bash") {
const { exec } = awaitImport("node:child_process");
exec(command, (err, stdout, stderr) => {
job.output = stdout || stderr || err?.message;
job.status = err ? "failed" : "completed";
});
} else {
agentLoop(command, { tools: CHILD_TOOLS }).then((r) => {
job.output = r; job.status = "completed";
}).catch((e) => { job.output = String(e); job.status = "failed"; });
}
}
第 2 步:job_output 读取任务状态与输出(可增量读取),job_list 返回概览:
define("job_output", { type: "object", properties: { job_id: { type: "string" } }, required: ["job_id"] },
({ job_id }) => { const j = jobs.get(job_id); return j ? JSON.stringify(j) : `未知任务 ${job_id}`; });
define("job_list", { type: "object", properties: {}, required: [] },
() => [...jobs.values()].map((j) => `[${j.status}] ${j.id} ${j.label}`).join("\n") || "(空)");
第 3 步:完成通知——这是后台任务的关键体验:模型不该轮询,而该被通知。在每轮循环开始前检查是否有新完成的任务:
const notified = new Set();
function pendingCompletions() {
return [...jobs.values()].filter((j) => j.status !== "running" && !notified.has(j.id))
.map((j) => { notified.add(j.id); return `任务 ${j.id} 已完成:${j.status}`; });
}
// 每轮:completions 作为 user 消息注入
模型于是可以:"后台跑着测试,我先改代码;测试好了通知我,我再决定下一步。"
试一下
运行 s13:
- "后台运行 npm test,同时继续做别的事"——观察模型是否先启动任务、再干别的
- 用 job_output 查看进行中的输出
- 等完成通知到达后,让模型基于结果继续
观察重点:完成通知在什么时候注入最自然?如果模型不看通知就继续干,会怎样?(DSH 的做法:通知作为 user 消息进入队列,模型迟早会看到。)
以下内容基于 packages/jobs 与 docs/subsystems/jobs.md 的核查。DSH 的后台任务协议(ctx.jobs)比你的实现多几个关键设计:
一、所有权围栏(owner fencing)
- 每个 job 有所有者 Agent:访问被其 session id 围栏;Agent 销毁时任务被取消并等待。你的
jobsMap 任何人可读,DSH 的访问受所有权约束。 JobId是<kind>-N形式的品牌化 id(如bash-3),权限依赖所有者授权而非 id 保密。
二、生命周期与生产者契约
JobStatus:running | stopping | completed | killed | failed——比你的三元组完整,因为需要区分"主动停止"与"失败"。JobStart声明身份与启动器:运行时先做预检(preflight)再调用run();生产者拥有执行资源,运行时拥有身份与生命周期状态。
三、消费方工具
tool-jobs 暴露 job_output / job_list / job_kill 给模型——与你的工具同名(命名直接来自设计),并负责完成通知的模型可见格式。bash 与 subagent 是内置的 JobKind。
| 你的最小实现 | DSH 的真实实现 |
|---|---|
jobs Map 全局可见 |
owner-fenced 注册表,销毁即取消 |
| 完成通知手写注入 | tool-jobs 的完成通知协议(含输出上限 outputLimitBytes) |
| 状态三元组 | 五态生命周期 + 停止/等待语义 |
一句话:后台任务 = "执行与推理解耦"。DSH 的版本把这种解耦做成了有所有权、有生命周期、有通知协议的一等机制。