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
+110
-6
@@ -386,6 +386,27 @@ class LocalExecutor(Executor):
|
||||
return self._goal_result_from_runner(
|
||||
task["id"], self.runner(cmd))
|
||||
|
||||
def goal_resume_task(self, task, goal_text) -> dict:
|
||||
"""调 host.py goal-resume --goal-text:在旧线程上开新目标
|
||||
循环(旧任务接续 goal 模式,云端 /continue 带 goal_mode=1
|
||||
时走这里)。命令:host.py goal-resume --task <host 名>
|
||||
--goal-text <新目标> --max-rounds 0(新语义不限轮次)。
|
||||
不传 --repo:goal-resume 操作已有 host 任务,不新建
|
||||
worktree。结论解析与 goal_task 同款。
|
||||
"""
|
||||
if not isinstance(task, dict) or task.get("id") is None:
|
||||
raise ValueError("任务缺少 id")
|
||||
if not goal_text:
|
||||
raise ValueError("goal-resume 缺少目标文本")
|
||||
cmd = self._base_command(model=task.get("model"),
|
||||
effort=task.get("effort"),
|
||||
approval=task.get("approval"))
|
||||
cmd += ["goal-resume", "--task", self._host_name(task),
|
||||
"--goal-text", goal_text,
|
||||
"--max-rounds", "0"]
|
||||
return self._goal_result_from_runner(
|
||||
task["id"], self.runner(cmd))
|
||||
|
||||
def _goal_result_from_runner(self, task_id, result):
|
||||
"""把 host.py goal 的 runner 返回值解析成 goal 结果 dict。
|
||||
|
||||
@@ -437,12 +458,14 @@ class LocalExecutor(Executor):
|
||||
start_new_session=True 让子进程自成进程组,worker 取消时
|
||||
killpg 才能连同子孙一起杀掉。注意:只有默认 runner(真实
|
||||
subprocess)时才用本方法;单测注入的假 runner 走同步路径,
|
||||
不走这里。kind=goal 时建 goal 命令(结论解析由 worker 的
|
||||
_execute_via_process 按 kind 分发到 _goal_result_from_runner)。
|
||||
goal 语义与同步 goal_task 一致:新形态(goal_mode)无验收
|
||||
命令时 --verify 不传、--max-rounds 传 0(0 不能用 or 兜底
|
||||
成 3);旧形态仍传验收命令与轮次。seed_dir 只对 dispatch/
|
||||
goal 追加(run 续跑不传,文件已在 worktree 里)。
|
||||
不走这里。kind=goal 时建 goal 命令、kind=goal-resume 时建
|
||||
goal-resume 命令(旧任务接续 goal 模式,目标文本取本轮
|
||||
prompt;结论解析由 worker 的 _execute_via_process 按 kind
|
||||
分发到 _goal_result_from_runner)。goal 语义与同步 goal_task
|
||||
一致:新形态(goal_mode)无验收命令时 --verify 不传、
|
||||
--max-rounds 传 0(0 不能用 or 兜底成 3);旧形态仍传验收
|
||||
命令与轮次。seed_dir 只对 dispatch/goal 追加(run 续跑与
|
||||
goal-resume 不传,文件已在 worktree 里)。
|
||||
"""
|
||||
if not isinstance(task, dict) or task.get("id") is None:
|
||||
raise ValueError("任务缺少 id")
|
||||
@@ -474,6 +497,13 @@ class LocalExecutor(Executor):
|
||||
cmd += ["--repo", repo]
|
||||
if seed_dir:
|
||||
cmd += ["--seed-dir", seed_dir]
|
||||
elif kind == "goal-resume":
|
||||
# 旧任务接续 goal:host 侧用 --goal-text 在旧线程上开
|
||||
# 新目标循环(新语义,不限轮次);task["prompt"] 在续跑
|
||||
# 时已被 worker 换成续跑消息。
|
||||
cmd += ["goal-resume", "--task", self._host_name(task),
|
||||
"--goal-text", task["prompt"],
|
||||
"--max-rounds", "0"]
|
||||
else:
|
||||
cmd += ["dispatch", "--name", self._host_name(task),
|
||||
"--task", task["prompt"]]
|
||||
@@ -1041,6 +1071,80 @@ class DockerExecutor(Executor):
|
||||
"usage": _parse_goal_usage(stdout_full),
|
||||
}
|
||||
|
||||
def goal_resume_task(self, task, goal_text, timeout=None,
|
||||
extra_mounts=None, network=None,
|
||||
extra_env=None) -> dict:
|
||||
"""容器内调 host.py goal-resume --goal-text:在旧线程上开
|
||||
新目标循环(旧任务接续 goal 模式)。解析与 goal_task 同款
|
||||
(完整 stdout 结论、存库截尾),但没有“已存在”分支:
|
||||
goal-resume 本来就是操作已有 host 任务,不存在 dispatch
|
||||
撞名问题;returncode 非 0 或无结论一律 failed。
|
||||
"""
|
||||
if not isinstance(task, dict) or task.get("id") is None:
|
||||
raise ValueError("任务缺少 id")
|
||||
if not goal_text:
|
||||
raise ValueError("goal-resume 缺少目标文本")
|
||||
inner = self._inner_host_command(
|
||||
"goal-resume", "--task", self._host_name(task),
|
||||
"--goal-text", goal_text, "--max-rounds", "0",
|
||||
model=task.get("model"), effort=task.get("effort"),
|
||||
approval=task.get("approval"))
|
||||
cmd = self.build_run_command(task, inner,
|
||||
extra_mounts=extra_mounts,
|
||||
network=network,
|
||||
extra_env=extra_env)
|
||||
try:
|
||||
result = self._call_runner(cmd, timeout=timeout)
|
||||
except Exception as exc:
|
||||
if exc.__class__.__name__ == "TimeoutExpired":
|
||||
self.kill_container(task)
|
||||
return {
|
||||
"executor": "docker", "task_id": task["id"],
|
||||
"status": "failed", "stdout": "docker run timeout",
|
||||
"returncode": None, "host_status": None, "stderr": "",
|
||||
"container": self.container_name(task),
|
||||
"goal_met": None, "goal_error": "docker run timeout",
|
||||
}
|
||||
return {
|
||||
"executor": "docker", "task_id": task["id"],
|
||||
"status": "failed",
|
||||
"stdout": _truncate_stdout(str(exc)),
|
||||
"returncode": None, "host_status": None, "stderr": "",
|
||||
"container": self.container_name(task),
|
||||
"goal_met": None, "goal_error": str(exc)[:500],
|
||||
}
|
||||
returncode = getattr(result, "returncode", 1)
|
||||
stdout_full = getattr(result, "stdout", "") or ""
|
||||
stderr = getattr(result, "stderr", "") or ""
|
||||
met, goal_error = _parse_goal_output(stdout_full)
|
||||
if returncode != 0:
|
||||
status = "failed"
|
||||
if met is None:
|
||||
goal_error = ((stderr.strip()[-500:] if stderr.strip()
|
||||
else stdout_full.strip()[-500:])
|
||||
or "host goal-resume failed")
|
||||
elif met is True:
|
||||
status = "completed"
|
||||
elif met is False:
|
||||
status = "failed"
|
||||
else:
|
||||
status = "failed"
|
||||
goal_error = goal_error or "host goal-resume 输出无结论"
|
||||
return {
|
||||
"executor": "docker",
|
||||
"task_id": task["id"],
|
||||
"status": status,
|
||||
"stdout": _goal_stdout_tail(stdout_full),
|
||||
"stderr": stderr,
|
||||
"returncode": returncode,
|
||||
"host_status": ("goal-met" if met is True else
|
||||
"goal-unmet" if met is False else None),
|
||||
"container": self.container_name(task),
|
||||
"goal_met": met,
|
||||
"goal_error": goal_error,
|
||||
"usage": _parse_goal_usage(stdout_full),
|
||||
}
|
||||
|
||||
def network_gateway(self, network_name):
|
||||
"""某 Docker 网络的网关 IP(宿主在该网桥上的地址)。
|
||||
|
||||
|
||||
Reference in new issue
Block a user