prototype

This commit is contained in:
kacper 2026-02-28 22:12:04 -05:00
commit 8534b15c20
8 changed files with 3048 additions and 0 deletions

388
supertonic_gateway.py Normal file
View file

@ -0,0 +1,388 @@
import asyncio
import contextlib
import os
import pty
import re
import shlex
import signal
import subprocess
import time
from collections import deque
from pathlib import Path
from wisper import WisperBus, WisperEvent
ANSI_ESCAPE_RE = re.compile(r"\x1B\[[0-?]*[ -/]*[@-~]")
CONTROL_CHAR_RE = re.compile(r"[\x00-\x08\x0b-\x1f\x7f]")
BRAILLE_SPINNER_RE = re.compile(r"[\u2800-\u28ff]")
SPINNER_ONLY_RE = re.compile(r"^[\s|/\\\-]+$")
BOX_DRAWING_ONLY_RE = re.compile(r"^[\s\u2500-\u257f]+$")
THINKING_LINE_RE = re.compile(r"^(?:agent|nanobot)\s+is\s+thinking\b", re.IGNORECASE)
TOOL_STREAM_LINE_RE = re.compile(
r"^(?:tool(?:\s+call|\s+output)?|calling\s+tool|running\s+tool|executing\s+tool)\b",
re.IGNORECASE,
)
EMOJI_RE = re.compile(
"[" # Common emoji and pictograph blocks.
"\U0001F1E6-\U0001F1FF"
"\U0001F300-\U0001F5FF"
"\U0001F600-\U0001F64F"
"\U0001F680-\U0001F6FF"
"\U0001F700-\U0001F77F"
"\U0001F780-\U0001F7FF"
"\U0001F800-\U0001F8FF"
"\U0001F900-\U0001F9FF"
"\U0001FA00-\U0001FAFF"
"\u2600-\u26FF"
"\u2700-\u27BF"
"\uFE0F"
"\u200D"
"]"
)
def _clean_output(text: str) -> str:
cleaned = ANSI_ESCAPE_RE.sub("", text)
cleaned = BRAILLE_SPINNER_RE.sub(" ", cleaned)
cleaned = CONTROL_CHAR_RE.sub("", cleaned)
return cleaned.replace("\r", "\n")
def _resolve_nanobot_command_and_workdir() -> tuple[str, Path]:
command_override = os.getenv("NANOBOT_COMMAND")
workdir_override = os.getenv("NANOBOT_WORKDIR")
if workdir_override:
default_workdir = Path(workdir_override).expanduser()
else:
default_workdir = Path.home()
if command_override:
return command_override, default_workdir
nanobot_dir = Path.home() / "nanobot"
nanobot_python_candidates = [
nanobot_dir / ".venv" / "bin" / "python",
nanobot_dir / "venv" / "bin" / "python",
]
for nanobot_venv_python in nanobot_python_candidates:
if nanobot_venv_python.exists():
if not workdir_override:
default_workdir = nanobot_dir
return f"{nanobot_venv_python} -m nanobot agent --no-markdown", default_workdir
return "nanobot agent --no-markdown", default_workdir
def _infer_venv_root(command_parts: list[str], workdir: Path) -> Path | None:
if not command_parts:
return None
binary = Path(command_parts[0]).expanduser()
if binary.is_absolute() and binary.name.startswith("python") and binary.parent.name == "bin":
return binary.parent.parent
for candidate in (workdir / ".venv", workdir / "venv"):
if (candidate / "bin" / "python").exists():
return candidate
return None
def _build_process_env(command_parts: list[str], workdir: Path) -> tuple[dict[str, str], Path | None]:
env = os.environ.copy()
env.pop("PYTHONHOME", None)
venv_root = _infer_venv_root(command_parts, workdir)
if not venv_root:
return env, None
venv_bin = str(venv_root / "bin")
path_entries = [entry for entry in env.get("PATH", "").split(os.pathsep) if entry]
path_entries = [entry for entry in path_entries if entry != venv_bin]
path_entries.insert(0, venv_bin)
env["PATH"] = os.pathsep.join(path_entries)
env["VIRTUAL_ENV"] = str(venv_root)
return env, venv_root
class NanobotTUIProcess:
def __init__(self, bus: WisperBus, command: str, workdir: Path) -> None:
self._bus = bus
self._command = command
self._workdir = workdir
self._process: subprocess.Popen[bytes] | None = None
self._master_fd: int | None = None
self._read_task: asyncio.Task[None] | None = None
self._pending_output = ""
self._suppress_noisy_ui = os.getenv("NANOBOT_SUPPRESS_NOISY_UI", "1").strip() not in {
"0",
"false",
"False",
"no",
"off",
}
self._dedup_window_s = max(0.2, float(os.getenv("NANOBOT_OUTPUT_DEDUP_WINDOW_S", "1.5")))
self._recent_lines: deque[tuple[str, float]] = deque()
self._last_tts_line = ""
@property
def running(self) -> bool:
return self._process is not None and self._process.poll() is None
async def start(self) -> None:
if self.running:
await self._bus.publish(WisperEvent(role="system", text="Nanobot TUI is already running."))
return
command_parts = [
os.path.expandvars(os.path.expanduser(part)) for part in shlex.split(self._command)
]
if not command_parts:
await self._bus.publish(WisperEvent(role="system", text="NANOBOT_COMMAND is empty."))
return
if not self._workdir.exists():
await self._bus.publish(
WisperEvent(
role="system",
text=f"NANOBOT_WORKDIR does not exist: {self._workdir}",
)
)
return
master_fd, slave_fd = pty.openpty()
child_env, child_venv_root = _build_process_env(command_parts=command_parts, workdir=self._workdir)
try:
self._process = subprocess.Popen(
command_parts,
stdin=slave_fd,
stdout=slave_fd,
stderr=slave_fd,
cwd=str(self._workdir),
start_new_session=True,
env=child_env,
)
except FileNotFoundError as exc:
os.close(master_fd)
os.close(slave_fd)
await self._bus.publish(
WisperEvent(
role="system",
text=(
"Could not start Nanobot process "
f"(command='{command_parts[0]}', workdir='{self._workdir}'): {exc}. "
"Check NANOBOT_COMMAND and NANOBOT_WORKDIR."
),
)
)
return
except Exception as exc:
os.close(master_fd)
os.close(slave_fd)
await self._bus.publish(
WisperEvent(role="system", text=f"Failed to spawn TUI process: {exc}")
)
return
os.close(slave_fd)
os.set_blocking(master_fd, False)
self._master_fd = master_fd
self._read_task = asyncio.create_task(self._read_output(), name="nanobot-tui-reader")
await self._bus.publish(
WisperEvent(
role="system",
text=f"Spawned Nanobot TUI with command: {' '.join(command_parts)}",
)
)
if child_venv_root:
await self._bus.publish(
WisperEvent(
role="system",
text=f"Nanobot runtime venv: {child_venv_root}",
)
)
async def send(self, text: str) -> None:
if not self.running or self._master_fd is None:
await self._bus.publish(
WisperEvent(role="system", text="Nanobot TUI is not running. Click spawn first.")
)
return
message = text.rstrip("\n") + "\n"
try:
os.write(self._master_fd, message.encode())
except OSError as exc:
await self._bus.publish(WisperEvent(role="system", text=f"Failed to write to TUI: {exc}"))
async def stop(self) -> None:
if self._read_task:
self._read_task.cancel()
with contextlib.suppress(asyncio.CancelledError):
await self._read_task
self._read_task = None
if self.running and self._process:
try:
os.killpg(self._process.pid, signal.SIGTERM)
except ProcessLookupError:
pass
except Exception:
self._process.terminate()
try:
self._process.wait(timeout=3)
except Exception:
self._process.kill()
self._process.wait(timeout=1)
if self._master_fd is not None:
try:
os.close(self._master_fd)
except OSError:
pass
self._master_fd = None
self._process = None
self._pending_output = ""
self._recent_lines.clear()
self._last_tts_line = ""
await self._bus.publish(WisperEvent(role="system", text="Stopped Nanobot TUI."))
async def _read_output(self) -> None:
if self._master_fd is None:
return
while self.running:
try:
chunk = os.read(self._master_fd, 4096)
except BlockingIOError:
await asyncio.sleep(0.05)
continue
except OSError:
break
if not chunk:
await asyncio.sleep(0.05)
continue
text = _clean_output(chunk.decode(errors="ignore"))
if not text.strip():
continue
displayable, tts_publishable = self._consume_output_chunk(text)
if displayable:
await self._bus.publish(WisperEvent(role="nanobot", text=displayable))
if tts_publishable:
await self._bus.publish(WisperEvent(role="nanobot-tts", text=tts_publishable))
trailing_display, trailing_tts = self._consume_output_chunk("\n")
if trailing_display:
await self._bus.publish(WisperEvent(role="nanobot", text=trailing_display))
if trailing_tts:
await self._bus.publish(WisperEvent(role="nanobot-tts", text=trailing_tts))
if self._process is not None:
exit_code = self._process.poll()
await self._bus.publish(
WisperEvent(role="system", text=f"Nanobot TUI exited (code={exit_code}).")
)
def _consume_output_chunk(self, text: str) -> tuple[str, str]:
self._pending_output += text
lines = self._pending_output.split("\n")
self._pending_output = lines.pop()
if len(self._pending_output) > 1024:
lines.append(self._pending_output)
self._pending_output = ""
kept_lines: list[str] = []
tts_lines: list[str] = []
for line in lines:
normalized = self._normalize_line(line)
if not normalized:
continue
if self._suppress_noisy_ui and self._is_noisy_ui_line(normalized):
continue
if normalized != self._last_tts_line:
tts_lines.append(normalized)
self._last_tts_line = normalized
if self._is_recent_duplicate(normalized):
continue
kept_lines.append(normalized)
return "\n".join(kept_lines).strip(), "\n".join(tts_lines).strip()
def _normalize_line(self, line: str) -> str:
without_emoji = EMOJI_RE.sub(" ", line)
return re.sub(r"\s+", " ", without_emoji).strip()
def _is_noisy_ui_line(self, line: str) -> bool:
if SPINNER_ONLY_RE.fullmatch(line):
return True
if BOX_DRAWING_ONLY_RE.fullmatch(line):
return True
candidate = re.sub(r"^[^\w]+", "", line)
if THINKING_LINE_RE.match(candidate):
return True
if TOOL_STREAM_LINE_RE.match(candidate):
return True
return False
def _is_recent_duplicate(self, line: str) -> bool:
now = time.monotonic()
normalized = line.lower()
while self._recent_lines and (now - self._recent_lines[0][1]) > self._dedup_window_s:
self._recent_lines.popleft()
for previous, _timestamp in self._recent_lines:
if previous == normalized:
return True
self._recent_lines.append((normalized, now))
return False
class SuperTonicGateway:
def __init__(self) -> None:
self.bus = WisperBus()
self._lock = asyncio.Lock()
self._tui: NanobotTUIProcess | None = None
async def subscribe(self) -> asyncio.Queue[WisperEvent]:
return await self.bus.subscribe()
async def unsubscribe(self, queue: asyncio.Queue[WisperEvent]) -> None:
await self.bus.unsubscribe(queue)
async def spawn_tui(self) -> None:
async with self._lock:
if self._tui and self._tui.running:
await self.bus.publish(WisperEvent(role="system", text="Nanobot TUI is already running."))
return
command, workdir = _resolve_nanobot_command_and_workdir()
self._tui = NanobotTUIProcess(bus=self.bus, command=command, workdir=workdir)
await self._tui.start()
async def send_user_message(self, text: str) -> None:
message = text.strip()
if not message:
return
await self.bus.publish(WisperEvent(role="user", text=message))
async with self._lock:
if not self._tui:
await self.bus.publish(
WisperEvent(role="system", text="Nanobot TUI is not running. Click spawn first.")
)
return
await self._tui.send(message)
async def stop_tui(self) -> None:
async with self._lock:
if self._tui:
await self._tui.stop()
async def shutdown(self) -> None:
await self.stop_tui()