Асинхронные операции агента (background run_command)
Долгие команды (yarn install, docker build, большой тест-сьют) больше не
связывают агенту руки: команда уходит в фон, агент продолжает работать, а
результат приходит автоматически — как дополнение в разговор.
#Как это работает
#Запуск: run_command с background: true
{"command": ["bash", "-lc", "yarn install"], "background": true, "timeout_sec": 900}
Модель мгновенно получает ответ вида:
Started in background as operation <operation_id>. Do not wait idle: continue
with other work, and the result (exit code + output tail) will arrive
automatically as a follow-up message. The operation times out after 900 s.
Use kill_operation to cancel it.
Команда исполняется в том же run-контейнере песочницы, вывод стримится в
живую консоль операции (в трейсе строка run_command остаётся открытой — с
шейдером текущего вывода, как у обычной команды во время исполнения).
#Ожидание и уведомление
Когда модель завершает ход без вызовов тулов, а в фоне что-то ещё работает, ран не завершается: workflow ждёт первое разрешение фоновой операции (завершение, падение, таймаут) или заметку человека. Разрешившиеся операции дренируются пачкой, и в разговор вставляется один батч-отчёт:
[Background operations report]
- <operation_id> finished: exit_code=0 (4m12s)
output tail:
exit_code=0
added 1284 packages in 42s
Still running in the background:
- <operation_id> `docker build -t app .` (running 2m10s, timeout 900s)
Incorporate these results and continue.
Модель получает новый ход, обрабатывает результат и продолжает. Цикл повторяется, пока фон не опустеет и модель не даст финальный ответ.
Если модель уже закончила работу, а фон работает — ран живёт, пока операции
не завершатся или не упрутся в свой timeout_sec; исходы журналируются
(tool.completed с background: true), но лишних ходов модель не тратит
(форс-финал ждёт фоновые операции перед завершением рана).
#Отмена: kill_operation
{"operation_id": "<operation_id>"}
Убивает дерево процессов команды внутри контейнера (TERM, пауза, KILL).
Идентичность процесса проверяется по маркеру __TEMPORALITY_OP в его
окружении — переиспользованный pid не может привести к убийству постороннего
процесса. Финальное состояние отменённой операции приходит обычным
уведомлением (exit_code=137 или 143).
#Механика и надёжность
- Обёртка: команда запускается через
sh -c 'echo $$ > /scratch/.ops/<id>.pid; "$@"; code=$?; echo $code > /scratch/.ops/<id>.code; exit $code'— pid и код выхода пишутся в run-scoped scratch. Файлы лежат на хост-монтировании scratch и потому переживают и перезапуск воркера, и перезапуск kernel. - Heartbeat: активность фоновой команды бьётся сердцебиением — Nudge и Stop доставляются в долгую команду.
- Перезапуск воркера: фоновая активность повторяется (до трёх попыток), и повтор — это восстановление наблюдения, а не перезапуск: первая попытка исполняет команду, каждая следующая цепляется к уже запущенному процессу через pid/code-файлы и возвращает его результат (вывод теряется — в тексте результата это помечено). Повторное исполнение исходной команды исключено маршрутизацией попыток. Консоль операции при этом пересоздаётся: в поток пишется пометка «вывод потерян вместе со старым воркером», а по завершении поток закрывается с кодом выхода — подписчик SSE видит done вместо вечного ожидания.
- Перезапуск kernel: контейнер помечен durable-лейблами (
temporality.scratch_key,temporality.run_fingerprint), а in-memory реестр восстанавливается из них — восстановленный kernel заново получает доступ к операции черезOperationStateиKillOperation. Контейнер, чью принадлежность или идентичность нельзя достоверно подтвердить, не переиспользуется. - Записанный результат главнее контейнера: завершённая операция возвращает сохранённый exit code, даже если контейнер уже удалён.
kill_operationзаписывает 137 — намеренная отмена везде читается как завершённая (137), а не как потеря. - Потеря — явное состояние: процесс исчез без записи результата либо контейнер/демон недоступны дольше grace-окна — активность завершается типизированной ошибкой
BackgroundOpLost; выход за бюджет (отмеренный от старта операции по pid-файлу) —BackgroundOpTimeoutс добивом остатков. Workflow пишетtool.failedсeffect=uncertain— судьба операции уходит в reconciliation, никогда — в ложныйexit_code=0. - Orphan reaper: отсутствие контейнера в in-memory реестре — не признак orphan. Возраст лишь открывает кандидата; решение принимают маркеры операций и таблица процессов (
.ops-файлы +docker top), а неопределённость сохраняет контейнер. Действительно заброшенные удаляются по-прежнему. - Живая консоль и трейс: поток вывода (console://) монтируется на строке
tool.started; окно трейса ограничено (~50 событий), и для долгого рана строка уезжает из окна, хотя команда ещё работает — вверху трейса появляется закреплённая панель «Живые фоновые операции» со стримом каждой такой команды (исчезает, когда прилетаетtool.completed). - Таймаут: свой бюджет фона — по умолчанию 900 с (если модель не передала
timeout_sec), потолок 3600 с (1 час,KERNEL_BG_TOOL_MAX_SECONDS); обычные команды остались на 300/900 (KERNEL_TOOL_MAX_SECONDS). Бюджет отмеряется от старта самой операции — attach после перезапуска не растягивает таймаут. Модель видит эти лимиты в описанииrun_commandи передаётtimeout_secдо 3600 для долгих установок/сборок/сюитов. StartToClose активности = таймаут + запас (10/15 мин). - Дедупликация: результат каждой операции доставляется ровно один раз — future удаляется из реестра после дренирования.
- Заметки людей: заметка (steer) будит ожидание так же, как завершение операции — комментарий не ждёт фона.
#Ограничения v1
- Фоновые команды требуют run-контейнера (ScratchKey) — прямой one-shot путь
(
docker runбез ключа рана) фон не поддерживает. - Вывод при перезапуске воркера не восстанавливается (код и pid — да).
- При нескольких контейнерах с одинаковым scratch-ключом (смена профиля посреди фоновой операции) восстановление отказывается подключаться — операция честно уходит в lost/unknown, а не цепляется к чужому контейнеру.
- Форс-финал ждёт операции, но не порождает новых ходов модели.
- Reviewer-раны (
submit_review) завершаются сразу, не дожидаясь фона — у ревьюера фоновые команды не ожидаемый сценарий.