长任务自动推进:goal-round-driver 与 Ralph
一句话版:DSH 有两条"没人盯着它也能持续向前"的路径。goal-round-driver 在同一会话里反复追加
<goal_round>提示,复用前缀 / KV cache / 工具结果,是有持久化检查点的同会话续推;Ralph 每轮开一个全新 Agent,只用共享工作区 + 一份有界报告跨轮,是无会话血缘的换新推进。多数长目标用 goal-round-driver,只有用户明确要"换新 agent 迭代试错"时才用 Ralph。
两种机制都回答同一个问题:"目标没完成,谁继续往下做?",但答案的心智模型完全不同:
| 维度 | goal-round-driver | Ralph |
|---|---|---|
| 轮次载体 | 同一条会话,追加 <goal_round> 用户角色提示 | 每轮一个全新子 Agent,无父会话 seed |
| 跨轮记忆 | 会话历史(KV cache / 工具结果 / 状态) | 共享工作区 + 一份有界结构化报告 |
| 状态归属 | ctx.goals 持久 goal/change 事件 | workflow + ctx.subagents(无独立 goal 域) |
| 上限 | maxGoalRounds,到顶自动 block(round-limit) | maxRounds,到顶返回 budget-limited |
| 完成/阻塞 | update_goal 自主 complete/blocked | 子 Agent 报告 status: continue/complete/blocked |
| 收尾 | concludeTurn() + <goal_complete>/<goal_blocked> 闭场 | 父拿到子报告,无闭场上下文注入 |
| 权威判定 | 同会话一次性 agent 的自动续推需 driver 授权轮次 | 子 Agent 只是父的普通子代理 |
一、goal-round-driver:同会话续推
@deepseek-ai/dsh-goal-round-driver 是一个 Cordis 插件,挂在公共的 agents / goals / sessions 服务之上,把 ctx.goals 里 active + armed 的目标转成连续的 goal round。它不调用私有 agent-loop,只用公共 Agent 接口和会话事件。
组合
- id: goal
name: '@deepseek-ai/dsh-goal'
- id: tool-goal
name: '@deepseek-ai/dsh-tool-goal'
- id: goal-round-driver
name: '@deepseek-ai/dsh-goal-round-driver'
goal-round-driver 自身没有可调配置;轮次上限 maxGoalRounds 属于目标定义(tool-goal 的 create_goal / 部署 defaultMaxGoalRounds),模型自我判 blocked 的门槛属于 tool-goal 的 blockedAfterConsecutiveRounds。源码 README 原话:把任一值复制进 driver 会造成策略分歧。
round 契约
当某个确切的 live agent 处于 idle、目标 active + armed、且还有剩余轮次时,driver 先持久化待提交的 goal 变更,再为当前 { goalId, revision } 预定第 roundsStarted + 1 轮。它通过 Agent.followup() 入队一条带 GoalMessageSource(kind: 'goal')的 <goal_round> 提示。真正被 admit 的 user/message 才推进 roundsStarted;被判定 stale 的预定不消耗轮次编号。
// packages/goal/goal-round-driver/src/prompt.ts(摘)
export function renderGoalRoundPrompt(goal: GoalView, round: number): ContentBlock[] {
return [{
type: 'text',
text: '<goal_round>\n'
+ `Objective: ${JSON.stringify(goal.objective)}\n`
+ `Round: ${round}/${goal.maxGoalRounds}\n\n`
+ 'Continue working toward the objective in this same session. Treat the current workspace, '
+ 'tool results, and durable session state as authoritative; inspect them instead of assuming '
+ 'earlier narration is still current. …',
}]
}
提示进入会话历史后,同一会话里既有人类消息、目标快照,也有助手输出与工具记录。这是增量追加:每一轮在前缀上继续,而不是复制一份会话前缀或派生历史。
每轮入队前 await sessions.flush()
goal/changed 事件会产生一个持久化义务。真正排队工作之前,driver 会 await ctx.sessions.flush(agent.session),并在 await 返回后重新核对目标 revision 与是否有竞态输入:
// packages/goal/goal-round-driver/src/index.ts(摘,checksum 前)
if (state.needsCheckpoint) {
state.needsCheckpoint = false
try {
await ctx.sessions.flush(agent.session)
} catch (error) {
ctx.logger.warn(`…durability checkpoint failed for agent "${agent.id}"…`)
disarm(state) // 检查点失败 → 关闭续推,绝不带病推进
return
}
if (!readyAfterCheckpoint(state)) return // flush 期间可能有新输入/变更
}
- 检查点失败会走
agent/error→disarm(),先于下一轮开始就撤销续推 - 持久化是检查点而非每个 step ——
ctx.sessions.flush()的语义是"把待写事件落到 durable 日志" flush期间若有普通人类消息或 mutation 到达,driver 会退让,给这条输入一个自己的检查点/轮次后再续推
round 上限:到顶自动 block
// packages/goal/goal-round-driver/src/index.ts(摘)
if (goal.roundsStarted >= goal.maxGoalRounds) {
ctx.goals.block(agent, goalRef(goal), {
code: 'round-limit',
message: `Goal reached its configured limit of ${goal.maxGoalRounds} rounds.`,
})
return
}
到顶后进入 block(durable phase),code 为 round-limit。maxGoalRounds 只计被 admit 的 goal-sourced 轮次;人类消息不计入上限。
不 spawn 新 agent、不 fork 会话前缀
源码 README 明确把这点列为设计边界:
Same-session execution only — this package deliberately does not spawn a fresh agent, fork a session prefix, or implement Ralph-style independent attempts.
每个被 admit 的轮次只增加一个固定指令块 + 目标文本;KV cache 是同 epoch 内只追加:每轮扩展既有会话的可复用前缀,压缩才可能移动此前缀边界。这是它比 Ralph 省 token/KV 的根本原因。
竞态栅栏(简短)
driver 精心处理"预定 vs 入队 vs admit"的竞态,超出本文范围,但要点是它的 agent/pre-step 监听器会在下游监听器之前和之后各校验一次完整的 claimed 记录与当前目标;只有 enter 的 user/message 才累加 roundsStarted;一个 pending 的自动 prompt 若落进混有人类消息的批次,则被 reject 并在检查点后重新预定。
二、tool-goal 的 wrap-up:自主收尾
@deepseek-ai/dsh-tool-goal 暴露 get_goal / create_goal / update_goal。在自主 goal round 中,模型调用 update_goal 的 complete 或 blocked 成功后,tool-goal 会用 exec.deferContext() 注入一段 <goal_complete> / <goal_blocked> 闭场上下文,并标记该次工具执行 concludeTurn(),让这一个物理轮次在 step 后停止——但模型仍会先给用户写一句收尾(源码原话:replacing the former hard turn stop)。
// packages/goal/tool-goal/src/index.ts(摘)
if (authority.kind === 'goal-round') {
exec.deferContext(createUserMessage({
content: args.action === 'complete'
? renderWrapupContext(goal.objective)
: renderWrapupContext(goal.objective, args.blocked_reason as string),
source: { kind: 'plugin', plugin: 'tool-goal', form: 'notice', … },
}))
}
闭场上下文要求 grounding(只报告本会话里实际建立的事实)、直接对用户讲话、且不再调用更多工具。
update_goal 的 blocked 在目标轮次权威下受硬门槛约束:
// packages/goal/tool-goal/src/index.ts(摘)
if (args.action === 'blocked' && authority.kind === 'goal-round'
&& authority.goal.roundsStarted < resolved.blockedAfterConsecutiveRounds) {
throw new HarnessError(
`blocked requires at least ${resolved.blockedAfterConsecutiveRounds} consecutive goal rounds; …`,
'GOAL_TOOL_BLOCK_THRESHOLD')
}
blockedAfterConsecutiveRounds默认3(z.number().step(1).min(1).default(3))- 它是模型自我判 block 的硬下界:未到 3 个 admit 轮次就 self-block 会被机械拒绝
- 语义"同一条件是否真持续"仍是模型判断——driver 只数不同的 admitted round 数,并持久化
code: 'model-reported'
三、Ralph:每轮全新 Agent
@deepseek-ai/dsh-tool-ralph 是模型面向的 fresh-agent iterative loop。它没在 agent-loop 里加任何 Ralph 模式,只是基于 ctx.workflowEngine 和 ctx.subagents 的一个普通插件:
Use the ralph tool ONLY when the direct human explicitly asks for a Ralph loop or fresh-agent
iterative execution. Each Ralph round starts a fresh child with no conversation seed and uses
the shared workspace as durable memory. Completion and blockers are worker reports, not
independent evaluation. Use same-session goal tools for ordinary long-running objectives…
每个 Ralph round 通过 subagentProvider 起一个不带父会话 seed 的全新子 Agent;父会话与之前的子会话都不 seed。跨轮的唯一记忆是共享工作区 + 一份有界结构化报告(status: continue | complete | blocked + summary / evidence / next steps / blocker text)。
- 报告超限 / 缺字段 / 无效 → fail workflow,不是截断或当成到顶
- 子失败给出带轮次号的错误并保留最后一份成功 handoff;Ralph 不重试该轮
- 普通子失败与 provider/transport 失败都是错误,部分输出永不当作成功
- 完成是 worker 自报:子 Agent 报告
complete,父侧只是转述,无独立 evaluator 认证
四、什么时候用哪个
| 场景 | 用 |
|---|---|
| 用户提了一个长期目标,希望它在同一条会话里被反复推进 | goal-round-driver(create_goal) |
| 目标是"完成 X",需要复用已累积的工具结果、KV cache、会话状态 | goal-round-driver |
| 需要持久化检查点:推进到一半重启也要接续 | goal-round-driver(sessions.flush 检查点) |
| 用户明确要求"Ralph 循环 / 换新 agent 迭代执行" | ralph |
| 目标稳定、需要反复试错推进,且每轮都希望从干净上下文开始 | ralph |
| 一两个独立委派 / 大规模 fan-out | 普通 subagent / workflow(别上 goal / ralph) |
源码里给模型的两段 guidance 也以此为界:Ralph 只在该直接人类明确要求 fresh-agent 迭代时用;目标工具用于普通的同会话长目标。
五、验证
# 断言"同一会话":goal round 是 user/message 里 source.kind 为 goal 的事件
zstdcat ~/.dsh/sessions/*/*/session.jsonl.zstd \
| grep -E '"source":\{"kind":"goal"' | head
# 看 goal 变更与 round 上限阻塞
zstdcat ~/.dsh/sessions/*/*/session.jsonl.zstd | grep -E 'goal/|"round-limit"' | head
# 组合树里看三个 goal 插件是否都装载
dsh web --dump-config | grep -iE "goal-round-driver|tool-goal|'goal'" | head
# Ralph 走 workflow run 事件(run-start/成员/run-end)
zstdcat ~/.dsh/sessions/*/*/session.jsonl.zstd | grep -E 'workflow|"run/' | head
对比两条路径,goal/change + user/message(source.kind=goal) 是同会话续推的指纹;workflow/run* 与 child session 是换新推进的指纹。
下一步
- 目标、作业与待办:
ctx.goals域状态机与/goal命令 - 工作流与 Ralph:Ralph 底层的 workflow / subagent 缝
- 子 Agent 与并行:Ralph 每轮用的
ctx.subagents