Implements the Lazarus Pit v2.0 foundation: - cell.py: Cell model, SQLite registry, filesystem packager with per-cell HERMES_HOME - operator_ctl.py: summon, invite, team, status, close, destroy commands - backends/base.py + process_backend.py: backend abstraction with process implementation - cli.py: operator CLI entrypoint - tests/test_cell.py: 13 tests for isolation, registry, TTL, lifecycle - README.md: quick start and architecture invariants All acceptance criteria for #274 and #269 are scaffolded and tested. 13 tests pass.
212 lines
7.9 KiB
Python
212 lines
7.9 KiB
Python
"""
|
|
Operator control surface for Lazarus Pit v2.0.
|
|
|
|
Provides the command grammar and API for:
|
|
summon, invite, team, status, close, destroy
|
|
"""
|
|
|
|
import json
|
|
import uuid
|
|
from datetime import datetime, timedelta
|
|
from typing import Dict, List, Optional
|
|
|
|
from .cell import Cell, CellMember, CellPackager, CellRegistry, CellRole, CellState
|
|
from .backends.process_backend import ProcessBackend
|
|
|
|
|
|
class OperatorCtl:
|
|
"""The operator's command surface for the Lazarus Pit."""
|
|
|
|
def __init__(self, registry: Optional[CellRegistry] = None, packager: Optional[CellPackager] = None):
|
|
self.registry = registry or CellRegistry()
|
|
self.packager = packager or CellPackager()
|
|
self.backend = ProcessBackend()
|
|
|
|
# ------------------------------------------------------------------
|
|
# Commands
|
|
# ------------------------------------------------------------------
|
|
def summon(self, agent: str, project: str, owner: str = "human:Alexander", backend: str = "process", ttl_minutes: int = 60) -> dict:
|
|
"""Summon a single agent into a fresh resurrection cell."""
|
|
cell_id = f"laz-{uuid.uuid4().hex[:8]}"
|
|
cell = self.packager.create(cell_id, backend=backend)
|
|
cell.project = project
|
|
cell.owner = owner
|
|
cell.ttl_minutes = ttl_minutes
|
|
cell.state = CellState.ACTIVE
|
|
cell.members.append(CellMember(
|
|
identity=f"agent:{agent}",
|
|
role=CellRole.EXECUTOR,
|
|
joined_at=datetime.now().astimezone().isoformat(),
|
|
))
|
|
|
|
# Spawn a simple keep-alive process (real agent bootstrap would go here)
|
|
res = self.backend.spawn(
|
|
cell_id=cell_id,
|
|
hermes_home=cell.hermes_home,
|
|
command=["python3", "-c", "import time; time.sleep(86400)"],
|
|
)
|
|
|
|
self.registry.save(cell)
|
|
return {
|
|
"success": res.success,
|
|
"cell_id": cell_id,
|
|
"project": project,
|
|
"agent": agent,
|
|
"backend": backend,
|
|
"pid": res.pid,
|
|
"message": res.message,
|
|
}
|
|
|
|
def invite(self, cell_id: str, guest: str, role: str, invited_by: str = "human:Alexander") -> dict:
|
|
"""Invite a guest bot or human into an active cell."""
|
|
cell = self.registry.load(cell_id)
|
|
if not cell:
|
|
return {"success": False, "message": f"Cell {cell_id} not found"}
|
|
if cell.state not in (CellState.PROPOSED, CellState.ACTIVE, CellState.IDLE):
|
|
return {"success": False, "message": f"Cell {cell_id} is not accepting invites (state={cell.state.value})"}
|
|
|
|
role_enum = CellRole(role)
|
|
cell.members.append(CellMember(
|
|
identity=guest,
|
|
role=role_enum,
|
|
joined_at=datetime.now().astimezone().isoformat(),
|
|
))
|
|
cell.state = CellState.ACTIVE
|
|
self.registry.save(cell)
|
|
return {
|
|
"success": True,
|
|
"cell_id": cell_id,
|
|
"guest": guest,
|
|
"role": role_enum.value,
|
|
"invited_by": invited_by,
|
|
"message": f"Invited {guest} as {role_enum.value} to {cell_id}",
|
|
}
|
|
|
|
def team(self, agents: List[str], project: str, owner: str = "human:Alexander", backend: str = "process", ttl_minutes: int = 120) -> dict:
|
|
"""Form a multi-agent team in one cell against a project."""
|
|
cell_id = f"laz-{uuid.uuid4().hex[:8]}"
|
|
cell = self.packager.create(cell_id, backend=backend)
|
|
cell.project = project
|
|
cell.owner = owner
|
|
cell.ttl_minutes = ttl_minutes
|
|
cell.state = CellState.ACTIVE
|
|
|
|
for agent in agents:
|
|
cell.members.append(CellMember(
|
|
identity=f"agent:{agent}",
|
|
role=CellRole.EXECUTOR,
|
|
joined_at=datetime.now().astimezone().isoformat(),
|
|
))
|
|
|
|
res = self.backend.spawn(
|
|
cell_id=cell_id,
|
|
hermes_home=cell.hermes_home,
|
|
command=["python3", "-c", "import time; time.sleep(86400)"],
|
|
)
|
|
|
|
self.registry.save(cell)
|
|
return {
|
|
"success": res.success,
|
|
"cell_id": cell_id,
|
|
"project": project,
|
|
"agents": agents,
|
|
"backend": backend,
|
|
"pid": res.pid,
|
|
"message": res.message,
|
|
}
|
|
|
|
def status(self, cell_id: Optional[str] = None) -> dict:
|
|
"""Show status of one cell or all active cells."""
|
|
if cell_id:
|
|
cell = self.registry.load(cell_id)
|
|
if not cell:
|
|
return {"success": False, "message": f"Cell {cell_id} not found"}
|
|
probe = self.backend.probe(cell_id)
|
|
return {
|
|
"success": True,
|
|
"cell": cell.to_dict(),
|
|
"probe": probe.message,
|
|
"expired": cell.is_expired(),
|
|
}
|
|
|
|
active = self.registry.list_active()
|
|
cells = []
|
|
for cell in active:
|
|
probe = self.backend.probe(cell.cell_id)
|
|
cells.append({
|
|
"cell_id": cell.cell_id,
|
|
"project": cell.project,
|
|
"backend": cell.backend,
|
|
"state": cell.state.value,
|
|
"members": [m.identity for m in cell.members],
|
|
"probe": probe.message,
|
|
"expired": cell.is_expired(),
|
|
})
|
|
return {"success": True, "active_cells": cells}
|
|
|
|
def close(self, cell_id: str, closed_by: str = "human:Alexander") -> dict:
|
|
"""Gracefully close a cell."""
|
|
cell = self.registry.load(cell_id)
|
|
if not cell:
|
|
return {"success": False, "message": f"Cell {cell_id} not found"}
|
|
|
|
res = self.backend.close(cell_id)
|
|
cell.state = CellState.CLOSING
|
|
self.registry.save(cell)
|
|
return {
|
|
"success": res.success,
|
|
"cell_id": cell_id,
|
|
"message": f"Closed {cell_id} by {closed_by}",
|
|
}
|
|
|
|
def destroy(self, cell_id: str, destroyed_by: str = "human:Alexander") -> dict:
|
|
"""Forcefully destroy a cell and all runtime state."""
|
|
cell = self.registry.load(cell_id)
|
|
if not cell:
|
|
return {"success": False, "message": f"Cell {cell_id} not found"}
|
|
|
|
res = self.backend.destroy(cell_id)
|
|
self.packager.destroy(cell_id)
|
|
cell.state = CellState.DESTROYED
|
|
cell.destroyed_at = datetime.now().astimezone().isoformat()
|
|
self.registry.save(cell)
|
|
return {
|
|
"success": True,
|
|
"cell_id": cell_id,
|
|
"message": f"Destroyed {cell_id} by {destroyed_by}",
|
|
}
|
|
|
|
# ------------------------------------------------------------------
|
|
# CLI helpers
|
|
# ------------------------------------------------------------------
|
|
def run_cli(self, args: List[str]) -> dict:
|
|
if not args:
|
|
return self.status()
|
|
cmd = args[0]
|
|
if cmd == "summon" and len(args) >= 3:
|
|
return self.summon(agent=args[1], project=args[2])
|
|
if cmd == "invite" and len(args) >= 4:
|
|
return self.invite(cell_id=args[1], guest=args[2], role=args[3])
|
|
if cmd == "team" and len(args) >= 3:
|
|
agents = args[1].split("+")
|
|
project = args[2]
|
|
return self.team(agents=agents, project=project)
|
|
if cmd == "status":
|
|
return self.status(cell_id=args[1] if len(args) > 1 else None)
|
|
if cmd == "close" and len(args) >= 2:
|
|
return self.close(cell_id=args[1])
|
|
if cmd == "destroy" and len(args) >= 2:
|
|
return self.destroy(cell_id=args[1])
|
|
return {
|
|
"success": False,
|
|
"message": (
|
|
"Unknown command. Usage:\n"
|
|
" lazarus summon <agent> <project>\n"
|
|
" lazarus invite <cell_id> <guest> <role>\n"
|
|
" lazarus team <agent1+agent2> <project>\n"
|
|
" lazarus status [cell_id]\n"
|
|
" lazarus close <cell_id>\n"
|
|
" lazarus destroy <cell_id>\n"
|
|
)
|
|
}
|