chore: import upstream snapshot with attribution
CI (OpenClaw E2E) / openclaw test (push) Has been cancelled
CI / coverage-report (push) Has been cancelled
CI / test-kubernetes (push) Has been cancelled
CI / should-run-thorough (push) Has been cancelled
CI / test-thorough (cloudwatch-demo) (push) Has been cancelled
CI / test-thorough (flink-ecs) (push) Has been cancelled
CI / test-thorough (upstream-lambda) (push) Has been cancelled
CI / test-thorough (prefect-ecs-fargate) (push) Has been cancelled
Release / build-binaries (zip, opensre.exe, onefile, windows-latest, windows-x64) (push) Has been cancelled
Benchmark image — build + push to ECR (any adapter) / build + push (push) Has been cancelled
CI / quality (ubuntu-latest) (push) Has been cancelled
CI / test (tools-runtime) (push) Has been cancelled
CI / test (e2e-general) (push) Has been cancelled
CI / test (cli-runtime) (push) Has been cancelled
CI / test (e2e-provider-and-openclaw) (push) Has been cancelled
CI / test (integrations-and-misc) (push) Has been cancelled
Release / verify (push) Has been cancelled
Release / build-python-dist (push) Has been cancelled
Release / build-binaries (tar.gz, opensre, onedir, macos-15-intel, darwin-x64) (push) Has been cancelled
Release / build-binaries (tar.gz, opensre, onedir, macos-latest, darwin-arm64) (push) Has been cancelled
Release / build-binaries (tar.gz, opensre, onedir, ubuntu-22.04, linux-x64) (push) Has been cancelled
Release / publish-release (push) Has been cancelled
Release / publish-main-release (push) Has been cancelled
Interactive Shell Live (PR + post-merge) / turn-checks (no-LLM) (push) Has been cancelled
CodeQL / Analyze (python) (push) Has been cancelled
Interactive Shell Live (PR + post-merge) / turn-live shard ${{ matrix.shard_index }} (push) Has been cancelled
Release / prepare (push) Has been cancelled
Release / build-binaries (tar.gz, opensre, onedir, ubuntu-22.04-arm, linux-arm64) (push) Has been cancelled
Synthetic Deterministic Tests / Synthetic offline (deterministic) (push) Has been cancelled
CI (OpenClaw E2E) / openclaw test (push) Has been cancelled
CI / coverage-report (push) Has been cancelled
CI / test-kubernetes (push) Has been cancelled
CI / should-run-thorough (push) Has been cancelled
CI / test-thorough (cloudwatch-demo) (push) Has been cancelled
CI / test-thorough (flink-ecs) (push) Has been cancelled
CI / test-thorough (upstream-lambda) (push) Has been cancelled
CI / test-thorough (prefect-ecs-fargate) (push) Has been cancelled
Release / build-binaries (zip, opensre.exe, onefile, windows-latest, windows-x64) (push) Has been cancelled
Benchmark image — build + push to ECR (any adapter) / build + push (push) Has been cancelled
CI / quality (ubuntu-latest) (push) Has been cancelled
CI / test (tools-runtime) (push) Has been cancelled
CI / test (e2e-general) (push) Has been cancelled
CI / test (cli-runtime) (push) Has been cancelled
CI / test (e2e-provider-and-openclaw) (push) Has been cancelled
CI / test (integrations-and-misc) (push) Has been cancelled
Release / verify (push) Has been cancelled
Release / build-python-dist (push) Has been cancelled
Release / build-binaries (tar.gz, opensre, onedir, macos-15-intel, darwin-x64) (push) Has been cancelled
Release / build-binaries (tar.gz, opensre, onedir, macos-latest, darwin-arm64) (push) Has been cancelled
Release / build-binaries (tar.gz, opensre, onedir, ubuntu-22.04, linux-x64) (push) Has been cancelled
Release / publish-release (push) Has been cancelled
Release / publish-main-release (push) Has been cancelled
Interactive Shell Live (PR + post-merge) / turn-checks (no-LLM) (push) Has been cancelled
CodeQL / Analyze (python) (push) Has been cancelled
Interactive Shell Live (PR + post-merge) / turn-live shard ${{ matrix.shard_index }} (push) Has been cancelled
Release / prepare (push) Has been cancelled
Release / build-binaries (tar.gz, opensre, onedir, ubuntu-22.04-arm, linux-arm64) (push) Has been cancelled
Synthetic Deterministic Tests / Synthetic offline (deterministic) (push) Has been cancelled
This commit is contained in:
@@ -0,0 +1,268 @@
|
||||
"""Top-level interactive-shell controller: prompt input, dispatch, and shutdown."""
|
||||
|
||||
from __future__ import annotations
|
||||
|
||||
import asyncio
|
||||
import logging
|
||||
import os
|
||||
from collections.abc import Iterator
|
||||
from contextlib import contextmanager
|
||||
|
||||
from prompt_toolkit import PromptSession
|
||||
from prompt_toolkit.patch_stdout import patch_stdout
|
||||
from rich.console import Console
|
||||
|
||||
from config.repl_config import ReplConfig
|
||||
from core.domain.alerts import inbox as _alert_inbox
|
||||
from surfaces.interactive_shell.runtime.background.workers import BackgroundTaskManager
|
||||
from surfaces.interactive_shell.runtime.context import (
|
||||
ReplRuntimeContext,
|
||||
create_repl_runtime_context,
|
||||
)
|
||||
from surfaces.interactive_shell.runtime.core.prompt_manager import PromptManager
|
||||
from surfaces.interactive_shell.runtime.core.state import (
|
||||
ReplState,
|
||||
SpinnerState,
|
||||
)
|
||||
from surfaces.interactive_shell.runtime.input import (
|
||||
PromptInputReader,
|
||||
)
|
||||
from surfaces.interactive_shell.runtime.input.actions import (
|
||||
CancelTurn,
|
||||
CloseShell,
|
||||
DeliverConfirmation,
|
||||
IgnoreInput,
|
||||
InputAction,
|
||||
SubmitTurn,
|
||||
)
|
||||
from surfaces.interactive_shell.runtime.turn_host import (
|
||||
AgentTurnRuntime,
|
||||
run_agent_turn,
|
||||
run_agent_turn_queue,
|
||||
run_input_loop,
|
||||
)
|
||||
from surfaces.interactive_shell.session import Session
|
||||
from surfaces.interactive_shell.ui import DIM
|
||||
|
||||
log = logging.getLogger(__name__)
|
||||
|
||||
|
||||
@contextmanager
|
||||
def _alert_listener_token(token: str | None) -> Iterator[None]:
|
||||
"""Install the configured alert token for this listener, restoring any prior value."""
|
||||
key = "OPENSRE_ALERT_LISTENER_TOKEN"
|
||||
previous = os.environ.get(key)
|
||||
try:
|
||||
if token:
|
||||
os.environ[key] = token
|
||||
else:
|
||||
os.environ.pop(key, None)
|
||||
yield
|
||||
finally:
|
||||
if previous is None:
|
||||
os.environ.pop(key, None)
|
||||
else:
|
||||
os.environ[key] = previous
|
||||
|
||||
|
||||
@contextmanager
|
||||
def _alert_listener(
|
||||
cfg: ReplConfig | None,
|
||||
console: Console,
|
||||
*,
|
||||
existing: _alert_inbox.AlertInbox | None = None,
|
||||
) -> Iterator[_alert_inbox.AlertInbox | None]:
|
||||
if existing is not None:
|
||||
yield existing
|
||||
return
|
||||
if cfg is None or not cfg.alert_listener_enabled:
|
||||
yield None
|
||||
return
|
||||
|
||||
from gateway.web_server import WebAppServerHandle, serve_webapp_in_thread
|
||||
|
||||
inbox: _alert_inbox.AlertInbox | None = None
|
||||
handle: WebAppServerHandle | None = None
|
||||
try:
|
||||
inbox = _alert_inbox.AlertInbox()
|
||||
_alert_inbox.set_current_inbox(inbox)
|
||||
with _alert_listener_token(cfg.alert_listener_token):
|
||||
handle = serve_webapp_in_thread(
|
||||
host=cfg.alert_listener_host,
|
||||
port=cfg.alert_listener_port,
|
||||
)
|
||||
console.print(f"[{DIM}]listening for alerts on http://{handle.bound_address}/alerts[/]")
|
||||
try:
|
||||
yield inbox
|
||||
finally:
|
||||
if handle is not None:
|
||||
handle.stop()
|
||||
_alert_inbox.set_current_inbox(None)
|
||||
except Exception as exc:
|
||||
log.warning("Alert listener could not start: %s — continuing without it.", exc)
|
||||
_alert_inbox.set_current_inbox(None)
|
||||
yield None
|
||||
|
||||
|
||||
def _resolve_runtime_context(
|
||||
session: Session | ReplRuntimeContext | None,
|
||||
*,
|
||||
state: ReplState | None,
|
||||
spinner: SpinnerState | None,
|
||||
pt_session: PromptSession[str] | None,
|
||||
inbox: _alert_inbox.AlertInbox | None,
|
||||
) -> ReplRuntimeContext:
|
||||
if isinstance(session, ReplRuntimeContext):
|
||||
if state is None and spinner is None and pt_session is None and inbox is None:
|
||||
return session
|
||||
return ReplRuntimeContext(
|
||||
session=session.session,
|
||||
state=state if state is not None else session.state,
|
||||
spinner=spinner if spinner is not None else session.spinner,
|
||||
pt_session=pt_session if pt_session is not None else session.pt_session,
|
||||
inbox=inbox if inbox is not None else session.inbox,
|
||||
)
|
||||
return create_repl_runtime_context(
|
||||
session,
|
||||
state=state,
|
||||
spinner=spinner,
|
||||
pt_session=pt_session,
|
||||
inbox=inbox,
|
||||
)
|
||||
|
||||
|
||||
class InteractiveShellController:
|
||||
"""Coordinate prompt input, queued dispatch, background workers, and shutdown."""
|
||||
|
||||
def __init__(
|
||||
self,
|
||||
session: Session | ReplRuntimeContext | None = None,
|
||||
*,
|
||||
config: ReplConfig | None = None,
|
||||
state: ReplState | None = None,
|
||||
spinner: SpinnerState | None = None,
|
||||
pt_session: PromptSession[str] | None = None,
|
||||
inbox: _alert_inbox.AlertInbox | None = None,
|
||||
console: Console | None = None,
|
||||
) -> None:
|
||||
self.runtime_context = _resolve_runtime_context(
|
||||
session,
|
||||
state=state,
|
||||
spinner=spinner,
|
||||
pt_session=pt_session,
|
||||
inbox=inbox,
|
||||
)
|
||||
self.config = config
|
||||
self.session = self.runtime_context.session
|
||||
self.inbox = self.runtime_context.inbox
|
||||
self.state = self.runtime_context.state
|
||||
self.spinner = self.runtime_context.spinner
|
||||
self.service_console = console or Console(
|
||||
highlight=False,
|
||||
force_terminal=True,
|
||||
color_system="truecolor",
|
||||
legacy_windows=False,
|
||||
)
|
||||
self.prompt = PromptManager(
|
||||
self.session,
|
||||
self.state,
|
||||
self.spinner,
|
||||
self.runtime_context.pt_session,
|
||||
)
|
||||
self.turn_runtime = AgentTurnRuntime(
|
||||
session=self.session,
|
||||
state=self.state,
|
||||
spinner=self.spinner,
|
||||
invalidate_prompt=lambda: self.prompt.invalidate_prompt(),
|
||||
request_exit=self.prompt.request_exit,
|
||||
)
|
||||
self.echo_console = Console(highlight=False, force_terminal=True, color_system="truecolor")
|
||||
self.input_reader = PromptInputReader(
|
||||
self.prompt,
|
||||
self.state,
|
||||
self.session,
|
||||
self.echo_console,
|
||||
)
|
||||
self.background: BackgroundTaskManager | None = None
|
||||
self.tasks: list[tuple[str, asyncio.Task[None]]] = []
|
||||
|
||||
async def start_interactive_shell(self) -> None:
|
||||
with _alert_listener(self.config, self.service_console, existing=self.inbox) as inbox:
|
||||
self.inbox = inbox
|
||||
self.runtime_context.inbox = inbox
|
||||
self._start_runtime_services()
|
||||
try:
|
||||
with patch_stdout(raw=True):
|
||||
# Main input loop: reads prompts and enqueues submitted turns
|
||||
# onto state.queue. The agent turns themselves run in
|
||||
# run_agent_turn_queue, started above in _start_runtime_services.
|
||||
await run_input_loop(
|
||||
state=self.state,
|
||||
session=self.session,
|
||||
background=self.background,
|
||||
input_reader=self.input_reader,
|
||||
echo_console=self.echo_console,
|
||||
handle_input_action=self._handle_input_action,
|
||||
)
|
||||
finally:
|
||||
await self._shutdown_runtime()
|
||||
|
||||
def _start_runtime_services(self) -> None:
|
||||
self.prompt.setup()
|
||||
self.background = BackgroundTaskManager(
|
||||
self.session,
|
||||
self.state,
|
||||
self.spinner,
|
||||
self.inbox,
|
||||
self.prompt.invalidate_prompt,
|
||||
)
|
||||
self.tasks = self.background.start_all(
|
||||
lambda: run_agent_turn_queue(
|
||||
state=self.state,
|
||||
run_turn=lambda text: run_agent_turn(self.turn_runtime, text),
|
||||
)
|
||||
)
|
||||
# Fleet sampler is lazy: /fleet triggers it on first live use.
|
||||
self.session.terminal.fleet_sampler_starter = self.background.ensure_fleet_sampler_started
|
||||
|
||||
async def _handle_input_action(self, action: InputAction) -> bool:
|
||||
match action:
|
||||
case IgnoreInput():
|
||||
return True
|
||||
case CloseShell():
|
||||
return False
|
||||
case CancelTurn(submitted_text=text):
|
||||
if text:
|
||||
self.prompt.render_submitted_prompt(self.echo_console, text)
|
||||
self.state.cancel_current_dispatch()
|
||||
return True
|
||||
case DeliverConfirmation(text=text):
|
||||
self.state.deliver_confirmation(text)
|
||||
return True
|
||||
case SubmitTurn(text=text, wait_until_idle=wait, warning=warning):
|
||||
if warning:
|
||||
self.echo_console.print(warning)
|
||||
self.prompt.render_submitted_prompt(self.echo_console, text)
|
||||
await self.state.queue.put(text)
|
||||
if wait:
|
||||
await self.state.queue.join()
|
||||
return True
|
||||
raise AssertionError(f"Unhandled input action: {action!r}")
|
||||
|
||||
async def _shutdown_runtime(self) -> None:
|
||||
self.state.request_exit()
|
||||
self.state.cancel_current_dispatch()
|
||||
|
||||
for _label, task in self.tasks:
|
||||
task.cancel()
|
||||
|
||||
shutdown_results = await asyncio.gather(
|
||||
*(task for _label, task in self.tasks),
|
||||
return_exceptions=True,
|
||||
)
|
||||
for (label, _task), result in zip(self.tasks, shutdown_results, strict=True):
|
||||
if isinstance(result, Exception) and not isinstance(result, asyncio.CancelledError):
|
||||
log.debug("%s task shutdown raised exception: %s", label, result)
|
||||
|
||||
|
||||
__all__ = ["InteractiveShellController"]
|
||||
Reference in New Issue
Block a user