Lifecycle hooks
Создать sandbox — полдела. Свежая cloud VM не знает ваш git user,
не ставила зависимости, не видела .env. Перед работой
агента что-то должно настроить среду; перед stop — сохранить
WIP или snapshot. Это lifecycle hooks: для local пустые, для cloud —
без них боль.
SandboxLifecycle в sandbox.py, вызовы в
main.py вокруг agent.run. С пустым lifecycle
поведение как в 4.3; с логами — видно порядок вызовов.
Слова, которые встретятся
- Lifecycle hook — колбэк на этапе жизни sandbox: старт, стоп, таймаут.
-
afterStart — sandbox готов, агент ещё не бежал:
git config,uv sync, копия env. - beforeStop — перед выключением: коммит WIP, snapshot, лог «не забудь».
-
onTimeout — cloud сам дёрнет при
expires_at(модуль 7); сейчас заглушка.
Быстрый путь
- Добавить
SandboxLifecycleвsandbox.py. - В
main.py:try/finally+ hooks. - Временно повесить print-hooks и прогнать промпт.
Какие файлы трогаем
| Файл | Что делать |
|---|---|
sandbox.py |
Править. Protocol SandboxLifecycle. |
main.py |
Править. Вызов hooks + finally. |
lifecycle.py |
Опционально позже — тела для cloud (npm install и т.д.). |
Шаг 1. Интерфейс hooks
sandbox.py — добавить
class SandboxLifecycle(Protocol):
async def after_start(self, sandbox: Sandbox) -> None: ...
async def before_stop(self, sandbox: Sandbox) -> None: ...
async def on_timeout(self, sandbox: Sandbox) -> None: ...
В Python все три можно сделать optional через отдельный класс с
пустыми методами или через проверку hasattr. Проще —
dataclass с no-op по умолчанию:
@dataclass
class EmptyLifecycle:
async def after_start(self, sandbox: Sandbox) -> None:
pass
async def before_stop(self, sandbox: Sandbox) -> None:
pass
async def on_timeout(self, sandbox: Sandbox) -> None:
pass
Шаг 2. Обмотать agent.run
main.py — main()
lifecycle = EmptyLifecycle()
async def main() -> None:
await lifecycle.after_start(sandbox)
try:
result = await agent.run(prompt, deps=sandbox)
print(result.output)
finally:
await lifecycle.before_stop(sandbox)
await sandbox.stop()
finally важен: агент упал — уборка всё равно.
Именно здесь позже проверят uncommitted git и snapshot.
Как выглядел бы cloud lifecycle (иллюстрация)
# lifecycle.py — не обязательно сейчас
class CloudLifecycle:
async def after_start(self, sandbox: Sandbox) -> None:
await sandbox.exec('git config user.name "Agent"')
await sandbox.exec("uv sync")
async def before_stop(self, sandbox: Sandbox) -> None:
status = await sandbox.exec("git status --porcelain")
if status["stdout"].strip():
await sandbox.exec('git add -A && git commit -m "WIP: auto-save"')
if hasattr(sandbox, "snapshot"):
await sandbox.snapshot()
uv sync вы делали сами.
Структура нужна, чтобы cloud не был «другой программой».
Проверьте
Временно замените lifecycle на логирующий:
@dataclass
class LoggingLifecycle:
async def after_start(self, sandbox: Sandbox) -> None:
print(f"[lifecycle] after_start: {sandbox.type}", file=sys.stderr)
async def before_stop(self, sandbox: Sandbox) -> None:
print(f"[lifecycle] before_stop: {sandbox.type}", file=sys.stderr)
async def on_timeout(self, sandbox: Sandbox) -> None:
pass
lifecycle = LoggingLifecycle()
uv run python main.py . "Прочитай pyproject.toml"
В stderr две строки bracketing ответа агента. Верните
EmptyLifecycle — снова тишина, поведение то же.
Готово, если:
- Есть тип/класс lifecycle с
after_start/before_stop after_startвызывается доagent.runbefore_stopиsandbox.stop()— вfinally- С пустым lifecycle агент ведёт себя как в 4.3
- С logging lifecycle виден порядок вызовов