209 lines
6.4 KiB
Python
209 lines
6.4 KiB
Python
from __future__ import annotations
|
|
|
|
import sys
|
|
from threading import Event, Thread
|
|
from typing import Optional
|
|
|
|
from agent_runtime import run_agent
|
|
from agents import Agent, build_agent
|
|
from mud_env import load_env_file, read_connection_settings, read_tool_settings
|
|
from mud_session import (
|
|
SessionState,
|
|
graceful_shutdown,
|
|
interactive_session,
|
|
login,
|
|
run_tool_loop,
|
|
)
|
|
from mud_tools import TOOL_DESCRIPTIONS, build_tool
|
|
from telnetclient import TelnetClient
|
|
from tools import Tool
|
|
|
|
|
|
DEFAULT_SEND_INTERVAL = 1.0
|
|
AUTO_STOP_IDLE_SECONDS = 2.0
|
|
|
|
|
|
def _start_tool_thread(
|
|
client: TelnetClient,
|
|
state: SessionState,
|
|
tool: Tool,
|
|
stop_event: Event,
|
|
*,
|
|
auto_stop: bool = False,
|
|
auto_stop_idle: float = AUTO_STOP_IDLE_SECONDS,
|
|
) -> Thread:
|
|
thread = Thread(
|
|
target=run_tool_loop,
|
|
args=(client, state, tool, stop_event),
|
|
kwargs={
|
|
"min_send_interval": DEFAULT_SEND_INTERVAL,
|
|
"auto_stop": auto_stop,
|
|
"auto_stop_idle": auto_stop_idle,
|
|
},
|
|
daemon=True,
|
|
)
|
|
thread.start()
|
|
return thread
|
|
|
|
|
|
def _build_sideload_tools(specs: list[str]) -> list[tuple[str, Tool]]:
|
|
sideload_tools: list[tuple[str, Tool]] = []
|
|
seen_sideloads: set[str] = set()
|
|
for spec in specs:
|
|
lowered = spec.lower()
|
|
if lowered in seen_sideloads:
|
|
continue
|
|
seen_sideloads.add(lowered)
|
|
try:
|
|
tool_instance = build_tool(spec)
|
|
except RuntimeError as exc:
|
|
print(f"[Tool] Failed to load sideload '{spec}': {exc}", file=sys.stderr)
|
|
continue
|
|
sideload_tools.append((spec, tool_instance))
|
|
return sideload_tools
|
|
|
|
|
|
def _prime_agent(agent: Agent, output: str) -> None:
|
|
if not output:
|
|
return
|
|
observe = getattr(agent, "observe", None)
|
|
if callable(observe):
|
|
try:
|
|
observe(output)
|
|
except Exception as exc: # pragma: no cover - defensive
|
|
print(f"[Agent] observe failed: {exc}", file=sys.stderr)
|
|
|
|
|
|
def _join_threads(threads: list[Thread], *, timeout: float = 1.0) -> None:
|
|
for thread in threads:
|
|
thread.join(timeout=timeout)
|
|
|
|
|
|
def main() -> int:
|
|
load_env_file()
|
|
|
|
connection = read_connection_settings()
|
|
tool_settings = read_tool_settings()
|
|
|
|
sideload_tools = _build_sideload_tools(tool_settings.sideload_specs)
|
|
|
|
state = SessionState()
|
|
stop_event = Event()
|
|
tool_thread: Optional[Thread] = None
|
|
tool: Optional[Tool] = None
|
|
ephemeral_tools: list[Thread] = []
|
|
sidecar_threads: list[Thread] = []
|
|
agent_threads: list[Thread] = []
|
|
|
|
with TelnetClient(host=connection.host, port=connection.port, timeout=10.0) as client:
|
|
login(
|
|
client,
|
|
user=connection.user,
|
|
password=connection.password,
|
|
login_prompt=connection.login_prompt,
|
|
state=state,
|
|
)
|
|
|
|
if tool_settings.tool_mode:
|
|
tool = build_tool(tool_settings.tool_spec)
|
|
tool_thread = _start_tool_thread(client, state, tool, stop_event)
|
|
|
|
if sideload_tools:
|
|
for sidecar_spec, sidecar_tool in sideload_tools:
|
|
thread = _start_tool_thread(client, state, sidecar_tool, stop_event)
|
|
sidecar_threads.append(thread)
|
|
print(
|
|
f"[Tool] Sideloading '{sidecar_spec}' ({sidecar_tool.__class__.__name__})"
|
|
)
|
|
|
|
def run_ephemeral_tool(spec: str) -> None:
|
|
spec = spec.strip()
|
|
if not spec:
|
|
print("[Tool] Usage: #execute <tool_spec>")
|
|
return
|
|
try:
|
|
temp_tool = build_tool(spec)
|
|
except RuntimeError as exc:
|
|
print(f"[Tool] Failed to load '{spec}': {exc}", file=sys.stderr)
|
|
return
|
|
thread = _start_tool_thread(
|
|
client,
|
|
state,
|
|
temp_tool,
|
|
stop_event,
|
|
auto_stop=True,
|
|
auto_stop_idle=AUTO_STOP_IDLE_SECONDS,
|
|
)
|
|
ephemeral_tools.append(thread)
|
|
print(f"[Tool] Executing {spec!r} once")
|
|
|
|
def run_ephemeral_agent(spec: str) -> None:
|
|
spec = spec.strip()
|
|
if not spec:
|
|
print("[Agent] Usage: #agent <agent_spec>")
|
|
return
|
|
try:
|
|
temp_agent = build_agent(spec, allowed_tools=TOOL_DESCRIPTIONS)
|
|
except RuntimeError as exc:
|
|
print(f"[Agent] Failed to configure '{spec}': {exc}", file=sys.stderr)
|
|
return
|
|
|
|
_prime_agent(temp_agent, state.snapshot_output())
|
|
|
|
def run_tool_instance(tool: Tool) -> bool:
|
|
run_tool_loop(
|
|
client,
|
|
state,
|
|
tool,
|
|
stop_event,
|
|
min_send_interval=DEFAULT_SEND_INTERVAL,
|
|
auto_stop=True,
|
|
auto_stop_idle=AUTO_STOP_IDLE_SECONDS,
|
|
)
|
|
_prime_agent(temp_agent, state.snapshot_output())
|
|
return True
|
|
|
|
thread = Thread(
|
|
target=run_agent,
|
|
args=(temp_agent,),
|
|
kwargs={
|
|
"build_tool": build_tool,
|
|
"run_tool": run_tool_instance,
|
|
"send_command": lambda cmd: state.send(client, cmd),
|
|
"stop_event": stop_event,
|
|
},
|
|
daemon=True,
|
|
)
|
|
agent_threads.append(thread)
|
|
print(f"[Agent] Executing {spec!r}")
|
|
thread.start()
|
|
|
|
interrupted = False
|
|
try:
|
|
interactive_session(
|
|
client,
|
|
state=state,
|
|
stop_event=stop_event,
|
|
exit_command=connection.exit_command,
|
|
tool_command=None if tool_settings.tool_mode else run_ephemeral_tool,
|
|
agent_command=run_ephemeral_agent,
|
|
)
|
|
except KeyboardInterrupt:
|
|
print()
|
|
interrupted = True
|
|
finally:
|
|
stop_event.set()
|
|
if tool_thread:
|
|
tool_thread.join(timeout=1.0)
|
|
_join_threads(sidecar_threads)
|
|
_join_threads(ephemeral_tools)
|
|
_join_threads(agent_threads)
|
|
|
|
if interrupted:
|
|
graceful_shutdown(client, connection.exit_command, state=state)
|
|
|
|
return 0
|
|
|
|
|
|
if __name__ == "__main__":
|
|
raise SystemExit(main())
|