cloud: production control plane (API, auth/RBAC, queue, sandbox, memory/KB, fallback/cost, webhooks)
This commit is contained in:
1 parent
b97c34465f
commit
b74d99e959
17 files changed
+3420
No files matched your search
+163
@@ -0,0 +1,163 @@
|
||||
"""派发队列:依赖拓扑排序、就绪筛选、预算判断与内存任务队列。
|
||||
|
||||
零第三方依赖,只用 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
|
||||
Reference in new issue
Block a user