feat(gate): гейт важности LLM-тика по per-level порогу + model в событиях

- гейт (e-260930062242/2510/2607/2741): collect_signals ранжирует события
  (unreplied_inbox/ready_task/critical-эскалация = critical; warn-эскалации,
  high_load = warn), gate_allows_llm решает по порогу уровня
  (SIMBIOT_GATE_<LEVEL>, дефолты min=critical/hour=warn/day=warn);
  тихий такт — GateSkipped + дайджест-заглушка, без LLM и без расхода роутера
- model в BudgetSpent/InvocationStarted (e-260930062120): видно, какая модель
  и уровень тратили кредиты (strands не выносит фактическую модель в usage —
  пишем запрошенный model_id)
- per-level лимиты тул-колов уже в дереве: resolve_tool_budget (min=30/hour=300,
  env-оверрайд) (e-260930060717/060904)
- tests/test_gate.py: 28 hermetic-проверок, ALL PASS
- simbiot-presentation.html: презентация Simbiot (t-260930053538), 10 слайдов
This commit is contained in:
Simbiot Harness 2026-09-30 08:29:54 +00:00
parent 5faf78fd09
commit 871a3b15a3
5 changed files with 548 additions and 10 deletions

View file

@ -147,13 +147,18 @@ def _extract_usage(agent) -> dict:
return usage
def _emit_budget_incremental(level: str, trace_id: str, usage_delta: dict):
def _emit_budget_incremental(level: str, trace_id: str, usage_delta: dict,
model: str = None):
"""Записать BudgetSpent за один модельный вызов.
Воркер — единственный автор spend-событий, но scheduler может убить
его по таймауту в любой момент (SIGKILL). Поэтому usage пишем
ИНКРЕМЕНТАЛЬНО: каждый завершённый модельный вызов — своё событие.
Хвост недописанного вызова теряется, но весь тик — никогда.
model (e-260930062120): запрошенный model_id уровня пишется в каждое
событие — в кредитах роутера видно, ЧЬИ это траты; "auto" резолвится
роутером, фактическую модель до usage strands не выносит.
"""
if not usage_delta:
return
@ -161,13 +166,14 @@ def _emit_budget_incremental(level: str, trace_id: str, usage_delta: dict):
from harness import es
es.append_event("harness", "budget", "BudgetSpent", {
"level": level, "trace_id": trace_id, "source": "worker",
**({"model": model} if model else {}),
**usage_delta,
})
except Exception:
pass # трекинг не должен валить тик
def _wire_budget_hooks(agent, level: str, trace_id: str):
def _wire_budget_hooks(agent, level: str, trace_id: str, model: str = None):
"""После каждого модельного вызова — usage-дельта в стор.
Strands вешает usage на assistant-сообщение в
@ -194,12 +200,27 @@ def _wire_budget_hooks(agent, level: str, trace_id: str):
seen.add(key)
_emit_budget_incremental(level, trace_id, {
"prompt_tokens": p, "completion_tokens": c,
})
}, model=model)
agent.add_hook(on_message_added, MessageAddedEvent)
return agent
def resolve_tool_budget(level: str) -> int:
"""Бюджет тул-коллов тика по уровню (e-260930071900, просьба Alex:
часовому уровню нужен большой цикл — «хоть 300»).
Приоритет: SIMBIOT_MAX_TOOL_CALLS_<LEVEL> > SIMBIOT_MAX_TOOL_CALLS
> per-level дефолт. Минута остаётся компактной (30) — жёсткий кап
и спасает её от таймаута (e-260930031800: 90+ шагов → 300с → смерть).
"""
env = (os.environ.get(f"SIMBIOT_MAX_TOOL_CALLS_{level.upper()}")
or os.environ.get("SIMBIOT_MAX_TOOL_CALLS"))
if env:
return int(env)
return {"min": 30, "hour": 300}.get(level, 30)
def main():
payload = json.loads(sys.stdin.read())
@ -236,8 +257,8 @@ def main():
memory=False,
skills=False,
)
agent = _wire_trace_hooks(agent, es, trace_id)
agent = _wire_budget_hooks(agent, level, trace_id)
agent = _wire_trace_hooks(agent, es, trace_id, model=payload.get("model"))
agent = _wire_budget_hooks(agent, level, trace_id, model=payload.get("model"))
# хард-кап шагов (e-260930031800): cancel_tool + end_turn.
# ВАЖНО: НЕ используем limits={"turns": N} — нативный стоп SDK
# обрывает цикл на границе хода, и result.message остаётся
@ -245,8 +266,7 @@ def main():
# (репродукция: stop_reason=limit_turns, text='', тики 04:18+).
# Наш end_turn (AfterToolsEvent) кладёт в историю ассистентский
# текст с итогом — дайджест не пустеет.
agent = _wire_tool_budget(
agent, int(os.environ.get("SIMBIOT_MAX_TOOL_CALLS") or 30))
agent = _wire_tool_budget(agent, resolve_tool_budget(level))
result = agent(payload.get("prompt", ""))
usage = _extract_usage(agent)
_emit(payload, {"ok": True, "text": str(result), "usage": usage, "trace_id": trace_id})

View file

@ -61,8 +61,13 @@ def setup_provider():
os.environ["OPENAI_BASE_URL"] = _router_base_url()
def _wire_trace_hooks(agent, es, trace_id: str):
"""Каждая стадия агентного цикла → событие агрегата trace."""
def _wire_trace_hooks(agent, es, trace_id: str, model: str = None):
"""Каждая стадия агентного цикла → событие агрегата trace.
model (e-260930062120): model_id уровня в InvocationStarted —
видно, КАКАЯ модель запускалась на тике (запрошенная; "auto"
резолвится роутером на его стороне).
"""
from strands import Agent
from strands.hooks import BeforeInvocationEvent, BeforeToolCallEvent, AfterToolCallEvent
@ -73,7 +78,10 @@ def _wire_trace_hooks(agent, es, trace_id: str):
es.append_event("trace", f"{trace_id}:{seq['n']}", kind, payload)
def on_before_invocation(event):
_trace("InvocationStarted", {"trace_id": trace_id, "ts": _now()})
data = {"trace_id": trace_id, "ts": _now()}
if model:
data["model"] = model
_trace("InvocationStarted", data)
def on_before_tool(event):
tool = getattr(event, "tool_use", None) or {}

View file

@ -74,6 +74,85 @@ def _lvl_rank(name: str) -> int:
return _ORDER.index(name) if name in _ORDER else len(_ORDER)
# ────────────────────────── ГЕЙТ ВАЖНОСТИ (e-260930062242/2510/2607/2741) ──────────────────────────
# Alex: LLM-тик уровня запускается ТОЛЬКО если есть событие ≥ порога важности
# этого уровня; иначе тик отрабатывает чисто детерминированной механикой (как sec).
# Ранжирование: critical — входящее TG без ответа, готовая задача уровня,
# открытая critical-эскалация; warn — warn-эскалации, высокая нагрузка;
# всё остальное — тишина, LLM не нужен (не тратим кредиты роутера).
# порог по умолчанию на уровень (env-оверрайд: SIMBIOT_GATE_<LEVEL>=critical|warn|off)
_GATE_DEFAULTS = {"min": "critical", "hour": "warn", "day": "warn"}
def _gate_threshold(level_cfg) -> str:
"""Порог важности уровня: off — гейт выключен, LLM всегда."""
env = os.environ.get(f"SIMBIOT_GATE_{level_cfg.name.upper()}")
if env:
return env.strip().lower()
return _GATE_DEFAULTS.get(level_cfg.name, "off")
def _pending_inbox(model: dict):
"""Входящие TG без ответа (не команды) — материал для минутного LLM."""
from . import tg_commands
return [m for m in (model.get("inbox") or {}).values()
if not m.get("replied") and not m.get("ignored")
and not tg_commands.parse(m.get("text") or "")
and (m.get("text") or m.get("kind") in ("voice", "audio", "video_note"))]
def collect_signals(level_cfg, model: dict, load: float = None):
"""События, важные именно для этого уровня, с рангом важности.
Возвращает [(severity, what, detail)] — чистая функция от read model,
без побочных эффектов; используется гейтом LLM и тестом (hermetic).
"""
sigs = []
# 1) входящее TG без ответа — критично (должен ответить этот или нижний уровень)
inbox = _pending_inbox(model)
if inbox:
ids = ", ".join(("m-" + str(m.get("update_id"))) for m in inbox[:5])
sigs.append(("critical", "unreplied_inbox",
f"{len(inbox)} сообщений без ответа ({ids})"))
# 2) готовая задача доступного уровня классу — критично (тик должен её взять)
ready = [t for t in (model.get("tasks") or {}).values()
if t.get("status") == "ready" and t.get("size") in level_cfg.task_classes]
if ready:
ids = ", ".join(f"{x['id']}[{x.get('size')}]" for x in
sorted(ready, key=lambda x: x.get("created", ""))[:5])
sigs.append(("critical", "ready_task", f"готовые задачи: {ids}"))
# 3) открытые эскалации, адресованные этому уровню и выше — ранг = severity
for e in (model.get("escalations") or {}).values():
if not isinstance(e, dict) or e.get("status") != "open" or e.get("stub"):
continue
to_level = e.get("to_level", "day")
if to_level not in _ORDER or _lvl_rank(level_cfg.name) < _lvl_rank(to_level):
continue
sev = e.get("severity", "info")
if sev in ("critical", "warn", "info"):
sigs.append((sev, f"escalation:{e.get('id')}",
(e.get("reason") or "")[:120]))
# 4) высокая нагрузка — вторично (алерт-порог как у сек-рефлекса)
if load is not None:
thr = (model.get("policy") or {}).get("cpu_alert_threshold", 8)
if load >= thr:
sigs.append(("warn", "high_load", f"load1 {load:.2f} >= {thr}"))
return sigs
def gate_allows_llm(level_cfg, signals) -> bool:
"""Решение гейта: запускать ли LLM-тик по порогу важности уровня."""
thr = _gate_threshold(level_cfg)
if thr == "off":
return True
order = {"critical": 0, "warn": 1, "info": 2}
if thr not in order:
return True
rank = order[thr]
return any(order.get(s[0], 3) <= rank for s in signals)
def build_context(level_cfg, model: dict, task=None):
"""Сборка контекста тика: мандат + память уровня + выжимки нижних + задача."""
parts = [level_cfg.mandate]
@ -265,6 +344,32 @@ def process_level(level_cfg, model: dict):
if not emit_tick(level_cfg, seq):
return "already" # тик этого такта уже был (идемпотентность)
# ГЕЙТ ВАЖНОСТИ (e-260930062242/2510/2607/2741): LLM-тик запускается
# только если есть событие ≥ порога уровня; тихий такт отрабатывает
# детерминированной механикой (как sec) — без расхода кредитов роутера.
if level_cfg.uses_llm:
try:
load = os.getloadavg()[0]
except Exception:
load = None
signals = collect_signals(level_cfg, model, load)
if not gate_allows_llm(level_cfg, signals):
period = datetime.fromtimestamp(seq * level_cfg.every, tz=timezone.utc)
es.append_event("cycle", f"digest:{level_cfg.name}:{seq}", "DigestWritten", {
"level": level_cfg.name, "period": period.isoformat(),
"summary_ref": None,
"text": f"[gate:quiet] {level_cfg.name}: событий важнее порога нет "
f"({len(signals)} ниже порога) — LLM не запускался",
"stub": True,
})
es.append_event("cycle", f"gate:{level_cfg.name}:{seq}", "GateSkipped", {
"level": level_cfg.name,
"threshold": _gate_threshold(level_cfg),
"signals": [{"severity": s[0], "what": s[1], "detail": s[2]}
for s in signals][:10],
})
return f"gate:quiet ({len(signals)} signals below threshold)"
st["busy"] = True
try:
if not level_cfg.uses_llm:

260
simbiot-presentation.html Normal file
View file

@ -0,0 +1,260 @@
<!DOCTYPE html>
<html lang="ru">
<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<title>Simbiot — общий AI-харнес организации · презентация</title>
<style>
:root{
--paper:#faf8f3; --ink:#15150f; --carbon:#1c1b16; --signal:#FF4F00;
--dim:#6b675e; --line:#d8d3c8; --card:#f3f0e8; --ok:#1d7a3e; --warn:#a3690f;
}
*{margin:0;padding:0;box-sizing:border-box}
html{scroll-behavior:smooth}
body{background:var(--paper);color:var(--ink);font:16px/1.62 -apple-system,"Segoe UI",Roboto,Arial,sans-serif;-webkit-font-smoothing:antialiased}
.mono,code{font-family:ui-monospace,"JetBrains Mono","SF Mono",Menlo,Consolas,monospace}
.wrap{max-width:1080px;margin:0 auto;padding:48px 28px 120px}
header{border-bottom:3px solid var(--ink);padding-bottom:28px;margin-bottom:40px;position:relative}
.kicker{font-family:ui-monospace,Menlo,monospace;font-size:12px;letter-spacing:.22em;color:var(--signal);font-weight:700;text-transform:uppercase}
h1{font-size:clamp(34px,6vw,58px);line-height:1.02;font-weight:800;letter-spacing:-.02em;margin:10px 0 14px;text-transform:uppercase}
.sub{font-size:17px;color:var(--dim);max-width:720px}
.badges{display:flex;flex-wrap:wrap;gap:8px;margin-top:18px}
.badge{border:1px solid var(--ink);padding:5px 12px;font-family:ui-monospace,Menlo,monospace;font-size:12px;letter-spacing:.08em;text-transform:uppercase;background:var(--paper)}
.badge.s{background:var(--ink);color:var(--paper)}
section{margin:64px 0 0}
.snum{font-family:ui-monospace,Menlo,monospace;font-size:12px;color:var(--signal);letter-spacing:.2em;font-weight:700}
h2{font-size:26px;font-weight:800;letter-spacing:-.01em;margin:4px 0 6px;text-transform:uppercase}
.lead{color:var(--dim);max-width:760px;margin-bottom:22px}
h3{font-size:15px;text-transform:uppercase;letter-spacing:.06em;margin:26px 0 10px;border-left:3px solid var(--signal);padding-left:10px}
p{margin:10px 0;max-width:820px}
.cardgrid{display:grid;grid-template-columns:repeat(auto-fit,minmax(240px,1fr));gap:14px;margin:18px 0}
.card{border:1px solid var(--ink);background:var(--card);padding:16px 16px 14px}
.card .cn{font-family:ui-monospace,Menlo,monospace;font-size:11px;color:var(--signal);font-weight:700;letter-spacing:.15em}
.card h4{font-size:15px;margin:6px 0 6px;text-transform:uppercase;letter-spacing:.02em}
.card p{font-size:13.5px;color:var(--dim);margin:0;line-height:1.55}
table{width:100%;border-collapse:collapse;font-size:13.5px;margin:14px 0}
th{background:var(--ink);color:var(--paper);text-align:left;padding:9px 10px;font-family:ui-monospace,Menlo,monospace;font-size:11px;letter-spacing:.1em;text-transform:uppercase}
td{border-bottom:1px solid var(--line);padding:9px 10px;vertical-align:top}
tr.hl td{background:#fff3ea}
td .m{font-family:ui-monospace,Menlo,monospace;font-size:12px}
.note{border:1px solid var(--ink);border-left:6px solid var(--signal);background:#fff;padding:14px 18px;margin:20px 0;max-width:860px}
.note b{font-family:ui-monospace,Menlo,monospace;font-size:12px;letter-spacing:.12em;text-transform:uppercase;color:var(--signal)}
.box{border:1px solid var(--ink);padding:16px;background:var(--card)}
.box h5{font-family:ui-monospace,Menlo,monospace;font-size:11px;letter-spacing:.15em;text-transform:uppercase;color:var(--signal);margin-bottom:10px}
ul{margin:8px 0 8px 18px;font-size:14px}
li{margin:5px 0}
li::marker{color:var(--signal)}
.kpi{display:grid;grid-template-columns:repeat(auto-fit,minmax(170px,1fr));gap:14px;margin:18px 0}
.kpi .k{border:1px solid var(--ink);background:#fff;padding:14px 16px}
.kpi .v{font-family:ui-monospace,Menlo,monospace;font-size:30px;font-weight:700;line-height:1.1}
.kpi .l{font-size:12px;color:var(--dim);text-transform:uppercase;letter-spacing:.05em;margin-top:4px}
.vs{display:grid;grid-template-columns:1fr 1fr;gap:14px;margin:18px 0}
.vs .col{border:1px solid var(--ink);padding:16px}
.vs .col.old{background:var(--card)}
.vs .col.new{background:#fff;border-left:6px solid var(--signal)}
.vs h4{font-family:ui-monospace,Menlo,monospace;font-size:11px;letter-spacing:.15em;text-transform:uppercase;margin-bottom:10px}
.vs .old h4{color:var(--dim)}
.vs .new h4{color:var(--signal)}
.flow{display:grid;grid-template-columns:repeat(auto-fit,minmax(150px,1fr));gap:10px;margin:18px 0}
.flow .step{border:1px solid var(--ink);background:#fff;padding:12px 12px 10px}
.flow .n{font-family:ui-monospace,Menlo,monospace;font-size:11px;color:var(--signal);font-weight:700;letter-spacing:.15em}
.flow h4{font-size:13.5px;text-transform:uppercase;margin:4px 0 4px}
.flow p{font-size:12px;color:var(--dim);margin:0;line-height:1.5}
footer{margin-top:80px;border-top:3px solid var(--ink);padding-top:18px;font-family:ui-monospace,Menlo,monospace;font-size:11px;color:var(--dim);display:flex;justify-content:space-between;flex-wrap:wrap;gap:8px;letter-spacing:.05em}
svg text{font-family:ui-monospace,Menlo,monospace}
@media (max-width:720px){.vs{grid-template-columns:1fr}}
@media print{body{background:#fff}.wrap{padding:10px}section{page-break-inside:avoid}}
</style>
</head>
<body>
<div class="wrap">
<header>
<div class="kicker">Презентация · Simbiot</div>
<h1>AI-харнес организации из&nbsp;коробки</h1>
<div class="sub">Студийная ОС, где агенты, проекты и метрики живут в одной системе. Фаундер приходит со своей идеей — а инфраструктуру, которую обычно собирают год, получает с первого дня. Проект на 100% остаётся у фаундера, оплата — из выручки.</div>
<div class="badges">
<span class="badge s">Студийная ОС</span>
<span class="badge">Слой над агентами</span>
<span class="badge">US LLC · Stripe · дашборды</span>
<span class="badge">Оплата из выручки</span>
<span class="badge">Кооператив 42 · 2026</span>
</div>
</header>
<!-- ================= 01 ================= -->
<section id="s01">
<div class="snum">01 / СУТЬ</div>
<h2>Что вы создаёте</h2>
<p class="lead">Общий AI-харнес организации «из коробки»: не отдельный агент, а среда, в которой агенты, проекты и метрики работают как единая система.</p>
<div class="kpi">
<div class="k"><div class="v">1-й день</div><div class="l">вместо года сборки</div></div>
<div class="k"><div class="v">US LLC</div><div class="l">юрконтур готов</div></div>
<div class="k"><div class="v">Stripe</div><div class="l">платёжка живая</div></div>
<div class="k"><div class="v">1 система</div><div class="l">агенты · проекты · метрики</div></div>
</div>
<p>Фаундер приносит идею — харнес даёт ей операционное тело: юрлицо в США, эквайринг, агентов и дашборды. Всё, что обычно тормозит запуск на месяцы, уже собрано и работает на реальных продуктах.</p>
<div class="note"><b>Одной фразой:</b> фаундер получает готовый бизнес-контур под своей идеей, а не голый набор инструментов.</div>
</section>
<!-- ================= 02 ================= -->
<section id="s02">
<div class="snum">02 / ПРОБЛЕМА</div>
<h2>Какую проблему решаем — и для кого</h2>
<p class="lead">Продукт готов на 40%. Остальные 60% — бизнес-контур: юрлицо, платёжка, продажи, метрики. И его некому собирать.</p>
<div class="vs">
<div class="col old">
<h4>Инди-фаундер / малая команда</h4>
<ul>
<li>Нет времени и экспертизы собирать бизнес-контур — разработкой занят</li>
<li>Аутсорс дорог, фултайм — дороже</li>
<li>Запуск буксует месяцами: не из-за кода, а из-за «обвязки»</li>
</ul>
</div>
<div class="col new">
<h4>Разработчики harness'ов</h4>
<ul>
<li>Каждый строит свой harness — и уносит его с собой</li>
<li>Опыт не переиспользуется, знание живёт в головах</li>
<li>Между собой не делятся: повторяют одну работу по сто раз</li>
</ul>
</div>
</div>
<p>Итог: рынок полон продуктов, которые «почти запущены» — и людей, которые десятками раз собирают одно и то же, каждый для себя.</p>
</section>
<!-- ================= 03 ================= -->
<section id="s03">
<div class="snum">03 / РЕШЕНИЕ</div>
<h2>Слой над агентами, а не ещё один агент</h2>
<p class="lead">Simbiot — это общий харнес организации с «нулевой точкой воздействия»: он переписывает сам себя и координирует целый флот агентов.</p>
<div class="cardgrid">
<div class="card">
<div class="cn">01</div>
<h4>Общий харнес</h4>
<p>Единая среда организации: агенты, проекты, метрики, память — в одной системе, а не в зоопарке скриптов у каждого в репо.</p>
</div>
<div class="card">
<div class="cn">02</div>
<h4>Нулевая точка воздействия</h4>
<p>Харнес переписывает сам себя: собственная доработка — это обычная рабочая задача внутри системы, а не внешняя миграция.</p>
</div>
<div class="card">
<div class="cn">03</div>
<h4>Координатор флота</h4>
<p>Не один мега-агент, а управление целым флотом: у каждого агента своя роль, у системы — общий ритм и приоритеты.</p>
</div>
<div class="card">
<div class="cn">04</div>
<h4>Реестр протоколов</h4>
<p>Решения фиксируются письменно. Агент исполняет протокол, а не догадывается, что имел в виду человек.</p>
</div>
</div>
<div class="note"><b>Почему пользователь выберет нас:</b> вместо «ещё одного агента» он получает организационный слой, который держит и агентов, и память, и правила игры — и который может дорабатывать сам себя.</div>
</section>
<!-- ================= 04 ================= -->
<section id="s04">
<div class="snum">04 / КАК ЭТО РАБОТАЕТ</div>
<h2>Путь фаундера — от идеи до работающего бизнеса</h2>
<p class="lead">Фаундер делает стратегические решения и держит фокус на продукте. Операционный контур собирает и ведёт Simbiot.</p>
<div class="flow">
<div class="step"><div class="n">ШАГ 1</div><h4>Идея</h4><p>Фаундер приходит со своей идеей и MVP-видением</p></div>
<div class="step"><div class="n">ШАГ 2</div><h4>Контур</h4><p>US LLC, Stripe, агенты и дашборды подключены с первого дня</p></div>
<div class="step"><div class="n">ШАГ 3</div><h4>Агенты</h4><p>Флот агентов берёт рутину: разработка, контент, поддержка, аналитика</p></div>
<div class="step"><div class="n">ШАГ 4</div><h4>Метрики</h4><p>Дашборды показывают состояние проектов и AUM в реальном времени</p></div>
<div class="step"><div class="n">ШАГ 5</div><h4>Выручка</h4><p>Оплата из выручки — когда проект заработает</p></div>
</div>
</section>
<!-- ================= 05 ================= -->
<section id="s05">
<div class="snum">05 / УСТРОЙСТВО</div>
<h2>Ритмы: харнес живёт тактами, как человек</h2>
<p class="lead">Simbiot Harness — живой процесс с собственной временной осью, а не функция, которую вызывают по запросу.</p>
<div class="cardgrid">
<div class="card"><div class="cn">СЕРДЦЕБИЕНИЕ</div><h4>Секунда</h4><p>Рефлексы: детерминированные реакции без LLM — мгновенный отклик</p></div>
<div class="card"><div class="cn">РЕАКЦИЯ</div><h4>Минута</h4><p>Триаж входящих, мелкие задачи S, первые ответы</p></div>
<div class="card"><div class="cn">ПОТОК</div><h4>Час</h4><p>Дайджесты, задачи M, разбор накопившегося</p></div>
<div class="card"><div class="cn">ПЛАН ДНЯ</div><h4>День</h4><p>Манифест дня, большие задачи L, планирование</p></div>
<div class="card"><div class="cn">РЕТРО</div><h4>Неделя / далее</h4><p>Цели, стратегия, идентичность: чем крупнее цикл — тем больше контекста и задач</p></div>
</div>
<p>Всё происходящее — тики, решения, трата бюджета — это события в append-only сторе. Состояние не хранится, а выводится replay'ем: выключили и включили процесс — он продолжил с того же места.</p>
</section>
<!-- ================= 06 ================= -->
<section id="s06">
<div class="snum">06 / ПАМЯТЬ</div>
<h2>Письменные протоколы вместо догадок</h2>
<p class="lead">Реестр решений — ядро долгосрочной работы агентов: организация помнит, <i>почему</i> принято каждое решение.</p>
<ul>
<li><b>Решение = запись.</b> Каждое значимое решение фиксируется письменно, с контекстом и мотивировкой</li>
<li><b>Агент исполняет, а не угадывает.</b> Протокол — источник истины; поведение агентов воспроизводимо</li>
<li><b>Интеллект не уходит с человеком.</b> Память организации накапливается в системе, а не в головах сотрудников</li>
<li><b>Кооператив и агенты растут вместе:</b> агенты снимают рутину, люди строят продуктовую и метрическую часть</li>
</ul>
<div class="note"><b>Следствие:</b> организация получает общий интеллект и память, которые переживают уход любого отдельного участника.</div>
</section>
<!-- ================= 07 ================= -->
<section id="s07">
<div class="snum">07 / ОТЛИЧИЯ</div>
<h2>Simbiot против привычных сценариев</h2>
<p class="lead">Рынок предлагает либо «агента в помощь», либо полный аутсорс. Simbiot занимает другую нишу — общий организационный слой.</p>
<table>
<tr><th>Сценарий</th><th>Что даёт</th><th>Чего не хватает</th></tr>
<tr class="hl"><td><b>Simbiot</b></td><td>Харнес организации: агенты + контур + память + метрики, из коробки, дорабатывает сам себя</td><td>—</td></tr>
<tr><td>Свой harness / фреймворк</td><td>Полный контроль, своя архитектура</td><td>Собирать год, поддерживать самому, знание не передаётся</td></tr>
<tr><td>Ещё один AI-агент (SaaS)</td><td>Быстрый старт на одной задаче</td><td>Нет организации вокруг: ни юрлица, ни памяти, ни координации флота</td></tr>
<tr><td>Аутсорс-студия</td><td>Готовая команда под заказ</td><td>Дорого, знание уходит с командой, метрики — в чужих руках</td></tr>
</table>
</section>
<!-- ================= 08 ================= -->
<section id="s08">
<div class="snum">08 / ЭКОНОМИКА</div>
<h2>Оплата из выручки — риск на нашей стороне</h2>
<p class="lead">Проект на 100% принадлежит фаундеру. Мы зарабатываем, когда заработал проект — выровненные стимулы вместо предоплаты за обещания.</p>
<div class="kpi">
<div class="k"><div class="v">100%</div><div class="l">проекта у фаундера</div></div>
<div class="k"><div class="v">0</div><div class="l">предоплат за сборку контура</div></div>
<div class="k"><div class="v">$0</div><div class="l">до первой выручки проекта</div></div>
</div>
<p>Нам выгодно, чтобы проект зарабатывал быстрее и дольше — вся модель мотивации смотрит в одну сторону. Фаундер рискует идеей и временем, мы — инфраструктурой и агентами.</p>
</section>
<!-- ================= 09 ================= -->
<section id="s09">
<div class="snum">09 / КОГДА ЭТО ВЫГОДНО</div>
<h2>Для кого Simbiot — правильный выбор</h2>
<p class="lead">Чем меньше команда и чем дальше она от операционки — тем выше выигрыш.</p>
<ul>
<li><b>Инди-фаундер с MVP:</b> бизнес-контур нужен, а собирать его некому и некогда</li>
<li><b>Малая команда без операционного директора:</b> рутина и обвязка съедают половину силы команды</li>
<li><b>Технический основатель:</b> хочет строить продукт, а не юрлицо, платёжку и дашборды</li>
<li><b>Существующий harness-владелец:</b> готов перестать держать всё в своих руках и получить общий слой</li>
</ul>
</section>
<!-- ================= 10 ================= -->
<section id="s10">
<div class="snum">10 / ИТОГ</div>
<h2>Результат с первого дня</h2>
<p class="lead">Фаундер получает не «инструмент», а работающую организационную систему — и фокусирует своё время там, где создаётся ценность.</p>
<div class="cardgrid">
<div class="card"><div class="cn">ИЗ КОРОБКИ</div><h4>Контур</h4><p>US LLC, Stripe, агенты, дашборды — уже в первый день, а не через год</p></div>
<div class="card"><div class="cn">ПОД ИДЕЮ</div><h4>Фокус</h4><p>Фаундер занимается продуктом; рутины и операционку держат агенты</p></div>
<div class="card"><div class="cn">НАД АГЕНТАМИ</div><h4>Слой</h4><p>Координация флота, письменные протоколы, самодоработка харнеса</p></div>
<div class="card"><div class="cn">ИЗ ВЫРУЧКИ</div><h4>Риск</h4><p>Оплата когда заработало; проект на 100% у фаундера</p></div>
</div>
<div class="note"><b>Simbiot — это общий интеллект и память кооператива,</b> которые не уходят вместе с человеком: организация и её агенты растут вместе.</div>
</section>
<footer>
<span>SIMBIOT · ПРЕЗЕНТАЦИЯ ПРОДУКТА</span>
<span>КООПЕРАТИВ 42 · 2026</span>
<span>ПРОЕКТ 100% У ФАУНДЕРА · ОПЛАТА ИЗ ВЫРУЧКИ</span>
</footer>
</div>
</body>
</html>

145
tests/test_gate.py Normal file
View file

@ -0,0 +1,145 @@
# -*- coding: utf-8 -*-
"""e-260930062242/2510/2607/2741: гейт важности LLM-тика (hermetic, без LLM/БД).
Запуск: ./venv/bin/python tests/test_gate.py
Проверяет: collect_signals ранжирует события (critical/warn),
gate_allows_llm решает по per-level порогу (SIMBIOT_GATE_<LEVEL>),
тихий тик не запускает LLM — гейт универсален для всех уровней.
"""
import os
import sys
sys.path.insert(0, "/root/simbiot-harness")
# чистое окружение: env-капы гейта не должны влиять на тесты
for _v in ("SIMBIOT_GATE_MIN", "SIMBIOT_GATE_HOUR", "SIMBIOT_GATE_DAY"):
os.environ.pop(_v, None)
from harness import scheduler
from harness.levels import LEVELS
fails = []
def check(name, cond, extra=""):
print(("PASS" if cond else "FAIL"), name, ("| " + extra) if not cond else "")
if not cond:
fails.append(name)
def _model(tasks=None, inbox=None, escs=None, policy=None):
return {
"tasks": tasks or {}, "inbox": inbox or {},
"escalations": escs or {}, "policy": policy or {},
}
# ── 1. Тишина: нет событий — гейт не пускает LLM ни на одном LLM-уровне ──
empty = _model()
for name in ("min", "hour", "day"):
cfg = LEVELS[name]
sigs = scheduler.collect_signals(cfg, empty)
check(f"{name}: пустая модель → нет сигналов", sigs == [], str(sigs))
check(f"{name}: тихий тик → LLM не запускается",
scheduler.gate_allows_llm(cfg, sigs) is False)
# ── 2. Входящее TG без ответа — critical, минутный уровень просыпается ──
inbox = {"m-1": {"update_id": 1, "text": "привет", "replied": False, "ignored": False},
"m-2": {"update_id": 2, "text": "/лестница", "replied": False, "ignored": False}}
sigs = scheduler.collect_signals(LEVELS["min"], _model(inbox=inbox))
crit = [s for s in sigs if s[0] == "critical" and s[1] == "unreplied_inbox"]
check("unreplied_inbox: 1 сигнал critical", len(crit) == 1, str(sigs))
check("unreplied_inbox: посчитаны только не-команды (1 из 2)",
crit and "1 сообщений без ответа" in crit[0][2], crit[0][2] if crit else "")
check("min: критичный сигнал → LLM запускается",
scheduler.gate_allows_llm(LEVELS["min"], sigs) is True)
# ── 3. Готовая задача доступного класса — critical ──
tasks = {"t-1": {"id": "t-1", "status": "ready", "size": "M", "created": "2026-09-30T08:00:00"}}
sigs = scheduler.collect_signals(LEVELS["hour"], _model(tasks=tasks))
check("hour: ready M-задача → critical", any(
s[0] == "critical" and s[1] == "ready_task" for s in sigs), str(sigs))
# та же задача недоступна минутному уровню (класс M) — сигнала нет
sigs_min = scheduler.collect_signals(LEVELS["min"], _model(tasks=tasks))
check("min: M-задача не его класса → сигнала ready_task нет",
all(s[1] != "ready_task" for s in sigs_min), str(sigs_min))
# ── 4. Эскалации: адресованные УРОВНЮ будят его; чужие — нет (как в build_context) ──
escs = {"e-1": {"id": "e-1", "status": "open", "severity": "critical",
"to_level": "hour", "reason": "упал тик"},
"e-2": {"id": "e-2", "status": "open", "severity": "warn",
"to_level": "hour", "reason": "warn-шум"}}
# hour: critical-эскалация адресована ему → сигнал critical
sigs_h = scheduler.collect_signals(LEVELS["hour"], _model(escs=escs))
check("hour: critical-эскалация → сигнал critical",
any(s[0] == "critical" and s[1] == "escalation:e-1" for s in sigs_h), str(sigs_h))
check("hour: warn-эскалация достаточно (порог warn)",
scheduler.gate_allows_llm(LEVELS["hour"], sigs_h) is True, str(sigs_h))
# min: те же эскалации адресованы hour — минутному они не видны, гейт закрыт
sigs_min = scheduler.collect_signals(LEVELS["min"], _model(escs=escs))
check("min: эскалации hour-адреса не будят минуту (сигналов нет)",
sigs_min == [], str(sigs_min))
check("min: только warn-эскалация → гейт закрыт (порог critical)",
scheduler.gate_allows_llm(LEVELS["min"], sigs_min) is False)
# эскалация, адресованная МИНУТЕ (если бы сек умел эскалировать) — будит минуту
escs_min = {"e-6": {"id": "e-6", "status": "open", "severity": "critical",
"to_level": "min", "reason": "критично для минуты"}}
sigs = scheduler.collect_signals(LEVELS["min"], _model(escs=escs_min))
check("min: critical-эскалация, адресованная min, будит минуту",
any(s[0] == "critical" and s[1] == "escalation:e-6" for s in sigs)
and scheduler.gate_allows_llm(LEVELS["min"], sigs) is True, str(sigs))
# acked-эскалация не будит уровень
escs_acked = {"e-3": {"id": "e-3", "status": "acked", "severity": "critical",
"to_level": "hour", "reason": "закрыта"}}
check("hour: acked-эскалация не создаёт сигналов",
scheduler.collect_signals(LEVELS["hour"], _model(escs=escs_acked)) == [])
# эскалация, адресованная ВЫШЕ уровня, не его зона (min не видит hour→day)
escs_up = {"e-4": {"id": "e-4", "status": "open", "severity": "critical",
"to_level": "day", "reason": "дневная тема"}}
check("min: эскалация hour→day не адресована min",
scheduler.collect_signals(LEVELS["min"], _model(escs=escs_up)) == [])
check("day: эскалация hour→day видна day-уровню",
any(s[1] == "escalation:e-4" for s in
scheduler.collect_signals(LEVELS["day"], _model(escs=escs_up))))
# ── 5. Нагрузка: warn-сигнал, будит только уровни с порогом warn ──
sig_load = scheduler.collect_signals(LEVELS["hour"], _model(policy={"cpu_alert_threshold": 8}), load=9.5)
check("high_load: сигнал warn при load >= порога",
any(s[0] == "warn" and s[1] == "high_load" for s in sig_load), str(sig_load))
check("min: high_load один не пробивает порог critical",
scheduler.gate_allows_llm(
LEVELS["min"],
scheduler.collect_signals(LEVELS["min"], _model(policy={"cpu_alert_threshold": 8}), load=9.5)
) is False)
# ── 6. Per-level порог из env: SIMBIOT_GATE_HOUR=critical / off ──
try:
os.environ["SIMBIOT_GATE_HOUR"] = "critical"
sigs = scheduler.collect_signals(LEVELS["hour"], _model(escs={
"e-5": {"id": "e-5", "status": "open", "severity": "warn",
"to_level": "hour", "reason": "warn"}}))
check("env SIMBIOT_GATE_HOUR=critical: warn-эскалации мало",
scheduler.gate_allows_llm(LEVELS["hour"], sigs) is False, str(sigs))
os.environ["SIMBIOT_GATE_HOUR"] = "off"
check("env SIMBIOT_GATE_HOUR=off: LLM всегда",
scheduler.gate_allows_llm(LEVELS["hour"], sigs) is True)
finally:
os.environ.pop("SIMBIOT_GATE_HOUR", None)
# ── 7. Воркер: resolve_tool_budget per-level (e-260930060717/060904) ──
from harness import _llm_worker
check("tool budget: min=30 по умолчанию",
_llm_worker.resolve_tool_budget("min") == 30)
check("tool budget: hour=300 по умолчанию (просьба Alex «хоть 300»)",
_llm_worker.resolve_tool_budget("hour") == 300)
os.environ["SIMBIOT_MAX_TOOL_CALLS_HOUR"] = "100"
try:
check("tool budget: env SIMBIOT_MAX_TOOL_CALLS_HOUR=100 бьёт дефолт",
_llm_worker.resolve_tool_budget("hour") == 100)
finally:
os.environ.pop("SIMBIOT_MAX_TOOL_CALLS_HOUR", None)
print()
print("FAILED: " + str(len(fails)) if fails else "ALL PASS")
sys.exit(1 if fails else 0)