server:POST /tasks/{id}/continue(终端非 merged 任务重排队,
resume_message 列存本轮消息,不传选项沿用原值,plan/goal 冲突
与新建一致,goal_mode=1 时转新 goal 语义;CAS 防并发改状态,
审计 task_continued);tasks.resume_message 三处迁移 + PG 建表。
worker:认领后本轮 prompt 换成续跑消息(只换内存副本),
goal 续跑走 goal-resume 而不是 goal,跑完清 resume_message
(进程被杀时不清,下次认领重跑同一条消息)。
host:goal-resume --goal-text(旧任务接续 goal 模式:跳过状态
与记录守卫,旧线程上从第 1 轮开新目标循环,新语义不限轮次;
无线程时拒绝)。
sandbox:Local/Docker goal_resume_task + popen goal-resume kind;
附带 popen goal 命令改成新语义(无验收不传 --verify,轮次 0
原样传,此前 goal 任务到本地 worker 会直接报错)。
console:详情页终端非 merged 任务出继续会话表单(消息 +
模型/强度/权限/计划/goal,模型下拉复用中转列表)。
测试:新增 test_task_continue.py 11 个 + test_host_continue.py 2 个。
87 lines
3.0 KiB
Python
87 lines
3.0 KiB
Python
"""旧任务接续 goal 模式:host.py goal-resume --goal-text。
|
|
|
|
已终局的旧任务没有 goal 记录是正常情况,--goal-text 模式跳过
|
|
状态与记录守卫,在旧线程上从第 1 轮开新目标循环(新语义:
|
|
无验收、不传轮次时不限轮次,模型宣告完成才结束)。
|
|
"""
|
|
|
|
import json
|
|
import os
|
|
import sys
|
|
import tempfile
|
|
import unittest
|
|
from pathlib import Path
|
|
from unittest import mock
|
|
|
|
|
|
REPO_ROOT = Path(__file__).resolve().parents[1]
|
|
sys.path.insert(0, str(REPO_ROOT))
|
|
|
|
import host
|
|
|
|
|
|
class FakeServer(object):
|
|
def __init__(self, text):
|
|
self.text = text
|
|
self.resumed = []
|
|
|
|
def initialize(self):
|
|
pass
|
|
|
|
def close(self):
|
|
pass
|
|
|
|
def resume_thread(self, thread_id):
|
|
self.resumed.append(thread_id)
|
|
|
|
def run_turn(self, thread_id, prompt, model=None, effort=None,
|
|
sandbox=None, timeout=None):
|
|
return {"status": "completed", "text": self.text,
|
|
"turn_id": "u1"}
|
|
|
|
|
|
class GoalResumeTextTest(unittest.TestCase):
|
|
def _run_main(self, task, goal_text):
|
|
with tempfile.TemporaryDirectory() as tmp:
|
|
state_path = os.path.join(tmp, "host-state.json")
|
|
cfg_path = os.path.join(tmp, "host-config.json")
|
|
with open(state_path, "w", encoding="utf-8") as f:
|
|
json.dump({"tasks": {"g": task}, "automations": []}, f)
|
|
with open(cfg_path, "w", encoding="utf-8") as f:
|
|
json.dump({}, f)
|
|
fake = FakeServer("done\n目标已达成")
|
|
argv = ["host.py", "--state", state_path,
|
|
"--config", cfg_path,
|
|
"goal-resume", "--task", "g"]
|
|
if goal_text is not None:
|
|
argv += ["--goal-text", goal_text]
|
|
with mock.patch.object(sys, "argv", argv):
|
|
with mock.patch.object(host, "Server",
|
|
lambda **kw: fake):
|
|
host.main()
|
|
with open(state_path, encoding="utf-8") as f:
|
|
saved = json.load(f)["tasks"]["g"]
|
|
return saved, fake
|
|
|
|
def test_goal_text_init_on_completed_task(self):
|
|
task = {"name": "g", "thread_id": "x", "cwd": "/tmp",
|
|
"sandbox": None, "max_retries": 0, "retry_count": 0,
|
|
"turns": [], "status": "completed"}
|
|
saved, fake = self._run_main(task, "new goal")
|
|
self.assertEqual(saved["status"], "goal-met")
|
|
self.assertEqual(saved["goal"], "new goal")
|
|
self.assertEqual(saved["goal_rounds_done"], 1)
|
|
self.assertEqual(fake.resumed, ["x"])
|
|
|
|
def test_goal_text_refuses_task_without_thread(self):
|
|
task = {"name": "g", "cwd": "/tmp",
|
|
"sandbox": None, "max_retries": 0, "retry_count": 0,
|
|
"turns": [], "status": "completed"}
|
|
saved, fake = self._run_main(task, "new goal")
|
|
self.assertEqual(saved["status"], "completed")
|
|
self.assertEqual(fake.resumed, [])
|
|
|
|
|
|
if __name__ == "__main__":
|
|
unittest.main()
|