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:
1 parent
3a5c5db9fb
commit
3096e04345
5 files changed
+720
-11
No files matched your search
+70
-4
@@ -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:
|
||||
# 本轮已跑完(无论成败):续跑消息已消费,清掉;进程
|
||||
# 中途被杀时清不到,下次认领会重跑同一条消息——可接受
|
||||
|
||||
Reference in new issue
Block a user