Files
codex-app-server/cloud/queue.py
T

164 lines
5.4 KiB
Python
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
"""派发队列:依赖拓扑排序、就绪筛选、预算判断与内存任务队列。
零第三方依赖,只用 Python 标准库;不依赖 host.py,纯函数 + 内存
状态实现。状态机与 host.py 对齐:
queued / running / completed / failed / interrupted / merged
/ merge-failed
注意:本模块只决定“按什么顺序派发”,不跑模型回合、不调 agent。
"""
VALID_STATUSES = frozenset({
"queued",
"running",
"completed",
"failed",
"interrupted",
"merged",
"merge-failed",
})
def _task_id(task):
task_id = task.get("id")
if task_id is None:
raise ValueError("任务缺少 id")
return task_id
def _depends_on(task):
deps = task.get("depends_on") or []
if isinstance(deps, str):
deps = [deps]
return list(deps)
def _priority(task):
return task.get("priority") or 0
def _topo_sort(tasks, initial_done=frozenset(), strict_missing=True):
"""按波次拓扑排序:依赖先排,同一波内 priority 大的先,
priority 相同时按 id 升序(保证确定性)。
initial_done:已视为完成的 id 集合(ready_tasks 用 completed_ids
填充它,这样就绪子集里的依赖不再被当成缺失依赖)。
strict_missing=True 时,依赖不在任务集且不在 initial_done 里
即 raise ValueError;成环同样 raise ValueError。
"""
by_id = {}
for task in tasks:
task_id = _task_id(task)
if task_id in by_id:
raise ValueError(f"任务 id 重复:{task_id}")
by_id[task_id] = task
for task in tasks:
for dep in _depends_on(task):
if dep == _task_id(task):
raise ValueError(f"任务 {_task_id(task)} 不能依赖自己")
if strict_missing and dep not in by_id and dep not in initial_done:
raise ValueError(
f"任务 {_task_id(task)} 依赖不存在的任务 {dep}")
ordered = []
done = set(initial_done)
remaining = list(tasks)
while remaining:
ready = [t for t in remaining
if all(d in done for d in _depends_on(t))]
if not ready:
names = ", ".join(str(_task_id(t)) for t in remaining)
raise ValueError(f"依赖成环,无法排序:{names}")
ready.sort(key=lambda t: (-_priority(t), str(_task_id(t))))
for task in ready:
ordered.append(task)
done.add(_task_id(task))
remaining = [t for t in remaining if _task_id(t) not in done]
return ordered
def order_tasks(tasks) -> list:
"""按依赖拓扑排序,同一波内 priority 大的先。
tasks 是 dict 列表,每项含 id、depends_on(list,可缺省)、
priority(int,可缺省 0)。缺失依赖或成环 raise ValueError。
不修改输入列表,返回新列表。
"""
return _topo_sort(list(tasks))
def ready_tasks(tasks, completed_ids) -> list:
"""返回依赖全部在 completed_ids 里、且状态为 queued 的任务,
再按 order_tasks 的规则(priority 降序、同波拓扑)排序。
未写 status 字段的任务按 queued 计;running/completed/failed 等
状态的任务一律不返回。
"""
completed = set(completed_ids or [])
ready = [t for t in tasks
if t.get("status", "queued") == "queued"
and all(d in completed for d in _depends_on(t))]
return _topo_sort(ready, initial_done=completed, strict_missing=False)
def budget_exceeded(usage_turns, max_turns, usage_cost_usd,
max_cost_usd) -> bool:
"""任一上限非 None 且用量 >= 上限即 True,否则 False。
上限为 None 表示该项不限;用量为 None 按 0 计。
"""
if max_turns is not None and (usage_turns or 0) >= max_turns:
return True
if max_cost_usd is not None and (usage_cost_usd or 0) >= max_cost_usd:
return True
return False
class TaskQueue:
"""内存队列:add / mark_running / mark_done / next_ready。
内部存任务 dict 的浅拷贝,add 时缺省状态为 queued;status
只能是 VALID_STATUSES 中的值,否则 raise ValueError。
"""
def __init__(self, tasks=None):
self._tasks = {}
for task in tasks or []:
self.add(task)
def add(self, task):
task_id = _task_id(task)
if task_id in self._tasks:
raise ValueError(f"任务 id 重复:{task_id}")
stored = dict(task)
status = stored.get("status", "queued")
if status not in VALID_STATUSES:
raise ValueError(f"非法任务状态:{status}")
stored["status"] = status
self._tasks[task_id] = stored
return stored
def mark_running(self, task_id):
task = self._get(task_id)
task["status"] = "running"
return task
def mark_done(self, task_id, status):
if status not in VALID_STATUSES:
raise ValueError(f"非法任务状态:{status}")
task = self._get(task_id)
task["status"] = status
return task
def next_ready(self, completed_ids):
"""按 ready_tasks 顺序的第一个可派任务,或 None。"""
ready = ready_tasks(list(self._tasks.values()), completed_ids)
return ready[0] if ready else None
def _get(self, task_id):
try:
return self._tasks[task_id]
except KeyError:
raise ValueError(f"未知任务:{task_id}") from None