调研对象:DeepSeek Harness(
deepseek-harness仓库,下称 dsh) 调研范围:agent-loop 轮次引擎、同会话 goal 体系、Ralph 新鲜代理循环、以及支撑它们的 workflow / subagent / session 事件源等机制 调研方法:源码精读(packages/*)+ 决策记录交叉验证(.agents/notes/implemented/*)+ 文档核对(docs/) 结论前置:dsh 没有也不打算做一个通用loop服务。 它把"长程任务"显式拆成两个正交策略——同会话 Goal 延续与新鲜代理 Ralph 循环——叠加在一个可替换的 turn/step 引擎(dsh-agent-loop)之上,全部以插件形式挂在公共扩展点上。
1. 摘要
dsh 对"长程任务"的处理可以概括为三句话:
底层只有一个可替换的 turn/step 引擎(packages/core/agent-loop)。它拥有一轮 turn:排空已受理输入、执行若干"模型请求+工具调用"的 step、然后停止。它不关心"目标"这种外层概念。 长程目标 = 引擎之上的两种显式策略,彼此独立: 同会话 Goal:在一个 session 内保留一个持久目标(packages/goal/goal),由轮次驱动插件(packages/goal/goal-round-driver)在条件满足时通过公共 Agent.followup() 自动再开一轮 turn;模型通过 get_goal / create_goal / update_goal 三个工具读写生命周期,人类通过 /goal 命令直接控制。 Ralph 循环:一个前台、固定脚本的 workflow(packages/workflow/tool-ralph),每轮生成一个全新、无会话种子的子代理,只携带不可变目标与上一轮的结构化交接报告;共享工作区是跨轮长期记忆。 贯穿性设计原则:everything-is-a-plugin、模型可见 ⟺ 已记录(事件源 session log)、持久状态与进程内"执行授权"分离、执行期权威校验(人类消息 vs 自动轮)、慷慨但受部署约束的轮数预算、以及"完成/阻塞是声明而非认证"的显式信任边界。
一个目标层级 Goal → Goal Round → Turn → Step 用于同会话延续;Ralph Run → Ralph Round → fresh child Turn → Step 用于新鲜代理迭代。"Round"是外层策略迭代,不是每个会话轮次的同义词。
2. 术语与层级(词典先行)
| turn | docs/glossary.md | |
| step | ||
| round | ||
| goal | active/paused/blocked/complete 相位与轮数上限;blocked 保留策略码与解释 | |
| goal activation | armed / disarmed | |
| Ralph loop | ||
| Ralph handoff |
3. 总体架构:为什么没有 packages/loop/
3.1 决策源头
核心决策记录是 .agents/notes/implemented/feature/2026-07-16-harness-level-loop.md("Harness-level goal-based execution")。它明确否决了一个"推测性的通用 loop 服务",因为 Evaluator、BudgetPolicy、RoundHandoff、GoalReflector、后台任务所有权、持久化、调度这些不是一个必须被揉在一起的一致性抽象。仓库里不存在 LoopDriver、LoopId、通用 StopCondition 或模型可见的通用 loop 工具。
两种策略共享仓库里普通的 agent、session、tools、workflow、subagent、UI 扩展点,但不假装一个生命周期能同时服务两者。第三种策略(定时 /loop / 调度执行)明确不在该决策内——它属于 scheduler 而非 goal 家族(注:仓库后来确实有了 packages/schedule,但定位是"面向未来根代理的持久提醒",见 §10.3)。
3.2 包拓扑与各自拥有的动词
@deepseek-ai/dsh-goalpackages/goal/goal/) | GoalIdGoalRef、GoalSnapshot、四态 GoalPhase、结构化 GoalBlockReason、进程内 GoalActivation、重放折叠,以及 get/create/edit/pause/resume/complete/block/clear/disarm | |
@deepseek-ai/dsh-tool-goalpackages/goal/tool-goal/) | get_goal/create_goal/update_goal;要求"当前 live 根代理轮次中的直接人类消息";把自动轮权限收窄为只能报 complete/blocked | |
@deepseek-ai/dsh-goal-round-driverpackages/goal/goal-round-driver/) | ||
@deepseek-ai/dsh-command-goalpackages/goal/command-goal/) | /goal 状态/创建/编辑/暂停/恢复/清除 | |
@deepseek-ai/dsh-tool-ralphpackages/workflow/tool-ralph/) | ralph({ objective, maxRounds? }),校验 fresh provider 与有界 RalphRoundReport,返回 complete / blocked / budget-limited |
底层的 dsh-agent-loop 仍是唯一的 turn/step 引擎;同会话驱动只用了公共 agent/session 事件,它向核心的唯一新增是一个通用的"取消前观察"通知 agent/cancel-requested(任何生命周期策略安全地结算取消都需要它)。
3.3 两种策略的本质差异
maxGoalRounds | maxRounds | |
4. 基础层:dsh-agent-loop 的 turn/step 引擎
4.1 职责边界
packages/core/agent-loop/src/index.ts(713 行)是具体驱动:AgentLoop extends Service implements AgentFactory,暴露为 ctx.agentLoop。它创建 ReactLoopAgent、把 agent 与 session 发布进注册表、并拥有有序 teardown。turn/step 推进完全由 ReactLoopAgent 内部状态机(Phase = idle | maintenance | running{turn, step, wakeRequested})驱动。
抽象层 packages/core/agent/ 只定义 Agent 接口、AgentRegistry(get/list/roots/enter/announce)与进程内 initiator 作用域(AsyncLocalStorage),不实现任何驱动逻辑——"Plugins, not loop changes"。
4.2 输入:唯一的消息载体与两条队列
所有输入都是 UserMessage(role: user),带 MessageSource 归属(merge-extensible:user / plugin / model / tool / goal / user-rpc…)。ReactLoopAgent 暴露三个入口,落在 inbox 的两条持久队列上(packages/core/agent/src/inbox.ts):
followup() | next-turn | ||
steer() | next-step | agent/turn-stopping 续跑、人类中途介入) | |
inject() | next-step |
inbox 是可重放的持久投影:每次 splice 都落一条 durable agent/inbox/spliced 事件(先落日志、后改投影),取消/清除以 canceled splice 记录。claim(target, turn) 批量取走 next-step、并在 turn 边界再取 1 条 next-turn,逐条发 agent/inbox/claimed。
4.3 turn/step 流程(与事件对应)
turn/start claim next-step 输入 + 1 条 next-turn 消息 组装 systemPrompt + 工具 schema -> agent/pre-step waterfall reject | enter(messages) reject 或首个 enter 被改空 -> 无 step 关轮(仍留 turn 记录) step/start append user/message(surfaceOp: append) 从日志派生模型历史 agent/request -> llm/stream -> assistant/chunk* -> assistant/message tool/call* -> tools/pre-execute -> tools/execute -> tools/post-execute -> tool/result* step/end 工具要求再发请求,或 next-step 又来了 -> claim -> 下一个 step -> agent/turn-stopping(serial,可 steer 续跑)turn/end
turn/*、step/*、user/message、assistant/*、tool/* 都是 durable session 事件;其余是三个域上的 live 扩展点。agent/pre-step、agent/request、agent/request-error、tools/* 是 waterfall(监听器必须 next() 委托),agent/turn-stopping 是 serial。
4.4 取消:一个 typed cause,一个每轮 AbortSignal
AgentCancelCause = {kind:'user'} | {kind:'parent'} | {kind:'hook', reason} | {kind:'disposed'}。 agent.cancel(cause) 是宽取消:默认先清 inbox(产生 canceled splices),再 abort 当前活动阶段的 controller;首个 cause 生效。keepInbox 可只中止活动轮、保留排队输入。 每个 prospective turn 私有持有一个 TurnCancellation(单个 AbortController),从 inbox claim、pre-step、prompt 组装、每个 step、模型与工具执行到 agent/turn-stopping 全程传递;turn/end 发布前立即清除——终端事件观察者与随后的持久化 flush 无法取消已完成的工作。 协作式取消:loop 在 await 边界前后检查中断,但不 Promise.race 抛弃 in-process 监听器/适配器/工具 Promise;忽略信号的工作必须在 whenIdle() 前自行收敛。 agent/cancel-requested 是 observe-only 通知(进程内、不持久),其监听器可同步排队替代工作(会被本次 clear 一并清掉),但不能 veto 停止。 落盘只记粗粒度的 durable {kind:'aborted'} 结局,不记 user/parent 等运行时原因。
4.5 turn 结局分类(TurnEndReasonMap,merge-extensible)
completed / aborted{reason}(取消)/ blocked(pre-step reject)/ error{LlmFailure} / max-tokens(粘性:后续正常 step 不能降级结局)/ interrupted(持久化后端修复崩溃孤儿轮专用,loop 从不发)。max-tokens 与 RATE_LIMIT/QUOTA 等结构化失败对 goal driver 的结算至关重要(见 §6.5)。
4.6 工具调度(tool-calls.ts)
按 ctx.tools.executionMode 分组:独占调用形成屏障,并行调用走 maxParallelToolCalls 有界滚动池(默认值见 DEFAULT_MAX_PARALLEL_TOOL_CALLS,运行时可热更新,下一分组生效)。结果按模型顺序提交,tool/result 以 sourceEventSeqs 引用对应 tool/call。取消先排干已启动调用并接受其 context,再为未启动调用补合成 tool/call+tool/result 对(TOOL_ABORTED_BEFORE_DISPATCH)——日志无洞,重放合法。长程工具通过共享的 step AbortSignal(exec.signal)取消。
4.7 事件源 session(packages/core/session)
SessionEventMap 是 merge-extensible 的:各包 declare module '@deepseek-ai/dsh-session/types' 并入新事件类型(goal 包并入 goal/change、agent 包并入 agent/inbox/spliced…),共 45 个已知事件类型。 seq 恒等于 log.length(连续无洞);每条事件是 deep-frozen 的 lossless JSON;Session.append 同步热路径、不阻塞 I/O。 模型可见 ⟺ 已记录:deriveMessages() 从 surface 有序节点(user/message / assistant/message / tool/result 带 surfaceOp/sourceEventSeqs)增量派生模型历史;任何新的模型可见输入必须是一个新的 session 事件。 SessionStore.flush(session) 是唯一 flush 入口,parallel 分发 session/flush、allSettled 后抛首个失败;loop 不在 turn 边界主动 flush(由 dsh-session-checkpoint-policy 之类的插件负责)。 fork(source, boundary) 禁止在开着 turn 中间 fork;持久化、resume、标题、遥测全部从这条流派生。
长程闭环的最小示例:goal 域 append goal/change → 发 scope-filtered goal/changed → goal-round-driver 监听后用 agent.followup(createUserMessage({ source: { kind: 'goal', goalId, revision, round } })) 开新轮。长程循环 = 一个带 goal 归属 source 的普通 followup turn,引擎本身零改动。
5. 同会话 Goal 领域(packages/goal/goal)
5.1 领域模型
GoalId(branded)、GoalRef { id, revision }(compare-and-set 栅栏:每次接受的持久变更 +1 revision)。 GoalSnapshot extends GoalRef:objective、phase、maxGoalRounds、blocked 时 blockedReason { code, message }。 GoalView extends GoalSnapshot:加 roundsStarted、createdAt、updatedAt、activation(进程内观察)。 GoalPhase 只有四个持久态:active | paused | blocked | complete。用量受限、轮数耗尽、模型失败、策略拒绝都是 blocked 的 reason code(round-limit / usage-limited / turn-error / max-tokens / prompt-rejected / queue-failed / model-reported…),而不是新增生命周期相位。 GoalActivation = armed | disarmed:进程内开关,永不持久化。
5.2 持久化:事件源 + 严格重放
每次变更 append 一条 durable goal/change 事件,负载是完整的 post-mutation 快照(或 clear 的 revisioned tombstone)。session log 是唯一事实源——没有第二个数据库,也没有"人工取消记录"。 严格重放 fold(fold.ts)只从 goal/change 派生生命周期状态,校验:JSON 形状(逐字段精确键集合)、revision 连续性、生命周期转移合法性、计数器、每目标单调时间戳、以及轮次只在"顺序正确、revision 匹配、未超上限"的 goal-sourced user/message 上递增。畸形当前版本记录让重放失败(fail-loud),而不是忽略或修复。 roundsStarted 只由已准入的正轮次推进;普通人类轮次、inbox splice 事件都不改变 goal 状态(inbox 状态与 goal 持久化解耦——2026-07-31-goal-owned-durable-events.md)。 增量重放把游标停在第一个损坏事件处;每个 agent/session-start 边界都 disarm 激活。 ./invariant 伴生插件用独立 fold 在事件进入持久日志之前校验候选事件(internal/dispatch 预检 + session/event 提交)。
5.3 服务语义(GoalService,ctx.goals)
只接受注册表里的精确 live Agent 实例(assertLive),不接受"id 相同"的对象。 create:没有非 complete 的当前 goal 时创建 revision-1 active goal 并 arm;defaultMaxGoalRounds 是部署配置(默认 256),create() 内部物化默认值。 edit:至少一个替换字段,保留 phase/blocker/activation。 resume:接受 paused/blocked/disarmed-active,仅当轮数预算有余量;清掉旧 blocker;arm。 complete/pause/block:相应相位转移 + disarm。 clear:留 revisioned tombstone。 disarm(agent):只移除进程内授权——不写事件、不改 revision、不发 goal/changed;生命周期所有者在 unload 前、持久化不确定时调用。 每次接受的变更在 durable 事件提交后发 scope-filtered goal/changed,监听器失败被包含。
5.4 关键设计张力:持久 vs 授权
"重新打开 session 看到 active goal"不等于"授权自动干活"。缓存以 disarmed 起步、session-start 重新 disarm;resume、fork、driver 替换都保留目标与历史但绝不自行开工。恢复后的续跑路径只能是:人类说"继续"(任意语言)→ 模型新开一轮 → 读 get_goal → 调 update_goal(action:'resume') 记录新 revision 并 arm。字面命令路径是 /goal resume。
6. 同会话轮次驱动(packages/goal/goal-round-driver)
这是整个"自动续轮"最复杂的部分(445 行):它决定何时再开一轮模型 turn,并处理所有并发与持久化竞态。
6.1 角色与状态
对每个精确 live Agent 拥有进程内调度状态(DriverState):最多一个 pending 预留(RoundAttempt)、competing 标记、checkpoint 标记、串行化 driver run。 每个 RoundAttempt 记录精确身份 { goalId, revision, round } + 完整渲染 prompt + MessageId + 阶段(queued/claimed/admitted)+ cancelled/stale 标记。
6.2 预留与准入
当 agent idle、无竞争排队工作、goal 为 active + armed 时,driver 先 ctx.sessions.flush(session) 做持久化 checkpoint,await 后重查所有谓词(readyAfterCheckpoint)。若 roundsStarted 已达 maxGoalRounds,直接 block(code:'round-limit')。否则预留精确身份 + 完整 prompt,然后 agent.followup() 投递带 GoalMessageSource 的消息。
agent/pre-step waterfall 是入口围栏:goal 正源消息必须精确匹配 driver 的 pending 身份与内容、live goal 仍是该 id/revision、激活仍 armed、round 仍是下一个数。插件在委托 next() 前检查一次、下游监听器返回后再检查一次(第二道检查防止异步监听器在进入旧 prompt 期间 edit/pause 了 goal)。只有最终落成的 user/message 才算"进入的轮次"并推进 goal fold;过期预留关闭一个无 step 的 blocked turn、不计轮。
6.3 人类工作与 revision 竞态
预留前已排队的普通工作 → 阻止调度。 自动 prompt 待处理期间来了普通工作 → 预留标记 stale,混合批次拒绝自动提议。 goal 轮内发生 mutation → revision 前进;旧 revision 的结算不能覆盖该 mutation:driver 丢弃旧尝试结果、读新投影、仅在新 revision 仍 active+armed 时继续。 普通工作到达被标记 competing,等 agent 再次 idle 时重新考虑延续。
6.4 结算分类表(一个 goal-owned turn 关闭后)
completed | |
aborted | |
errorRATE_LIMIT/QUOTA | usage-limited |
error | turn-error |
max-tokens | max-tokens |
disposedinterrupted | |
没有一种异常结局会请求自动重试。 之后人类可任意语言要求继续,模型读停止的 goal 并 resume(新 revision + arm)。
6.5 取消与卸载
每个 goal/changed 通知产生 checkpoint 义务:先 flush 再预留;turn 结束时的 flush 失败由 agent/error 上报,driver 把失败关联到精确关闭的那一轮。 宽取消清 inbox、abort 活动 phase;driver 沿预留消息的 claim/discard 事件与 durable aborted turn 结算。取消无匹配 goal 尝试时只移除进程内激活;pause mutation 抛错时回退为 disarm(绝不让被取消的自动工作重启)。 卸载:监听器与清理嵌套在一个有序 Cordis effect 里——先关闭准入、disarm、取消已准入尝试、await agent 与 driver 双静默,最后才注销监听器(防止并发卸载兄弟 effect 时围栏先消失)。 承认一个微任务竞态:inbox 接受可能正好赢在 unload 开始前——那一轮照常持久计费,但一旦 unload 开始即被取消、不再调度后续轮,goal 保持 active+disarmed。假装没发生过会让重置计数失真。
7. 模型工具与执行期权威(packages/goal/tool-goal)
7.1 工具面
get_goal():返回当前 goal 或 null(含 id/revision、objective、phase、roundsStarted/maxGoalRounds、blocker、进程内 activation 观察)。 create_goal(objective, max_goal_rounds?):从直接人类请求推断"长程目标"意图(任意语言),禁止把一次性活变成 goal。 update_goal(goal_id, revision, action, ...):edit | pause | resume | complete | blocked;严格 schema 的空串/零值填充按"省略"处理;blocked_reason 只在 blocked 时必填、持久为 model-reported code。 三个工具都独占执行(exclusive scheduling),保证模型批次内先读后写看到新 revision。UI 呈现是 args 的纯函数(generic read/mutation cards)。
7.2 权威校验(authority.ts,执行期、不可被 prompt 注入绕过)
goalToolExecution 要求:exec.agent 存在、是 AgentRegistry 里精确 live 的对象、状态 running、且是 ctx.agents.currentInitiator()(当前继承的 driver initiator)——同时存在一个开着的 turn(在 session 事件里倒查 turn/start 边界)。
create / edit / pause / resume:额外要求当前轮次里有宿主证明的直接人类消息(user/message 且 source.kind === 'user'),且调用者是运行时根代理(ctx.agents.roots(),从 live agent 图推导,不是持久 fork 祖先——被 fork 后独立 resume 的顶层会话可重新获得人类权威,live child 永远是 subagent 不可变更状态)。 complete / blocked:要么直接人类权威,要么精确当前 goal round(goal-sourced user/message 的 goalId/revision/round 与折叠的当前 goal 全等)。goal-round 权威只授予这两个终态报告——自动轮不能 edit/pause/resume/替换人类目标。 blockedAfterConsecutiveRounds(配置,默认 3):自动轮自报 blocked 前机械性要求至少这么多已准入轮次 + 非空解释;语义等价性(是不是同一个阻塞)仍是模型判断。 自动轮报 complete/blocked 后,工具结果上挂一条 deferContext(wrapup.ts 的 <goal_complete>/<goal_blocked> 指令),让模型在 turn 结束前照常给人类写收尾消息(替代早先的硬停)。
7.3 与模型面的 prompt 政策
systemPrompt section tool:goal(order 114):可以任意语言推断意图、get_goal 先于 update_goal、恢复/续跑后 resume 重新 arm、只在真正达成时 complete、同一阻塞持续 ≥ 阈值才 blocked、困难/不确定不是 blocker。
8. 人类命令与 UI
8.1 /goal(packages/goal/command-goal)
Codex 形状的紧凑语法:空 /goal 显示状态;/goal <objective> 创建;/goal edit <objective> / pause / resume / clear 直接生命周期操作。控制词仅在全后缀时才是控制(/goal pause after verification 是目标文本)。不消耗模型轮次,直接经 ctx.commands 分派(human command 平面,输出不进模型历史);成功变更仍由 goal 域记录为 durable 事件(模型可见 ⟺ 已记录)。状态输出区分持久 phase 与 live activation,blocked 时展示策略 code + 人类可读原因;隐藏 branded id 与 revision。
8.2 Web UI(packages/client/ui-goal)
GoalBar 停靠在输入 dock:显示目标 glyph、phase 标签、截断的 objective,以及按状态推导的图标动作(active→pause,paused→resume,edit、clear);创建走 /goal 命令不在这里。数据来自 goal session projection(host 计算的全量快照),动词经注入的 actions(Remote 到 host GoalService)——CAS ref 由 UI 持有,快速连点不会重复提交同一 revision。
8.3 组合矩阵
/goal | ||||
|---|---|---|---|---|
maxRounds: 64 | ||||
goals 配置,默认开) | ||||
9. Ralph:新鲜代理循环(packages/workflow/tool-ralph)
9.1 定位
Ralph 是一个模型工具,也是一个"复杂固定执行策略可以完全作为插件组合出来"的示范:它不创建 goal 状态、不给 dsh-agent-loop 加分支、不继承父对话、不写持久轮次记录。每个 round 通过选中 provider 生成一个全新子代理(spawn:无 seed、无父上下文、保留 cwd/lineage),父子之间只传不可变目标 + 上一轮的有界结构化报告;共享工作区是权威跨轮记忆(提示明确要求子代理先检查工作区、别信叙述性交接)。
9.2 固定脚本与报告协议
RALPH_SCRIPT(部署方持有,模型只给数据)在 workflow worker 里跑:
for round in 1..maxRounds: prompt = 不可变目标 + 轮次/上限 + 工作区即权威 + 上一轮报告 report = await agent(prompt, { schema: reportSchema }) # 结构化输出 validateReport(report) # 语义 + 序列化大小(maxHandoffChars) complete -> return complete blocked -> return blocked continue -> previous = reportreturn budget-limited
RalphRoundReport = { status: continue|complete|blocked, summary, evidence[], nextSteps[], blocker }:
continue:必须有 nextSteps、blocker 为空; complete:必须有 evidence、无 nextSteps、blocker 为空; blocked:必须有具体 blocker。 字符串必须规范化(trim 且非空)。超限报告直接失败而非静默截断(截断可能吃掉 status/evidence 却仍像权威交接)。
脚本内校验一次,消费者在 workflow seam 另一侧再防御性解码一次(readRunResult/readReport:精确键集合、normalized 字符串、budget-limited 必须等于轮限、round-failed 必须带 lastReport)。maxRounds 默认 256 且是调用覆盖的部署上限;maxHandoffChars/maxResultChars 默认各 16384。
9.3 终止语义
complete/blocked:立即返回最终报告 + roundsStarted。 最后一轮继续后:budget-limited。 普通子失败(workflow 语言把"正常落定但不成功"的子映射为 null):返回 round-failed + 失败轮 + 最后一个成功 handoff(如有),工具报错而非误分类为畸形报告或预算耗尽;无重试策略。 致命 workflow 基础设施失败(provider start、传输、worker、结果物化)走通用 workflow 错误路径。 结果信封明确说"worker 报告了该结果"而不是独立认证;父 transcript 只保留一次调用 + 一个有界终态文本(截断带 … [truncated] 标记),中间子对话不进父 transcript。
9.4 与 workflow 引擎的接缝:provider 路由与子上限
WorkflowStartRequest 增加两个可选字段(workflow/src/runtime-types.ts):
subagentProvider:per-run provider 覆盖(override ?? 配置默认)。Ralph 要求所选 provider 已注册、capabilities.outputSchema 为真、**inheritsParentContext === false**(fork 之类的继承型 provider 在 workflow start 前就大声失败)。 maxTotalAgents:per-run 子代理总数上限,worker 引擎要求 ≤ 其配置 ceiling(默认 1000)并装进该 run 的 WorkerLimits。Ralph 把自己的 maxRounds 直接作为这个上限——固定循环的轮数预算与通用 runaway 子代理兜底不可能不一致。
9.5 前台与取消
Ralph 工具是前台且进程内:父工具调用等完整 run 结束;exec.signal abort → run.cancel('parent step aborted');**每条路径上都 await run.dispose()**,保证取消到达 worker 引擎的有界结算与子静默之后调用才返回。
10. 支撑设施
10.1 workflow 引擎(packages/workflow)
契约层(workflow/):WorkflowEngine Service Definition、WorkflowStartRequest/WorkflowRun/WorkflowResult(result 永不 reject;stopReason 闭集 completed|cancelled|error)、WorkflowError(机器可路由 code + fatal 标志)、workflow/* 生命周期事件(start/phase/log/agent-start/agent-end/end)。 执行层(workflow-worker-thread/):每个 run 在一个全新 worker 线程的 vm 上下文里执行模型写的脚本(agent/pipeline/parallel/phase/log/args 钩子);worker 是宿主循环隔离 + 可强制终止,不是安全边界(vm 里 getter/proxy 可执行,跨 realm 值按无损 JSON 物化)。启动校验全同步:meta 形状 → host 侧同 wrapper 预解析脚本体(SCRIPT_PARSE)→ provider 路由 → cap(INVALID_ARGUMENT),无效请求永不发布 run。引擎默认:provider spawn、maxConcurrentAgents 自动解析为 min(16, max(1, cores-2))、maxTotalAgents 1000(deployment ceiling)、syncTimeoutMs/disposeGraceMs 各 5000。 取消双通道:post Cancel RPC(worker 端每个钩子入口抛 CANCELLED)+ 中止所有子共用的 AbortController + disposeGraceMs 宽限 timer(unref)force-settle + worker.terminate()。dispose 幂等、有界:host 立即驱动子回收(即使 worker 卡死),Promise.race 以 grace 为界,worker 线程绝不比 run 活得久。 workflow/agent-end 账本保证每条停止路径(正常/取消/宽限强结/worker 死亡/exit)上每个 started 子恰好一个 end、且先于 workflow/end。worker 环境被 scrub(无凭据)。 模型面 workflow 工具(tool-workflow):模型自己写 script/meta/args,用于大规模 fan-out;与 Ralph 的关键差异就是脚本来源、provider 默认、maxTotalAgents 传递与防御性解码。非 completed stopReason → isError;top-level 调用向父 Session 写 tool-workflow/* 持久记录(run-end 在 quiescence 后写)。
workflow 工具 vs ralph 工具(模型面差异一览)
workflow | ralph | |
|---|---|---|
script/meta/args | RALPH_SCRIPT,模型只给 objective + 可选 maxRounds | |
subagentProvider(引擎默认) | subagentProvider(默认 spawn),且要求已注册 + 结构化输出 + inheritsParentContext === false | |
maxTotalAgents(引擎默认 1000) | maxTotalAgents: maxRounds(轮数预算 = 子总数上限) | |
maxResultChars) | budget-limited 必须等于轮限、round-failed 必须带 lastReport),区分 complete/blocked/budget-limited/round-failed | |
tool-workflow/* |
10.2 subagent 能力(packages/subagent)
能力缝:Service Definition(SubagentRuntime,ctx.subagents)+ 多个命名 Provider 共存(spawn / fork / ACP / Codex / Claude Code / dsh-sdk,类似 LLM adapter 注册表)+ Consumer(subagent、subagent_fork、send_message/interrupt_agent/list_agents、子作用域 report)。 SubagentCapabilities = { outputSchema, depthLimit, toolFilter, persona }(types.ts:86):start-time 特性声明,服务在 start 前逐项校验,缺能力就 UNSUPPORTED_CAPABILITY 抛错,绝不 accept-then-ignore(out-of-process.ts 的四个远程 provider 全部 NO_START_CAPABILITIES,带 outputSchema 的请求直接拒绝)。 SubagentProvider.inheritsParentContext(types.ts:295)与实现差异: spawn(subagent-spawn-in-process)= false:无 seed,全新子会话;prepareContinuable() 返回 {};保留父 cwd/lineage/深度。 fork(subagent-fork-in-process)= true:seed = 父会话最后一个 turn/end 之前的平衡前缀(completedTurnPrefix:当前进行中工具轮不平衡、不可作为合法子会话重放);fork 前缀成为子自身持久 transcript 的一部分。fork 保持 one-shot 定位(2026-08-10-fork-children-stay-one-shot;前缀一次性捕获、续聊复用仍是 TODO)。 两者共用 startInProcessRun(subagent-in-process-driver),子 header 继承 cwd/parentSession/origin/delegationDepth/seedLength/preset;delegationDepth 持久化且单调(resume 的 child 不会被重算成顶层)。 结构化输出链:服务层 assertObjectJsonSchema(子集)→ capability 门控 → in-process 驱动在子创建窗口装一个子作用域的 structured_output 工具(参数即真实 schema)+ 尾部提示段 + 单调终结守卫 + 两阶段提交(staged → 权威 tools/result 成功才 captured)→ SubagentResult.structured。completed 但无捕获 = 子失败。 continuable 子代理(continuation.ts,现行设计 2026-07-28-continuable-subagent-conversations):弃用"每 activation 一个 Task"模型,改为 一个持久 Session + 至多一个进程内 Activation(residency epoch);Agent inbox 是唯一 FIFO 队列,无第二个队列。prepareContinuable 方法存在即能力,只返回 detached spec({ seed? })。 followup(send_message 的薄适配器)按 residency 路由:running 入队 / waiting 唤醒 / 无 Activation 冷恢复(coldResume:persistence.inspect → 授权校验 → foldSubagentDescriptor(只认子自身后缀的 continuable 描述符,不经过 provider,descriptor 是全部重建输入)→ agents.resume())。只接受精确 live 的直接父授权。 interrupt(interrupt_agent 工具):同步授权 + 异步生效——Agent.cancel(cause, { keepInbox: true }),只停当前 turn,保留未认领 inbox 工作/Activation/已发布后代;目标不存在是接受的无操作。 完成通知:continuation manager 自己发 notifySettlement(不是外部 subagent/end 监听器)——对每个拿到过 child id 的 child 无条件发送(含 token 上限/模型失败/取消/teardown 等 child 来不及主动 report 的结局),在释放所有权之前投递;措辞区分"已完成将不再工作除非你再发消息";idle 父 → followup(普通 later turn),busy 父 → steer(多个 child 同时 settle 只花一个 step),draining 父 → inject(不唤醒);投递失败仅记日志、绝不阻塞 disposal。 后台默认:continuable 模式省略 run_in_background 即后台(立即返回 durable child id,无 Task),显式 false 才前台;one-shot 模式默认前台、显式后台走通用 Jobs Task(settleRun 映射 stop reason:completed→completed、aborted→killed、error/max-tokens/refusal→failed;无 readOutput,中间详情留在子会话)。 list_agents 只向模型投影 continuable 子(one-shot 不可被 send_message 续聊),status 用 live registry 细化为 running/idle/ready(无 live Agent = ready,可 resume 但非终态)。 子会话持久化:每个 in-process 子都是普通持久 Session(header origin:'subagent');身份由 versioned、model-hidden 的 subagent/descriptor 事件记录(log-only、无 surfaceOp、跨 compaction 保留);父侧经 scoped 事件 + subagent/subagentTiming 两个 projection unit + listChildren 三级身份阶梯(live registry → projection-cache 行 → persistence.inspect 冷读)观察,全程不加载/恢复 Agent。 相关决策记录:2026-07-08-background-subagent-tasks(one-shot 后台复用通用 Jobs)、2026-07-28-continuable-subagent-conversations(现行:持久 Session + Activation)、2026-08-06-continuable-subagent-interrupt、2026-08-11-background-first-continuable-delegation(后台优先 + 完成唤醒 idle parent)等。
小结(与"长程任务"的关系):subagent 体系给出"后台长程任务"的两条正交路径——one-shot 后台 = 通用 Jobs Task(结果/取消/通知由 job 体系承担);continuable 后台 = 无 Task 的持久 Session + 进程内 Activation(send_message 续聊、interrupt 只停当前 turn、settlement notice 由 manager 无条件投递、list_agents 只投影 continuable 子)。continuable 是"长程对话"的正主:子会话持久化到 session log、身份经 projection 折叠、冷恢复不依赖 provider(descriptor 是全部重建输入),天然支撑跨 turn/跨进程恢复。它是 Ralph 与 workflow 子代理的执行底座(agent(prompt) 的 child 都由这些 provider 启动),但不是 goal 的延续机制——goal 在同一会话内复用父 agent 本体。
10.3 相邻但不同的机制
schedule(packages/schedule):未来根代理的持久提醒(after_seconds / 绝对 at / ≥5 分钟的 every_seconds),状态在 session log(schedule/change),到点经 runMaintenance() + followup() 开一个普通后续 turn。它不是 goal、不是 Ralph、也不是通用 /loop cron——但补上了"时间触发"这个第三策略的一角。 jobs(packages/jobs):宿主平面后台任务注册表 + job_* 工具(与 goal/ralph 不同,jobs 是宿主级、跨会话的)。 compaction(packages/compaction):用 surfaceOp: replace 折叠无关节点;goal 历史作为普通 session 数据可被保留。 todo(packages/todo):todo/write 全量快照,纯 UI 状态,不进模型历史派生。
11. 设计原则提炼(从代码里读出来的规律)
Everything is a plugin,改插件不改 loop:goal、Ralph、schedule 全部是挂在公共扩展点(agent/* 事件、ctx.tools、ctx.workflowEngine、ctx.subagents)上的插件;核心 loop 的唯一新增是通用的 agent/cancel-requested 观察通知。docs/architecture.md 明确:改 loop 必须更新架构文档。 模型可见 ⟺ 已记录:任何到达模型请求的输入都能从 session log 重建。goal/change 全量快照、agent/inbox/spliced、tool/call+result 无洞取消都是这条规则的实例化。 持久状态与执行授权分离:durable phase 记录"发生了什么",进程内 activation 决定"现在能不能自动干活"。恢复/续跑绝不自动开工。 执行期权威,不靠 prompt:人类消息是宿主 attestation(source.kind),live 根代理 + 开着的 turn + 精确 revision 是运行时校验,prompt 注入伪造不了。 Compare-and-set 一切可变的跨边界身份:GoalRef、revision、RoundIdentity(goalId/revision/round)、子 callId——旧结算不能覆盖新状态。 预算慷慨但受部署约束,且只数一种东西:maxGoalRounds/maxRounds 只数已准入轮次,不管 token/金额/墙钟/提供商用量;blocked 用一个 reason code 承载所有停止原因。 显式信任边界:完成/阻塞是模型或 worker 的声明,不是独立评估/认证;信封措辞与 prompt 政策都明说这一点(独立 evaluator 被明确推迟)。 失败保守、可观察:没有自动重试(异常结局只停 + 等人类 resume);持久化不确定就 disarm 而不是继续;畸形/超限交接直接失败而非截断。 有界 quiescence:取消/卸载/子回收全部有界结算,whenIdle()/dispose()/grace timer 是收敛手段,绝不无限等。 来源克制:外部产品(Codex 的 goal UX、Claude Code 的 goal 政策)是比较对象,不是兼容目标;实现遵循仓库自己的插件、事件源、显式默认解析规则。
12. 已知限制与未决工作(来自决策记录原文)
无独立评估:同会话完成/阻塞与 Ralph 终态都是模型/worker 声明;独立 evaluator、完成证书、对抗性验证器、criteria/executor/isolation 契约全部推迟。 无聚合预算:只有轮数上限;token、货币、墙钟、提供商用量、每轮价格准入政策缺失。 无持久自动运行器:goal 事实持久但激活/调度进程内,重启后等人类输入;Ralph 前台、进程丢失不可恢复;后台收集、重启恢复、无人值守常驻执行推迟。 无时间调度器(完整版):interval /loop、cron、主动维护、云端/桌面调度在 goal 决策之外(schedule 包只覆盖提醒)。 无通用 loop 日志 / 执行世界回卷:session 重放重建 goal 历史,不重建文件/进程/环境/凭据/外部副作用。 无 goal reflector:concern 事件、自动无进展启发、独立反射器改目标、卡死模式检测、loop_split 未实现。 Ralph 政策窄:一轮一子;轮内 fan-out、evaluator/worker 角色分离、动态 provider/model 选择、结构化的递归 Ralph 禁用需要独立政策 API(现在只是 prompt 引导)。 Ralph 不重试失败子:普通失败保留失败轮 + 最后好 handoff;致命基础设施失败可能早于该状态可用。 可移植 UI 有限:TUI 是纯文本状态 + generic Ralph card;ACP/headless/JSON-RPC 没有连续状态 widget、可重连命令输出、模态 goal 编辑器。
13. 验证与测试覆盖(证据概览)
goal 域:创建默认值、exact-live-agent 校验、CAS 拒绝、每个生命周期转移、blocker 校验与保留、resume 上限执行、clear/替换、seed 重放与 SessionStore.fork() 继承、session-start 与生命周期所有者 disarm、active 重 arm、durable 事件折叠、inbox 独立性、稳定损坏事件重放、顺序轮次归属;keyless Loader/stdio 进程测试外部读取持久 JSONL 验证 goal 记录与"创建不自动开轮"。 轮次驱动:真实 agent loop + 脚本化模型:顺序准入与上限执行、load/resume 惰性、每个结局分类、限速/请求错误/max-tokens、下游 prompt veto、准入前与在途取消、无关人类取消、failed-pause 回退、人类输入顺序、排队与下游 revision 竞态、伪造 goal 归属、失败 mutation/turn checkpoint(含后来的一次性注入)、scheduler/自定义 agent 失败、session-start 重置、精确生命周期退役、排队/运行中插件 teardown;keyless ACP snapshot(同一会话、round sources 1,2、取消第二个停滞轮记录 durable pause)。 模型工具:注册/独占/提示政策、填充安全 generic 呈现、非英文直接人类创建、exact/stale/non-running agent 与 driver 检查、live-child 拒绝、resumed-fork 根权威、steering、initiator 不匹配、严格 schema 填充、条件 blocker 解释、session-start 后重 arm、authority-before-conditional-arguments、精确 goal-round complete、自动轮仅终态、配置的阻塞阈值、人类立即阻塞;keyless headless replay snapshot 走真实 loop + 持久化栈并外部检查 goal change。 Ralph:config 与调用上限解析、provider 能力拒绝、固定 start-request 路由与子 ceiling、全部成功终态、普通子失败信封、畸形/超限边界值、精确成功结果截断、abort 时序、dispose、呈现意图、prompt 生命周期、HMR;真实栈集成(worker-thread 引擎 + spawn provider + 结构化输出 + agent loop):不同子身份、无 seed、继承 cwd、无父历史标记、上一轮报告只在下一轮出现、一轮 phase 事件、终态 complete、两子 dispose;headless snapshot:两轮、无 seed 子会话、round-one handoff 只出现在 round-two。 workflow 引擎:provider 路由覆盖与回退、per-run ceiling ≤ 部署 ceiling、取消(worker 钩子全抛 CANCELLED / 排队 waiter 拒绝 / grace force-settle + terminate / 完成 vs 取消竞速取消胜)、dispose 幂等有界、worker 死亡 first-wins、agent-end 恰好一次、realm 物化(__proto__ 防污染、环/稀疏/符号拒绝)、监听器隔离。 invariant 伴生:goal 流、workflow 事件配对等运行时契约在事件进入日志前/后双重校验。
14. 参考文件索引
决策记录(Agent Notes,.agents/notes/implemented/)
feature/2026-07-16-harness-level-loop.md — 总决策:两种显式策略,拒绝通用 loop feature/2026-07-19-persisted-same-session-goal-domain.md — goal 域 feature/2026-07-19-same-session-goal-round-driver.md — 轮次驱动 feature/2026-07-19-model-facing-goal-tools.md — 模型工具与权威 feature/2026-07-19-human-goal-command.md — /goal 命令与组合 feature/2026-07-19-fresh-agent-ralph-workflow-tool.md — Ralph architecture/2026-07-31-goal-owned-durable-events.md — goal/change 独立于 inbox architecture/2026-07-16-explicit-turn-cancellation.md — 每轮 AbortSignal 与取消契约 architecture/2026-07-15-agent-initiator-scope.md、2026-06-18-agent-lifecycle-and-ownership-contracts.md — agent 归属/initiator 后台/延续子代理系列(2026-07-08、07-21、07-28、08-06、08-11) feature/2026-07-05-dynamic-workflows.md、feature/2026-08-10-durable-workflow-runs-in-chat.md
源码
packages/core/agent-loop/src/{index,agent,tool-calls}.ts packages/core/agent/src/{index,inbox,dispatch,runtime-types}.ts packages/core/session/src/{index,types}.ts packages/goal/goal/src/{index,domain,fold,types,runtime,invariant}.ts packages/goal/goal-round-driver/src/{index,prompt,invariant}.ts packages/goal/tool-goal/src/{index,authority,wrapup}.ts packages/goal/command-goal/src/index.ts packages/workflow/tool-ralph/src/index.ts packages/workflow/workflow/src/{index,types,runtime-types}.ts packages/workflow/workflow-worker-thread/src/(index/runtime/host/protocol/realm/meta/session/worker) packages/subagent/subagent/src/{index,types,continuation}.ts、subagent-spawn-in-process、subagent-fork-in-process packages/schedule/schedule/src/、packages/jobs/ packages/client/ui-goal/src/client/GoalBar.tsx
文档
docs/architecture.md(turn flow、事件、扩展点表)、docs/glossary.md(loop hierarchy 词条)、docs/subsystems/goal.md(生成目录)、docs/config-catalog.md(goal/ralph 配置项)、docs/tool-catalog.md
附:一次完整的长程任务生命周期(同会话 goal)
人类在 TUI/Web 输入 创建一个目标:xxx(或 /goal xxx)。 模型在 turn 内调 create_goal → GoalService 校验直接人类权威 → append goal/change(active, revision 1, rounds 0, armed)→ 发 goal/changed。 driver 收到通知 → checkpoint flush → 重查谓词 → 预留 round 1 → followup(<goal_round> 目标…)。 引擎 claim 该消息 → pre-step 围栏双重校验 → 进入 goal-sourced user/message(fold 计 round 1)→ 模型干活(若干 step)。 模型判断没做完 → 正常 completed 关轮 → driver 见 active+armed 未超上限 → 再来一轮(round 2…)。 模型判断做完 → update_goal(action:'complete')(goal-round 权威)→ append goal/change(complete, disarmed)→ deferContext 收尾指令 → 模型给人类写总结 → turn 正常结束,driver 不再续轮。 中途人类点暂停//goal pause 或模型遇 RATE_LIMIT → 相应 pause/blocked + disarm;人类说"继续" → 模型 resume 重新 arm,从断点继续。 进程重启后重开 session:goal 仍 active(重放 goal/change)但 disarmed——模型看到的是"目标在、未授权",直到人类再次表达继续意图。
(若换 Ralph:1′. 模型调 ralph({ objective }) → 前台 workflow 每轮 spawn 一个无 seed 子代理 → 工作区 + 上一轮报告跨轮 → complete/blocked/budget-limited 返回一个父面结果,中间子对话不进父 transcript。)


