simbiot-harness/docs/ARCHITECTURE.html

611 lines
51 KiB
HTML
Raw Blame History

This file contains ambiguous Unicode characters

This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.

<!DOCTYPE html><html lang="ru"><head><meta charset="UTF-8"><meta name="viewport" content="width=device-width, initial-scale=1.0"><title>Архитектура Simbiot Harness (Коди)</title><style>
:root{--paper:#faf8f3;--ink:#15150f;--signal:#FF4F00;--dim:#6b675e;--line:#d8d3c8;--card:#f3f0e8}
*{box-sizing:border-box}
body{margin:0;background:var(--paper);color:var(--ink);font:16px/1.62 -apple-system,"Segoe UI",Roboto,Arial,sans-serif;-webkit-font-smoothing:antialiased}
.wrap{max-width:1000px;margin:0 auto;padding:48px 28px 120px;display:grid;grid-template-columns:220px 1fr;gap:48px}
nav{position:sticky;top:24px;align-self:start;font-size:13.5px}
nav .kicker{font-family:ui-monospace,Menlo,monospace;font-size:11px;letter-spacing:.22em;color:var(--signal);font-weight:700;text-transform:uppercase;margin-bottom:12px}
nav a{display:block;color:var(--dim);text-decoration:none;padding:4px 0;border-left:2px solid transparent;padding-left:10px}
nav a:hover{color:var(--ink);border-left-color:var(--signal)}
main{min-width:0}
h1{font-size:clamp(30px,5vw,46px);line-height:1.05;font-weight:800;letter-spacing:-.02em;margin:0 0 8px}
h1+p{color:var(--dim);font-size:14px;margin-top:0;border-bottom:3px solid var(--ink);padding-bottom:20px}
h2{font-size:24px;margin:56px 0 12px;padding-top:12px;border-top:1px solid var(--line);letter-spacing:-.01em}
h2::before{content:"";display:inline-block;width:10px;height:10px;background:var(--signal);margin-right:12px;vertical-align:middle}
code{font-family:ui-monospace,"JetBrains Mono",Menlo,monospace;font-size:.88em;background:var(--card);padding:1px 5px;border-radius:4px}
pre{background:var(--card);padding:14px;border-radius:8px;overflow-x:auto}
table{border-collapse:collapse;width:100%;margin:16px 0;font-size:14px;display:block;overflow-x:auto}
th,td{border:1px solid var(--line);padding:7px 10px;text-align:left;vertical-align:top}
th{background:var(--ink);color:var(--paper);font-weight:600}
tr:nth-child(even) td{background:#f6f3ec}
ul,ol{padding-left:22px} li{margin:4px 0}
strong{font-weight:700}
.diagram{margin:24px 0;background:#fff;border:1px solid var(--line);border-radius:10px;padding:12px}
.diagram svg{width:100%;height:auto;display:block}
figcaption{font-size:13px;color:var(--dim);margin-top:8px}
@media(max-width:860px){.wrap{grid-template-columns:1fr} nav{position:static}}
</style></head><body><div class="wrap"><nav><div class="kicker">Simbiot Harness</div><a href="#обзор">Обзор</a><a href="#процессы-и-сервисы">Процессы и сервисы</a><a href="#event-sourcing-и-криптоаудит">Event Sourcing и криптоаудит</a><a href="#тактовая-лестница">Тактовая лестница</a><a href="#llm-воркер">LLM-воркер</a><a href="#telegram-канал">Telegram-канал</a><a href="#decision-модель">Decision-модель</a><a href="#ядро-коди">Ядро Коди</a><a href="#куб-4-4-4-круги-доверия">Куб 4×4×4: круги доверия</a><a href="#динамическая-память-d0-d3">Динамическая память D0–D3</a><a href="#конфигурация-cli-деплой-и-тесты">Конфигурация, CLI, деплой и тесты</a><a href="#известные-ограничения-и-дорожная-карта">Известные ограничения и дорожная карта</a></nav><main><h1 id="архитектура-simbiot-harness-коди">Архитектура Simbiot Harness (Коди)</h1>
<p>17.10.2026 · HTML: <code>docs/ARCHITECTURE.html</code> (собирается <code>scripts/build-architecture-html.py</code>) · онлайн-версия для комментариев: https://claude.ai/code/artifact/195fd476-44e7-47cc-9093-2d41ea53c8c9</p>
<h2 id="обзор">Обзор</h2>
<p>Simbiot Harness — ритмичный агент кооператива: он живёт тактами (секунда, минута, час, день), а в Telegram отвечает как Коди (@juno42bot). Главная метрика — долгосрочный AUM кооператива: каждое решение проверяется вопросом «добавит ли это ≥1% к AUM на горизонте год+».</p>
<p>Архитектура держится на пяти принципах:</p>
<ul>
<li><strong>Всё — события.</strong> Единственный источник правды — append-only стор событий; состояние (read model) — проекция. Каждое событие подписано ed25519 и сцеплено с предыдущим.</li>
<li><strong>Дешёвое решает раньше дорогого.</strong> Детерминированные правила и локальные/облачные decision-модели отсекают лишнее до запуска LLM: гейт важности, фильтр адресации, выбор глубины памяти.</li>
<li><strong>LLM — в изоляции.</strong> Каждый LLM-тик — отдельный процесс с бюджетом шагов, таймаутами и стражем прав на каждом вызове инструмента.</li>
<li><strong>Доверие — по личности, не по тексту.</strong> Права тика определяет самый недоверенный вход его контекста.</li>
<li><strong>Учится на исходах.</strong> Ядро Коди собирает решения и их последствия, обучает маленьких учеников и допускает их в бой только при честной победе над базовой линией.</li>
</ul>
<p>Куб 4×4×4 задаёт поведение одной таблицей политик: 4 круга доверия × 4 тика × 4 глубины памяти. Это не 64 программы, а один код и параметры.</p>
<figure class="diagram"><svg viewBox="0 0 760 500" role="img" aria-label="Сообщение проходит фильтры до LLM, всё оседает в сторе" font-size="13" font-family="-apple-system,Segoe UI,Roboto,Arial,sans-serif"><defs><marker id="a" viewBox="0 0 10 10" refX="9" refY="5" markerWidth="6" markerHeight="6" orient="auto-start-reverse"><path d="M0 0L10 5L0 10z" fill="#6b675e"/></marker></defs><text x="24" y="32" font-size="15" font-weight="600" fill="#15150f">Сообщение проходит фильтры до LLM, всё оседает в сторе</text><g fill="none" stroke="#6b675e" stroke-width="1.25"><path d="M208 86.0H264" marker-end="url(#a)"/><path d="M208 158.0H264" marker-end="url(#a)"/><path d="M380.0 112V132" marker-end="url(#a)"/><path d="M380.0 184V204" marker-end="url(#a)"/><path d="M380.0 256V276" marker-end="url(#a)"/><path d="M380.0 328V348" marker-end="url(#a)"/><path d="M380.0 400V428" marker-end="url(#a)"/><path d="M116.0 400V428" marker-end="url(#a)"/><path d="M496 374.0H552" marker-end="url(#a)"/><path d="M552 158.0H496" stroke-dasharray="4 4" marker-end="url(#a)"/><path d="M552 302.0H496" stroke-dasharray="4 4" marker-end="url(#a)"/></g><g><rect x="24" y="60" width="184" height="52" rx="8" fill="#ffffff" stroke="#b9b3a6" stroke-width="1.25"/><text x="116.0" y="82" text-anchor="middle" font-weight="600" fill="#15150f">Telegram</text><text x="116.0" y="100" text-anchor="middle" font-size="11.5" fill="#6b675e">@juno42bot: личка, группы</text></g><g><rect x="24" y="132" width="184" height="52" rx="8" fill="#ffffff" stroke="#b9b3a6" stroke-width="1.25"/><text x="116.0" y="154" text-anchor="middle" font-weight="600" fill="#15150f">Портал и агенты</text><text x="116.0" y="172" text-anchor="middle" font-size="11.5" fill="#6b675e">MCP, канал агент↔агент</text></g><g><rect x="24" y="348" width="184" height="52" rx="8" fill="#ffffff" stroke="#b9b3a6" stroke-width="1.25"/><text x="116.0" y="370" text-anchor="middle" font-weight="600" fill="#15150f">Человек</text><text x="116.0" y="388" text-anchor="middle" font-size="11.5" fill="#6b675e">cli.py, TUI</text></g><g><rect x="264" y="60" width="232" height="52" rx="8" fill="#ffffff" stroke="#b9b3a6" stroke-width="1.25"/><text x="380.0" y="82" text-anchor="middle" font-weight="600" fill="#15150f">TG-поллер</text><text x="380.0" y="100" text-anchor="middle" font-size="11.5" fill="#6b675e">приём, файлы, голос, кнопки</text></g><g><rect x="264" y="132" width="232" height="52" rx="8" fill="#ffffff" stroke="#b9b3a6" stroke-width="1.25"/><text x="380.0" y="154" text-anchor="middle" font-weight="600" fill="#15150f">Адресация</text><text x="380.0" y="172" text-anchor="middle" font-size="11.5" fill="#6b675e">кому сообщение: боту или людям</text></g><g><rect x="264" y="204" width="232" height="52" rx="8" fill="#ffffff" stroke="#b9b3a6" stroke-width="1.25"/><text x="380.0" y="226" text-anchor="middle" font-weight="600" fill="#15150f">Гейт важности</text><text x="380.0" y="244" text-anchor="middle" font-size="11.5" fill="#6b675e">есть ли событие важнее порога</text></g><g><rect x="264" y="276" width="232" height="52" rx="8" fill="#ffffff" stroke="#b9b3a6" stroke-width="1.25"/><text x="380.0" y="298" text-anchor="middle" font-weight="600" fill="#15150f">Круг доверия и память</text><text x="380.0" y="316" text-anchor="middle" font-size="11.5" fill="#6b675e">права тика, глубина D0–D3</text></g><g><rect x="264" y="348" width="232" height="52" rx="8" fill="#ffe9dd" stroke="#FF4F00" stroke-width="2"/><text x="380.0" y="370" text-anchor="middle" font-weight="600" fill="#15150f">LLM-воркер</text><text x="380.0" y="388" text-anchor="middle" font-size="11.5" fill="#6b675e">инструменты под стражем прав</text></g><g><rect x="552" y="132" width="184" height="52" rx="8" fill="#ffffff" stroke="#b9b3a6" stroke-width="1.25"/><text x="644.0" y="154" text-anchor="middle" font-weight="600" fill="#15150f">Decision-модель</text><text x="644.0" y="172" text-anchor="middle" font-size="11.5" fill="#6b675e">clef:27b → nimble</text></g><g><rect x="552" y="276" width="184" height="52" rx="8" fill="#ffffff" stroke="#b9b3a6" stroke-width="1.25"/><text x="644.0" y="298" text-anchor="middle" font-weight="600" fill="#15150f">Память и ядро</text><text x="644.0" y="316" text-anchor="middle" font-size="11.5" fill="#6b675e">индекс nomic, ученики</text></g><g><rect x="552" y="348" width="184" height="52" rx="8" fill="#ffffff" stroke="#b9b3a6" stroke-width="1.25"/><text x="644.0" y="370" text-anchor="middle" font-weight="600" fill="#15150f">9router и портал</text><text x="644.0" y="388" text-anchor="middle" font-size="11.5" fill="#6b675e">LLM-тики, MCP-вызовы</text></g><rect x="24" y="428" width="712" height="52" rx="8" fill="#f3f0e8" stroke="#b9b3a6" stroke-width="1.25"/><text x="380" y="450" text-anchor="middle" font-weight="600" fill="#15150f">Стор событий</text><text x="380" y="468" text-anchor="middle" font-size="11.5" fill="#6b675e">подписи ed25519 · проекции · сжатие с архивом · каждый шаг пишет события</text></svg><figcaption>Слева — входы, по центру — фильтры перед LLM по порядку, справа — помощники (пунктир — консультируют, сплошная — вызывает воркер); стор событий — единственный источник правды.</figcaption></figure>
<h2 id="процессы-и-сервисы">Процессы и сервисы</h2>
<p>Харнес — два долгоживущих процесса на одном сторе событий плюс короткоживущие LLM-воркеры; код — <code>/root/simbiot-harness</code>, данные инстанса — <code>/var/lib/simbiot-harness</code> (<code>SIMBIOT_HOME</code>).</p>
<table>
<thead>
<tr>
<th>Процесс</th>
<th>Запуск</th>
<th>Что делает</th>
</tr>
</thead>
<tbody>
<tr>
<td>Лестница</td>
<td><code>simbiot-harness.service</code> → <code>python -m harness.scheduler</code></td>
<td>такты sec/min/hour/day в отдельных потоках, демон доставки и сирот, триаж, ночная тренировка ядра</td>
</tr>
<tr>
<td>TG-поллер</td>
<td><code>simbiot-harness-tg.service</code> → <code>python -m harness.tg_inbox</code></td>
<td>long polling @juno42bot: входящие, файлы, расшифровка голоса, кнопки</td>
</tr>
<tr>
<td>LLM-воркер</td>
<td><code>harness/_llm_worker.py</code>, сабпроцесс на каждый LLM-тик</td>
<td>агент Strands с инструментами, страж прав, бюджет шагов</td>
</tr>
<tr>
<td>Дашборд</td>
<td><code>simbiot-dashboard.service</code>, HTTP 8765</td>
<td>доска задач и тиков, только чтение стора</td>
</tr>
<tr>
<td>CLI</td>
<td><code>cli.py</code> (и обёртка <code>simbiot</code>)</td>
<td>команды агентов и людей: ответы, задачи, эскалации, ядро, память, доверие</td>
</tr>
</tbody>
</table>
<p>Каталоги инстанса (<code>/var/lib/simbiot-harness</code>):</p>
<table>
<thead>
<tr>
<th>Путь</th>
<th>Содержимое</th>
</tr>
</thead>
<tbody>
<tr>
<td><code>db/harness.db</code></td>
<td>стор событий + read model + журнал доставок (SQLite, WAL)</td>
</tr>
<tr>
<td><code>env/.env</code></td>
<td>секреты и флаги (600)</td>
</tr>
<tr>
<td><code>audit/</code></td>
<td>ключ ed25519 харнеса</td>
</tr>
<tr>
<td><code>archive/</code></td>
<td>сжатые события (jsonl.gz, подписаны)</td>
</tr>
<tr>
<td><code>core/</code></td>
<td>чемпионы учеников ядра + кеш эмбеддингов</td>
</tr>
<tr>
<td><code>memory/</code></td>
<td>индекс памяти Коди</td>
</tr>
<tr>
<td><code>inbox/</code></td>
<td>файлы из чатов</td>
</tr>
</tbody>
</table>
<p>Внешние зависимости:</p>
<table>
<thead>
<tr>
<th>Система</th>
<th>Зачем</th>
</tr>
</thead>
<tbody>
<tr>
<td>9router (<code>127.0.0.1:20128</code>)</td>
<td>LLM для тиков, модель <code>auto</code></td>
</tr>
<tr>
<td>Ollama Cloud</td>
<td>decision-модель <code>clef:27b</code></td>
</tr>
<tr>
<td>Ollama локально (<code>127.0.0.1:11434</code>)</td>
<td>запасная decision-модель <code>nimble</code>, эмбеддинги <code>nomic-embed-text</code></td>
</tr>
<tr>
<td>Портал cil.by (MCP)</td>
<td>звонки, реестры, канал агент↔агент</td>
</tr>
<tr>
<td>Telegram Bot API</td>
<td>чаты Коди</td>
</tr>
<tr>
<td>Deepgram</td>
<td>расшифровка голосовых</td>
</tr>
<tr>
<td>RAG <code>/root/rag</code></td>
<td>поиск по базе знаний кооператива</td>
</tr>
</tbody>
</table>
<h2 id="event-sourcing-и-криптоаудит">Event Sourcing и криптоаудит</h2>
<p>Всё состояние харнеса выводится из подписанной цепочки событий (<code>harness/es.py</code>, <code>harness/audit.py</code>); после сжатия 17.10 стор — 200 MB, полная проверка — около 30 с.</p>
<p><strong>Стор.</strong> Таблица <code>events</code> (aggregate, aggregate_id, event_type, data, timestamp, version, sig, key_id, prev_sig). Уникальность <code>(aggregate, aggregate_id, version)</code> делает запись идемпотентной: тик <code>level:seq</code> не произойдёт дважды. Клейм задачи — атомарный (<code>claim_task_if_ready</code>, BEGIN IMMEDIATE), поэтому два уровня не возьмут одну задачу.</p>
<p><strong>Проекции.</strong> <code>run_projections()</code> инкрементально строит read model (задачи, инбокс, эскалации, дайджесты, круги доверия, политика AUM) и сохраняет снапшот в <code>read_model</code>. Перед применением хвост проверяется подписями: подделка останавливает проекции.</p>
<p><strong>Подписи.</strong> Каждое событие подписано ключом харнеса (ed25519, key_id <code>faf8d19d9aed71fe</code>), <code>prev_sig</code> ссылается на подпись более раннего. События до внедрения подписей помечены <code>genesis:&lt;id&gt;</code>, неподписанные — legacy-границы. Защита от чужого ключа: стор, уже подписанный одним ключом, другим не подписывается. Полная проверка — <code>cli.py verify</code>.</p>
<p><strong>Сжатие с подписью</strong> (<code>harness/compact.py</code>, <code>scripts/compact-store.sh</code>). Шум сек-уровня старше часа уходит в архив:</p>
<ol>
<li>Строки целиком, со всеми подписями, пишутся в <code>archive/events-&lt;first&gt;-&lt;last&gt;-&lt;id&gt;.jsonl.gz</code> (600).</li>
<li>Подписи удалённых строк, на которые ссылаются остающиеся, и маркеры <code>boundary:&lt;id&gt;</code> для legacy-границ попадают в мост <code>compaction_bridge</code>.</li>
<li>Подписанное событие <code>StoreCompacted</code> фиксирует sha256 архива, строк и моста.</li>
<li>Удаление идёт пачками по 20 000 строк без остановки сервисов; VACUUM — под остановленными писателями (секунды).</li>
</ol>
<p><code>verify</code> принимает мост, только если его хеш совпадает с подписанным <code>StoreCompacted</code>; архивы проверяются построчно (<code>cli.py compact --verify-archives</code>). Первое сжатие: 2 886 765 строк в архив (246 MB), 1389 → 200 MB.</p>
<p>Сек-уровень больше не шумит: <code>CycleTicked</code>/<code>ReflexObserved</code> пишутся heartbeat'ом раз в 60 с и в такты с событием (алерт, команды, каналы).</p>
<h2 id="тактовая-лестница">Тактовая лестница</h2>
<p>Четыре уровня тикают независимо, каждый в своём потоке (<code>harness/scheduler.py</code>, <code>harness/levels.py</code>); дорогой LLM-тик запускается, только если есть событие важнее порога уровня.</p>
<table>
<thead>
<tr>
<th>Уровень</th>
<th>Интервал</th>
<th>LLM</th>
<th>Порог гейта</th>
<th>Задачи</th>
<th>Шагов на тик</th>
<th>Таймаут</th>
<th>Память по умолчанию</th>
</tr>
</thead>
<tbody>
<tr>
<td>sec</td>
<td>1 с</td>
<td>нет</td>
<td>—</td>
<td>—</td>
<td>—</td>
<td>—</td>
<td>—</td>
</tr>
<tr>
<td>min</td>
<td>1 мин + по входящему</td>
<td>effort low, контекст 256k</td>
<td>critical</td>
<td>S</td>
<td>30</td>
<td>480 с</td>
<td>D1</td>
</tr>
<tr>
<td>hour</td>
<td>1 ч</td>
<td>effort medium, 512k</td>
<td>warn</td>
<td>S, M, L</td>
<td>300</td>
<td>2880 с</td>
<td>D2</td>
</tr>
<tr>
<td>day</td>
<td>1 д</td>
<td>effort high, 1M</td>
<td>warn</td>
<td>S, M, L</td>
<td>10 000</td>
<td>таймаут уровня</td>
<td>D3</td>
</tr>
</tbody>
</table>
<p><strong>sec — рефлекс без LLM.</strong> Телеметрия и алерт нагрузки, мгновенные команды бота (<code>/лестница</code>, <code>/статус</code>, <code>/задачи</code>), каналы портала и агент↔агент.</p>
<p><strong>Гейт важности.</strong> Сигналы: входящее без ответа и готовая задача — critical; эскалации — по их severity; высокая нагрузка — warn. Ниже порога тик закрывается детерминированно (<code>GateSkipped</code> + заглушка-дайджест). Серая зона (только warn/info) спрашивает decision-модель — сейчас в режиме shadow.</p>
<p><strong>Тик по входящему.</strong> Демон раз в секунду смотрит хвост стора; через 3 с тишины после <code>MessageReceived</code> или <code>MessageTranscribed</code> запускает внеочередной мин-тик в слот следующей минуты. Ответ в Telegram приходит за 10–40 с вместо ~80 с медианы до нуджа.</p>
<p><strong>Батч задач.</strong> Уровень берёт все ready-задачи своих классов в бюджете контекста: из 9 старейших первой ставится самая ценная для AUM (decision-модель), остальные — FIFO. В один батч попадают задачи одного круга доверия, и уровень берёт только обслуживаемые круги.</p>
<p><strong>Эскалация недоделанных.</strong> «Бюджет исчерпан» или смерть воркера возвращают задачу в очередь; после 2 захватов на уровне она растёт до размера, которого этот уровень не берёт (S → M, к часу). До этого S-задача могла браться минутным уровнем 33 раза подряд. Задачи про внешние системы (портал, сервер, деплой, UI, git) сразу уходят часу — правило головы <code>task_fit</code>.</p>
<p>Занятый уровень поглощает тик (<code>TickAbsorbed</code>), запущенная задача не прерывается, наверх идут только дайджесты и эскалации (<code>cli.py escalate</code>). Демон доставки возвращает задачи-сироты после рестартов и доставляет результаты в чат идемпотентно (журнал доставок).</p>
<h2 id="llm-воркер">LLM-воркер</h2>
<p>Каждый LLM-тик — отдельный процесс (<code>isolated_runner.py</code> → <code>_llm_worker.py</code>) со свежим asyncio-рантаймом Strands: падение или таймаут не роняют лестницу, а вся группа процессов добивается по таймауту уровня.</p>
<p><strong>Вход воркера (payload):</strong> мандат уровня, собранный контекст тика, модель и effort, флаг shell, круг доверия тика. Результат возвращается JSON-файлом; трассу (<code>InvocationStarted</code>, <code>ToolCalled</code>, <code>ToolResult</code>, <code>BudgetSpent</code>) воркер пишет в стор сам и инкрементально, чтобы таймаут не съел учёт расходов.</p>
<p><strong>Инструменты:</strong></p>
<table>
<thead>
<tr>
<th>Инструмент</th>
<th>Ограничение</th>
</tr>
</thead>
<tbody>
<tr>
<td><code>shell</code>, <code>read</code>, <code>write</code>, <code>edit</code></td>
<td>только уровни с <code>can_shell</code>; в кругах ≥ 2 — страж прав</td>
</tr>
<tr>
<td><code>rag_lookup</code></td>
<td>1 запрос на тик, k ≤ 5, таймаут 20 с</td>
</tr>
<tr>
<td><code>portal_mcp</code></td>
<td>4 вызова на тик, деньги-тулы запрещены по умолчанию</td>
</tr>
<tr>
<td><code>cli.py …</code> через shell</td>
<td>основной способ действовать: <code>tg-reply</code>, <code>task</code>, <code>complete</code>, <code>escalate</code>, <code>tg-deliver-file</code>, <code>decide</code></td>
</tr>
</tbody>
</table>
<p><strong>Хуки на каждом вызове</strong> (<code>BeforeToolCallEvent</code>, до исполнения):</p>
<ol>
<li><strong>Страж прав</strong> — <code>trust.check_tool</code>: запрещённый вызов отменяется (<code>cancel_tool</code>) и пишется <code>TrustDenied</code>.</li>
<li><strong>Бюджет шагов</strong> — сверх лимита уровня вызовы отменяются, цикл завершается итоговым текстом; задачи без <code>complete</code> возвращаются или эскалируются.</li>
<li><strong>Трасса</strong> — <code>ToolCalled</code>/<code>ToolResult</code> с trace_id, уровнем и длительностью.</li>
</ol>
<p><strong>TTFT-сторож.</strong> Если апстрим 9router принял запрос, но за 45 с не прислал первый токен, стрим закрывается и делается новый вызов (до 3 попыток). Начавший отвечать стрим не трогается.</p>
<p><strong>Окружение.</strong> <code>SIMBIOT_TICK_CIRCLE</code> наследуют shell и <code>cli.py</code>: задачи из тика штампуются его кругом, а <code>cli.py trust set</code> из тика отказывает.</p>
<h2 id="telegram-канал">Telegram-канал</h2>
<p>Коди в Telegram — бот @juno42bot (id 7245454391); поллер пишет входящие в стор, а отвечает минутный (реже часовой) тик через <code>cli.py tg-reply</code>.</p>
<p><strong>Приём</strong> (<code>harness/tg_inbox.py</code>). Long polling (<code>message</code>, <code>edited_message</code>, <code>callback_query</code>). Отправитель вне <code>SIMBIOT_TG_ALLOWED</code> → <code>MessageIgnored(not_allowed)</code>. Документы и фото сразу сохраняются на диск, голосовые расшифровываются. Сохраняется, на чьё сообщение сделан reply и упомянут ли бот. Правка уже принятого сообщения не становится новым входящим.</p>
<p><strong>Адресация в группах</strong> (<code>harness/addressing.py</code>) — до гейта, без LLM:</p>
<ol>
<li>Личка, портал, агенты — всегда адресовано.</li>
<li>В группе «да» без модели: <code>@juno42bot</code>, reply на сообщение бота, звательное обращение («Коди, …», «…, Коди», «Коди!»).</li>
<li>«Нет» без модели: reply на сообщение другого человека.</li>
<li>Серая зона — decision-модель: «кому адресовано — боту, человеку или группе», P(бот) ≥ 0.5. Разговор О Коди в третьем лице обращением не считается. Модель недоступна — бот молчит.</li>
</ol>
<p>Вердикт: <code>MessageTriaged{addressed}</code> + 👀 или <code>MessageIgnored(not_addressed)</code> — тогда ни один уровень его не видит. В тени рядом работает ученик ядра <code>addressing</code>.</p>
<p><strong>Ответы.</strong> В группе — reply'ем на последнее сообщение батча (<code>allow_sending_without_reply</code>), в личке — без цитаты. Сводный ответ на несколько сообщений — <code>tg-reply m-1,m-2 -c</code>. Правило «сначала ответ, потом работа»: если нужно больше ~30 с, сначала короткий ответ, итог — <code>tg-reply &lt;id&gt; --followup</code> в тот же чат (идемпотентно по тексту).</p>
<p><strong>Реакции.</strong> 👀 ставится при приёме в личке и триажем в группе — только на адресованное боту. Параметр Bot API — <code>reaction</code> (массив); до 17.10 ошибка в имени параметра делала реакцию невидимой.</p>
<p><strong>Журнал исходящих.</strong> Каждое сообщение бота → <code>OutboundMessage</code> (chat:message_id → полный текст). Reply на сообщение бота получает <code>reply_to_full</code> (до 4000 символов вместо 300 от Telegram), а тик видит блок «ЭТО ОТВЕТ НА ТВОЁ СООБЩЕНИЕ». Собственные рассылки бота за 48 ч попадают в память тика.</p>
<p><strong>Кнопки.</strong> <code>core:…</code> — «Это было мне?» для ученика ядра; <code>trust:…</code> — «Разрешить полные права», нажать может только владелец.</p>
<h2 id="decision-модель">Decision-модель</h2>
<p>Решения «выбрать один из нескольких» и «да/нет» принимает не LLM, а decision-модель Ollama <code>/v1/systemone</code> (<code>harness/decision.py</code>): облачная <code>clef:27b</code> отвечает за 0.6–0.9 с, запасная локальная <code>nimble</code> (qwen3.5-9B Q8_0, CPU) — за 10–30 с.</p>
<p><strong>Контракт.</strong> <code>choice</code> — варианты объектом <code>{ключ: описание}</code>, ответ — ключ и распределение вероятностей; <code>noul</code> — P(«да»). Шортлист строго ≤ 9 вариантов: по замерам systemone-bench точность падает со 100% на 6 до 50% на 10. <code>confidence</code> — концентрация распределения, а не вероятность правильности.</p>
<p><strong>Надёжность.</strong> Бэкенды опрашиваются по порядку, у каждого свой предохранитель: облако — таймаут 30 с, после сбоя пауза 60 с; локальная — 90 с и 5 минут. Ответы детерминированы, поэтому кешируются на час по хешу вопроса и состояния. Каждый реальный вызов → <code>DecisionMade</code> (бэкенд, модель, латентность, применено ли). Модель недоступна — вызывающий берёт свой детерминированный фолбэк.</p>
<table>
<thead>
<tr>
<th>Потребитель</th>
<th>Вопрос</th>
<th>Режим</th>
<th>Фолбэк</th>
</tr>
</thead>
<tbody>
<tr>
<td>адресация в группах</td>
<td>кому адресовано сообщение</td>
<td>on</td>
<td>молчать</td>
</tr>
<tr>
<td>порядок батча задач</td>
<td>какая из 9 старейших ценнее для AUM</td>
<td>on</td>
<td>FIFO</td>
</tr>
<tr>
<td>тема инициативы</td>
<td>что сейчас полезнее предложить</td>
<td>on</td>
<td>по кругу</td>
</tr>
<tr>
<td>серая зона гейта</td>
<td>нужен ли дорогой LLM-тик</td>
<td>shadow</td>
<td>порог</td>
</tr>
<tr>
<td>агент в тике</td>
<td><code>cli.py decide -q … -o ключ=описание</code></td>
<td>on</td>
<td>—</td>
</tr>
</tbody>
</table>
<p>Режимы: <code>SIMBIOT_DECISION=off|shadow|on</code>, точечно <code>SIMBIOT_DECISION_&lt;ПОТРЕБИТЕЛЬ&gt;</code>; ключ облака — <code>SIMBIOT_DECISION_API_KEY</code> в <code>.env</code> инстанса. Статус и статистика — <code>cli.py decide</code>, <code>cli.py decide --stats</code>.</p>
<h2 id="ядро-коди">Ядро Коди</h2>
<p>Ядро (<code>harness/kodi_core.py</code>) — цикл самообучения по образцу systemone-bench (engine42/router42): учитель и правила решают, маленький ученик учится на их решениях и реальных исходах и получает право решать сам только после честной проверки. Сейчас у ядра три головы.</p>
<table>
<thead>
<tr>
<th>Голова</th>
<th>Решение</th>
<th>Метки</th>
<th>Режим</th>
<th>Состояние на 17.10</th>
</tr>
</thead>
<tbody>
<tr>
<td><code>addressing</code></td>
<td>отвечать ли в группе</td>
<td>учитель (вес 1), реакция чата (3), кнопки владельца (5)</td>
<td>shadow</td>
<td>чемпион на 262 примерах</td>
</tr>
<tr>
<td><code>task_fit</code></td>
<td>влезет ли S-задача в мин-тик</td>
<td>исходы задач из истории</td>
<td>on (правило «внешние системы»)</td>
<td>ML-чемпион снят: не бил базу; действует правило (точность 0.74)</td>
</tr>
<tr>
<td><code>depth</code></td>
<td>нужна ли глубокая память</td>
<td>решения правил (1), нехватка памяти по трассе (3)</td>
<td>shadow</td>
<td>копит данные, нужно 40 примеров</td>
</tr>
</tbody>
</table>
<p><strong>Ученик.</strong> Признаки — эмбеддинг <code>nomic-embed-text</code> (локально, ~70 мс, с дисковым кешем) плюс флаги головы. Модель — логистическая регрессия на numpy (Adam, L2): на сотнях примеров она устойчивее MLP. kNN-память даёт «новизну»; её пороги калибруются при обучении.</p>
<p><strong>Роутер.</strong> Знакомая ситуация и |p − 0.5| ≥ 0.4 → решает ученик; незнакомая и учитель сомневается → вопрос владельцу кнопками (≤ 5 в сутки); иначе — учитель. В shadow ученик только пишет <code>CorePrediction</code>.</p>
<p><strong>Честный допуск</strong> (ночная тренировка в дневной тик, <code>cli.py core train</code>):</p>
<ol>
<li>Чемпион помнит <code>trained_upto</code>; с претендентом сравнивается только на свежих данных (≥ 10), которых он не видел.</li>
<li>Претендент обязан бить базовую линию — константу, равную доле класса.</li>
<li>Чемпион, который на свежих данных хуже базы, снимается (<code>CoreModelRetired</code>) — ученик молчит.</li>
<li>Смена чемпиона — подписанное <code>CoreModelPromoted</code> с sha256 файла; подменённый файл не грузится; откат — <code>cli.py core rollback --head &lt;голова&gt;</code>.</li>
</ol>
<p><strong>Рефлексия.</strong> Раз в сутки: ≥ 3 расхождений адресации с метками человека/чата или ≥ 3 S-задач, не влезших в мин-тик, → задача часу с автодоставкой: разобрать случаи и предложить владельцу правило. Первая рефлексия (17.10) привела к правилу <code>task_fit</code> «внешние системы → сразу к часу».</p>
<p><strong>Недельный отчёт.</strong> По понедельникам владельцу в личку, без LLM, идемпотентно по неделе (<code>cli.py core report</code>).</p>
<h2 id="куб-4-4-4-круги-доверия">Куб 4×4×4: круги доверия</h2>
<p>Поведение задаёт одна таблица <code>POLICY</code> (<code>harness/trust.py</code>): круг доверия определяет инструменты, уровни времени и потолок глубины памяти. Вторая ось — тики (sec/min/hour/day), третья — глубина D0–D3.</p>
<table>
<thead>
<tr>
<th>Круг</th>
<th>Кто</th>
<th>Shell</th>
<th>Файлы</th>
<th>Портал MCP</th>
<th>Уровни</th>
<th>Память до</th>
</tr>
</thead>
<tbody>
<tr>
<td>1 владелец</td>
<td>Alex, локальный терминал, система (тик без внешних входов)</td>
<td>полный</td>
<td>полный доступ</td>
<td>всё (деньги — по флагу)</td>
<td>все</td>
<td>D3</td>
</tr>
<tr>
<td>2 правление</td>
<td>Vlad, Yura</td>
<td><code>cli.py</code>: ответы, задачи, эскалации, доставка файлов, обзор доски</td>
<td>чтение docs/out/sites/artifacts и присланного</td>
<td>звонки, реестры, фичи; без казны и member_create</td>
<td>min, hour, day</td>
<td>D3</td>
</tr>
<tr>
<td>3 свои</td>
<td>участники портала, MCP-агенты, партнёры</td>
<td><code>cli.py</code>: ответы, задачи, эскалации</td>
<td>нет</td>
<td>запись на звонок, проекты, фичи</td>
<td>min, hour</td>
<td>D2</td>
</tr>
<tr>
<td>4 остальные</td>
<td>прочие Telegram-пользователи</td>
<td><code>cli.py</code>: только ответ и эскалация</td>
<td>нет</td>
<td>нет</td>
<td>min</td>
<td>D1</td>
</tr>
</tbody>
</table>
<p><strong>Как определяется круг.</strong> Только по проверенной личности: Telegram user_id, канал MCP-агента или портала, локальный терминал. Текст сообщения прав не даёт. Меняет круг только человек на сервере (<code>cli.py trust set &lt;субъект&gt; &lt;1-4&gt;</code>) — из LLM-тика это запрещено и стражем, и самим CLI.</p>
<p><strong>Круг тика</strong> = самый недоверенный вход его контекста (входящие + задачи батча). Задачи наследуют круг тика, в котором поданы: просьба правления не исполняется позже часовым уровнем с правами владельца.</p>
<p><strong>Страж</strong> проверяет каждый вызов инструмента до исполнения. Shell в кругах 2–4 — одна команда <code>cli.py</code> из белого списка круга, без <code>;</code> <code>&amp;&amp;</code> <code>|</code> <code>&gt;</code> <code>$()</code> и обратных кавычек; файлы с секретами закрыты всегда. Задача правления упёрлась в права → владельцу приходят кнопки «Разрешить / Нет»; «Разрешить» даёт задаче круг 1.</p>
<p>Вживую проверено на реальном воркере в круге 2: <code>cat /etc/hostname</code> отменён до исполнения, <code>cli.py decide --stats</code> выполнился.</p>
<h2 id="динамическая-память-d0-d3">Динамическая память D0–D3</h2>
<p>Перед каждым LLM-тиком харнес выбирает, сколько собственной истории подгрузить (<code>harness/memory.py</code>, <code>SIMBIOT_MEMORY=on</code>); индекс на 17.10 — 1650 разговоров, 270 итогов задач, 91 дайджест.</p>
<table>
<thead>
<tr>
<th>Глубина</th>
<th>Что в контексте</th>
<th>Когда</th>
</tr>
</thead>
<tbody>
<tr>
<td>D0</td>
<td>только текущие входящие</td>
<td>все реплики односложные: «ок», «спасибо»</td>
</tr>
<tr>
<td>D1</td>
<td>+ 12 последних реплик чата + свои сообщения за 48 ч</td>
<td>минутный тик по умолчанию</td>
</tr>
<tr>
<td>D2</td>
<td>+ до 8 эпизодов из своей истории по смыслу (≤ 5000 символов)</td>
<td>час; «помнишь», «вчера», «как договаривались»; задачи в батче; reply на сообщение бота старше 48 ч</td>
</tr>
<tr>
<td>D3</td>
<td>+ до 15 записей архива (≤ 12 000 символов)</td>
<td>день; «за месяц», «что ты делал», «итоги», «план на»</td>
</tr>
</tbody>
</table>
<p>Потолок задаёт круг: «свои» — до D2, «остальные» — до D1.</p>
<p><strong>Индекс</strong> (<code>&lt;home&gt;/memory/index.sqlite</code>) инкрементально догоняет стор перед поиском (~3.5 с, только чтение):</p>
<ul>
<li><code>conv</code> — сообщение + ответ Коди (в батче — ответ головы);</li>
<li><code>out</code> — собственные рассылки бота, не ответы;</li>
<li><code>task</code> — задача и её итог;</li>
<li><code>digest</code> — содержательные дайджесты часа и дня (заглушки отсеиваются).</li>
</ul>
<p><strong>Приватность.</strong> У каждой записи — круг: сказанное в чате видно всем его участникам, поэтому круг записи = самый недоверенный участник чата (личка владельца — 1, «Правление» — 2); задача — её круг; дайджест — 1. Поиск отдаёт только записи с кругом ≥ круга тика: тик с сообщением правления не поднимет личку владельца. Круги читаются прямо из событий <code>TrustSet</code>, чтобы отставшая проекция не дала записи слишком широкий круг.</p>
<p><strong>Релевантность.</strong> Эмбеддинги <code>nomic-embed-text</code> (<code>search_document</code> / <code>search_query</code>). Калибровка на прод-индексе: релевантное — 0.78–0.88, посторонний запрос — до 0.75. Порог 0.76 и окно 0.07 от лучшего совпадения; свежий разговор того же чата (уже в D1) не дублируется.</p>
<p><strong>Обучение выбора.</strong> Решение о глубине → <code>DepthChosen</code> с причинами и признаками нехватки памяти по трассе: агент полез в базу знаний или в свой стор, ответил «не помню». Это сырьё для головы ядра <code>depth</code>.</p>
<h2 id="конфигурация-cli-деплой-и-тесты">Конфигурация, CLI, деплой и тесты</h2>
<p>Настройки читаются из env процесса, затем из <code>/var/lib/simbiot-harness/env/.env</code>; новые подсистемы по умолчанию выключены или в shadow, а на проде включаются строкой в <code>.env</code>.</p>
<table>
<thead>
<tr>
<th>Переменная</th>
<th>Прод</th>
<th>Что делает</th>
</tr>
</thead>
<tbody>
<tr>
<td><code>SIMBIOT_DECISION</code></td>
<td>on</td>
<td>decision-модель (гейт — shadow)</td>
</tr>
<tr>
<td><code>SIMBIOT_DECISION_API_KEY</code></td>
<td>задан</td>
<td>облако <code>clef:27b</code>, запасная — локальная <code>nimble</code></td>
</tr>
<tr>
<td><code>SIMBIOT_MEMORY</code></td>
<td>on</td>
<td>индекс памяти и блоки D2/D3</td>
</tr>
<tr>
<td><code>SIMBIOT_CORE_ADDRESSING</code></td>
<td>shadow</td>
<td>ученик адресации</td>
</tr>
<tr>
<td><code>SIMBIOT_CORE_TASK_FIT</code></td>
<td>on</td>
<td>сразу к часу задачи про внешние системы</td>
</tr>
<tr>
<td><code>SIMBIOT_CORE_DEPTH</code></td>
<td>shadow</td>
<td>ученик глубины памяти</td>
</tr>
<tr>
<td><code>SIMBIOT_TG_ALLOWED</code></td>
<td>Alex, Yura, Vlad</td>
<td>кто вообще обрабатывается</td>
</tr>
<tr>
<td><code>SIMBIOT_MIN_NUDGE</code></td>
<td>on</td>
<td>тик по входящему</td>
</tr>
<tr>
<td><code>SIMBIOT_TASK_ATTEMPTS_PER_LEVEL</code></td>
<td>2</td>
<td>попыток до эскалации размера</td>
</tr>
<tr>
<td><code>SIMBIOT_MAX_TOOL_CALLS_&lt;УРОВЕНЬ&gt;</code></td>
<td>min 30, hour 300</td>
<td>бюджет шагов тика</td>
</tr>
</tbody>
</table>
<p><strong>CLI</strong> (<code>cli.py</code>, для людей и агентов в тике):</p>
<table>
<thead>
<tr>
<th>Группа</th>
<th>Команды</th>
</tr>
</thead>
<tbody>
<tr>
<td>чат</td>
<td><code>tg-reply [--followup]</code>, <code>tg-notify -k</code>, <code>tg-deliver-file</code>, <code>tg-list</code>, <code>tg-seen</code></td>
</tr>
<tr>
<td>задачи</td>
<td><code>task</code>, <code>tasks</code>, <code>complete</code>, <code>block</code>, <code>promote</code>, <code>requeue</code>, <code>close</code></td>
</tr>
<tr>
<td>уровни</td>
<td><code>ladder</code>, <code>status</code>, <code>escalate</code>, <code>escalations</code>, <code>escalation-ack</code></td>
</tr>
<tr>
<td>решения</td>
<td><code>decide</code>, <code>decide --stats</code></td>
</tr>
<tr>
<td>ядро</td>
<td><code>core status / train / backfill / feedback / reflect / report / rollback / retire [--head]</code></td>
</tr>
<tr>
<td>память</td>
<td><code>memory sync [--all] / stats / search "…" --circle N</code></td>
</tr>
<tr>
<td>доверие</td>
<td><code>trust list</code>, <code>trust set &lt;субъект&gt; &lt;1-4&gt;</code> (только человек)</td>
</tr>
<tr>
<td>стор</td>
<td><code>events</code>, <code>verify</code>, <code>compact [--dry-run / --vacuum / --verify-archives]</code></td>
</tr>
</tbody>
</table>
<p><strong>Деплой.</strong> Код — ветка <code>master</code> в <code>git.cil.by/symbiot/simbiot-harness</code>. Перезапуск лестницы — только <code>scripts/safe-restart.sh</code>: он ждёт, пока закончится идущий LLM-воркер, чтобы задача не застряла в running. TG-поллер перезапускается напрямую: Telegram копит апдейты. Сжатие стора — <code>scripts/compact-store.sh</code>.</p>
<p><strong>Тесты.</strong> <code>tests/test_*.py</code> — скрипты (<code>./venv/bin/python tests/&lt;имя&gt;.py</code>), каждый со своим временным стором (<code>SIMBIOT_DB</code>). Внешние системы подменены: фейковый <code>/v1/systemone</code>, эмбеддинги «мешок слов», заглушки Telegram. Перед каждым коммитом гоняется полный набор и сравнивается с предыдущим прогоном. Несколько старых тестов (<code>test_tg_commands</code>, <code>test_reply_routing</code>, <code>test_portal_mcp</code>, <code>test_tool_budget</code>) падают и на чистом коде — они зависят от живого состояния.</p>
<h2 id="известные-ограничения-и-дорожная-карта">Известные ограничения и дорожная карта</h2>
<p>Главное открытое место — косвенные каналы между кругами и автономность часового уровня в круге 1.</p>
<p><strong>Ограничения:</strong></p>
<ul>
<li><strong>Выжимки нижних уровней считаются внутренними.</strong> Пересказ сообщений правления в дайджесте попадает в часовой и дневной тик с кругом 1.</li>
<li><strong>Часовой уровень в круге 1 может менять код сам.</strong> Рефлексия 17.10 закоммитила правило вопреки «код не меняй».</li>
<li><strong>Адресация ошибается на коротких репликах без контекста.</strong> «Выбирай любую.», сказанное боту, фильтр пропустит; выход — «Коди, …» или reply.</li>
<li><strong>Ученики пока слабые.</strong> <code>addressing</code> на свежих данных совпадает с учителем в 71% и уверен в ~10% случаев; ML-версия <code>task_fit</code> не бьёт базу; <code>depth</code> ещё не обучался.</li>
<li><strong>Расход токенов часового уровня</strong> — в среднем 46k токенов на вызов, до 205 вызовов инструментов на тик. Лимит 300 оставлен по решению владельца.</li>
<li><strong>Уровни неделя/месяц/квартал/год</strong> пока не работают; куб сознательно сжат до 4 тиков.</li>
</ul>
<p><strong>Дальше:</strong></p>
<ul>
<li>☐ Рефлексия и другие задачи «только предложить» — в ограниченном круге, внедрение после «да» владельца.</li>
<li>☐ Круг приватности для дайджестов — закрыть косвенный канал.</li>
<li>☐ Команда в боте для смены кругов (только владелец), чтобы не ходить на сервер.</li>
<li>☐ Решение по ученику адресации по цифрам недельного отчёта.</li>
<li>☐ Режим on для серой зоны гейта — по статистике shadow.</li>
<li>☐ Уровни неделя и месяц на основе D3 и дайджестов дня.</li>
</ul></main></div></body></html>