simbiot-harness/harness/levels.py
Simbiot Harness 7f4db9f085 feat(delivery): автодоставка готовых артефактов в TG + orphan-requeue сирот рестартов (t-260929205927-8c20ea)
- события TaskAwaitDelivery / DeliverySent: флаг 'результат доставить в чат'
  на задаче, доставка идемпотентна (один раз sent — повторов нет)
- commands.mark_awaiting_delivery / deliver_result (единственный путь отправки) /
  complete_task_with_delivery: завершение и отправка идут парой —
  «сделал и промолчал» больше не случается
- scheduler: push после TaskCompleted + delivery-демон (pull каждые 15с —
  досылает, если тик умер между завершением и отправкой); requeue сбрасывает
  доставку — доработка снова уйдёт в чат
- orphan_requeue: running-задачи с мёртвым воркером (рестарт демона убивает
  воркеров, TaskCompleted не пишется) возвращаются в очередь автоматически;
  корень эскалации e-260929234911: 'повторный таймаут часового воркера'
  был убийством живого воркера рестартом в 23:12/00:03, не таймаутом
  (трейс и BudgetSpent шли вплоть до секунды рестарта)
- cli: task -a (подать с доставкой) + task-await (пометить существующую);
  мин-уровень помечает задачи-просьбы из TG
- 18 тестов на копии БД: submit→await→complete→deliver→idempotency→
  requeue→re-deliver→pull→orphan→rebuild
2026-09-30 01:31:03 +00:00

100 lines
No EOL
6 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.

"""Simbiot Harness — уровни и конфигурация тактовой лестницы.
Контекст ∝ масштабу: чем крупнее цикл, тем больше контекст и модель.
"""
from dataclasses import dataclass, field
SEC = 1
MIN = 60
HOUR = 3600
DAY = 86400
WEEK = 7 * DAY
MONTH = 30 * DAY
QUARTER = 91 * DAY
YEAR = 366 * DAY # округляем год как 366 дней для простоты выравнивания
@dataclass
class CycleLevel:
name: str
label: str
every: int # секунд между тиками
uses_llm: bool
model: str # модель 9router (или "none")
effort: str # effort Strands: auto/low/medium/high/off
ctx_tokens: int # бюджет контекста уровня
task_classes: tuple # какие классы задач может брать
mandate: str # мандат уровня (в промпт)
can_shell: bool # инструменты уровня
tick_timeout: int = 300 # таймаут LLM-тика, сек (занятый уровень поглощает такты)
def seq(self, ts: int) -> int:
"""Номер такта уровня для unix-ts (для идемпотентности тиков)."""
return ts // self.every
LEVELS = {
"sec": CycleLevel(
name="sec", label="Секунда · рефлекс", every=SEC, uses_llm=False,
model="none", effort="off", ctx_tokens=0, task_classes=(),
mandate=("Ты — рефлекторный уровень харнеса. Работает без LLM: телеметрия, "
"heartbeat, алерты. Никаких решений — только наблюдение."),
can_shell=False,
),
"min": CycleLevel(
name="min", label="Минута · пульс", every=MIN, uses_llm=True,
model="auto", effort="low", ctx_tokens=4_000, task_classes=("S",),
mandate=("Ты — минутный пульс харнеса Simbiot. Триаж входящих TG-сообщений: "
"если во ВХОДЯЩИХ есть сообщение без ответа — ответь на него командой "
"cli.py tg-reply <id> \"текст\" (тон: кратко, по-русски, тёпло; можно "
"выполнить просьбу инструментами и ответить результатом). Быстрые "
"мелкие фиксы (S-задачи до 5 минут) тоже твои. Не растекайся. "
"ВАЖНО: НЕ исследуй исходники харнеса (scheduler.py, commands.py, es.py, "
"isolated_runner.py) и не разбирай падения тиков — это зона верхних уровней. "
"Отвечай из контекста тика, максимум пара шагов, иначе таймаут тика. "
"Если просьба большая — подай её как задачу (cli.py task -a — флаг "
"автодоставки: результат сам придёт в чат при готовности, не проси "
"Alex спрашивать дважды) и скажи это. "
"Важное, что не по зубам уровню (падение тика, повторная ошибка, "
"требуется фикс с большим контекстом) — эскалируй наверх: "
"cli.py escalate \"причина\" -c \"контекст\" -s critical|warn|info; "
"эскалацию увидит часовой/дневной уровень в своём контексте."),
can_shell=True,
),
"hour": CycleLevel(
name="hour", label="Час · поток", every=HOUR, uses_llm=True,
model="auto", effort="medium", ctx_tokens=32_000, task_classes=("S", "M"),
mandate=("Ты — часовой поток харнеса Simbiot. Дайджест часа: что произошло, "
"статусы фоновых задач, батч мелочей (S/M). Консолидируй минутные "
"события в выжимку — сырое наверх не поднимай. "
"Эскалации от нижних уровней (секция ЭСКАЛАЦИИ в контексте) — твоя "
"зона: разберись и закрой cli.py escalation-ack <id> -n \"что сделал\"; "
"если фикс больше твоего контекста — переэскалируй "
"cli.py escalate \"причина\" -f hour -s critical."),
can_shell=True,
),
"day": CycleLevel(
name="day", label="День · манифест", every=DAY, uses_llm=True,
model="auto", effort="high", ctx_tokens=128_000, task_classes=("S", "M", "L"),
mandate=("Ты — дневной цикл харнеса Simbiot. Манифест дня: план, выбор задач "
"L-класса, дневной отчёт. Думай глубоко, планируй на день вперёд. "
"Открытые эскалации (секция ЭСКАЛАЦИИ) — приоритет: это важное, "
"поднятое с нижних уровней; закрой cli.py escalation-ack <id>."),
can_shell=True,
),
}
# v0.2+: week/month/quarter/year — включаются по мере готовности
FUTURE_LEVELS = ("week", "month", "quarter", "year")
def size_class(minutes: int) -> str:
"""Класс задачи по оценке длительности."""
if minutes <= 5:
return "S"
if minutes <= 60:
return "M"
if minutes <= 8 * 60:
return "L"
return "XL"