cloud: worker dispatch, DB abstraction, packaging 0.1.0, repo CI workflow

This commit is contained in:
host committed 2026-10-26 01:44:15 +08:00
1 parent 076ba01c74
commit 07ef869267
13 files changed
+1916 -9

No files matched your search

+105 -2
View File
@@ -9,6 +9,8 @@
- CloudExecutor:占位,未接通云端执行。
"""
import json
import re
import shutil
import subprocess
import sys
@@ -50,20 +52,121 @@ class LocalExecutor(Executor):
runner 默认是 subprocess.run 的薄封装;单测可注入假 runner,假
runner 返回 CompletedProcess 风格的 result 对象(含 returncode、
stdout),本类只读这两个字段,不依赖其它字段。
execute() 之外另有 dispatch_task()/run_task():它们是 worker
派发时调 host.py 真实子命令的入口——dispatch 对应 host.py 的
dispatch(建任务并跑一轮 turn),run 对应 host.py 的 run
(在已有 host 任务上再跑一轮 turn)。两者都只发真实存在的
host.py 子命令,不编造命令名;execute() 本身仍只发只读 status,
绝不在 execute 里直接跑模型回合(见 test_queue_sandbox 的断言)。
"""
def __init__(self, host_py_path, config_path=None, runner=None):
def __init__(self, host_py_path, config_path=None, runner=None,
repo=None):
self.host_py_path = host_py_path
self.config_path = config_path
self.runner = runner if runner is not None else _default_runner
self.repo = repo
def _command(self, task_id):
def _base_command(self):
cmd = [sys.executable, self.host_py_path]
if self.config_path is not None:
cmd += ["--config", self.config_path]
return cmd
def _command(self, task_id):
cmd = self._base_command()
cmd += ["status", "--task", str(task_id)]
return cmd
def _result_from_runner(self, task_id, result):
"""把 runner 返回值统一成 execute() 同款结果 dict。
host.py dispatch/run 成功时打印 JSON(含 status/text),
失败时以 SystemExit(1) 退出并打印错误文本。因此:returncode
非 0 一律 failed;returncode 为 0 时优先取 JSON 里的 status
(completed 外的状态,如 timeout,均按失败计,worker 侧再
判断是否可 fallback),取不到 JSON 时按 completed 计。
"""
returncode = getattr(result, "returncode", 1)
stdout = _truncate_stdout(getattr(result, "stdout", ""))
status = "failed"
if returncode == 0:
status = "completed"
try:
parsed = json.loads(stdout)
except (TypeError, ValueError):
parsed = None
if isinstance(parsed, dict) and parsed.get("status"):
status = ("completed" if parsed["status"] == "completed"
else "failed")
host_status = parsed["status"]
else:
host_status = None
else:
host_status = None
return {
"executor": "local",
"task_id": task_id,
"status": status,
"stdout": stdout,
"returncode": returncode,
"host_status": host_status,
# host.py 的 RuntimeError(如“任务已存在”)走 traceback 到
# stderr,worker 判断 dispatch 续跑时需要它;execute()
# 的旧结果 dict 不含此字段,保持不变。
"stderr": _truncate_stdout(getattr(result, "stderr", "")),
}
def _host_name(self, task):
"""cloud 任务 id -> host 任务名:只保留安全字符。
server 生成的 id 本就是 uuid hex,走这一步只是防御自定义 id;
同一 id 每次算出同一名字,dispatch 失败后的 run 续跑才能
找到同一个 host 任务。
"""
raw = str(task.get("id"))
return re.sub(r"[^A-Za-z0-9_.-]", "-", raw)
def dispatch_task(self, task) -> dict:
"""调 host.py dispatch:建 host 任务并跑一轮 turn。
命令:host.py [--config CFG] dispatch --name <host 名>
--task <prompt> [--repo <repo>]。repo 取任务的 repo 字段,
没有则取构造时传的 repo,都没有则不传(此时 host.py 用其
配置文件里的 repo,配置里也没有时 host.py 自己报错,
worker 把该报错原样写回 failed,不编造成功)。
"""
if not isinstance(task, dict) or task.get("id") is None:
raise ValueError("任务缺少 id")
if not task.get("prompt"):
raise ValueError("任务缺少 prompt")
cmd = self._base_command()
cmd += ["dispatch", "--name", self._host_name(task),
"--task", task["prompt"]]
repo = task.get("repo") or self.repo
if repo:
cmd += ["--repo", repo]
return self._result_from_runner(
task["id"], self.runner(cmd))
def run_task(self, task) -> dict:
"""调 host.py run:在已有 host 任务上再跑一轮 turn。
命令:host.py [--config CFG] run --task <host 名>
--prompt <prompt>。worker 只在 dispatch 报“任务已存在”时
用它续跑,不用它做首次派发。
"""
if not isinstance(task, dict) or task.get("id") is None:
raise ValueError("任务缺少 id")
if not task.get("prompt"):
raise ValueError("任务缺少 prompt")
cmd = self._base_command()
cmd += ["run", "--task", self._host_name(task),
"--prompt", task["prompt"]]
return self._result_from_runner(
task["id"], self.runner(cmd))
def execute(self, task) -> dict:
task_id = task.get("id") if isinstance(task, dict) else None
if task_id is None: