惯性聚合 高效追踪和阅读你感兴趣的博客、新闻、科技资讯
阅读原文 在惯性聚合中打开

推荐订阅源

aimingoo的专栏
aimingoo的专栏
量子位
freeCodeCamp Programming Tutorials: Python, JavaScript, Git & More
S
Schneier on Security
Cisco Talos Blog
Cisco Talos Blog
T
ThreatConnect
J
Java Code Geeks
博客园 - 司徒正美
A
Arctic Wolf
T
True Tiger Recordings
C
Cybersecurity and Infrastructure Security Agency CISA
Cyberwarzone
Cyberwarzone
Know Your Adversary
Know Your Adversary
T
Threat Research - Cisco Blogs
V
Vulnerabilities – Threatpost
Recorded Future
Recorded Future
P
Palo Alto Networks Blog
The Hacker News
The Hacker News
The Register - Security
The Register - Security
S
Securelist
www.infosecurity-magazine.com
www.infosecurity-magazine.com
C
CXSECURITY Database RSS Feed - CXSecurity.com
Application and Cybersecurity Blog
Application and Cybersecurity Blog
I
Intezer
P
Privacy & Cybersecurity Law Blog
Scott Helme
Scott Helme
K
Kaspersky official blog
博客园 - 聂微东
Last Week in AI
Last Week in AI
V
V2EX
小众软件
小众软件
F
Fox-IT International blog
Martin Fowler
Martin Fowler
Apple Machine Learning Research
Apple Machine Learning Research
T
Tenable Blog
F
Future of Privacy Forum
Microsoft Security Blog
Microsoft Security Blog
让小产品的独立变现更简单 - ezindie.com
让小产品的独立变现更简单 - ezindie.com
腾讯CDC
Stack Overflow Blog
Stack Overflow Blog
C
Check Point Blog
阮一峰的网络日志
阮一峰的网络日志
GbyAI
GbyAI
T
Threatpost
I
InfoQ
P
Proofpoint News Feed
CTFtime.org: upcoming CTF events
CTFtime.org: upcoming CTF events
T
Tor Project blog
G
GRAHAM CLULEY
D
DataBreaches.Net

Все публикации подряд на Хабре

MemForge2: загрузочная флешка, которая за минуту говорит — какую планку памяти менять Лицензии важны. Разбор ошибок авторов и пользователей программ От RAG-прототипа к агенту в продакшн: путь по метрикам, а не по моде Serial Terminal: кастомный веб-терминал для последовательного порта на Web Serial API Китайский стартап GigaAI обещает робота-домработника за 1 млн рублей уже в 2027 году — правда или PR? Open-source VPN клиент Tunguska Роман за 6 недель без идеи на старте: миф или реальность? ИИ построит ваш план действий за 10 секунд Security Week 2622: эффективность Claude Mythos по версии Cloudflare Reactive Forms vs Signal Forms: Эволюция сложных форм в Angular TorFlash — приложение для Linux: поиск торрентов, скачивание и копирование на флешку в одно нажатие Как я решил проблему русской диктовки для ИИ Оверинжиниринг, потопивший немецкую подлодку или некоторые «баги» не чинятся десятилетиями Как ставить цели и не забывать о них: пошаговая система с примерами в таск-менеджере Как настроить observability в Spring Boot 3 HackTheBox. Прохождение Mini Pro Lab Puppet Обзор серверного ускорителя NVIDIA Tesla V100 16 Gb в корпусе от RTX 4090: Часть 3 — Запуск локальных моделей ИИ Редактирование текста нейросетью: как сделать диплом и курсовую более человечными Самодельный ARM ноутбук, реально ли? Как 100+ авторов пишут 100+ процессов в 3 версиях и не путаются. Или как мы переехали с Wiki на Git Прошла AnalystDays – хорошие выступления и нетворкинг VSCode как IDE для embedded разработки Моделирование широкополосной антенны с двойной круговой поляризацией и высокой изоляцией Ваше прошлое физически существует прямо сейчас. И вы заморожены там навсегда От списка инструментов к technical output: как security engineer’у описывать hands-on опыт в CV и на интервью I just want an agent. Часть 1. Как я научил ИИ собирать ИИ-агентов за пользователей и выиграл конкурс I just want an agent. Часть 1. Как я научил ИИ собирать ИИ-агентов за пользователей и выиграл конкурс Вайбкодинг спас меня от подрядчиков. А потом я поняла, что сама стала подрядчиком для своих агентов Святой Августин и GAN: почему борьба добра и зла — это генеративная состязательная сеть В каждом QR-коде зашита половина лишней информации. Намеренно Я открываю автомат ключом, меняю рулон бумаги и зарабатываю 180 тысяч в месяц с точки Мастер восстановления. Культура достиженства и выгорание Недельный геймдев: #279 — 24 мая, 2026 Защита от дублирования кода агентами: семантические концепции Frontend Status: свежий дайджест фронтенда и AI — 25.05.2026 Где искать IT-работу кроме HH: подборка платформ 2026 Почему простые числа собираются в спирали? OCR для Data Lakehouse: от Apache Tika к собственному решению на базе Docling Jira — Тьюринг-полная Kubernetes-аудит после Wiz и Prisma: как живут без CNAPP в 2026 «Тестируем MVP в 4 раза быстрее»: как нейросети изменили жизнь предпринимателей На каком стеке и железе работает умное наблюдение в вашем городе: обзор технологий от разработчиков видеоаналитики Как мы ускорили согласования на двух заводах в 24 раза Heartbeat-мониторинг cron-job'ов: dead-man-switch на FastAPI [Перевод] Сегодня нет джуниоров, а в 2031 году не станет и синьоров Профайлер для PostgreSQL: от идеи до работающего MVP за сутки [Перевод] Ограничения размера cookie в ASP.NET Core в продакшене: причины и способы решения Проблема «божественного» Obsidian: почему я отказался от централизованного подхода в работе Лицензии GNU GPL: как пройти проверку Минцифры и заказчика для госзакупок и КИИ Хакатон Samsung IT Academy Hack 2026: как студенты оптимизировали поиск в корпоративном мессенджере Хакатон Samsung IT Academy Hack 2026: как студенты оптимизировали поиск в корпоративном мессенджере MTProxy jumper — делаем автоматическое переключение прокси-серверов Telegram Ты уже используешь агента. Просто не заметил Книжный салон. Послевкусие и благодарности Как отлаживать мини‑приложения в MAX и почему без DevTools это боль Cбор биометрических данных. Как защищается наша биометрия на практике Как запустить учет активов без цифровой свалки: первые 90 дней CGE: визуализация кравлера и скрытых связей между поддоменами Зачем банки тратят миллиарды на науку (спойлер: не благотворительности ради) Книга: «Современный Java Concurrency. Глубокое погружение в Virtual Threads, Structured Concurrency и Scoped Values» Как использовать подписку ChatGPT и Claude в Cursor без оплаты за API токены Специализированная ИСУП или модуль в универсальной платформе: вот в чем вопрос Обход белых списков через WebRTC на стероидах (с поддержкой iOS и десктопа) Регата INFOSTART CIO CAMP: когда команда проверяется не в переговорной, а на воде Пет-проект, который не умер: система бронирования устройств как полигон для AI-разработки Не надо встраивать ИИ в каждую корпоративную систему, это архитектурная ошибка Нейросети для дизайна интерьера: Выбираем лучший ИИ для генерации концептов и планировок квартиры Что там с Ил-114-300 Что такое DAS: как и зачем продукт-менеджеры саботируют запуск новых продуктов 8% компаний измеряют критическое мышление руководителей. Что делают остальные 92% CVE, Shell и побег из контейнера: испытываем возможности PT Cloud Application Firewall Как я научил Алису петь: генерация музыки по голосовой команде Восстановление данных с помощью бесплатной утилиты Easy Disk Checker Как мы построили сквозную аналитику в Power BI Год разработки iOS-игры, 266 тысяч показов и $33: как я делал Vault и почти ничего не заработал Ты прокрастинируешь потому, что избегаешь напрасных усилий, а не чрезмерных нагрузок Я построила диагностику «стоит ли это автоматизировать» — и она трижды говорила глупости. Разбор ошибок Как устроены world models, что показал Google на прошлой неделе и где это меняет gamedev и робототехнику Двухдневная рабочая неделя — будущий стандарт CPU не умер, он просто ждал. Китай строит двухэксафлопсный суперкомпьютер без единого GPU — прорыв, необходимость, фейк? 3Sound: поиск бесплатных звуков для игр больше не боль? 3 Тбит/с по-русски: почему DDoS в 2026 году стал угрозой для любого бизнеса 10 Гбит/с — зачем вам такая скорость передачи данных в облаке Ремонтируем аналоговый XY-самописец Endim 622 [Перевод] IPO компании SpaceX: хорошая попытка, но нет «Ща будет шрифт»: история одного русского embedded‑шрифта Как аквариум на подоконнике превратился в full-stack платформу с AI GiftsHub — из чат-бота в полноценный backend-продукт Пиратство, копирайт и DMCA: как Napster, The Pirate Bay и YouTube изменили закон. Часть II Как найти внутренние резервы для развития предприятия Как один французский чиновник от безысходности начал платил зарплаты картами и практически изобрёл банкноты RAG в энтерпрайзе: почему демо работает, а прод нет AI-агент для финансовых процессов: как мы научили ИИ считать числа из базе данных без галлюцинаций Автопостинг на 8 платформах: архитектура waterfall, custom publisher'ы и API-ловушки Кинетика против бронзы: Почему Голиаф был обречен в дуэли с Давидом [Перевод] Масштабирование LLM: от одного чипа до ЦОДа. Глава 2. Шардинг LLM не работает за вас. Она работает с вами Чем лучше защищает минеральный SPF, тем страшнее он выглядит Стимпанк как часть жизни. История паровых двигателей и место, которое они занимали в мире в XIX-XX веках. Часть 1 Гастарбайтеры ворвались в IT и зарабатывают на рекламе: тут вам не снег лопатой кидать
Свой AI-агент из почты, systemd и LLM
3draven · 2026-05-26 · via Все публикации подряд на Хабре

