"""Bottle runner: drive bot_bottle to manage a bottle's life. `BottleRunner` is the interface the lifecycle depends on; `ProgrammaticBottleRunner` calls into the bot_bottle Python API directly (no subprocess). The slug returned by `start` is the actual slug minted at launch time — not a post-hoc derivation from the label — so it is authoritative even if bot-bottle's slugification logic changes. `slugify` is retained for `FakeRunner` (tests) and for the label scheme the orchestrator uses to predict collision-free slugs. """ from __future__ import annotations import re from collections.abc import Sequence from typing import Protocol class BottleRunner(Protocol): def start( self, *, agent: str, bottles: Sequence[str], label: str, prompt: str, forge_env: dict[str, str], ) -> str: ... def freeze(self, slug: str) -> None: ... def resume(self, slug: str, prompt: str) -> None: ... def destroy(self, slug: str) -> None: ... _SLUG_RE = re.compile(r"[^a-z0-9]+") def slugify(label: str) -> str: """Lowercase, collapse non-alphanumerics to single hyphens, strip leading/trailing hyphens — matches bot-bottle's slug rule.""" return _SLUG_RE.sub("-", label.lower()).strip("-") class ProgrammaticBottleRunner: """Calls into the bot_bottle Python API directly — no subprocess. Imports are deferred to call time so this module can be imported before `bot_bottle.api` is available (e.g. in isolated test runs that mock the API surface).""" def start( self, *, agent: str, bottles: Sequence[str], label: str, prompt: str, forge_env: dict[str, str], ) -> str: import bot_bottle.api as api return api.start_headless( agent, prompt=prompt, bottles=list(bottles) or None, label=label, forge_env=forge_env, ) def freeze(self, slug: str) -> None: import bot_bottle.api as api api.freeze(slug) def resume(self, slug: str, prompt: str) -> None: import bot_bottle.api as api api.resume_headless(slug, prompt=prompt) def destroy(self, slug: str) -> None: import bot_bottle.api as api api.destroy(slug)