Архитектура · для ревью

Simbiot Harness

Принципиально новый агентный харнес на CQRS + Event Sourcing: агент живёт в тактах разного масштаба — секунда, минута, час, день, неделя, месяц, квартал, год — как человек. Стартовая точка — strands-agents/harness-sdk (Apache-2.0): его исполнительное ядро, поверх — наша временная ось и событийная спина.
CQRS + Event Sourcing Strands Harness SDK 9router — LLM-шлюз Python 3.12 · SQLite WAL v0.1 · ревью пройдено 29.09
01 / СУТЬ

Что мы строим

Все существующие харнесы (Strands, Hermes, OpenClaw) — пассивны: они просыпаются, когда пришёл внешний запрос, и не имеют собственной временной оси. Simbiot Harness — экзистенциальный: он тикает сам.

Харнес — это не «функция, которую вызывают», а живой процесс с ритмами. Восемь вложенных циклов бьют одновременно: секундный — как сердцебиение, минутный — как реакция, часовой — как рабочий поток, дневной — как план дня, недельный — как ретро, месячный — как цели, квартальный — как стратегия, годовой — как идентичность. Чем крупнее цикл, тем больше контекста он может себе позволить и тем крупнее задачу он вправе взять.

Всё, что происходит — тики, решения, наблюдения, память, трата бюджета — это события в append-only сторе. Состояние харнеса не хранится, а выводится replay'ем событий. Выключили процесс — включили — он продолжил с того же места, потому что его «личность» — это события, а не RAM.

Одной фразой: Strands даёт нам тело (агентный цикл, инструменты, хуки). Simbiot Harness даёт ему метаболизм — время, память и ритм — на событийной спине.
02 / ПРИНЦИПЫ

Шесть принципов

P-1

Всё есть событие

Тик, взятая задача, дайджест, трасса каждого tool-call агента — события. Event store — единственный источник правды. Никакого «состояния в RAM».

P-2

Тактовая лестница

8 уровней: 1с → 1м → 1ч → 1д → 1н → 1мес → 1кв → 1г. У каждого — свой мандат, бюджет и модель. Мелкое решает мелкое, крупное — крупное.

P-3

Контекст ∝ масштабу

Секундный цикл не зовёт LLM вовсе, минутный получает 4K токенов, дневной — 128K, годовой — до 1M. Задача S-класса не занимает слот годового цикла.

P-4

Непрерываемость

Запущенная задача не прерывается тиками. Если исполнитель уровня занят — тик поглощается (skip), а не копится в очередь. Работа важнее метронома.

P-5

Переваривание

Верхние циклы едят выжимку нижних: дневной читает не сырые минуты, а часовые дайджесты. Пирамида консолидации памяти — как сон у человека.

P-6

Воспроизводимость

Стор можно переиграть с нуля — и харнес, и память, и решения соберутся заново. Отладка агента = чтение потока событий, а не логов.

03 / ЛЕСТНИЦА

Тактовая лестница

Каждый уровень — отдельная «личность»: свой мандат (что ему можно), свой бюджет контекста, своя модель и свой класс задач. Цикл может взять задачу своего класса и ниже; задача выше класса — ожидает уровень повыше.

ЦиклМандатМозг (9router)КонтекстЗадачиАналогия у человека
1 секРефлекс: телеметрия, watchdog, heartbeat, алертыбез LLM — детерминированный код0микро замер, триггерСердцебиение
1 минПульс: триаж входящих, быстрые ответы, мелкие фиксыglm-5.3-flash≤ 4KS ≤ 5 мин работыРеакция в разговоре
1 часДайджест: сборка часа, батч мелочей, проверка прогрессаglm-5.3-flash≤ 32KM ≤ 1 чРабочий поток
1 деньМанифест: план дня, выбор задач, дневной отчётglm-5.3 (full)≤ 128KL ≤ 1 деньПлан на день
1 неделяРетро: разбор недели, обучение, обновление скилловglm-5.3 · effort: high≤ 200KXL неделяНедельное ретро
1 месяцЦели: бюджет, OKR, ранги, накопленияfull≤ 256KXXL месяцМесячная цель
1 кварталСтратегия: пересмотр направления, ставкиpro-слот*≤ 512KXXL кварталКвартальная стратегия
1 годИдентичность: миссия, «зачем существую», самоопределениеpro-слот*≤ 1MXXL годЖизненный выбор

* pro-слот — зарезервировано под сильную модель (Claude Opus как личный брейн / Gemini 3 Pro через KIE): годовой цикл тикает 1 раз, дорогой запуск раз в год — приемлемо.