В прошлых статьях я строил домашнее облако на Proxmox. Теперь внутри него живёт кое-что поинтереснее — полностью автономный AI-агент, которого я могу пнуть письмом из обычного почтового клиента или сообщением в Telegram, и он ответит, подумав. Причём подумав по-настоящему: с многошаговым рассуждением, долговременной памятью и возможностью выполнять команды. Зовут его Threlium, и устроен он чертовски необычно и просто, например может модифицировать сам себя.

Зачем, почему и что получилось — расскажу ниже.


Зачем свой агент

Все облачные LLM-ассистенты — чужие. Данные уходят на сервера провайдера, контекст ограничен одним окном чата, настоящей долговременной памяти можно сказать нет, есть чат-сессия. При этом умные модели уже бегают локально на средненьком GPU: Qwen, Llama, Mistral. Возникает вопрос: а нельзя ли собрать агента, который будет жить на моём сервере, работать с моими данными и при этом иметь настоящий цикл рассуждений — не просто «спросил → ответил», а полноценный конечный автомат с ветвлениями, памятью и инструментами Причем хочется что бы кода в нем было, почти не было по сравнению с аналогами и он мог править сам себя или легко отлаживаться.

Оказывается, можно. Причём из довольно простых Unix-кирпичиков.


Что дает ниже описанный поход

  1. Мало кода, если выкинуть клеекод и типизацию (модуль types), то останется примерно 6к строк пайтон скриптов. У аналогов примерно то же быстро становится сотнями тысяч строк.

  2. Наблюдаемоесть, вся работа агента сразу видна и никаких инфраструктур для отслеживания его работы просто не нужно.

  3. Простота конструкции, это просто набор конфигов и немного скриптов. В качестве инсталлятора ansible playbook.

  4. Минимальное потребление ресурсов, агент влезет на самую дешевую VPS.

  5. Агент может править себя.

  6. Вся эта конструкция не требует инфраструктуры вовсе, никаких сложных миллионов контейнеров, мониторинг вытекает их ее свойств, отлаживать проблемы этой штуки очень легко. Достаточно дать ssh на сервер агенту и показать папку где отлаживаемый агент развернут, наблюдаемость у системы просто отличная.


Философия: событие = письмо

Самое неожиданное архитектурное решение Threlium — в его основе лежит электронная почта. Не как транспорт «для связи с пользователем», а как фундаментальная модель данных.

Любое событие в системе — это RFC 5322 сообщение. Буквально MIME-файл с заголовками и телом. Переход между состояниями конечного автомата — доставка нового письма в другой Maildir. Хранилище — тоже Maildir (формат tmp/, new/, cur/, атомарная запись через rename(2)). Индекс — notmuch поверх всех этих Maildir’ов. Это просто обыкновенная переписка между почтовыми ящиками, которая может моделировать как конечный автомат, так и модель акторов если хочется.

