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
|
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 за один модельный вызов.
|
"""Записать BudgetSpent за один модельный вызов.
|
||||||
|
|
||||||
Воркер — единственный автор spend-событий, но scheduler может убить
|
Воркер — единственный автор spend-событий, но scheduler может убить
|
||||||
его по таймауту в любой момент (SIGKILL). Поэтому usage пишем
|
его по таймауту в любой момент (SIGKILL). Поэтому usage пишем
|
||||||
ИНКРЕМЕНТАЛЬНО: каждый завершённый модельный вызов — своё событие.
|
ИНКРЕМЕНТАЛЬНО: каждый завершённый модельный вызов — своё событие.
|
||||||
Хвост недописанного вызова теряется, но весь тик — никогда.
|
Хвост недописанного вызова теряется, но весь тик — никогда.
|
||||||
|
|
||||||
|
model (e-260930062120): запрошенный model_id уровня пишется в каждое
|
||||||
|
событие — в кредитах роутера видно, ЧЬИ это траты; "auto" резолвится
|
||||||
|
роутером, фактическую модель до usage strands не выносит.
|
||||||
"""
|
"""
|
||||||
if not usage_delta:
|
if not usage_delta:
|
||||||
return
|
return
|
||||||
|
|
@ -161,13 +166,14 @@ def _emit_budget_incremental(level: str, trace_id: str, usage_delta: dict):
|
||||||
from harness import es
|
from harness import es
|
||||||
es.append_event("harness", "budget", "BudgetSpent", {
|
es.append_event("harness", "budget", "BudgetSpent", {
|
||||||
"level": level, "trace_id": trace_id, "source": "worker",
|
"level": level, "trace_id": trace_id, "source": "worker",
|
||||||
|
**({"model": model} if model else {}),
|
||||||
**usage_delta,
|
**usage_delta,
|
||||||
})
|
})
|
||||||
except Exception:
|
except Exception:
|
||||||
pass # трекинг не должен валить тик
|
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-дельта в стор.
|
"""После каждого модельного вызова — usage-дельта в стор.
|
||||||
|
|
||||||
Strands вешает usage на assistant-сообщение в
|
Strands вешает usage на assistant-сообщение в
|
||||||
|
|
@ -194,12 +200,27 @@ def _wire_budget_hooks(agent, level: str, trace_id: str):
|
||||||
seen.add(key)
|
seen.add(key)
|
||||||
_emit_budget_incremental(level, trace_id, {
|
_emit_budget_incremental(level, trace_id, {
|
||||||
"prompt_tokens": p, "completion_tokens": c,
|
"prompt_tokens": p, "completion_tokens": c,
|
||||||
})
|
}, model=model)
|
||||||
|
|
||||||
agent.add_hook(on_message_added, MessageAddedEvent)
|
agent.add_hook(on_message_added, MessageAddedEvent)
|
||||||
return agent
|
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():
|
def main():
|
||||||
payload = json.loads(sys.stdin.read())
|
payload = json.loads(sys.stdin.read())
|
||||||
|
|
||||||
|
|
@ -236,8 +257,8 @@ def main():
|
||||||
memory=False,
|
memory=False,
|
||||||
skills=False,
|
skills=False,
|
||||||
)
|
)
|
||||||
agent = _wire_trace_hooks(agent, es, trace_id)
|
agent = _wire_trace_hooks(agent, es, trace_id, model=payload.get("model"))
|
||||||
agent = _wire_budget_hooks(agent, level, trace_id)
|
agent = _wire_budget_hooks(agent, level, trace_id, model=payload.get("model"))
|
||||||
# хард-кап шагов (e-260930031800): cancel_tool + end_turn.
|
# хард-кап шагов (e-260930031800): cancel_tool + end_turn.
|
||||||
# ВАЖНО: НЕ используем limits={"turns": N} — нативный стоп SDK
|
# ВАЖНО: НЕ используем limits={"turns": N} — нативный стоп SDK
|
||||||
# обрывает цикл на границе хода, и result.message остаётся
|
# обрывает цикл на границе хода, и result.message остаётся
|
||||||
|
|
@ -245,8 +266,7 @@ def main():
|
||||||
# (репродукция: stop_reason=limit_turns, text='', тики 04:18+).
|
# (репродукция: stop_reason=limit_turns, text='', тики 04:18+).
|
||||||
# Наш end_turn (AfterToolsEvent) кладёт в историю ассистентский
|
# Наш end_turn (AfterToolsEvent) кладёт в историю ассистентский
|
||||||
# текст с итогом — дайджест не пустеет.
|
# текст с итогом — дайджест не пустеет.
|
||||||
agent = _wire_tool_budget(
|
agent = _wire_tool_budget(agent, resolve_tool_budget(level))
|
||||||
agent, int(os.environ.get("SIMBIOT_MAX_TOOL_CALLS") or 30))
|
|
||||||
result = agent(payload.get("prompt", ""))
|
result = agent(payload.get("prompt", ""))
|
||||||
usage = _extract_usage(agent)
|
usage = _extract_usage(agent)
|
||||||
_emit(payload, {"ok": True, "text": str(result), "usage": usage, "trace_id": trace_id})
|
_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()
|
os.environ["OPENAI_BASE_URL"] = _router_base_url()
|
||||||
|
|
||||||
|
|
||||||
def _wire_trace_hooks(agent, es, trace_id: str):
|
def _wire_trace_hooks(agent, es, trace_id: str, model: str = None):
|
||||||
"""Каждая стадия агентного цикла → событие агрегата trace."""
|
"""Каждая стадия агентного цикла → событие агрегата trace.
|
||||||
|
|
||||||
|
model (e-260930062120): model_id уровня в InvocationStarted —
|
||||||
|
видно, КАКАЯ модель запускалась на тике (запрошенная; "auto"
|
||||||
|
резолвится роутером на его стороне).
|
||||||
|
"""
|
||||||
from strands import Agent
|
from strands import Agent
|
||||||
from strands.hooks import BeforeInvocationEvent, BeforeToolCallEvent, AfterToolCallEvent
|
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)
|
es.append_event("trace", f"{trace_id}:{seq['n']}", kind, payload)
|
||||||
|
|
||||||
def on_before_invocation(event):
|
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):
|
def on_before_tool(event):
|
||||||
tool = getattr(event, "tool_use", None) or {}
|
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)
|
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):
|
def build_context(level_cfg, model: dict, task=None):
|
||||||
"""Сборка контекста тика: мандат + память уровня + выжимки нижних + задача."""
|
"""Сборка контекста тика: мандат + память уровня + выжимки нижних + задача."""
|
||||||
parts = [level_cfg.mandate]
|
parts = [level_cfg.mandate]
|
||||||
|
|
@ -265,6 +344,32 @@ def process_level(level_cfg, model: dict):
|
||||||
if not emit_tick(level_cfg, seq):
|
if not emit_tick(level_cfg, seq):
|
||||||
return "already" # тик этого такта уже был (идемпотентность)
|
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
|
st["busy"] = True
|
||||||
try:
|
try:
|
||||||
if not level_cfg.uses_llm:
|
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