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:
parent
5faf78fd09
commit
871a3b15a3
5 changed files with 548 additions and 10 deletions
|
|
@ -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})
|
||||
|
|
|
|||
|
|
@ -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 {}
|
||||
|
|
|
|||
|
|
@ -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
260
simbiot-presentation.html
Normal 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-харнес организации из коробки</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
145
tests/test_gate.py
Normal 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)
|
||||
Loading…
Reference in a new issue