Звучит безумно? Возможно. Но вот что это даёт:

  • Каждое событие — файл на диске. Можно открыть mutt’ом, grep’нуть, написать скрипт.

  • Никакого отдельного брокера сообщений. Maildir — это и очередь, и canonical event store.

  • Идемпотентность из коробки: файл либо в new/, либо в cur/, третьего не дано.

  • Отказоустойчивость: упал процесс — файл остался в new/, следующий запуск подберёт.

  • Полная история навсегда: после обработки файл переезжает в cur/<id>:2,S, не удаляется.

Один notmuch search '*' — и вы видите абсолютно все события системы за всё время. Это логический «архив». Простая почта позволяет просто открыть веб-интерфейс и посмотреть все “размышления” агента.


Конечный автомат на Maildir’ах

Threlium — IRT-tree FSM. Расшифрую: конечный автомат, у которого состояния — очереди Maildir, а граф переходов определяется In-Reply-To цепочками писем. Глобального координатора нет. Состояние фрейма (бюджет шагов, права) живёт прямо в заголовках письма — X-Threlium-Hop-Budget, X-Threlium-Capabilities.

Стадии FSM

Пока что их немного, я просто закончил базу и так как буду развивать ее далее, решил не тянуть и описать ее.

  • ingress — единая точка входа. Сюда приходят все сообщения от всех каналов.

  • enrich — обогащение контекстом. Здесь подключается LightRAG (граф знаний) и хронология треда.

  • reasoning — собственно рассуждение. LLM получает промпт с контекстом и отвечает через tool calls.

  • egress_router — маршрутизатор выхода. По depth IRT-цепочки решает: ответ пользователю или возврат в субагент.

  • egress_email, egress_telegram, egress_matrix — терминальные стадии доставки наружу.

  • cli_intent — политика: можно ли выполнять команду?

  • cli_exec — песочница для исполнения shell-команд.

  • thread_memory, global_memory — FSM-состояния для работы с памятью.

  • archive — финальная запись об отправке.

Контракт стадии

Каждая стадия — Python-модуль threlium.states.<stage> с одной функцией:

def main(msg: EmailMessage, stage: FsmStage, *, config: Config) -> EmailMessage | None:

Принимает письмо — возвращает новое письмо (переход дальше) или None (терминальная стадия). Всё остальное — транспорт и оркестрация — за пределами стадии. Стадия не трогает файлы, не вызывает systemctl, не знает про fdm. Чистая функция над stdlib email.message.EmailMessage. По возможности конечно.

Граф переходов FSM

PlantUML-исходник: Граф переходов FSM
@startuml
left to right direction

package "Вход" {
  actor "Пользователь" as User
  rectangle "bridge-email" as EB
  rectangle "bridge-telegram" as TB
  rectangle "bridge-matrix" as MB
  User --> EB : Email
  User --> TB : Telegram
  User --> MB : Matrix
}

rectangle "fdm → notmuch insert" as FDM
EB --> FDM : run_fdm
TB --> FDM : run_fdm
MB --> FDM : run_fdm

rectangle "ingress" as ING
rectangle "enrich" as ENR
rectangle "reasoning" as REA
FDM --> ING
ING --> ENR
ENR --> REA

rectangle "egress_router" as EGR
rectangle "cli_intent" as CLI
rectangle "thread_memory" as TM
rectangle "global_memory" as GM
rectangle "subagent_intent" as SI
rectangle "reflect" as REF

REA --> EGR : "tool: egress_router"
REA --> CLI : "tool: cli_intent"
REA --> TM : "tool: thread_memory"
REA --> GM : "tool: global_memory"
REA --> SI : "tool: subagent_intent"
REA --> REF : "tool: reflect"

rectangle "cli_exec" as EXEC
rectangle "cli_hitl_out" as HITL
CLI --> EXEC : allow
CLI --> ING : deny
CLI --> HITL : HITL
HITL --> EGR
EXEC --> ING

TM --> ING
GM --> ING
REF --> ING
SI --> ING

rectangle "egress_email" as EE
rectangle "egress_telegram" as ET
rectangle "egress_matrix" as EM
rectangle "subagent_end" as SE
rectangle "archive" as ARC

EGR --> EE : "depth == 0"
EGR --> ET : "depth == 0"
EGR --> EM : "depth == 0"
EGR --> SE : "depth > 0"
SE --> ING

EE --> ARC
ET --> ARC
EM --> ARC

actor "Пользователь" as UserOut
EE --> UserOut
ET --> UserOut
EM --> UserOut
@enduml
Граф переходов FSM

Граф переходов FSM


Маршрутизация: fdm + notmuch insert

Для доставки писем между стадиями используется fdm — лёгкий mail delivery agent. Конфиг ~/.fdm.conf (генерируется Ansible из Jinja2-шаблона) содержит правила маршрутизации по заголовку To::

match "To" ... action pipe "notmuch insert --folder=stages/reasoning/Maildir ... && threlium-dispatch.sh"

Ключевое: notmuch insert — это атомарная операция. Файл записывается в Maildir и индексируется в notmuch одной транзакцией. Никакого notmuch new отдельно не нужно. После успешного insert тут же вызывается dispatch-скрипт, который поднимает воркер для обработки.

Почту давно придумали, не нужно писать ее снова для модели акторов. Нам нет острой необходимости экономить миллисекунды так как агенты работают десяткти минут.


Оркестрация: systemd --user

Никаких Celery, RabbitMQ, Kubernetes. Оркестрация — это systemd --user. Вот как работает цепочка:

PlantUML-исходник: Оркестрация systemd
@startuml
top to bottom direction

rectangle "fdm → notmuch insert\n--folder=stages/‹stage›/Maildir" as FDM
rectangle "threlium-dispatch.sh\nnotmuch search tag:unread\nAND folder:‹stage›/Maildir\n→ systemctl start --no-block" as DISP
rectangle "threlium-work@‹stage›:‹thread_id›\nType=exec\npython -m threlium.runners.engine_submit %i\n→ JSON в UNIX-сокет" as WORK
rectangle "threlium-engine.service\nДолгоживущий демон\nparse_rfc822 → main() → run_fdm()" as ENGINE
rectangle "nm_settle()\nnew/‹id› → cur/‹id›:2,S\ntags.discard unread + to_maildir_flags" as SETTLE
rectangle "threlium-sweep@‹stage›:‹thread_id›\nRace backstop\nthrelium-dispatch.sh %i\nперепроверка backlog" as SWEEP
rectangle "RAG-loop в engine\nrag.ainsert + tag +lightrag_indexed" as RAG
rectangle "JSON error → submit exit 1\nRestart=on-failure\nбез sweep" as ERR

