Goal 模式(长时程任务)¶
2026-05 起 omicos-core 加了一套 long-horizon goal 子系统,
对齐 codex / claude code 的 /goal 体验。
一句话动机:让用户写一句"帮我把这份 bulk RNA 数据从 fastq 跑到 文章级 figure",sidecar 自己一回合接一回合跑下去,跑到达成 目标 / 超预算 / 用户喊停才停。
native.rs的/api/threads/:session_id/goal路由。
Goal 的状态机¶
create
│
▼
┌──────┐ pause ┌────────┐
│Active│ ───────▶ │Paused │
└──┬───┘ ◀─────── └────────┘
│ resume
tokens >= budget
▼
┌──────────────┐
│BudgetLimited │ ←—— 自动转入,需要 resume + 加预算才能继续
└──────────────┘
│ complete (model 调 update_goal)
▼
Complete
四种状态在 wire 上是 snake_case:active / paused / budget_limited /
complete。
持久化¶
Goal 是每个 thread 一条,写在 thread meta.json 的 goal 字段。
shape:
{
"goal": {
"goal_id": "f7c8...", // UUIDv4 — 乐观锁
"objective": "Bulk RNA-seq end-to-end on GSE166925, finish to figure-grade plots.",
"status": "active",
"token_budget": 500000,
"tokens_used": 129036,
"tokens_in_used": 126345,
"tokens_out_used": 2691,
"created_at": "2026-05-12T10:11:00Z",
"updated_at": "2026-05-12T10:34:21Z"
}
}
goal_id 在 PATCH 时必须带回去匹配——目标是防 SPA / CLI / 自动续跑
同时改一份 goal 时的 lost-update。clear 是个例外,不要求匹配。
HTTP / Tool 接口¶
方向 |
接口 |
用途 |
|---|---|---|
SPA → sidecar |
|
拉当前 goal |
SPA → sidecar |
|
新建 |
SPA → sidecar |
|
pause / resume / clear / complete |
LLM → sidecar(tool) |
|
同 POST。模型不能擅自创建——只有 system prompt 明确允许的 agent 才暴露 |
LLM → sidecar(tool) |
|
自检:现在还剩多少预算、状态是什么 |
LLM → sidecar(tool) |
|
模型自己宣布完成,触发停回合 |
模型只能 complete,不能 pause / clear
update_goal 工具暴露给 LLM 只是为了模型自己宣布完成——以及读
budget。pause / clear 必须走 SPA → 用户在 UI 点。这是设计选择,避免
模型说"任务结束了"然后立刻把 goal 清掉、用户后来再发消息时看到一
个空状态。
continuation engine —— 自动续回合¶
Goal 的核心特色不是"记录一句话",而是让回合自己续下去。
runtime.rs 在每个回合结束时检查当前 thread 的 goal 状态:
如果 goal 是
active,且模型最后一回合没说自己完成:从goal_templates::continuation_prompt()渲染一段"你还没说完, 继续"的 user-role 消息,自动开下一回合。如果
tokens_used >= token_budget且状态还是active:apply_token_increment自动把状态翻成budget_limited, 下一回合 user-side prompt 改成budget_limit.md模板("你已经 达到预算上限,整理一份 final summary 给我")然后停。如果
paused/complete/budget_limited:完全不开新回合, 等用户操作。
模板在 omicos-core/templates/goals/{continuation,budget_limit}.md
作为 include_str! 嵌入兜底。允许用户在
<workspace>/.omicos/templates/goals/<name>.md 覆盖(仅工作区,
不支持 ~/.omicos 或环境变量覆盖——见
Workspace = cwd)。
占位符
模板里支持的占位符:
{{objective}} {{goal_id}} {{tokens_used}} {{token_budget}}
{{budget_suffix}}(渲染成 " / budget 500000" 或
" (no budget)"){{elapsed}}。
token 计费¶
PR omicos-core #142 + #143 让 token 计费成为 goal 系统的核心副作 用——每个回合结束 sidecar 都会调一次:
goal.apply_token_increment(delta_in, delta_out);
delta_* 来自 provider 返回的 usage 块(OpenAI / DeepSeek /
Anthropic 都有)。这两个数字分别记到 tokens_in_used /
tokens_out_used,相加成 tokens_used。budget 阈值用的是
总和——但 UI 显示分两条(input 通常远大于 output,分开能让用户
看清"我大头花在装上下文还是产文本")。
apply_token_increment 自带单向 budget 翻转:
只有
active且tokens_used >= budget才翻成budget_limitedpaused/complete永远不被自动翻
SPA UI 入口¶
omicOS-ui 把这套暴露成三个组件:
文件 |
角色 |
|---|---|
|
pinia store;订阅 active session,poll |
|
composer 上方的 pill —— 显示 objective、剩余预算、暂停按钮 |
|
composer 上方更细的 token bar: |
/goal 在 composer 里是 slash command(BenchView.vue 拦截):
写法 |
行为 |
|---|---|
|
打开 GoalPill,让用户在 UI 里输入 objective |
|
直接 |
|
走 PATCH |
实时 token 显示
GoalStatusBar 在 isStreaming 上升沿快照
workspaceStore.currentUsage.input_tokens/output_tokens,下降沿减一下
得到当前回合 delta;同时 goalStore.refresh() 拉服务端权威累加值。
两者交叉避免"流式中的数字跳跃"和"流式结束后才更新"之间的撕裂。
加一个自定义模板¶
最简单的"我想换措辞"方式:
mkdir -p <workspace>/.omicos/templates/goals
cat > <workspace>/.omicos/templates/goals/continuation.md <<'EOF'
You still have an active goal:
> {{objective}}
Used: {{tokens_used}}{{budget_suffix}}. Continue working — execute the
next concrete step. When the entire goal is done, call
`update_goal {complete: true}` and STOP.
EOF
下一回合自动走你的模板。budget_limit.md 同理。
不在工作区路径下的模板不会被加载。
故障排查¶
现象 |
排查 |
|---|---|
|
composer 拦截器没装好;看 |
token bar 显示 |
provider 没 emit usage(部分 OAuth Gemini 不发);只能依赖客户端估算(未实现) |
budget 没生效,过了预算还在跑 |
|
status 永远是 |
模型回合里没调 |
进一步¶
对话与工作区 — workspace 路径是 goal templates 的搜索根
Tool pipeline —
create_goal/get_goal/update_goal的注册位置