Files
bot-bottle/bot_bottle/orchestrator/runner.py
T
didericis-claude d5fb159857
lint / lint (push) Failing after 2m15s
test / unit (pull_request) Successful in 51s
test / integration (pull_request) Successful in 21s
test / coverage (pull_request) Successful in 1m7s
refactor(orchestrator): swap SubprocessBottleRunner → ProgrammaticBottleRunner
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>
2026-07-01 19:48:06 +00:00

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)