FDM --> DISP : "&& threlium-dispatch.sh"
DISP --> WORK
WORK --> ENGINE
ENGINE --> SETTLE
SETTLE --> SWEEP : "exit 0 → OnSuccess"
SWEEP ..> DISP : "хвост unread"
SETTLE ..> RAG : "schedule_index_pending"
ENGINE --> ERR : "exception"
@enduml
Оркестрация systemd

Оркестрация systemd

Имя инстанса воркера threlium-work@enrich:000000000012ab.service — это одновременно и мьютекс. systemd гарантирует: один инстанс с данным именем — один тред. Параллельно обрабатываются разные треды (разные thread_id), последовательно — письма одного треда (oldest-first FIFO).

Никакого flock. Никакого пула потоков. Никакого coordinator. Всё бесплатно из systemd: перезапуски при сбоях (Restart=on-failure), лимиты ресурсов (threlium-work.slice с TasksMax, MemoryMax, CPUQuota), cgroup-изоляция, логи через journalctl.

Никакого в сотый раз написанного с нуля с косяками superviser for actors не нужно, KISS…my ass. Причем systemd --user выбран не просто так, это именно пользовательские unit, они живут в папке пользователя и в одном git репозитории все, так что их легко править и коммитить самому агенту если нужно.


Многоканальный вход: Email, Telegram, Matrix

Threlium принимает сообщения из трёх каналов. Каждый канал — это мост (threlium-bridge@<chan>.service), который нормализует входящий сигнал в каноническое RFC 5322 письмо:

Канал

Транспорт

From:

Email

IMAP IDLE (imap-tools)

email@localhost

Telegram

Long-poll (Bot API)

telegram@localhost

Matrix

Sync-loop (matrix-nio)

matrix@localhost

Маршрутная информация канала (chat_id, room_id, update_id, reply targets) кодируется в заголовок X-Threlium-Route как base62(JSON). Это позволяет при ответе вернуть сообщение ровно в тот чат/комнату/ящик, откуда оно пришло.

На выходе egress_router определяет канал по X-Threlium-Route и маршрутизирует в egress_email, egress_telegram или egress_matrix. Симметрия: вход и выход устроены одинаково.

PlantUML-исходник: Ingress/Egress мосты
@startuml
left to right direction

package "Ingress-мосты" {
  rectangle "Email\nIMAP IDLE" as E
  rectangle "Telegram\nLong-poll" as T
  rectangle "Matrix\nmatrix-nio sync" as M
}

rectangle "run_fdm\n→ fdm → notmuch insert\nstages/ingress/Maildir" as FDM
rectangle "FSM\ningress → enrich → reasoning → ..." as FSM
rectangle "egress_router\nresolve X-Threlium-Route\n→ channel" as EGR

E --> FDM : "From: email@localhost\nX-Threlium-Route: b62(JSON)"
T --> FDM : "From: telegram@localhost\nX-Threlium-Route: b62(JSON)"
M --> FDM : "From: matrix@localhost\nX-Threlium-Route: b62(JSON)"

FDM --> FSM
FSM --> EGR

package "Egress" {
  rectangle "egress_email\nmsmtp → SMTP" as EE
  rectangle "egress_telegram\nBot API" as ET
  rectangle "egress_matrix\nClient-Server API" as EM
}

EGR --> EE
EGR --> ET
EGR --> EM
@enduml
Ingress/Egress мосты

Ingress/Egress мосты

Чекпоинтов вне union-индекса нет. При рестарте мост восстанавливает курсор из X-Threlium-Route последнего доставленного письма через notmuch-поиск. Никакой отдельной БД offset’ов. Тот же KISS.


LLM: tool calls как единственный механизм

Стадия reasoning — точка контакта с LLM. Используется litellm (Python SDK для OpenAI-совместимых API, версия 1.83 уже без взломов :) ). Модель получает:

  1. System-промпт из Jinja2-шаблона reasoning/system.j2.

  2. User-промпт с обогащённым контекстом из enrich (контекст графа знаний + хронология треда).

  3. Список tool_specs для каждого возможного маршрута: egress_router, cli_intent, thread_memory, global_memory, subagent_intent.

Модель отвечает tool call’ом, а не свободным текстом. Это принципиальное решение: LLM — источник намерения, FSM — исполнитель. Парсинга свободного текста для выбора маршрута нет. Если модель вернула текст без tool call — ReasoningStageError, письмо остаётся в new/+unread, воркер завершается с exit 1, systemd делает retry. Позже я сделаю дополнительные стадии FSM для формирования больших ответов, но пока что есть минимум.

Аргументы tool call валидируются jsonschema (JSON Schema с additionalProperties: false и maxLength-лимитами). Прошла валидация — из аргументов собирается новое письмо с To: <next_stage>@localhost и отправляется через run_fdm в следующую стадию.

Для разных маршрутов — разные JSON-Schema инструментов, разные шаблоны тела и темы письма. Всё живёт в prompts/reasoning/<route>/tool_spec.j2, email_body.j2, email_subject.j2. Оператор может редактировать промпты без правки Python-кода.


Трёхслойная память

У агента три уровня памяти:

1. Локальный тред

Хронология текущего диалога собирается из union notmuch index’а. Стадия enrich проходит по цепочке In-Reply-To до корня ветки и формирует unified_messages — полную хронологию.

2. Глобальные факты

global_memory и thread_memory — обычные FSM-состояния. reasoning может вызвать tool call thread_memory или global_memory, записать факт, и он вернётся в ingress для следующей итерации.

3. Граф знаний (LightRAG)

Embedded LightRAG (lightrag-hku) работает как single-writer внутри threlium-engine. После каждого nm_settle() (когда письмо обработано и переехало в cur/) запускается schedule_index_pending — RAG-loop подбирает settled-сообщения, которые ещё не проиндексированы (NOT tag:unread AND NOT tag:lightrag_indexed), и вставляет их в граф через rag.ainsert(). Это не просто RAG, а граф связанных знаний.

PlantUML-исходник: LightRAG write/read
@startuml
left to right direction

package "Запись (async, после settle)" {
  rectangle "nm_settle()" as SETTLE
  rectangle "schedule_index_pending" as SCHED
  rectangle "Селектор:\nNOT tag:unread\nAND NOT tag:lightrag_indexed" as SEL
  rectangle "rag.ainsert(batch)" as INS
  rectangle "+lightrag_indexed" as TAG
  SETTLE --> SCHED
  SCHED --> SEL
  SEL --> INS
  INS --> TAG
}

