Files
penguin-harness/packages/docs/content/goal-mode.zh.md
T

6.5 KiB
Raw Blame History

title, description
title description
目标模式 给 Agent 一个目标而不是一条消息——系统在同一 Session 上循环驱动 Task,直到目标完成、受阻或 token 预算耗尽。

是什么

普通 Task 在模型不再调用工具、给出回复时就结束了。目标模式反转了这个契约:你给出一个目标(objective),系统在同一个 Session 上持续驱动 Task——每一轮重新注入目标并检查控制文件——直到目标进入终态。模型不能靠"不说话"来停下:它必须通过下述协议声明完成(或真正的僵局),否则循环继续。

三个入口都能发起目标:

入口 用法
Web App 输入框的 + 菜单 →「目标模式」(或输入 /goal);chip 上可填 token 预算(500k、2m,留空不限)。输入框选中的技能以 [use_skills] 块前缀在第一轮消息上,与普通发送完全一致
CLI chat /goal[:<预算>] <目标>,例如 /goal:500k 让所有测试通过
CLI 单次运行 penguin run --goal [预算] -m "<目标>";仅目标完成时退出码为 0
Server API POST /api/sessions/:id/tasks,body 带 { input, goal: { budget } }(budget 为 -1 或缺省 = 不限额)

在 SDK 中,目标模式是唯一入口 run 的一个选项——session.run(input, { goal: { budget } })——而不是独立 API:输入文本即目标,轮次在这一次调用内部循环,流的最后一条消息是携带结局的 goal_finished 事件。

控制文件:GOAL.yaml

循环的状态通道是一个文件,位于 <agent_dir>/scratchpad/<session_id>/GOAL.yaml(与模型的 PLAN.md 约定同级),目标启动时由系统创建:

objective: 让所有测试通过
status: active

系统对这个文件只写一次(创建时),此后只读 status:

字段 写入方 说明
objective 系统,创建时 正典值在循环内存里、每轮协议块中重申——文件被改动也不影响任何行为
status 模型 只允许改为 complete 或 blocked——模型回传循环的信箱,每轮结束后被读取

预算数字随每轮的 [goal] 块给出,不在文件里;系统侧终局(budget_limited、aborted)只存在于 goal_finished 事件与服务端状态——文件永远保持模型自己最后写下的样子,这正是中断目标想要的断点。读取是容错的:文件缺失、YAML 解析失败、协议外的 status 一律归一化为 blocked——控制通道坏了就停下循环,而不是无限空转。

循环

每一轮的 user 消息是一个 [goal] 协议块加纯文本正文——第一轮原样携带你的原始消息(含技能调用块等前缀);后续轮重新注入目标文本。Web App 把协议块折叠为普通用户气泡下方的「目标 · 第 N 轮」提示;Trace 中原样保留。协议块内嵌 GOAL.yaml 的内容(模型看到的就是它要编辑的那个文件,按创建时的同一份值组合)、自带一行当前预算数字,并附工作规则——声明完成前必须基于证据逐项核验、不许把目标缩水成更容易的子集、关键进展写入 PLAN.md 以跨越上下文压缩。Task 结束后系统读取 status:

  • complete → 目标完成,循环停止。
  • blocked → 循环停止;模型缺什么写在它最后一条回复里。注入规则要求同一阻塞条件持续三个连续轮次后才允许声明 blocked,临时性障碍不会终结目标。
  • active → 预算允许则进入下一轮。

目标里的图片

目标可以附带图片——「把页面改成这张设计稿的样子」本身就是一个目标,而一张截图比一段描述说得清楚。图片一律写入会话 scratchpad,在目标文本里以 [attached image: <路径>] 行引用,与模型是否支持视觉无关:目标每轮都作为协议块的文本被重新注入,图片没法以图片的形态跟着走。只在第一轮发,后续每一轮的目标就指向了一个早已被压缩掉的东西,而目标文本读起来却毫无破绽。作为路径,它跨越每一轮、每一次压缩都稳定存在,而模型只在真的要看的时候才付出 token(有视觉用 read_image,没有则 describe_image)。图片不能替代文字——一张图说明不了目标,所以没有文字的目标输入会被拒绝。

聊天页在第一轮气泡下方完整展示附图,后续轮次收成一行 chip(点击展开):它确实在每一轮的输入里,但二十轮的目标不该把同一张图重复二十次。

某一轮以中断结束(用户停止、LLM 故障)时整个目标随之结束、不再续推——磁盘上的状态保持 active,工作区与目标文件就是干净的断点。Web App 中常规停止按钮即中止整个循环;CLI 中是 Ctrl-C。被单 Task 轮次上限(max_turns)掐断的轮同理:模型没来得及写目标文件,循环以 aborted 结束,而不是永远重演同一次掐断。

Token 预算

计数是增量制——非缓存 input + output(request.total − cache_read),对每一轮的每个请求累加,包括 run_subagent 派生的子 Session。used 从 0 开始。这个累加值是花费的估算而非账单:缓存读取并非免费,只是单价远低于非缓存 input,忽略它既不失真,也免去了依赖各模型价目表。

预算在轮与轮之间检查。耗尽时不会把模型拦腰斩断:系统注入最后一个收尾轮——总结进展、列出剩余工作、给出明确的下一步,并且不许因为钱花完了就标 complete——之后系统以 budget_limited 终局(goal_finished 事件;不写文件)。正因为只在轮间检查,进行中的一轮不会被截断:实际花费最多可超出预算一轮,外加收尾轮。未设预算时循环一直跑到 complete 或 blocked——边界是模型对两个终态的诚实,外加 100 轮的硬性兜底上限,防止一个从不写目标文件的模型无限循环。

服务端状态与事件

Web 服务端把每次目标运行记入 goal_state 表(objective、status、budget、used、rounds)——聊天页的目标 banner 加载时从最新一行恢复,实时进度通过会话 SSE 通道的 goal_started / goal_round / goal_finished 事件到达。系统侧终态(aborted、budget_limited)仅存在于表与流事件中;磁盘文件保持模型最后写下的内容以便续跑。删除 Session 会连同 scratchpad(包括 GOAL.yaml)一起清除其目标记录。