04 / ДИАГРАММА

Архитектура

CQRS — ПЛОСКОСТЬ КОМАНД И ЗАПРОСОВ ИНТЕРФЕЙСЫ Telegram · Matrix · Web cron · webhook · человек COMMAND SIDE валидация · идемпотентность submit_task · set_policy EVENT STORE APPEND-ONLY · СПИНА events(aggregate, id, type, data, version) CycleTicked · TaskClaimed DigestWritten · BudgetSpent replay → вся «личность» харнеса восстанавливается SQLite · WAL ПРОЕКЦИИ task_board · digest_* metrics · agent_state (перестраиваются replay'ем) QUERY API · дашборд человек смотрит APPEND STREAM→ ВРЕМЕННАЯ ПЛОСКОСТЬ — ТАКТОВАЯ ЛЕСТНИЦА SCHEDULER 8 уровней бьют по границам тактов: 1с 1м 1ч 1д 1н 1мес 1кв 1г тик занят → absorbed (skip, не очередь) asyncio · один процесс CYCLE RUNNERS по одному исполнителю на уровень → сериализация, задача не прерывается мандат + бюджет + класс задач — из конфига уровня (P-2, P-3) STRANDS RUNTIME create_harness() на каждый тик: agent loop · tools · MCP session (задача) · effort context_manager · guardrails HOOKS → каждая стадия цикла агента пишет трассу событием LLM ШЛЮЗ 9router: flash / full / pro модель задаётся уровнем цикла — не задачей ИНСТРУМЕНТЫ shell · files · web · MCP · скиллы TICK LLM CycleTicked → APPEND читает контекст: дайджесты нижних уровней + задачи класса HOOKS: трасса агента → события
РИС. 1 — Две плоскости. CQRS: интерфейсы → команды → event store → проекции → запросы. Временная: планировщик → исполнители циклов → Strands-агент → LLM. Замкнутый контур: события порождают циклы, циклы порождают события.
05 / ТИК

Жизненный цикл тика

Один тик уровня — от границы такта до записи результата. Ключевое правило — непрерываемость: занятый исполнитель поглощает тик.

Граница тактаScheduler ловит момент 1с/1м/…/1г. Эмитит событие CycleTicked{level, seq}.
Исполнитель свободен?Занят → TickAbsorbed: детерминированная дайджест-заглушка (без LLM), очередь не копим. Дорога (v0.2+): срочная реакция сможет прервать бегущую задачу или поставить её на паузу.
Сборка контекстаМандат уровня + идентичность + память уровня + выжимка нижних (дайджесты) + задачи доступного класса. В пределах бюджета P-3.
Выбор задачиЕсть подходящая задача → TaskClaimed + сессия. Нет → цикл делает мандатную работу (дайджест/наблюдение) или проходит впустую — это честно.
Strands-циклcreate_harness(model, effort, tools уровня) → invoke. Каждый шаг — hook → событие. Задача не прерывается никакими тиками.
ЗавершениеTaskCompleted / TaskFailed (+ политика ретраев и промоушена выше). Результат → событие с ссылкой на артефакт.
ВыжимкаRunner пишет DigestWritten{level, period} — сырое остаётся в трассе, в пирамиду идёт сжатое.
Non-preemption на практике: у каждого уровня ровно один исполнитель (worker). Это даёт сериализацию бесплатно: день не может «отложить» час. Долгие L-задачи идут в фоне сессии, дневной цикл на следующих тиках лишь проверяет их статус — и пока они живы, новых L не берёт.
06 / ПАМЯТЬ

Контекстная пирамида

Контекст цикла — не «всё, что случилось», а многослойная сборка. Сырые данные живут в трассе событий; наверх поднимаются только выжимки. Это главный эконом токенов: наш фактический опыт — 3.1 млрд prompt-токенов за 14 дней в основном из-за перечитывания сырых контекстов.

ГОД · 1M
Идентичность: миссия, ценности, «кто я». Пишется раз в год, кэшируется во все нижние уровни.
КВАРТАЛ · 512K
Ставки: 3–5 стратегических направлений + критерии их проверки.
МЕСЯЦ · 256K
Цели: OKR, бюджет, лимиты расходов на уровень.
НЕДЕЛЯ · 200K
Уроки: что сработало/нет, обновления скиллов и политики.
ДЕНЬ · 128K
Манифест дня: план + часовые дайджесты (не сырые минуты!) + доска задач L-класса.
ЧАС · 32K
Дайджест часа: сжатые минутные логи, статус фоновых задач, триаж входящих.
МИНУТА · 4K
Пульс: последние входящие, алерты секундного уровня, S-задачи.
СЕК · 0
Рефлекс: без LLM. Телеметрия и правила. Дёшево тикать каждую секунду.

Сборка контекста дневного цикла: идентичность (кэш) + квартальные ставки + месячные цели + недельные уроки + дайджесты 24 часов + доска задач. Сырые трассы не поднимаются выше своего уровня никогда — вверх идёт только сжатое.

07 / СОБЫТИЯ

Событийная модель

Стор — по образцу кооп-портала (aggregate, aggregate_id, event_type, data, timestamp, version). События иммутабельны; тестовые — единственное исключение при чистке.

Aggregate: cycle
  • CycleTicked {level, seq} — граница такта
  • TickAbsorbed {reason: busy|skipped}
  • DigestWritten {level, period, summary_ref}
Aggregate: task
  • TaskSubmitted {title, size S/M/L/XL, source}
  • TaskClaimed {level, session_id}
  • TaskCompleted {result_ref} / TaskFailed {error}
  • TaskPromoted {from, to} — не по зубам уровню
Aggregate: harness
  • PolicyChanged {budgets, mandates}
  • BudgetSpent {level, tokens, $}
  • LevelPaused / LevelResumed
Aggregate: trace
  • InvocationStarted / ToolCalled / ToolResult — из hooks Strands: BeforeInvocationEvent, Before/AfterToolCallEvent
  • Полная трасса агентного цикла = поток событий. Дебаг = читать стор.

Проекции (query side)

08 / STRANDS

Маппинг на Strands Harness SDK

Берём SDK как исполнительное ядро — не форкаем, а собираем поверх. Всё, чего нет в Strands (время, событийная спина, консолидация) — наш слой.

Наш концептМеханизм StrandsКомментарий
Cycle Runner уровняcreate_harness() на тикФабрика с конфигом уровня: model, effort, tools, instructions
Мандат уровняinstructions / system promptСобственный слой поверх HARNESS_CONTRACT
Бюджет контекстаcontext_manager + sessionagentic-режим для день+, sliding window для минут
Непрерываемостьbackground tasks + session idЗадача = сессия; тики не рвут её, цикл опрашивает
Трасса в сторhooks: add_hook(BeforeInvocationEvent, BeforeToolCallEvent…)Наш плагин: каждый хук → append_event. Трасса = события
Инструментыbuiltin_tools + mcp_servers + skills dirshell/read/write/edit/web — по уровню: минуте не даём shell, дню — даём
Модель по уровнюmodel: provider/name, effort: auto…highЧерез наш 9router-провайдер: flash/full/pro
Осторожностьinterventions (guardrails)Необратимые действия — через подтверждение или только на высоких уровнях
Долгая памятьmemory storesБэкенд — наши проекции digest_*, не файлы
Берём из Strands как есть
  • Agent loop: лимиты ходов, токен-бюджеты, stop reasons
  • Hooks — типизированные события жизненного цикла
  • Сессии, structured output, MCP, мультитул-оркестрация
  • Guardrails/interventions, context offloader
Принципиально своё (новое)
  • Временная ось: тактовая лестница — в Strands нет никакого «времени»
  • Событийная спина: у Strands сессии = файлы; у нас — append-only стор + replay
  • Каскад консолидации памяти (digestion) — у Strands нет иерархии памяти
  • Классы задач ↔ уровни цикла — планировщик как у человека
09 / СТЕК

Стек и размещение

Объём v0.1 (что строим первым)

10 / РЕШЕНИЯ

Ревью пройдено — решения

Прошли с владельцем 29.09.2026. Все шесть вопросов закрыты.

ВопросРешение
Стоимость ритма (~$1–3/день за «жизнь» без задач)✅ Принято — норма
Пропущенные такты✅ Поглощённый тик пишет детерминированную дайджест-заглушку (без LLM). Дальше (v0.2+): срочная реакция сможет прервать бегущую задачу или поставить её на паузу
Один исполнитель на уровень✅ Принято; N воркеров минутного уровня — v0.2 при необходимости
Идемпотентность тиков✅ UNIQUE(level, seq); пропущенное время не навёрстывается
Pro-слот для квартала/года✅ Принято
Имя и место✅ Simbiot Harness — отдельный репо-продукт (отдельный продукт = отдельный репо)
Следующий шаг: сборка v0.1 — уровни 1с + 1м + 1ч + 1д, задачи S/M/L, ES-спина, дайджесты, hooks-плагин Strands, E2E: таска из Telegram → минутный цикл → дневной манифест → отчёт.