86 lines
No EOL
3.5 KiB
Python
86 lines
No EOL
3.5 KiB
Python
"""Simbiot Harness — command side (CQRS): валидация + события.
|
|
|
|
Задачи подаются через CLI/Telegram/файл-инбокс; каждая команда
|
|
валидируется и только потом пишет событие.
|
|
"""
|
|
import os
|
|
import secrets
|
|
from datetime import datetime, timezone
|
|
|
|
from . import es
|
|
from .levels import size_class
|
|
|
|
|
|
def submit_task(title: str, details: str = "", minutes: int = 5, source: str = "cli"):
|
|
"""SubmitTask: класс по оценке длительности. Идемпотентно по id."""
|
|
task_id = f"t-{datetime.now(timezone.utc).strftime('%y%m%d%H%M%S')}-{secrets.token_hex(3)}"
|
|
size = size_class(minutes)
|
|
es.append_event("task", task_id, "TaskSubmitted", {
|
|
"id": task_id, "title": title, "details": details,
|
|
"size": size, "est_minutes": minutes, "source": source,
|
|
})
|
|
return task_id, size
|
|
|
|
|
|
def promote_task(task_id: str, reason: str = "manual promote"):
|
|
"""Промоушен ready-задачи на уровень выше (TaskPromoted → ready, size выше)."""
|
|
order = ["S", "M", "L"]
|
|
model = es.run_projections()
|
|
t = model["tasks"].get(task_id)
|
|
if t is None:
|
|
raise ValueError(f"задача {task_id} не найдена")
|
|
if t["status"] != "ready":
|
|
raise ValueError(f"задача {task_id} в статусе {t['status']} — promote только из ready")
|
|
i = order.index(t["size"]) if t["size"] in order else 0
|
|
if i >= len(order) - 1:
|
|
raise ValueError(f"задача {task_id} уже размера {t['size']} — выше некуда")
|
|
new_size = order[i + 1]
|
|
es.append_event("task", task_id, "TaskPromoted",
|
|
{"to": new_size, "from": t["size"], "reason": reason})
|
|
return task_id, new_size
|
|
|
|
|
|
def requeue_task(task_id: str, reason: str = "manual requeue"):
|
|
"""Вернуть failed/прерванную задачу в очередь (TaskPromoted → ready)."""
|
|
model = es.run_projections()
|
|
t = model["tasks"].get(task_id)
|
|
if t is None:
|
|
raise ValueError(f"задача {task_id} не найдена")
|
|
if t["status"] not in ("failed", "done"):
|
|
raise ValueError(f"задача {task_id} в статусе {t['status']} — requeue не нужен")
|
|
es.append_event("task", task_id, "TaskPromoted",
|
|
{"to": t["size"], "from": t["size"], "reason": reason})
|
|
return task_id
|
|
|
|
|
|
def set_policy(identity: str = None, cpu_alert_threshold: int = None):
|
|
"""PolicyChanged: идентичность + пороги. Мержим поверх текущей политики."""
|
|
model = es.run_projections()
|
|
current = dict(model.get("policy") or {})
|
|
patch = {}
|
|
if identity is not None:
|
|
patch["identity"] = identity
|
|
if cpu_alert_threshold is not None:
|
|
patch["cpu_alert_threshold"] = cpu_alert_threshold
|
|
current.update(patch)
|
|
es.append_event("harness", "policy", "PolicyChanged", current)
|
|
return current
|
|
|
|
|
|
def list_tasks():
|
|
model = es.run_projections()
|
|
out = []
|
|
for t in model["tasks"].values():
|
|
out.append(t)
|
|
return out
|
|
|
|
|
|
def status():
|
|
model = es.run_projections()
|
|
return {
|
|
"cycle_state": model["cycle_state"],
|
|
"tasks_ready": sum(1 for t in model["tasks"].values() if t["status"] == "ready"),
|
|
"tasks_done": sum(1 for t in model["tasks"].values() if t["status"] == "done"),
|
|
"digests": len(model["digests"]),
|
|
"spend": model.get("spend", [])[-8:],
|
|
} |