160 lines
5.2 KiB
Python
160 lines
5.2 KiB
Python
#!/usr/bin/env python3
|
|
"""Turn interactive tabs in the Agent coordinator space into Hermes sessions."""
|
|
|
|
from __future__ import annotations
|
|
|
|
import argparse
|
|
import json
|
|
import os
|
|
import re
|
|
import shlex
|
|
import subprocess
|
|
import time
|
|
from pathlib import Path
|
|
from typing import Any, Callable
|
|
|
|
|
|
HERDR_BIN = Path("/opt/data/tools/bin/herdr")
|
|
WORKSPACE_ROOT = Path("/opt/data/workspace")
|
|
PROJECT_ROOT = WORKSPACE_ROOT / "projects"
|
|
|
|
|
|
def _slug(value: str) -> str:
|
|
"""Return a stable label accepted by Herdr's agent-name argument."""
|
|
normalized = re.sub(r"[^a-z0-9]+", "-", value.strip().lower()).strip("-")
|
|
return normalized or "session"
|
|
|
|
|
|
def project_for_label(label: str, project_root: Path = PROJECT_ROOT) -> Path | None:
|
|
"""Resolve a tab label to an existing isolated project directory."""
|
|
wanted = _slug(label)
|
|
if not project_root.is_dir():
|
|
return None
|
|
for candidate in project_root.iterdir():
|
|
if candidate.is_dir() and _slug(candidate.name) == wanted:
|
|
return candidate.resolve()
|
|
return None
|
|
|
|
|
|
def _run_json(command: list[str]) -> dict[str, Any]:
|
|
"""Run a Herdr command and return its JSON object response."""
|
|
completed = subprocess.run(
|
|
command,
|
|
text=True,
|
|
capture_output=True,
|
|
timeout=130,
|
|
check=False,
|
|
env=os.environ.copy(),
|
|
)
|
|
if completed.returncode != 0:
|
|
detail = completed.stderr.strip() or completed.stdout.strip()
|
|
raise RuntimeError(detail or f"command failed with {completed.returncode}")
|
|
try:
|
|
payload = json.loads(completed.stdout)
|
|
except json.JSONDecodeError as error:
|
|
raise RuntimeError("Herdr returned non-JSON output") from error
|
|
if not isinstance(payload, dict):
|
|
raise RuntimeError("Herdr returned an unexpected response")
|
|
return payload
|
|
|
|
|
|
def _result_list(payload: dict[str, Any], key: str) -> list[dict[str, Any]]:
|
|
"""Extract a typed list from a Herdr response."""
|
|
result = payload.get("result") or {}
|
|
values = result.get(key) or []
|
|
return [item for item in values if isinstance(item, dict)]
|
|
|
|
|
|
def find_workspace(
|
|
label: str,
|
|
run: Callable[[list[str]], dict[str, Any]] = _run_json,
|
|
) -> str | None:
|
|
"""Return the live workspace id for the named Agent coordinator space."""
|
|
payload = run([str(HERDR_BIN), "workspace", "list"])
|
|
matches = [
|
|
str(item.get("workspace_id") or "")
|
|
for item in _result_list(payload, "workspaces")
|
|
if item.get("label") == label
|
|
]
|
|
return next((item for item in matches if item), None)
|
|
|
|
|
|
def route_unmanaged_tabs(
|
|
workspace_id: str,
|
|
run: Callable[[list[str]], dict[str, Any]] = _run_json,
|
|
project_root: Path = PROJECT_ROOT,
|
|
) -> list[str]:
|
|
"""Start routed Hermes in every unmanaged pane of the coordinator space."""
|
|
tabs_payload = run([str(HERDR_BIN), "tab", "list"])
|
|
panes_payload = run(
|
|
[str(HERDR_BIN), "pane", "list", "--workspace", workspace_id]
|
|
)
|
|
tab_labels = {
|
|
str(item.get("tab_id") or ""): str(item.get("label") or "session")
|
|
for item in _result_list(tabs_payload, "tabs")
|
|
if item.get("workspace_id") == workspace_id
|
|
}
|
|
started: list[str] = []
|
|
for pane in _result_list(panes_payload, "panes"):
|
|
if pane.get("workspace_id") != workspace_id or pane.get("agent"):
|
|
continue
|
|
pane_id = str(pane.get("pane_id") or "")
|
|
if not pane_id:
|
|
continue
|
|
label = tab_labels.get(str(pane.get("tab_id") or ""), "session")
|
|
project = project_for_label(label, project_root)
|
|
if project is not None and Path(str(pane.get("foreground_cwd") or "")) != project:
|
|
run(
|
|
[
|
|
str(HERDR_BIN),
|
|
"pane",
|
|
"run",
|
|
pane_id,
|
|
f"cd {shlex.quote(str(project))}",
|
|
]
|
|
)
|
|
pane_suffix = _slug(pane_id.split(":")[-1])
|
|
agent_name = f"{_slug(label)}-{pane_suffix}"
|
|
run(
|
|
[
|
|
str(HERDR_BIN),
|
|
"agent",
|
|
"start",
|
|
agent_name,
|
|
"--kind",
|
|
"hermes",
|
|
"--pane",
|
|
pane_id,
|
|
"--timeout",
|
|
"120000",
|
|
]
|
|
)
|
|
started.append(pane_id)
|
|
return started
|
|
|
|
|
|
def run_loop(label: str, interval: float) -> None:
|
|
"""Continuously reconcile Agent tabs while allowing Herdr to own workers."""
|
|
while True:
|
|
try:
|
|
workspace_id = find_workspace(label)
|
|
if workspace_id:
|
|
for pane_id in route_unmanaged_tabs(workspace_id):
|
|
print(f"Started routed Hermes session in {pane_id}", flush=True)
|
|
except Exception as error:
|
|
print(f"Agent tab routing retry: {type(error).__name__}: {error}", flush=True)
|
|
time.sleep(interval)
|
|
|
|
|
|
def main() -> None:
|
|
"""Parse controller options and reconcile the Agent coordinator space."""
|
|
parser = argparse.ArgumentParser()
|
|
parser.add_argument("--workspace-label", default="coordinator")
|
|
parser.add_argument("--interval", type=float, default=1.0)
|
|
args = parser.parse_args()
|
|
run_loop(args.workspace_label, max(args.interval, 0.25))
|
|
|
|
|
|
if __name__ == "__main__":
|
|
main()
|