feat: goal 模式新语义——prompt 即目标,无验收命令,不限轮次,模型宣告完成 (#67)
console 打勾 goal 模式时不再有专用 goal 文本、验收命令、轮次 三个输入(已移除,换成说明行),只发 goal_mode=1;server 侧 goal_mode=1 时 goal 存 prompt、验收命令空、轮次存 0,带验收 命令或与计划模式同用时 400,旧的带验收命令任务照原样可用。 host.py goal 循环:verify 为空时不跑验测,轮次提示里写明 标记约定;模型在完整跑完的回合末尾单独一行输出“目标已 达成”即达标(超时回合的部分文本含标记也不算);--max-rounds 0 为不限轮次,goal-resume 续跑按 0 处理(不用 or 兜底成 3)。 刹车只有取消与预算耗尽(后者记 interrupted、可续跑)。 迁移与 worker:tasks 加 goal_mode 列(server 迁移、worker 列 补齐、PG schema 三处同步);worker 按 goal_mode 或 verify_command 判定 goal 路径,重试与后续消息排空对新语义 同样只跑一次/不排空;Local/Docker goal_task 的 --verify 可选、 --max-rounds 0 原样传。 Co-authored-by: host <host@test.local>
This commit is contained in:
1 parent
4179fb1a0a
commit
c86b450851
10 files changed
+519
-108
No files matched your search
+55
-33
@@ -416,7 +416,9 @@ def migrate_tasks_table(conn):
|
||||
|
||||
新列:result_text、error_text、actual_model、started_at、
|
||||
finished_at、child_pid、executor、pr_url、pr_number,以及
|
||||
goal、verify_command、goal_max_rounds(goal 模式用)。
|
||||
goal、verify_command、goal_max_rounds(goal 模式用)、
|
||||
goal_mode(新 goal 语义:1 时 prompt 即目标,无验收命令、
|
||||
不限轮次;老行 NULL 按 0 计)。
|
||||
PRAGMA table_info 查现有列,缺哪个补哪个;已迁移的库再跑
|
||||
一次是 no-op。worker 写执行结果、server 读任务时都依赖
|
||||
这些列存在。
|
||||
@@ -436,6 +438,7 @@ def migrate_tasks_table(conn):
|
||||
"goal": "TEXT",
|
||||
"verify_command": "TEXT",
|
||||
"goal_max_rounds": "INTEGER",
|
||||
"goal_mode": "INTEGER",
|
||||
# HA 阶段 1:认领任务的 worker 所在主机名;/pr 据此判断
|
||||
# worktree 在本机(同步跑)还是别机(排 pr 任务给该主机)。
|
||||
"worktree_host": "TEXT",
|
||||
@@ -620,6 +623,7 @@ def _task_to_dict(row):
|
||||
"goal": _row_get(row, "goal"),
|
||||
"verify_command": _row_get(row, "verify_command"),
|
||||
"goal_max_rounds": _row_get(row, "goal_max_rounds"),
|
||||
"goal_mode": _row_get(row, "goal_mode") or 0,
|
||||
"worktree_host": _row_get(row, "worktree_host"),
|
||||
"model": _row_get(row, "model"),
|
||||
"effort": _row_get(row, "effort"),
|
||||
@@ -1540,35 +1544,53 @@ class CloudControlPlaneHandler(BaseHTTPRequestHandler):
|
||||
self._send_json(400, {
|
||||
"error": "plan 模式与 approval=full 冲突(只读 vs 完全访问)"})
|
||||
return
|
||||
# goal 字段:verify_command 非空即 goal 模式(worker 走
|
||||
# host.py goal 循环);goal 文本缺省用 prompt;轮次缺省 3
|
||||
# (与 host.py 默认一致),非法值 400 且不建任务。
|
||||
goal = body.get("goal")
|
||||
if goal is not None and not isinstance(goal, str):
|
||||
self._send_json(400, {"error": "goal must be a string"})
|
||||
return
|
||||
verify_command = body.get("verify_command")
|
||||
if verify_command is not None and not isinstance(
|
||||
verify_command, str):
|
||||
self._send_json(400, {
|
||||
"error": "verify_command must be a string"})
|
||||
return
|
||||
# 计划模式与 goal 模式也冲突:goal 循环就是要改文件
|
||||
# 直到验测通过,只读沙箱下它永远做不成。
|
||||
if plan and verify_command:
|
||||
self._send_json(400, {
|
||||
"error": "plan 模式与 goal 模式冲突(只读 vs 改文件)"})
|
||||
return
|
||||
try:
|
||||
goal_max_rounds = int(body.get("goal_max_rounds", 3))
|
||||
except (TypeError, ValueError):
|
||||
self._send_json(400, {
|
||||
"error": "goal_max_rounds must be an integer"})
|
||||
return
|
||||
if goal_max_rounds < 1:
|
||||
self._send_json(400, {
|
||||
"error": "goal_max_rounds must be >= 1"})
|
||||
return
|
||||
# goal 模式分新旧两种语义(2026-10-27 起 console 只发新语义):
|
||||
# 新语义 goal_mode=1:prompt 本身就是目标,不需要专用 goal
|
||||
# 文本、不需要验收命令、不设轮次上限(goal_max_rounds 存 0,
|
||||
# worker 走 host.py goal 循环的模型自决分支);传了
|
||||
# verify_command 是语义冲突,400。旧语义(verify_command
|
||||
# 非空)保留:goal 文本缺省用 prompt,轮次缺省 3,非法值
|
||||
# 400 且不建任务;老客户端与已建的旧 goal 任务继续可用。
|
||||
goal_mode = 1 if body.get("goal_mode") in (True, 1) else 0
|
||||
if goal_mode:
|
||||
if plan:
|
||||
self._send_json(400, {
|
||||
"error": "plan 模式与 goal 模式冲突(只读 vs 改文件)"})
|
||||
return
|
||||
if body.get("verify_command"):
|
||||
self._send_json(400, {
|
||||
"error": "goal 模式不需要验收命令(达标由模型宣告)"})
|
||||
return
|
||||
goal = prompt
|
||||
verify_command = None
|
||||
goal_max_rounds = 0
|
||||
else:
|
||||
goal = body.get("goal")
|
||||
if goal is not None and not isinstance(goal, str):
|
||||
self._send_json(400, {"error": "goal must be a string"})
|
||||
return
|
||||
verify_command = body.get("verify_command")
|
||||
if verify_command is not None and not isinstance(
|
||||
verify_command, str):
|
||||
self._send_json(400, {
|
||||
"error": "verify_command must be a string"})
|
||||
return
|
||||
# 计划模式与 goal 模式也冲突:goal 循环就是要改文件
|
||||
# 直到验测通过,只读沙箱下它永远做不成。
|
||||
if plan and verify_command:
|
||||
self._send_json(400, {
|
||||
"error": "plan 模式与 goal 模式冲突(只读 vs 改文件)"})
|
||||
return
|
||||
try:
|
||||
goal_max_rounds = int(body.get("goal_max_rounds", 3))
|
||||
except (TypeError, ValueError):
|
||||
self._send_json(400, {
|
||||
"error": "goal_max_rounds must be an integer"})
|
||||
return
|
||||
if goal_max_rounds < 1:
|
||||
self._send_json(400, {
|
||||
"error": "goal_max_rounds must be >= 1"})
|
||||
return
|
||||
idempotency_key = body.get("idempotency_key")
|
||||
if idempotency_key is not None and not isinstance(idempotency_key, str):
|
||||
self._send_json(400, {"error": "idempotency_key must be a string"})
|
||||
@@ -1596,13 +1618,13 @@ class CloudControlPlaneHandler(BaseHTTPRequestHandler):
|
||||
" idempotency_key, depends_on, priority, created_by,"
|
||||
" created_at, updated_at, last_heartbeat, executor,"
|
||||
" goal, verify_command, goal_max_rounds,"
|
||||
" model, effort, approval, plan)"
|
||||
" model, effort, approval, plan, goal_mode)"
|
||||
" VALUES(?, ?, ?, 'queued', ?, ?, ?, ?, ?, ?, NULL, ?,"
|
||||
" ?, ?, ?, ?, ?, ?, ?)",
|
||||
" ?, ?, ?, ?, ?, ?, ?, ?)",
|
||||
(task_id, project_id, prompt, idempotency_key,
|
||||
json.dumps(depends_on), priority, user_id, now, now,
|
||||
executor, goal, verify_command, goal_max_rounds,
|
||||
model, effort, approval, plan),
|
||||
model, effort, approval, plan, goal_mode),
|
||||
)
|
||||
conn.commit()
|
||||
except (sqlite3.IntegrityError, _PgIntegrityError):
|
||||
|
||||
Reference in new issue
Block a user