package "Чтение (sync, в FSM)" {
  rectangle "enrich" as ENR
  rectangle "LLM: enrich_query_plan.j2" as PLAN
  rectangle "rag.aquery(...)" as AQ
  rectangle "Payload:\n--- user message ---\n--- lightrag context ---" as PAY
  rectangle "reasoning" as REA
  ENR --> PLAN
  PLAN --> AQ
  AQ --> PAY
  PAY --> REA
}

INS ..> AQ : "общий\nworking_dir/"
@enduml
LightRAG write/read

LightRAG write/read

Стадия enrich при формировании контекста для reasoning вызывает rag.aquery() — семантический запрос к графу. Результат вместе с хронологией треда упаковывается в тело письма для reasoning через Jinja2-шаблоны. Пока что опять же это простейшее решение несмотря на мощь GraphRAG подхода, нужно дорабатывать подходы к экономии контекста, но когда кода совсем мало это не сложно, даже не шибко умные нейронки вайбкодят при таких объемах проекта легко.

Линейных цепочек для контекста отслеживать не нужно — весь тред со всеми форками индексируется как единая база знаний (глобальные знания агента). Mutex на весь тред от ingress до ответа не нужен.


Безопасность: решение / политика / исполнение

Слой CLI построен на строгом разделении:

Стадия

Ответственность

reasoning

Формирует намерение через tool call: «хочу выполнить echo hello»

cli_intent

Политика: allow / deny / ask-human. Команды не исполняет

cli_exec

Исполнение разрешённой команды в песочнице systemd-run --scope

