true 强制后台;false 强制同步;null/省略才用启发式(cargo build/compile/deploy/docker build/npm install/pip install/pytest 等关键词)。工具定义中的 concurrency: background_eligible 是第二道门。
const backgroundShellInputSchema = z.strictObject({
command: z.string().min(1),
run_in_background: z.boolean().nullable().optional().default(null),
});
function shouldRunInBackground(input) {
if (input.run_in_background !== undefined && input.run_in_background !== null)
return input.run_in_background;
return markers.some(m => input.command.toLowerCase().includes(m));
}
Pre Hook 阻断时,不创建 job;权限拒绝时,不创建 job、不启动 executor、不调用 shell handler;权限通过后才按『容量检查→持久化 running→登记 worker』提交。
parse + validate → Pre Hook → PermissionPolicy → dispatcher
→ 容量检查 → 持久化 running → 登记 worker → 返回占位
后台命令未完成时,原始 tool_call_id 立即得到占位:{job_id, status:running, tool_name}——它是普通成功 ToolResult,只说「已受理」,不说「已完成」。真实结果包成 typed RuntimeEvent(kind=background_job,必带 eventId)注入为 role=user 消息:后台结果与原 tool_call 已无配对关系,硬塞成第二条 tool 消息会直接破坏协议。
占位: {"job_id":"...","status":"running","tool_name":"shell"}
事件: {"runtime_event":{"event_id":"...","kind":"background_job",
"status":"completed","result":{"content":"404 passed"}},
"batch":{"index":0,"total":1}}
Task 说「这件事该做」,后台 job 说「这次执行怎么样了」——一个 Task 可对应零/一/多次执行尝试。job 六状态:running 唯一活状态,五终态不可变。终态由 finishRunning 条件迁移仲裁:谁先写进磁盘谁定终态,晚到一方拿到 undefined 安静跳过发事件——「只发一次」靠磁盘状态,不靠内存标记位。
running -> completed / failed / timed_out / cancelled
restart: running -> interrupted(重启不重放,恢复幂等)
每轮模型请求前 drain events;模型暂时返回无工具文本而仍有 active job 时,事件泵等待终态事件,在同一个 run() 中继续下一次模型请求。事件按 eventId 去重并带 batch 位置。
let events = pump.drainEvents();
if (!events.length && waitForPendingWork && pump.hasPendingWork)
events = await pump.waitForEvents();
for (const [i, ev] of injected.entries())
history.push(runtimeEventMessage(ev, { index: i, total: injected.length }));
刻意不做进度事件:3 分钟每秒报一次 = 180 条 user 消息吃光上下文预算。模型想知道进度就调 query_background_job 查一次——这正是查询工具存在的意义。同样刻意不做的:排队(满了直接报 background_capacity,否则「提交成功」变成谎言)、抢占式取消(JS 杀不死 Promise,协作式发信号+等+超时报错)、子 Agent 后台、自动重试。P13 的后台任务本质是 Promise 不是子进程——架构可平移,换的只是 BackgroundOperation 内部实现。
PreToolUse → 权限 → 后台提交(不在之前)。
容量 4、单 job 超时 120s、关闭时限 10s。
闭合当前工具轮,快工具继续。
BackgroundJobEvent → EventInbox → 普通 user 消息。
query_background_job / cancel_background_job 按 job_id。