feat: 旧任务继续会话——终端任务带新消息 + 全部选项同一会话续跑
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 个。
This commit is contained in:
1 parent
cf293914d9
commit
3a4997d346
9 files changed
+823
-20
No files matched your search
@@ -0,0 +1,86 @@
|
||||
"""旧任务接续 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()
|
||||
Reference in new issue
Block a user