feat: improved TUI
This commit is contained in:
parent
d49b31a3e4
commit
2a6953db9d
2 changed files with 274 additions and 117 deletions
|
|
@ -67,7 +67,10 @@ Python-based Telnet helper for connecting to MUD servers, handling login flows,
|
||||||
|
|
||||||
Add guidance text after the type and optional modifiers separated by `|`, e.g. `#agent intelligent explore carefully|model=mistral/mistral-large-2407|delay=2`. The agent only calls built-in tools (`look`, `move`, `movement`, `explore`, `communication`, `intelligentcommunication`) and refuses unknown names.
|
Add guidance text after the type and optional modifiers separated by `|`, e.g. `#agent intelligent explore carefully|model=mistral/mistral-large-2407|delay=2`. The agent only calls built-in tools (`look`, `move`, `movement`, `explore`, `communication`, `intelligentcommunication`) and refuses unknown names.
|
||||||
|
|
||||||
9. Prefer a TUI? Run `python textual_ui.py` to open a two-pane interface (MUD output on the left, agent output on the right) with an input box at the bottom. It accepts the same commands as the CLI (`#execute …`, `#agent …`, or raw MUD commands).
|
9. Prefer a TUI? Run `python textual_ui.py` to open a two-pane interface with a bottom input field:
|
||||||
|
- Left pane: live MUD stream (server output plus outgoing actions from you, tools, and agents).
|
||||||
|
- Right pane: LLM reasoning plus tool/agent diagnostic output.
|
||||||
|
It accepts the same commands as the CLI (`#execute …`, `#agent …`, or raw MUD commands).
|
||||||
|
|
||||||
## Environment Variables
|
## Environment Variables
|
||||||
|
|
||||||
|
|
|
||||||
378
textual_ui.py
378
textual_ui.py
|
|
@ -2,33 +2,39 @@ from __future__ import annotations
|
||||||
|
|
||||||
import io
|
import io
|
||||||
import os
|
import os
|
||||||
from contextlib import redirect_stdout, redirect_stderr
|
from contextlib import redirect_stderr, redirect_stdout
|
||||||
from threading import Event, Thread, current_thread
|
from threading import Event, Thread, current_thread
|
||||||
from typing import Callable
|
from typing import Callable, Optional
|
||||||
|
|
||||||
from textual.app import App, ComposeResult
|
from textual.app import App, ComposeResult
|
||||||
from textual.containers import Horizontal, Vertical
|
from textual.containers import Horizontal, Vertical
|
||||||
from textual.widgets import Footer, Header, Input, Label, Log
|
from textual.widgets import Footer, Header, Input, Label, Log, Static
|
||||||
|
|
||||||
from mud_env import load_env_file, read_connection_settings
|
|
||||||
from mud_session import SessionState, login, run_tool_loop
|
|
||||||
from mud_tools import TOOL_DESCRIPTIONS, build_tool
|
|
||||||
from agent_runtime import run_agent
|
from agent_runtime import run_agent
|
||||||
from agents import build_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, login, run_tool_loop
|
||||||
|
from mud_tools import TOOL_DESCRIPTIONS, build_tool
|
||||||
from telnetclient import TelnetClient
|
from telnetclient import TelnetClient
|
||||||
|
from tools import Tool
|
||||||
|
|
||||||
|
|
||||||
|
SEND_INTERVAL_SECONDS = 1.0
|
||||||
|
AUTO_STOP_IDLE_SECONDS = 2.0
|
||||||
|
|
||||||
|
|
||||||
class _QueueWriter(io.TextIOBase):
|
class _QueueWriter(io.TextIOBase):
|
||||||
|
"""Send captured stdout/stderr lines into a UI logger callback."""
|
||||||
|
|
||||||
def __init__(self, emit: Callable[[str], None]) -> None:
|
def __init__(self, emit: Callable[[str], None]) -> None:
|
||||||
super().__init__()
|
super().__init__()
|
||||||
self._emit = emit
|
self._emit = emit
|
||||||
self._buffer: str = ""
|
self._buffer = ""
|
||||||
|
|
||||||
def write(self, s: str) -> int: # type: ignore[override]
|
def write(self, s: str) -> int: # type: ignore[override]
|
||||||
self._buffer += s
|
self._buffer += s
|
||||||
while "\n" in self._buffer:
|
while "\n" in self._buffer:
|
||||||
line, self._buffer = self._buffer.split("\n", 1)
|
line, self._buffer = self._buffer.split("\n", 1)
|
||||||
if line:
|
|
||||||
self._emit(line)
|
self._emit(line)
|
||||||
return len(s)
|
return len(s)
|
||||||
|
|
||||||
|
|
@ -38,20 +44,83 @@ class _QueueWriter(io.TextIOBase):
|
||||||
self._buffer = ""
|
self._buffer = ""
|
||||||
|
|
||||||
|
|
||||||
|
class UISessionState(SessionState):
|
||||||
|
"""Session state that emits outgoing commands to the MUD pane."""
|
||||||
|
|
||||||
|
def __init__(self, emit_action: Callable[[str, str], None]) -> None:
|
||||||
|
super().__init__()
|
||||||
|
self._emit_action = emit_action
|
||||||
|
|
||||||
|
def send(self, client: TelnetClient, message: str) -> None:
|
||||||
|
self.send_with_source(client, message, source="YOU")
|
||||||
|
|
||||||
|
def send_with_source(self, client: TelnetClient, message: str, *, source: str) -> None:
|
||||||
|
super().send(client, message)
|
||||||
|
self._emit_action(source, message)
|
||||||
|
|
||||||
|
def tool_send(
|
||||||
|
self,
|
||||||
|
client: TelnetClient,
|
||||||
|
message: str,
|
||||||
|
*,
|
||||||
|
min_interval: float,
|
||||||
|
stop_event: Event,
|
||||||
|
) -> bool:
|
||||||
|
sent = super().tool_send(
|
||||||
|
client,
|
||||||
|
message,
|
||||||
|
min_interval=min_interval,
|
||||||
|
stop_event=stop_event,
|
||||||
|
)
|
||||||
|
if sent:
|
||||||
|
self._emit_action("TOOL", message)
|
||||||
|
return sent
|
||||||
|
|
||||||
|
|
||||||
class MudUI(App):
|
class MudUI(App):
|
||||||
CSS = """
|
CSS = """
|
||||||
Screen {
|
Screen {
|
||||||
layout: vertical;
|
layout: vertical;
|
||||||
}
|
}
|
||||||
#logs {
|
|
||||||
|
#main {
|
||||||
height: 1fr;
|
height: 1fr;
|
||||||
|
padding: 0 1;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
.pane {
|
||||||
|
width: 1fr;
|
||||||
|
height: 1fr;
|
||||||
|
padding: 0 1;
|
||||||
|
}
|
||||||
|
|
||||||
|
.pane-title {
|
||||||
|
text-style: bold;
|
||||||
|
padding: 1 0 0 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
#mud-log {
|
||||||
|
border: round #4c7a8f;
|
||||||
|
padding: 0 1;
|
||||||
|
}
|
||||||
|
|
||||||
|
#brain-log {
|
||||||
|
border: round #7d6f5a;
|
||||||
|
padding: 0 1;
|
||||||
|
}
|
||||||
|
|
||||||
#input-row {
|
#input-row {
|
||||||
padding: 1 2;
|
height: auto;
|
||||||
|
padding: 0 2 1 2;
|
||||||
}
|
}
|
||||||
Log {
|
|
||||||
border: round #888881;
|
#command {
|
||||||
padding: 1 1;
|
width: 1fr;
|
||||||
|
}
|
||||||
|
|
||||||
|
#status {
|
||||||
|
padding: 0 2;
|
||||||
|
color: #999999;
|
||||||
}
|
}
|
||||||
"""
|
"""
|
||||||
|
|
||||||
|
|
@ -61,37 +130,49 @@ class MudUI(App):
|
||||||
|
|
||||||
def compose(self) -> ComposeResult:
|
def compose(self) -> ComposeResult:
|
||||||
yield Header(show_clock=True)
|
yield Header(show_clock=True)
|
||||||
with Vertical(id="logs"):
|
with Horizontal(id="main"):
|
||||||
yield Label("MUD Output")
|
with Vertical(classes="pane"):
|
||||||
self.mud_log = Log(classes="mud")
|
yield Label("MUD Stream", classes="pane-title")
|
||||||
|
self.mud_log = Log(id="mud-log", auto_scroll=True)
|
||||||
yield self.mud_log
|
yield self.mud_log
|
||||||
yield Label("Agent Output")
|
with Vertical(classes="pane"):
|
||||||
self.agent_log = Log(classes="agent")
|
yield Label("LLM, Agent, and Tool Stream", classes="pane-title")
|
||||||
yield self.agent_log
|
self.brain_log = Log(id="brain-log", auto_scroll=True)
|
||||||
|
yield self.brain_log
|
||||||
with Horizontal(id="input-row"):
|
with Horizontal(id="input-row"):
|
||||||
self.input = Input(placeholder="Type command or #execute/#agent ...", id="command")
|
self.input = Input(
|
||||||
|
placeholder="Type MUD command, #execute <tool>, or #agent <spec>",
|
||||||
|
id="command",
|
||||||
|
)
|
||||||
yield self.input
|
yield self.input
|
||||||
|
self.status = Static("Disconnected", id="status")
|
||||||
|
yield self.status
|
||||||
yield Footer()
|
yield Footer()
|
||||||
|
|
||||||
def on_mount(self) -> None:
|
def on_mount(self) -> None:
|
||||||
self._stop_event = Event()
|
|
||||||
self._ui_thread = current_thread()
|
self._ui_thread = current_thread()
|
||||||
|
self._stop_event = Event()
|
||||||
|
self._worker_threads: list[Thread] = []
|
||||||
|
self._reader_thread: Optional[Thread] = None
|
||||||
|
|
||||||
load_env_file()
|
load_env_file()
|
||||||
connection = read_connection_settings()
|
connection = read_connection_settings()
|
||||||
|
tool_settings = read_tool_settings()
|
||||||
timeout = float(os.environ.get("MISTLE_TIMEOUT", "10"))
|
timeout = float(os.environ.get("MISTLE_TIMEOUT", "10"))
|
||||||
|
|
||||||
self.state = SessionState()
|
self._exit_command = connection.exit_command
|
||||||
self.client = TelnetClient(
|
self.state = UISessionState(self._log_action)
|
||||||
host=connection.host, port=connection.port, timeout=timeout
|
self.client = TelnetClient(host=connection.host, port=connection.port, timeout=timeout)
|
||||||
)
|
|
||||||
try:
|
try:
|
||||||
self.client.connect()
|
self.client.connect()
|
||||||
except Exception as exc: # pragma: no cover - network specific
|
except Exception as exc: # pragma: no cover - network specific
|
||||||
self.log_mud(f"[error] Failed to connect: {exc}")
|
self.log_mud(f"[error] Failed to connect: {exc}")
|
||||||
|
self._set_status("Connection failed")
|
||||||
return
|
return
|
||||||
|
|
||||||
writer = _QueueWriter(lambda line: self._emit_to_log(self.mud_log, line))
|
login_writer = _QueueWriter(self.log_mud)
|
||||||
with redirect_stdout(writer), redirect_stderr(writer):
|
with redirect_stdout(login_writer), redirect_stderr(login_writer):
|
||||||
login(
|
login(
|
||||||
self.client,
|
self.client,
|
||||||
user=connection.user,
|
user=connection.user,
|
||||||
|
|
@ -99,85 +180,100 @@ class MudUI(App):
|
||||||
login_prompt=connection.login_prompt,
|
login_prompt=connection.login_prompt,
|
||||||
state=self.state,
|
state=self.state,
|
||||||
)
|
)
|
||||||
writer.flush()
|
login_writer.flush()
|
||||||
|
|
||||||
self.reader_thread = Thread(target=self._reader_loop, daemon=True)
|
self._reader_thread = Thread(target=self._reader_loop, daemon=True, name="mud-reader")
|
||||||
self.reader_thread.start()
|
self._reader_thread.start()
|
||||||
|
|
||||||
|
if tool_settings.tool_mode:
|
||||||
|
self._launch_persistent_tool(tool_settings.tool_spec)
|
||||||
|
|
||||||
|
sideload_seen: set[str] = set()
|
||||||
|
for spec in tool_settings.sideload_specs:
|
||||||
|
lowered = spec.lower()
|
||||||
|
if lowered in sideload_seen:
|
||||||
|
continue
|
||||||
|
sideload_seen.add(lowered)
|
||||||
|
self._launch_persistent_tool(spec)
|
||||||
|
|
||||||
|
self._set_status(f"Connected to {connection.host}:{connection.port}")
|
||||||
self.input.focus()
|
self.input.focus()
|
||||||
self.log_mud(f"Connected to {connection.host}:{connection.port}")
|
self.log_mud(f"Connected to {connection.host}:{connection.port}")
|
||||||
|
|
||||||
def on_input_submitted(self, event: Input.Submitted) -> None:
|
|
||||||
command = event.value.strip()
|
|
||||||
event.input.value = ""
|
|
||||||
if not command:
|
|
||||||
return
|
|
||||||
if command.startswith("#execute"):
|
|
||||||
parts = command.split(maxsplit=1)
|
|
||||||
if len(parts) == 1:
|
|
||||||
self.log_agent("Usage: #execute <tool_spec>")
|
|
||||||
else:
|
|
||||||
self._start_tool(parts[1])
|
|
||||||
return
|
|
||||||
if command.startswith("#agent"):
|
|
||||||
parts = command.split(maxsplit=1)
|
|
||||||
if len(parts) == 1:
|
|
||||||
self.log_agent("Usage: #agent <agent_spec>")
|
|
||||||
else:
|
|
||||||
self._start_agent(parts[1])
|
|
||||||
return
|
|
||||||
self.state.send(self.client, command)
|
|
||||||
self.log_agent(f"> {command}")
|
|
||||||
|
|
||||||
def on_unmount(self) -> None:
|
def on_unmount(self) -> None:
|
||||||
self._stop_event.set()
|
self._stop_event.set()
|
||||||
|
|
||||||
|
try:
|
||||||
|
graceful_shutdown(self.client, self._exit_command, state=self.state)
|
||||||
|
except Exception:
|
||||||
|
pass
|
||||||
|
|
||||||
|
if self._reader_thread:
|
||||||
|
self._reader_thread.join(timeout=1.0)
|
||||||
|
for thread in self._worker_threads:
|
||||||
|
thread.join(timeout=1.0)
|
||||||
|
|
||||||
try:
|
try:
|
||||||
self.client.close()
|
self.client.close()
|
||||||
except Exception:
|
except Exception:
|
||||||
pass
|
pass
|
||||||
|
|
||||||
def _emit_to_log(self, log: Log, message: str) -> None:
|
def on_input_submitted(self, event: Input.Submitted) -> None:
|
||||||
if current_thread() is self._ui_thread:
|
raw = event.value
|
||||||
log.write(message)
|
event.input.value = ""
|
||||||
|
command = raw.strip()
|
||||||
|
if not command:
|
||||||
|
return
|
||||||
|
|
||||||
|
if command.startswith("#execute"):
|
||||||
|
parts = command.split(maxsplit=1)
|
||||||
|
if len(parts) == 1:
|
||||||
|
self.log_brain("[Tool] Usage: #execute <tool_spec>")
|
||||||
else:
|
else:
|
||||||
log.write(message)
|
self._start_tool(parts[1])
|
||||||
|
return
|
||||||
|
|
||||||
def log_mud(self, message: str) -> None:
|
if command.startswith("#agent"):
|
||||||
self._emit_to_log(self.mud_log, message)
|
parts = command.split(maxsplit=1)
|
||||||
|
if len(parts) == 1:
|
||||||
|
self.log_brain("[Agent] Usage: #agent <agent_spec>")
|
||||||
|
else:
|
||||||
|
self._start_agent(parts[1])
|
||||||
|
return
|
||||||
|
|
||||||
def log_agent(self, message: str) -> None:
|
self.state.send(self.client, command)
|
||||||
self._emit_to_log(self.agent_log, message)
|
|
||||||
|
|
||||||
def _reader_loop(self) -> None:
|
def _reader_loop(self) -> None:
|
||||||
while not self._stop_event.is_set():
|
while not self._stop_event.is_set():
|
||||||
data = self.client.receive(timeout=0.3)
|
data = self.client.receive(timeout=0.3)
|
||||||
if data:
|
if not data:
|
||||||
|
continue
|
||||||
self.state.update_output(data)
|
self.state.update_output(data)
|
||||||
self._emit_to_log(self.mud_log, data)
|
self.log_mud(data)
|
||||||
|
|
||||||
def _wrap_run(self, func: Callable[[], None]) -> Thread:
|
def _start_worker(self, fn: Callable[[], None], *, name: str) -> Thread:
|
||||||
def runner() -> None:
|
def runner() -> None:
|
||||||
writer = _QueueWriter(lambda line: self._emit_to_log(self.agent_log, line))
|
writer = _QueueWriter(self.log_brain)
|
||||||
with redirect_stdout(writer), redirect_stderr(writer):
|
with redirect_stdout(writer), redirect_stderr(writer):
|
||||||
func()
|
fn()
|
||||||
writer.flush()
|
writer.flush()
|
||||||
|
|
||||||
thread = Thread(target=runner, daemon=True)
|
thread = Thread(target=runner, daemon=True, name=name)
|
||||||
thread.start()
|
thread.start()
|
||||||
|
self._worker_threads.append(thread)
|
||||||
return thread
|
return thread
|
||||||
|
|
||||||
def _start_tool(self, raw_spec: str) -> None:
|
def _launch_persistent_tool(self, spec: str) -> None:
|
||||||
spec = raw_spec.strip()
|
clean_spec = spec.strip()
|
||||||
if not spec:
|
if not clean_spec:
|
||||||
self.log_agent("Usage: #execute <tool_spec>")
|
|
||||||
return
|
return
|
||||||
self.log_agent(f"[Tool] Executing {spec!r}")
|
self.log_brain(f"[Tool] Starting persistent tool {clean_spec!r}")
|
||||||
|
|
||||||
def worker() -> None:
|
def worker() -> None:
|
||||||
try:
|
try:
|
||||||
tool = build_tool(spec)
|
tool = build_tool(clean_spec)
|
||||||
except RuntimeError as exc:
|
except RuntimeError as exc:
|
||||||
print(f"[Agent] Failed to load tool {spec}: {exc}")
|
print(f"[Tool] Failed to load '{clean_spec}': {exc}")
|
||||||
return
|
return
|
||||||
|
|
||||||
run_tool_loop(
|
run_tool_loop(
|
||||||
|
|
@ -185,22 +281,53 @@ class MudUI(App):
|
||||||
self.state,
|
self.state,
|
||||||
tool,
|
tool,
|
||||||
self._stop_event,
|
self._stop_event,
|
||||||
min_send_interval=1.0,
|
min_send_interval=SEND_INTERVAL_SECONDS,
|
||||||
auto_stop=True,
|
auto_stop=False,
|
||||||
auto_stop_idle=2.0,
|
|
||||||
)
|
)
|
||||||
|
|
||||||
self._wrap_run(worker)
|
self._start_worker(worker, name=f"tool-{clean_spec}")
|
||||||
|
|
||||||
|
def _start_tool(self, raw_spec: str) -> None:
|
||||||
|
spec = raw_spec.strip()
|
||||||
|
if not spec:
|
||||||
|
self.log_brain("[Tool] Usage: #execute <tool_spec>")
|
||||||
|
return
|
||||||
|
|
||||||
|
self.log_brain(f"[Tool] Executing {spec!r}")
|
||||||
|
|
||||||
|
def worker() -> None:
|
||||||
|
try:
|
||||||
|
tool = build_tool(spec)
|
||||||
|
except RuntimeError as exc:
|
||||||
|
print(f"[Tool] Failed to load '{spec}': {exc}")
|
||||||
|
return
|
||||||
|
|
||||||
|
run_tool_loop(
|
||||||
|
self.client,
|
||||||
|
self.state,
|
||||||
|
tool,
|
||||||
|
self._stop_event,
|
||||||
|
min_send_interval=SEND_INTERVAL_SECONDS,
|
||||||
|
auto_stop=True,
|
||||||
|
auto_stop_idle=AUTO_STOP_IDLE_SECONDS,
|
||||||
|
)
|
||||||
|
|
||||||
|
self._start_worker(worker, name=f"ephemeral-tool-{spec}")
|
||||||
|
|
||||||
def _start_agent(self, raw_spec: str) -> None:
|
def _start_agent(self, raw_spec: str) -> None:
|
||||||
spec = raw_spec.strip()
|
spec = raw_spec.strip()
|
||||||
if not spec:
|
if not spec:
|
||||||
self.log_agent("Usage: #agent <agent_spec>")
|
self.log_brain("[Agent] Usage: #agent <agent_spec>")
|
||||||
return
|
return
|
||||||
self.log_agent(f"[Agent] Executing {spec!r}")
|
|
||||||
|
|
||||||
def build(spec_str: str) -> Tool:
|
try:
|
||||||
return build_tool(spec_str)
|
agent = build_agent(spec, allowed_tools=TOOL_DESCRIPTIONS)
|
||||||
|
except RuntimeError as exc:
|
||||||
|
self.log_brain(f"[Agent] Failed to configure '{spec}': {exc}")
|
||||||
|
return
|
||||||
|
|
||||||
|
self.log_brain(f"[Agent] Executing {spec!r}")
|
||||||
|
self._prime_agent(agent)
|
||||||
|
|
||||||
def run_tool_instance(tool: Tool) -> bool:
|
def run_tool_instance(tool: Tool) -> bool:
|
||||||
run_tool_loop(
|
run_tool_loop(
|
||||||
|
|
@ -208,47 +335,74 @@ class MudUI(App):
|
||||||
self.state,
|
self.state,
|
||||||
tool,
|
tool,
|
||||||
self._stop_event,
|
self._stop_event,
|
||||||
min_send_interval=1.0,
|
min_send_interval=SEND_INTERVAL_SECONDS,
|
||||||
auto_stop=True,
|
auto_stop=True,
|
||||||
auto_stop_idle=2.0,
|
auto_stop_idle=AUTO_STOP_IDLE_SECONDS,
|
||||||
)
|
)
|
||||||
output_after = self.state.snapshot_output()
|
self._prime_agent(agent)
|
||||||
if output_after:
|
|
||||||
observe = getattr(agent, "observe", None)
|
|
||||||
if callable(observe):
|
|
||||||
try:
|
|
||||||
observe(output_after)
|
|
||||||
except Exception as exc: # pragma: no cover
|
|
||||||
print(f"[Agent] observe failed: {exc}")
|
|
||||||
return True
|
return True
|
||||||
|
|
||||||
def send_command(command: str) -> None:
|
def send_command(command: str) -> None:
|
||||||
self.state.send(self.client, command)
|
self.state.send_with_source(self.client, command, source="AGENT")
|
||||||
self._emit_to_log(self.agent_log, f"[Agent] command: {command}")
|
self.log_brain(f"[Agent] command: {command}")
|
||||||
|
|
||||||
try:
|
def run() -> None:
|
||||||
agent = build_agent(spec, allowed_tools=TOOL_DESCRIPTIONS)
|
run_agent(
|
||||||
except RuntimeError as exc:
|
|
||||||
self.log_agent(f"[Agent] Failed to configure '{spec}': {exc}")
|
|
||||||
return
|
|
||||||
|
|
||||||
last_output = self.state.snapshot_output()
|
|
||||||
observe = getattr(agent, "observe", None)
|
|
||||||
if last_output and callable(observe):
|
|
||||||
try:
|
|
||||||
observe(last_output)
|
|
||||||
except Exception as exc: # pragma: no cover
|
|
||||||
self.log_agent(f"[Agent] observe failed: {exc}")
|
|
||||||
|
|
||||||
self._wrap_run(
|
|
||||||
lambda: run_agent(
|
|
||||||
agent,
|
agent,
|
||||||
build_tool=build,
|
build_tool=build_tool,
|
||||||
run_tool=run_tool_instance,
|
run_tool=run_tool_instance,
|
||||||
send_command=send_command,
|
send_command=send_command,
|
||||||
stop_event=self._stop_event,
|
stop_event=self._stop_event,
|
||||||
)
|
)
|
||||||
)
|
|
||||||
|
self._start_worker(run, name=f"agent-{spec}")
|
||||||
|
|
||||||
|
def _prime_agent(self, agent: Agent) -> None:
|
||||||
|
last_output = self.state.snapshot_output()
|
||||||
|
if not last_output:
|
||||||
|
return
|
||||||
|
observe = getattr(agent, "observe", None)
|
||||||
|
if not callable(observe):
|
||||||
|
return
|
||||||
|
try:
|
||||||
|
observe(last_output)
|
||||||
|
except Exception as exc: # pragma: no cover - defensive guard
|
||||||
|
self.log_brain(f"[Agent] observe failed: {exc}")
|
||||||
|
|
||||||
|
def _set_status(self, text: str) -> None:
|
||||||
|
def update() -> None:
|
||||||
|
self.status.update(text)
|
||||||
|
|
||||||
|
if current_thread() is self._ui_thread:
|
||||||
|
update()
|
||||||
|
return
|
||||||
|
try:
|
||||||
|
self.call_from_thread(update)
|
||||||
|
except RuntimeError:
|
||||||
|
pass
|
||||||
|
|
||||||
|
def _write_log(self, log: Log, message: str) -> None:
|
||||||
|
def update() -> None:
|
||||||
|
normalized = message.replace("\r\n", "\n").replace("\r", "\n")
|
||||||
|
for line in normalized.split("\n"):
|
||||||
|
log.write_line(line)
|
||||||
|
|
||||||
|
if current_thread() is self._ui_thread:
|
||||||
|
update()
|
||||||
|
return
|
||||||
|
try:
|
||||||
|
self.call_from_thread(update)
|
||||||
|
except RuntimeError:
|
||||||
|
pass
|
||||||
|
|
||||||
|
def _log_action(self, source: str, message: str) -> None:
|
||||||
|
self.log_mud(f"[{source}] {message}")
|
||||||
|
|
||||||
|
def log_mud(self, message: str) -> None:
|
||||||
|
self._write_log(self.mud_log, message)
|
||||||
|
|
||||||
|
def log_brain(self, message: str) -> None:
|
||||||
|
self._write_log(self.brain_log, message)
|
||||||
|
|
||||||
|
|
||||||
if __name__ == "__main__":
|
if __name__ == "__main__":
|
||||||
|
|
|
||||||
Loading…
Add table
Reference in a new issue