feat: 主线 /goal 命令自动建 goal

发给主线的一句话(含字面 /goal)即可自动建 goal,不用先
知道主线是进行中还是已终态,也不用勾选项:
- cloud/goal_command.py:server 与 worker 共用的纯文本解析,
  只认命令形状(/goal 开头、分隔符/目标标记、开启类动词),
  讨论里提到 /goal 不误触发;空目标返回空串供调用方 400。
- server followup/thread/message/continue 三入口同款:queued
  原地转 goal,已终态复活并转 goal,running 排队后由 worker
  在轮次边界转 goal;已是 goal 409、plan 400、merged 409。
- worker 排空:/goal 不当普通轮次跑,CAS 把 running 主线
  改回 queued 并写 goal/resume_message,下次认领走 goal-resume
  在同一 thread 上开新目标循环;取消/回收时 CAS 失败则把
  followup 放回 pending,绝不把取消过的任务改回 queued。
- 单测 14 项:解析、三态转换、拒绝口径、普通消息不受影响、
  continue/thread 同款、worker 轮次边界转换 + 下次 goal 跑通。
This commit is contained in:
host committed 2026-10-30 07:15:05 +08:00
1 parent 3a5c5db9fb
commit 3096e04345
5 files changed
+720 -11

No files matched your search

+70 -4
View File
@@ -144,6 +144,7 @@ from cloud.coordinator import nonterminal_children as _coord_nonterminal
from cloud.cost import CostStore, estimate_tokens, price_usd_micros, \
sum_usage, unpriced_token_cap
from cloud.db import connect as _db_connect
from cloud.goal_command import parse_goal_command as _parse_goal_command
from cloud.logging_config import get_logger, log_event, setup_logging
from cloud.models import ModelRouter, is_fallbackable, record_fallback
from cloud.auto_memory import AutoMemoryStore
@@ -3212,6 +3213,36 @@ class Worker:
except Exception:
pass
def _convert_running_task_to_goal(self, task_id, target):
"""把正在跑的主线在轮次边界转成新语义 goal 任务。
只在排空循环里调用:本轮首轮已 completed、任务在库里
仍是 running,CAS 把它改回 queued 并写 goal 字段与
resume_message——下次 worker 认领时走 /continue 同款
goal-resume,在同一 thread 上开新目标循环(不是新任务、
不是分支)。CAS 失败(任务已被取消/回收、状态已不是
running)返回 False,调用方把 followup 放回 pending,
绝不把取消过的任务悄悄改回 queued。
成功返回 True。"""
try:
conn = _db_connect(self.db_path)
try:
self._ensure_result_columns(conn)
cur = conn.execute(
"UPDATE tasks SET goal_mode = 1, goal = ?,"
" verify_command = NULL, goal_max_rounds = 0,"
" status = 'queued', resume_message = ?,"
" result_text = NULL, error_text = NULL,"
" actual_model = NULL, updated_at = ?"
" WHERE id = ? AND status = 'running'",
(target, target, time.time(), task_id))
conn.commit()
return cur.rowcount == 1
finally:
conn.close()
except Exception:
return False
def _restore_followup(self, fu_id):
"""取消时把跑中的后续消息放回 pending,留给重试入口。"""
try:
@@ -5693,6 +5724,11 @@ class Worker:
# 这里只负责把轮次跑出来。一条失败即停:任务记 failed,
# 剩余 pending 留给重试入口(重试走已存在续跑,跑完继续
# 排空剩余消息);取消时该条回 pending,任务走中断收尾。
# /goal 命令不在这里当普通轮次跑:排空到它时把主线转
# 成 goal 任务并回 queued,下次认领走 goal-resume 开
# 新目标循环(服务端已在 followup/thread 入口把 running
# 的 /goal 排成 followup,见 server._create_followup)。
goal_converted = False
if (result is not None and result.get("status") == "completed"
and not quota_stop
and not task.get("verify_command")
@@ -5701,6 +5737,23 @@ class Worker:
fu = self._claim_next_followup(task_id)
if fu is None:
break
goal_target = _parse_goal_command(fu["body"])
if goal_target is not None:
if not goal_target or task.get("plan"):
# 服务端已拦空目标与 plan 冲突,这里是
# 兜底:不跑、不转,标 failed 停排空。
self._finish_followup(fu["id"], "failed")
break
if self._convert_running_task_to_goal(
task_id, goal_target):
self._finish_followup(fu["id"], "done")
self._audit(task["project_id"],
"worker_goal_converted",
"%s:goal=%s" % (task_id, goal_target))
goal_converted = True
else:
self._restore_followup(fu["id"])
break
# 后续消息执行时查配额:超限时该条回 pending(留
# 给重试入口),任务走配额中断收尾,不再排空剩余;
# 建消息时不查(排队本身不花钱,顶上来后可跑)。
@@ -5909,10 +5962,23 @@ class Worker:
self._notify_task_finished(
task_id, task["project_id"], final, error=error)
else:
# 已被取消/回收:DB 状态以 interrupted 为准,不再
# 补一条 worker_completed/failed 审计误导排查。
final = "interrupted"
self._audit(task["project_id"], "worker_interrupted", task_id)
if goal_converted:
# /goal 转换:DB 状态已是本轮转换写回的 queued,
# _set_status_if_running 因状态不是 running 而
# 没改行是预期,不是取消/回收;绝不能把 local
# 的 final 改成 interrupted(那只会误导返回体,
# DB 本身已是 queued)。审计也按转换记,不按中断记。
final = "queued"
self._audit(task["project_id"], "worker_goal_converted",
task_id)
log_event(self.logger, "worker_goal_converted",
task_id=task_id, project_id=task["project_id"],
status="queued")
else:
# 已被取消/回收:DB 状态以 interrupted 为准,不再
# 补一条 worker_completed/failed 审计误导排查。
final = "interrupted"
self._audit(task["project_id"], "worker_interrupted", task_id)
if resume_message and not quota_stop:
# 本轮已跑完(无论成败):续跑消息已消费,清掉;进程
# 中途被杀时清不到,下次认领会重跑同一条消息——可接受