Агент работал уже шесть минут. Спиннер всё ещё крутился. Когда я его убил, /info показал счёт в $4,18, а /report насчитал восемьдесят девять вызовов инструментов на задаче, которую я оценил в три вызова и пару центов. Он прочитал один и тот же файл одиннадцать раз. Он искал символ через grep, получал пустой результат, переформулировал grep, получал ещё один пустой результат — и продолжал это делать, вежливо, уверенно, дорого, потому что ничто в цикле не говорило ему остановиться.
Хуже всего были не четыре доллара. Хуже было то, что я понятия не имел, почему, пока не пошёл разбираться. Агент не выбрасывает стектрейс, когда идёт вразнос. Он не падает. Он просто тихо делает не то, рапортует об успехе и выставляет счёт. Снаружи блестящий прогон и катастрофический выглядят одинаково: текст летит мимо, инструменты срабатывают, в конце ответ.
Эта статья — тот инструментарий, который мне хотелось бы подключить до того прогона, а не после. Всё это есть в Octomind, нашем рантайме агентов на Rust с открытым исходным кодом, а в последнем разделе перед ним встаёт OctoHub — чтобы перехватить единственное, что сам агент показать не может: сырые байты, которые на самом деле ушли в модель.
Почему агенты падают молча
Традиционный софт падает громко. Null deref, 500-я, проваленный assert — у сбоя есть форма, место, номер строки. У сбоев агентов ничего этого нет, потому что с точки зрения рантайма не сломалось ничего. Каждый вызов API вернул 200. Каждый инструмент вышел с кодом 0. Модель на каждом шаге выдавала грамматически верный и правдоподобный текст. Совокупное поведение было неверным, но ни одна отдельная операция — нет.
На практике сбои группируются в четыре формы, и каждая невидима ровно в тот момент, когда вы хотели бы её поймать:
- Не тот инструмент, та же уверенность. Модель тянется к
grep, когда надо было прочитать файл, или бьёт в обобщённый shell, когда рядом есть точный инструмент. Она не колеблется — неверный выбор инструмента снаружи выглядит точно так же, как верный. (Мы написали целую статью о сужении набора инструментов ради борьбы с этим: кастомные MCP должны жить в вашем репозитории.) - Усечение контекста. Диалог вырос за пределы окна, сработало сжатие, и факт, нужный агенту, был ужат и потерян. Теперь он рассуждает по памяти с потерями, а шва вы не видите.
- Разогнавшиеся циклы. Пустой результат → переформулировать → пустой результат → переформулировать. Каждый ход по отдельности разумен. Багом является сам паттерн, и замечаете вы его только по числу вызовов инструментов.
- Взрывы токенов. Инструмент сбросил в контекст файл на 200 КБ, или один аргумент вызова раздулся — и теперь каждый последующий ход пересылает всё это заново. Стоимость становится сверхлинейной, и единственный симптом — счёт.
Нельзя отлаживать то, чего не видишь. Поэтому первая задача — сделать прогон наблюдаемым, и постфактум, и во время.
Уровень 0: /info — куда ушли деньги
Самый быстрый вопрос для ответа — «сколько эта сессия реально стоила и какой формы». Внутри любой интерактивной сессии /info:
╭ /info
│ session my-feature-x
│ model openrouter:anthropic/claude-sonnet-5
│ tokens 214,883 total
│ breakdown 18,402 in · 9,114 out · 184,201 cache rd · 2,890 cache wr · 276 reasoning
│ cost $4.18661
│ throughput 41.3 tok/s
╰ /info my-feature-x
Каждое поле здесь — диагностика. Тем, что вскрыло мой случай с разогнавшимся циклом, была строка breakdown. 184,201 cache rd против 18,402 in означает, что один и тот же контекст перечитывался почти на каждом ходу — подпись цикла, который всё дописывает и никогда не разрешается. Число вызовов инструментов, которое это подтвердило, живёт уровнем ниже — в /report.
/info также раскладывает траты за пределами основного цикла — собственные токены и стоимость модели сжатия, расстановка кэш-маркеров с итогами чтения/записи, любые субагенты и супервизор получают каждый свою секцию — так что при высоком счёте видно, съела ли его основная беседа или фоновый процесс.
Если сессия хоть раз сжималась, /info показывает блок compression: сколько сжатий сработало, сколько сообщений удалено, сколько токенов сэкономлено, средний коэффициент. Это ваше раннее предупреждение об усечении. Если вы видите три сжатия уровня проекта в короткой сессии, агент что-то забывал — и об этом стоит знать прежде, чем доверять его выводам.
Уровень 1: /report — стоимость по запросу, а не по сессии
/info — это итог сессии. /report — это детализированный чек: одна строка на каждый запрос пользователя, реконструированный из лога сессии:
╭ /report
│ # request cost tools task ai proc
│ ── ─────────────────────────────── ──────── ───── ─────── ─────── ───────
│ 1 add a health-check endpoint $0.04120 3 18s 12s 1s
│ 2 wire it into the router $0.02980 2 11s 9s 1s
│ 3 why is the test flaky $3.98120 84 5m 31s 3m 40s 20s
│ ── ─────────────────────────────── ──────── ───── ─────── ─────── ───────
│ Σ 3 request(s) $4.05220 89 6m 00s 4m 01s 22s
╰ /report 3 request(s) · $4.05220
Вот оно. Запрос 3 — «why is the test flaky» — это весь счёт. Восемьдесят четыре вызова инструментов на один вопрос. Два благополучных запроса обрамляют одну катастрофу, и без разбивки по запросам все они сливаются в общий итог сессии. /report — это как вы находите, какой именно промпт отправил агента под откос; а это и есть тот вопрос, на который надо ответить прежде, чем что-то чинить.
Колонки разделяют время task (весь запрос, от начала до конца) и время ai (только вызовы модели). Когда task сильно превышает ai — ваши инструменты медленные. Когда они идут вровень — модель делает много round-trip'ов, снова подпись цикла.
Уровень 2: /context — прочтите, о чём агент на самом деле думает
Стоимость и счётчики говорят, что что-то пошло не так. Чтобы увидеть что именно, читаешь сообщения. /context выгружает живой диалог как структурированный JSON, с фильтрами:
/context # всё
/context large # только сообщения длиннее 1000 символов — найди пожирателя контекста
/context tool # только результаты инструментов — посмотри, что агент реально получил
/context assistant # только ходы самой модели
/context large — то, к чему я тянусь при взрывах токенов. Он выносит на поверхность ровно те сообщения, что раздувают окно — обычно один результат инструмента, вернувший гигантский файл или огромный JSON-блоб, — и теперь вы знаете, какой инструмент научить пагинации или усечению. В моей катастрофе с flaky-тестом /context tool показал восемьдесят с лишним пустых результатов grep. Модель ни разу не сменила стратегию, потому что ничто не говорило ей, что стратегия не работает. Чтение результатов инструментов сделало цикл очевидным секунд за пятнадцать.
Уровень 3: лог сессии — полная, воспроизводимая запись
Всё вышеперечисленное читает из одного артефакта: файла сессии. Octomind пишет каждую сессию в
~/.local/share/octomind/sessions/<name>.jsonl.zst
Это сжатый zstd JSONL — по одному JSON-объекту на строку. Большинство строк — это сырые сообщения диалога (role: user|assistant|tool|system). Вперемешку идут типизированные маркеры, нужные рантайму для реконструкции состояния при возобновлении:
| Маркер | Что записывает |
|---|---|
STATS |
Текущие итоги — стоимость, время API, время инструментов — на этой точке |
COMPRESSION_POINT |
Сработало сжатие: тип, удалённые сообщения, сэкономленные токены |
RESTORATION_POINT |
Чекпоинт /done — прежние сообщения схлопываются при перезагрузке |
KNOWLEDGE_ENTRY |
Факт, извлечённый при сжатии, переинъецируемый при возобновлении |
COMMAND |
Команда рантайма (/model, /role, /effort…), воспроизводимая при возобновлении |
PLAN_SNAPSHOT / SCHEDULE_SNAPSHOT |
Активный план / расписание, чтобы пережить перезапуск |
Этот файл — истина в первой инстанции. /report буквально генерируется распаковкой этого файла и проходом по записям STATS и USER/COMMAND, чтобы раскидать стоимость между запросами. То же самое можно сделать самому:
zstd -dc ~/.local/share/octomind/sessions/my-feature-x.jsonl.zst \
| jq -r 'select(.role == "tool") | "\(.content | length)\t\(.name)"' \
| sort -n | tail
Этот one-liner ранжирует результаты инструментов по размеру прямо из лога — ваши пожиратели токенов, даже не открывая сессию. Поскольку лог — это append-only JSONL, он ещё и идеальный артефакт для replay: возобновите ровно ту же сессию через octomind run --resume my-feature-x, или возьмите самую свежую для текущего каталога через --resume-recent, и рантайм восстановит состояние из этих самых строк.
Уровень 4: RUST_LOG — когда нужно заглянуть внутрь рантайма
Лог сессии показывает, каким был диалог. Когда нужно увидеть, что сделал рантайм — почему не загрузился MCP-сервер, почему был пропущен инструмент, что на самом деле вернул провайдер — включайте трассировку. Octomind построен на крейте tracing, за стандартной переменной окружения RUST_LOG. В CLI-режиме подписчик трассировки намеренно не создаётся, пока вы его не попросите — пользователи получают чистый цветной вывод, а не пожарный шланг. Поставьте RUST_LOG — и шланг открывается:
# Всё на уровне debug
RUST_LOG=debug octomind run
# Сузить до одного модуля — на практике куда полезнее
RUST_LOG=octomind::mcp=debug octomind run
# Несколько областей, смешанные уровни
RUST_LOG=octomind::session=debug,octomind::mcp=trace octomind run
Область важна. RUST_LOG=debug на загруженной сессии нечитаем; RUST_LOG=octomind::mcp=debug, когда инструмент не появляется, точно скажет вам, какой кандидат был принят или отвергнут и почему. Это тот уровень, на котором «агент не видит мой инструмент» перестаёт быть загадкой.
В режимах структурированного вывода — ACP и WebSocket — stdout и stderr зарезервированы под протокол, поэтому трассировка идёт в файлы:
~/.local/share/octomind/logs/acp-debug.log ← трассировка ACP
~/.local/share/octomind/logs/acp-errors.jsonl ← ошибки ACP, структурированно
~/.local/share/octomind/logs/websocket-debug.log ← трассировка WebSocket
Если вы запускаете Octomind за редактором по ACP и что-то идёт не так — улики живут в этих файлах.
Уровень 5: --format jsonl — направьте прогон в свой собственный тулинг
Всё предыдущее — для человека, читающего терминал. Как только агент попадает в CI или пайплайн, прогон нужен как поток структурированных событий, по которым можно делать проверки. octomind run --format jsonl выдаёт по одному JSON-объекту на строку — тот же внутренний поток событий, что использует WebSocket-сервер, — помеченный по type:
echo "audit the auth module" | octomind run developer:general --format jsonl
Каждая строка — отдельное событие. Варианты, которые вам важны:
type |
Несёт |
|---|---|
assistant |
Фрагмент ответного текста модели |
thinking |
Содержимое рассуждений, отдельно от ответа |
tool_use |
tool, tool_id, server, params — агент собирается действовать |
tool_result |
tool, content, success — что вернулось |
cost |
session_tokens, session_cost, input_tokens, output_tokens, cache_read_tokens, cache_write_tokens, reasoning_tokens |
error |
Сообщение о сбое |
injected |
Не-пользовательский ход — запланированный таймер, фоновый агент, skill — со своим source_kind |
skill |
Skill активирован, использован или забыт |
Теперь режимы сбоя становятся проверками. Считайте события tool_use и роняйте сборку, если один запрос превышает порог — это ваша растяжка против разогнавшихся циклов. Следите за session_cost в событии cost и алертите при превышении бюджета. Фильтруйте tool_result по success: false. Агент, который раньше падал молча, теперь падает так, что это ловит jq:
echo "run the migration check" \
| octomind run --format jsonl \
| jq -c 'select(.type == "tool_use") | .tool' \
| sort | uniq -c | sort -rn
Это печатает гистограмму вызовов инструментов за весь прогон. Если shell наверху со счётом 60, вы нашли свой цикл прежде, чем он нашёл ваш кошелёк — и это однострочник в шаге CI, а не человек, смотрящий на спиннер.
Уровень 6: OctoHub — перехватите каждый upstream-запрос
Есть одна вещь, которую ничто из вышеперечисленного показать не может, потому что она происходит ниже агента: точные байты, которые Octomind отправил провайдеру, и точные байты, что вернулись. Взгляд агента — это его собственные сообщения. Он не может показать вам запрос на уровне провода — разрешённую модель, полный сериализованный payload, сырой ответ провайдера, реальную задержку. Когда вы подозреваете, что проблема в слое трансляции (системный промпт не тот, что вы думаете; схема инструмента, которую провайдер исковеркал; модель не та, что вы настроили), нужно увидеть провод.
OctoHub — это наш прокси для LLM, и полное логирование запросов/ответов — смысл его существования. Направьте Octomind на него вместо провайдера, и каждый completion приземлится в базе данных с входом, выходом и метриками. Запустите его:
./octohub # по умолчанию слушает 127.0.0.1:8080
Он говорит и POST /v1/completions (родная форма), и POST /v1/chat/completions (классический OpenAI, drop-in для любого OpenAI-совместимого клиента), и оба бьют в один движок и пишут в одну таблицу completions с одним и тем же id записи. После прогона вытащите сырые записи через admin-API:
curl "http://127.0.0.1:8080/v1/admin/completions?limit=50" \
-H "Authorization: Bearer <master-key>"
Каждая запись несёт ту полную картину, что агент дать не мог:
{
"id": "cmpl_<uuid>",
"session_id": "<uuid>",
"input_model": "my-model",
"resolved_model": "gpt-5.5",
"provider": "openai",
"usage": {
"input_tokens": 10,
"output_tokens": 5,
"total_tokens": 15,
"cost": 0.0001,
"request_time_ms": 320
},
"input": [...],
"output": [...],
"created_at": 1700000000
}
input_model против resolved_model сам по себе ловит целый класс багов «почему оно ведёт себя иначе» — вы запросили одну модель, алиас разрешился в другую. Массивы input и output — это буквальные payload'ы, так что схема инструмента, которой подавился провайдер, прямо тут, читай. request_time_ms — реальная задержка провайдера, отдельно от всего, что добавил Octomind. А GET /v1/admin/usage агрегирует всё по API-ключу и временно́му интервалу — так вы переходите от «агент дорогой» к «этот ключ, эта модель, этот час» без гаданий. (Если вы гоняете агентов сразу по нескольким провайдерам, прокси — ещё и то место, где это становится вменяемым — см. один агент на множестве моделей.)
Прокси превращает границу модели из непрозрачного края в логируемую, запрашиваемую поверхность. Хранилище по умолчанию — SQLite (db_url = "sqlite://octohub.db", или поставьте OCTOHUB_DB_URL), так что никакой инфраструктуры поднимать не надо, прежде чем начать читать запросы.
Когда агент делает X — смотрите в Y
Весь смысл — превратить «агент сделал нечто непонятное» в известный поиск. Вот таблица, приклеенная над моим столом:
| Симптом | Сначала смотрите | Что ищете |
|---|---|---|
| Стоимость сессии из ниоткуда | /info |
cache rd ≫ in в строке breakdown |
| Один промпт стоил всего | /report |
Единственная строка, несущая счёт |
| Окно контекста заполнено / модель «забыла» | блок compression в /info, затем /context large |
Число сжатий и негабаритное сообщение |
| Цикл / повторяющиеся вызовы инструментов | колонка tools в /report, или jsonl + uniq -c |
Тот же инструмент, те же аргументы, без прогресса |
| Инструмент недоступен агенту | RUST_LOG=octomind::mcp=debug |
Почему кандидат был пропущен |
| Результат инструмента выглядит неверно | /context tool |
Реальные байты, которые получила модель |
| Ведёт себя как другая модель | OctoHub GET /v1/admin/completions |
input_model против resolved_model |
| Ошибка провайдера или странная задержка | input/output записи OctoHub, request_time_ms |
Сырой payload и реальный тайминг |
| Нужна растяжка в CI | --format jsonl |
Считать tool_use, следить за cost, ловить error |
| Воспроизвести точный прогон | octomind run --resume <name> |
Replay из <name>.jsonl.zst |
Двухминутная диагностика
Вот как прошла бы катастрофа с flaky-тестом, будь это подключено с самого начала, вместо шестиминутной загадки, которой она была на самом деле.
Спиннер крутится долго. /report — запрос 3 стоит $3,98 и 84 вызова инструментов; другие два в порядке. Значит, дело в этом промпте. /context tool — восемьдесят пустых результатов grep, модель ни разу не сменила стратегию. Вот цикл, и вот почему. Полное время до корневой причины: секунд девяносто, без четырёх долларов, потому что в настоящем пайплайне гистограмма вызовов инструментов из jsonl сработала бы по порогу и убила прогон на двадцатом вызове.
Ничего из этого не экзотика. Это тот же инстинкт, что логи, метрики и трассировка в любой системе, которую вы выкатываете в прод, — применённый к системе, чьи сбои молчаливы по природе. Агент не скажет вам, что он потерялся. Но прогон полностью наблюдаем, если спросить его правильно: /info для формы, /report для виновника, /context для рассуждений, лог сессии для записи, RUST_LOG для рантайма, --format jsonl для машины и OctoHub для провода.
Подключите это до дорогого прогона, а не после.
— Don
Octomind и OctoHub — открытый исходный код под Apache-2.0. Если нужной вам поверхности для отладки не хватает, откройте issue — наблюдаемость из этой статьи во многом существует потому, что наши собственные агенты не переставали нас удивлять.