cli_intent использует грубый фильтр: запрещённые подстроки (;, |, $(, &&), белый список базовых команд. Осознанно жёсткий — ложные срабатывания лучше, чем пропущенная инъекция. Если команда не попала ни в allow, ни в deny — уходит на подтверждение к человеку (HITL). Это решение я пока оставил совсем простым так же как и другие, суть в использовании systemd-run в будущем, пока просто заготовка.

HITL-прерывание

PlantUML-исходник: HITL-прерывание
@startuml
participant "reasoning" as R
participant "cli_intent" as CI
participant "cli_hitl_out" as HO
participant "egress_router" as ER
actor "Пользователь" as U
participant "ingress" as I
participant "cli_resume" as CR
participant "cli_exec" as CE

R -> CI : tool call: rm -rf /tmp/data
CI -> CI : classify: не в allow, не в deny
CI -> HO : HITL: спросить пользователя
HO -> ER : письмо с вопросом
ER -> U : «Разрешить rm -rf /tmp/data?»
U -> I : «Да» (ответ через канал)
I -> I : IRT-обход → From: cli_hitl_out
I -> CR : cli_resume
CR -> CE : cli_exec (разрешено)
CE -> I : observation (результат)
@enduml
HITL-прерывание

HITL-прерывание

cli_exec запускает команду через systemd-run --scope с лимитами из X-Threlium-Capabilities текущего фрейма: MemoryMax, CPUQuota, TasksMax, timeout.


Субагенты: рекурсия через IRT-цепочку

Threlium поддерживает вложенные вызовы агентов (L0 → L1 → L2). Реализация — маркеры subagent_intent / subagent_end в IRT-дереве:

  • reasoning на L0 вызывает tool subagent_intent → маркер в IRT-цепочке с изолированным hop/cap.

  • Субагент на L1 не знает, кто его вызвал. Работает как обычный агент.

  • По завершении egress_router по depth > 0 маршрутизирует результат не наружу, а в subagent_end.

  • subagent_end находит соответствующий subagent_intent по IRT, копирует hop/cap родителя и возвращает письмо в ingress.

PlantUML-исходник: Субагенты L0/L1
@startuml
top to bottom direction

package "L0 — основной диалог" {
  rectangle "reasoning L0" as R0
  rectangle "subagent_intent\n(маркер + изолированный hop/cap)" as SI
  rectangle "ingress" as ING1
  R0 --> SI : "tool: subagent_intent"
  SI --> ING1
}

package "L1 — субагент" {
  rectangle "enrich" as ENR1
  rectangle "reasoning L1" as R1
  rectangle "egress_router\ndepth=1 > 0" as EGR1
  ING1 --> ENR1
  ENR1 --> R1
  R1 --> EGR1 : "tool: egress_router"
}

rectangle "subagent_end\nIRT-обход → subagent_intent\nкопия hop/cap родителя" as SE
rectangle "ingress L0" as ING0
rectangle "enrich L0" as ENR0
rectangle "reasoning L0\n(с результатом субагента)" as R0_2
rectangle "egress_router\ndepth=0" as EGR0
rectangle "egress_‹chan›\n→ пользователь" as OUT

EGR1 --> SE
SE --> ING0
ING0 --> ENR0
ENR0 --> R0_2
R0_2 --> EGR0 : "tool: egress_router"
EGR0 --> OUT
@enduml
Субагенты L0/L1

Субагенты L0/L1

Глубина определяется линейным обходом IRT-цепочки: каждый subagent_intent → depth+1, каждый subagent_end → depth−1. Промежуточного in-memory state нет. Пока что решение довольно линейное и предназначено как и вся идея subagent для изоляции контекста, subagent начинает работать как бы “от запроса пользвоателя”, просто пользователь это другой агент. Позже можно реализовать параллельные агенты это в целом уже придумано, сделаю когда захочется, все можно построить на форках почтового треда и стадии слияния в FSM - это ведь по сути просто “обсуждение в почте”. По сравнению с почтой тут только будет ромб в графе, но это решаемо и совсем просто.


Идентификаторы: base62 и msgspec

Внутри системы все Message-ID канонизированы в форму <base62(payload)@localhost>:

  • Email: payload = msgspec JSON EmailNativeId(v=1, message_id="<оригинальный MID>")

  • Telegram/Matrix: payload = utf8(composite_inner)

Схема обратима: base62.decodebytes + msgspec.json.decode восстанавливают исходный struct. На egress для email — полное восстановление оригинального Message-ID, In-Reply-To, References для корректной цепочки в почтовом клиенте получателя.

base62 использует алфавит [0-9A-Za-z] — строгое подмножество atext RFC 5322, так что канонический Message-ID всегда валиден. Двоеточия Matrix, слэши msgid, $ Matrix-v3 — всё безопасно уходит в base62.

Сериализация через msgspec — детерминистическая (фиксированный порядок полей, без пробелов). Никаких mapping-таблиц и state: всё выводится из самого id через обратное преобразование. Это позволяет превратить в внутреннее почтовое сообщение любое сообщение из внешнего канала. Внутри агент это просто набор папок с eml файлами на диске и все.


Промпты: Jinja2 шаблоны, редактируемые оператором

Всё, что видит LLM и пользователь, генерируется из Jinja2-шаблонов в $THRELIUM_HOME/prompts/<stage>/<purpose>.j2. Код вызывает только render_prompt(name, **vars). Шаблоны деплоятся Ansible из roles/threlium/files/prompts/.

Это касается не только «обычных» промптов, но и:

  • Overlay внутренних промптов LightRAG — 12 файлов, копии lightrag.prompt.PROMPTS для текущей версии lightrag-hku.

  • addon_params для LightRAG — language, entity_types — JSON из Jinja2.

  • Per-route tool-specs для reasoning — 6 маршрутов × 3 файла = 18 артефактов.

Смена поведения агента — правка шаблона и systemctl --user restart threlium-engine.service. Без коммита в Python. Но можно и скрипты свободно править конечно, это не компилируемый язык и это сознательное решение, так как сам агент можнт это делать, а так как ресурсов он ест мало, то на той же машине можно завести второго и когда один агент доломал себя, второго попросить просто откатить git репозиторий в котором живет почивший. Никаких компиляций сложной отладки и прочего.


Развёртывание: Ansible push-модель

Threlium не клонируется git clone-ом на целевой хост. Развёртывание — push-модель: ansible-playbook ansible/playbooks/site.yml с control node заливает всё на target.

Что попадает на target

  • Python-пакет threlium (editable install в единый .venv).

  • Конфигурации: threlium.yaml, env/*.env, ~/.fdm.conf, ~/.msmtprc.

  • systemd-юниты (симлинки в ~/.config/systemd/user/).

  • Промпты (Jinja2-шаблоны).

  • Dispatch-скрипт и вспомогательные утилиты.

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

После первого деплоя Ansible свою роль заканчивает. Дальше хост живёт автономно: правки коммитятся в локальный git в threlium_repo_path, применяются оператором или самим агентом (через cli_exec, если capability-профиль разрешает). Повторный прогон Ansible — disaster-recovery.

Конфигурация LLM

Конфигурация LLM (endpoints, модели, таймауты) живёт в threlium.yaml, который генерируется из структурных Ansible-переменных. Два слота для одной модели с разными параметрами? Пожалуйста:

llm_endpoints:
  - model: "openai/qwen3-35b"
    api_base: "http://vllm-host:8000/v1"
    score: 0.0
    chat_template_kwargs:
      enable_thinking: false
  - model: "openai/qwen3-35b"
    api_base: "http://vllm-host:8000/v1"
    score: 1.0
    chat_template_kwargs:
      enable_thinking: true

Маршрутизация вызовов — по LitellmRoutingSite (reasoning, enrich_plan, lightrag_llm и т.д.), каждый site может ехать на свой endpoint с разным score. Это все проработано пока скорее как концепт конечно, но уже работает, дорогие вызовы делает reasoning, а дешевые делает GraphRAG.


Тестирование: e2e через Docker и WireMock

Единственный автоматизированный pytest-gate — e2e в tests/e2e/. Никаких unit-тестов отдельно, просто они для вайбкоженного проекта все равно бесполезны, было 400 юнит-тестов и они просто проверяли, что проект верно не работает. Поведение системы эмерджентно: связка fdm + notmuch + systemd + FSM + LLM — её невозможно адекватно замокать по частям.

Тестовый стек

PlantUML-исходник: Тестовый стек
@startuml
top to bottom direction

rectangle "pytest\n(control node)" as PY

package "Docker Compose" {
  rectangle "sut\nUbuntu 24.04 + полный site.yml\nprivileged, cgroup host\nживой threlium-engine" as SUT
  rectangle "greenmail\nSMTP :3025\nIMAP :3143\nIMAPS :3993" as GM
  rectangle "wiremock\nOpenAI + Matrix mock\nState Extension\nhost :9080 → :8080" as WM
}

PY --> SUT : "compose up/down"
PY --> SUT : "docker exec"
PY --> WM : "Admin API"
SUT --> GM : "docker DNS: greenmail"
SUT --> WM : "docker DNS: wiremock\nthrelium_openai_api_base"
@enduml
Тестовый стек

Тестовый стек

Стратегия baked-образа SUT: один раз прогоняется полный site.yml на голом Ubuntu → docker committhrelium/e2e-sut:baked. Дальше тесты стартуют мгновенно из baked-образа.

WireMock с State Extension обеспечивает изоляцию параллельных сценариев: контекст State привязан к X-Threlium-Route конкретного теста. Десять xdist-воркеров бьют в один SUT параллельно — каждый со своим notmuch-тредом и своим контекстом в WireMock. Работает с переменным успехом, но мне хватает.

L0 happy-path

PlantUML-исходник: L0 happy-path
@startuml
left to right direction

rectangle "SMTP inject" as SMTP
rectangle "GreenMail\nINBOX" as GM1
rectangle "IMAP bridge\n(IDLE → fetch)" as IMAP
rectangle "ingress" as ING
rectangle "enrich\n(LightRAG aquery)" as ENR
rectangle "reasoning\n(WireMock: tool call)" as REA
rectangle "egress_router" as EGR
rectangle "egress_email\n(msmtp)" as EE
rectangle "GreenMail\nINBOX pytest@" as GM2
rectangle "pytest\nassert In-Reply-To" as ASSERT

SMTP --> GM1
GM1 --> IMAP
IMAP --> ING
ING --> ENR
ENR --> REA
REA --> EGR
EGR --> EE
EE --> GM2
GM2 --> ASSERT
@enduml
L0 happy-path

L0 happy-path


Отказоустойчивость

  • Крэш стадии: файл остаётся в new/+unread. Restart=on-failure повторяет попытку. Sweep (после успеха) перепроверяет backlog.

  • Крэш между rename(2) и Xapian-commit: файл уже в cur/, но notmuch думает, что он в new/. settle_recovery_for_stage() на старте воркера лечит через from_maildir_flags().

  • Крэш моста: sys.exit(1) + Restart=on-failure. Курсор восстанавливается из notmuch.

  • Крэш движка: все submit’ы получают BindsTo на threlium-engine.service. При рестарте движка воркеры перезапускаются автоматически.

  • LightRAG drain прервался: тег +lightrag_indexed не поставлен → следующий drain повторит ainsert. LightRAG dedup гарантирует безопасность.

Отдельной стадии errors/ и error-mail нет. Сбои — structured log в journald и ненулевой exit code. Просто и предсказуемо, а весь процесс “мышления” видно просто в почте.


Админка: Cockpit + Caddy + Roundcube + Dovecot

Раз каждое событие — письмо, логично дать оператору смотреть «мысли» агента через обычный почтовый веб-интерфейс. Для этого на target поднимается стек из четырёх компонентов, которые вместе превращаются в полноценную админ-панель.

Dovecot: IMAP поверх Maildir’ов стадий

Dovecot подключается к тем же Maildir’ам, что и FSM. Единственный конфиг — drop-in 99-threlium-webmail.conf:

namespace inbox {
  inbox = yes
  location = maildir:$THRELIUM_HOME/stages:LAYOUT=fs:DIRNAME=Maildir
}

Каждая стадия FSM (ingress, enrich, reasoning, …) становится IMAP-папкой. Дополнительно настроен virtual namespace — виртуальная папка «All», которая собирает письма из всех стадий в единую ленту. Авторизация — через PAM (тот же POSIX-пользователь, что и агент).

ACL выставлены в режим read-only: lr (list + read) — можно просматривать, но не менять флаги, не удалять, не вставлять. Maildir пишут только Threlium и notmuch, Dovecot — чисто на чтение.

Roundcube: веб-интерфейс для почты агента

Roundcube подключается к локальному Dovecot (localhost:143, plaintext — всё на loopback). SMTP отключён — это read-only интерфейс. Из коробки настроен:

  • Режим threads с сортировкой по дате — цепочки рассуждений видны как нити писем.

  • Виртуальная папка Virtual/All подключена как архив и в default_folders.

  • mail_read_time = -1 — Roundcube не ставит флаг «прочитано» при открытии (в паре с ACL Dovecot).

  • SQLite для хранения сессий — никакого MySQL/PostgreSQL.

Caddy: единый edge-proxy

Caddy работает как точка входа, объединяя Cockpit и Roundcube на одном порту:

:8080 {
    handle_path /webmail/* {
        root * /usr/share/roundcube
        php_fastcgi unix//run/php/php-fpm.sock
        file_server
    }
    route * {
        reverse_proxy 127.0.0.1:9090 {
            transport http { tls_insecure_skip_verify }
        }
    }
}

Маршрутизация простая: /webmail/* → Roundcube через PHP-FPM, всё остальное → Cockpit на :9090. TLS — tls internal (self-signed) для прода, отключён в e2e. Порт настраивается через threlium_mail_archive_caddy_bind_port.

Cockpit: системная админ-панель с Roundcube внутри

Cockpit даёт из коробки: терминал, просмотр journald-логов (а значит всех логов агента), управление systemd-юнитами (можно рестартовать стадии, мосты, engine), мониторинг ресурсов, файловый менеджер (если доступен cockpit-files из backports).

В Cockpit регистрируется кастомный пакет threlium-mail-archive — это manifest.json + index.html с iframe на /webmail/. В итоге Roundcube появляется прямо как вкладка в Cockpit. Оператор видит в одном окне: системные метрики, логи, юниты и полную переписку агента. Это все вовсе без сложной инфры, оно еще и не ест ресурсов почти.

Cockpit слушает только на 127.0.0.1:9090 — наружу не торчит. Origin-проверка (защита от CSWSH) настраивается через threlium_mail_archive_cockpit_origins_extra в host_vars. Специальный oneshot-юнит threlium-cockpit-tls-clean.service чистит /run/cockpit/tls перед стартом Cockpit — workaround для известного бага с cockpit-certificate-ensure.

Как это выглядит вместе

PlantUML-исходник: Админка: стек Cockpit/Caddy/Roundcube/Dovecot
@startuml
left to right direction

actor "Оператор" as OP

package "Target host" {
  package "Caddy :8080" as CADDY {
    rectangle "/webmail/*" as WM
    rectangle "/* (всё остальное)" as ROOT
  }

  rectangle "Roundcube\nPHP-FPM" as RC
  rectangle "Cockpit :9090\n(loopback only)" as CP
  rectangle "Dovecot :143\n(loopback, PAM)" as DOV

  WM --> RC
  ROOT --> CP

  RC --> DOV : "IMAP localhost:143"

  package "$THRELIUM_HOME/stages/" {
    rectangle "ingress/Maildir" as MD1
    rectangle "enrich/Maildir" as MD2
    rectangle "reasoning/Maildir" as MD3
    rectangle "…/Maildir" as MDN
  }

  DOV --> MD1 : "read-only\nACL: lr"

  rectangle "Cockpit package\nthrelium-mail-archive\niframe → /webmail/" as PKG
  CP -- PKG
  PKG ..> WM : "iframe src"
}

OP --> CADDY : "браузер"
@enduml
Админка: стек Cockpit/Caddy/Roundcube/Dovecot

Админка: стек Cockpit/Caddy/Roundcube/Dovecot

Весь стек включается одной переменной threlium_mail_archive_web_enabled: true (по умолчанию включён) и деплоится только при полном прогоне (--tags deploy). При --tags refresh веб-стек не трогается.


Ansible playbook: структура и режимы работы

Я уже упоминал push-модель развёртывания, но стоит рассказать подробнее о самом плейбуке — он устроен осознанно непохоже на типичный Ansible-проект.

Один плейбук, одна роль

Весь деплой — единственный файл ansible/playbooks/site.yml. Задачи живут прямо в нём, а не в roles/threlium/tasks/. Почему? Файл короткий, читается как последовательный сценарий, а перенос в роль дал бы include_role с тем же числом строк и сломал бы относительные пути. Роль threlium используется только для хранения переменных, шаблонов, файлов и дефолтов.

ansible/
  playbooks/
    site.yml                    # единственный сценарий
    tasks/
      refresh.yml               # узкий тег: чистка + рестарт
      mail_archive_web.yml      # веб-стек (Cockpit/Caddy/…)
      mail_archive_web_acceptance.yml
      ssh_hardening.yml
  roles/threlium/
    defaults/main.yml           # дефолтные переменные
    vars/main.yml               # канон FSM-стадий
    files/
      scripts/                  # Python-код FSM + bash-скрипты
      prompts/                  # Jinja2-промпты для LLM
      mail-archive/             # статика: dovecot-virtual
    templates/
      config/                   # fdm.conf, msmtprc, threlium.yaml
      systemd/user/             # шаблоны unit-файлов
      mail-archive/             # Caddyfile, cockpit.conf, …
      env/threlium.env.j2
      pyproject.toml.j2
  host_vars/                    # per-host: LLM endpoints, секреты
  group_vars/                   # общие переменные и e2e-оверрайды
  inventory/                    # hosts (прод и e2e)

Фазы деплоя

Каждая фаза — предусловие для следующей:

#

Фаза

Что делает

1

Assert

Проваливает прогон до изменений при пустых обязательных переменных

2

Bootstrap ОС

apt: fdm, msmtp, notmuch, python3, python3-venv

3

Каталог артефактов

threlium_repo_path/ + идемпотентный git init

4

Раскладка $THRELIUM_HOME

Стадийные Maildir’ы из vars/main.yml

5

Код FSM

copy Python-пакета + dispatch-скрипт

6

Конфиги

template: fdm.conf, msmtprc, threlium.yaml, threlium.env

7

Unit-файлы

Шаблоны systemd: engine, work@, sweep@, bridge@

8

Симлинки

~/.fdm.conf, ~/.msmtprc, все юниты в ~/.config/systemd/user/

9

Venv + pip

pyproject.toml.j2 → target, pip install .

10

linger + start

loginctl enable-linger + daemon-reload + state: started

11

Веб-стек

Cockpit + Caddy + Roundcube + Dovecot (если включён)

12

Acceptance

Сквозная самопроверка: Maildir’ы, юниты, notmuch, fdm.conf, Python

13

Bundle

tar.gz снимок установки → fetch на control node

Два закона идемпотентности

Плейбук разделяет два класса операций:

Класс A — внешние зависимости (apt, pip): state: present — «install if missing». Стандартная идемпотентность Ansible. Не обновляет уже установленное.

Класс B — артефакты Threlium (код, конфиги, юниты, симлинки): перетирание каждый прогон. Файл на target сравнивается с репо и перезаписывается при расхождении. Никаких creates: или маркерных файлов — через baked-образ они превращаются в зашитый T₀.

Исключения из класса B: локальный .git (не стирать историю оператора) и физическая раскладка durable Maildir’ов (не пересоздавать event store с данными).

Два режима: deploy и refresh

PlantUML-исходник: Deploy vs Refresh
@startuml
top to bottom direction

package "--tags deploy (полный прогон)" {
  rectangle "apt: fdm, notmuch,\npython3, cockpit, caddy…" as D1
  rectangle "Каталоги + Maildir'ы" as D2
  rectangle "copy: Python-код FSM" as D3
  rectangle "template: конфиги,\nunit-файлы, env" as D4
  rectangle "pip install + venv" as D5
  rectangle "Веб-стек\n(Cockpit/Caddy/…)" as D6
  rectangle "Acceptance" as D7
  rectangle "Bundle" as D8
  D1 --> D2
  D2 --> D3
  D3 --> D4
  D4 --> D5
  D5 --> D6
  D6 --> D7
  D7 --> D8
}

package "--tags refresh (узкий прогон)" {
  rectangle "Остановка engine + мостов" as R1
  rectangle "Синхронизация:\nscripts/, env,\nшаблоны конфигов/юнитов" as R2
  rectangle "daemon-reload" as R3
  rectangle "Чистка Maildir/notmuch/\nLightRAG" as R4
  rectangle "Рестарт user-units" as R5
  R1 --> R2
  R2 --> R3
  R3 --> R4
  R4 --> R5
}
@enduml
Deploy vs Refresh

Deploy vs Refresh

deploy — полный bootstrap: apt, venv, pip, веб-стек, acceptance, bundle. Используется для нового хоста или disaster-recovery.

refresh — узкий прогон: синхронизация кода и конфигов с control node + сброс Maildir/notmuch/LightRAG + рестарт user-units. Без apt, без pip, без веб-стека. Основной режим для e2e-тестов: baked-образ SUT переиспользуется, refresh накатывает актуальные артефакты идемпотентно.

Разметка тегов — три контракта:

Разметка

Полный прогон

--tags refresh

deploy только

да

нет

deploy + refresh

да

да

never + refresh

нет

да

После bootstrap: автономная эволюция

Ключевое отличие от типичных Ansible-проектов: плейбук — не governor хоста. После bootstrap ответственность переходит локальному git в threlium_repo_path.

  • Оператор правит скрипт/конфиг прямо на target → daemon-reloadgit commit. Симлинки сразу видят новое.

  • Агент — через cli_exec в рамках capability-профиля. Может менять свои промпты, конфиги, даже Python-код, коммитя изменения в локальный git.

  • Обратной синхронизации target → control нет и не предполагается. Каждая установка эволюционирует независимо.

Повторный полный ansible-playbook site.yml на живом хосте — только disaster-recovery. Он перетрёт локальные коммиты. Для штатного обновления кода — локальные правки или refresh.

Канон стадий — одна точка правды

Все FSM-стадии определены в единственном месте: roles/threlium/vars/main.yml (threlium_fsm_mailbox_stages). Все задачи плейбука — циклы по этому списку. Добавить стадию = добавить строчку. Рассинхронизация между Maildir’ами, fdm.conf и systemd-юнитами невозможна по конструкции.


Что имеем в итоге

Threlium — самохостный AI-агент, построенный из Unix-примитивов:

Компонент

Реализация

Хранилище событий

Maildir (файлы на диске)

Индекс

notmuch (Xapian)

Очередь

Maildir new/cur/

Оркестрация

systemd --user

Маршрутизация

fdm (~/.fdm.conf)

Рассуждение

litellm + tool calls

Память

LightRAG (NanoVectorDB + NetworkX)

Каналы

IMAP IDLE, Telegram Bot API, Matrix (nio)

Промпты

Jinja2 шаблоны

Развёртывание

Ansible push-модель

Конфигурация

pydantic-settings + YAML (threlium.yaml)

Тестирование

pytest e2e + Docker + WireMock + GreenMail

Безопасность CLI

cli_intent (политика) → cli_exec (песочница)

Вся система — один Python-пакет с единым venv, один systemd --user manager, один notmuch union-индекс. Никаких Docker-compose’ов в продакшене, никаких баз данных, никаких внешних брокеров. Файлы на диске, процессы в systemd, промпты в Jinja2.

Работает ли это? Работает. Я пишу агенту письмо — он думает, обогащает контекст из графа знаний, рассуждает, при необходимости выполняет команды (с подтверждением или без), и отвечает. Telegram и Matrix — пока не проверял :) Все каналы симметричны, история хранится вечно, контекст глобален.

P.S. Рекомендую LLM для консультаций при настройке. Особенно когда дебажишь, почему notmuch insert повесил +unread, а dispatch-скрипт не поднял воркер. Оказалось — опечатка в folder: термине. Эта конструкция домашнего агента совершенно прозрачна для отладки и модификации другими агентами! :)

Опубликован тут