d5fb159857
BottleRunner Protocol tightened: start() → str, freeze/resume/destroy → None. RunResult removed. lifecycle.py unpacks the slug directly. FakeRunner and test_runner updated to match. Config.bot_bottle_cli dropped (nothing uses it). Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
83 lines
2.3 KiB
Python
83 lines
2.3 KiB
Python
"""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)
|