4b6817381b
Benchmark image — build + push to ECR (any adapter) / build + push (push) Waiting to run
CI / quality (ubuntu-latest) (push) Waiting to run
CI / test (tools-runtime) (push) Waiting to run
CI / test (e2e-general) (push) Waiting to run
CI / test (cli-runtime) (push) Waiting to run
CI / test (e2e-provider-and-openclaw) (push) Waiting to run
CI / test (integrations-and-misc) (push) Waiting to run
CI / coverage-report (push) Blocked by required conditions
CI / test-kubernetes (push) Waiting to run
CI / should-run-thorough (push) Waiting to run
CI / test-thorough (cloudwatch-demo) (push) Blocked by required conditions
CI / test-thorough (flink-ecs) (push) Blocked by required conditions
CI / test-thorough (upstream-lambda) (push) Blocked by required conditions
CI / test-thorough (prefect-ecs-fargate) (push) Blocked by required conditions
CodeQL / Analyze (python) (push) Waiting to run
Release / build-binaries (zip, opensre.exe, onefile, windows-latest, windows-x64) (push) Blocked by required conditions
Release / publish-release (push) Blocked by required conditions
Release / publish-main-release (push) Blocked by required conditions
Release / prepare (push) Waiting to run
Release / verify (push) Blocked by required conditions
Release / build-python-dist (push) Blocked by required conditions
Release / build-binaries (tar.gz, opensre, onedir, macos-15-intel, darwin-x64) (push) Blocked by required conditions
Release / build-binaries (tar.gz, opensre, onedir, macos-latest, darwin-arm64) (push) Blocked by required conditions
Release / build-binaries (tar.gz, opensre, onedir, ubuntu-22.04, linux-x64) (push) Blocked by required conditions
Release / build-binaries (tar.gz, opensre, onedir, ubuntu-22.04-arm, linux-arm64) (push) Blocked by required conditions
Synthetic Deterministic Tests / Synthetic offline (deterministic) (push) Waiting to run
Interactive Shell Live (PR + post-merge) / turn-checks (no-LLM) (push) Waiting to run
Interactive Shell Live (PR + post-merge) / turn-live shard ${{ matrix.shard_index }} (push) Waiting to run
CI (OpenClaw E2E) / openclaw test (push) Has been cancelled
217 lines
9.5 KiB
Python
217 lines
9.5 KiB
Python
"""Interactive-shell (terminal) session facet.
|
|
|
|
Groups the shell-surface-only session state (prompt-toolkit, theme, background jobs,
|
|
metrics, per-turn analytics staging) that ``core``, ``gateway``, and ``tools``
|
|
consumers never touch. Composed onto :class:`~surfaces.interactive_shell.session.session.Session`
|
|
as ``session.terminal`` and always present (empty for non-shell sessions), so shell
|
|
code accesses fields without a None-guard.
|
|
|
|
Populated cluster-by-cluster as the #3690 split lands; theme is the first cluster.
|
|
"""
|
|
|
|
from __future__ import annotations
|
|
|
|
import threading
|
|
from collections.abc import Callable
|
|
from dataclasses import dataclass, field
|
|
from typing import TYPE_CHECKING, Any
|
|
|
|
from surfaces.interactive_shell.session.background_investigations import (
|
|
BackgroundInvestigationRecord,
|
|
BackgroundNotificationPreferences,
|
|
)
|
|
from surfaces.interactive_shell.session.terminal_metrics import TerminalMetrics
|
|
|
|
if TYPE_CHECKING:
|
|
from prompt_toolkit.history import History
|
|
|
|
|
|
@dataclass
|
|
class TerminalSession:
|
|
"""Shell-surface session state, composed onto ``Session`` for the interactive shell."""
|
|
|
|
active_theme_name: str = "green"
|
|
"""Interactive shell palette name for this REPL session (``/theme``, prompts)."""
|
|
|
|
pending_theme_refresh: bool = False
|
|
"""When True, apply the active palette to prompt-toolkit before the next prompt."""
|
|
|
|
trust_mode: bool = False
|
|
"""When True, confirmation prompts for elevated REPL actions are skipped."""
|
|
|
|
prompt_history_backend: History | None = None
|
|
"""The live ``prompt_toolkit.History`` object backing the input prompt.
|
|
|
|
Stored here so ``/history`` and ``/privacy`` slash commands can mutate its
|
|
``paused`` flag (when it is a ``RedactingFileHistory``) without needing access to
|
|
the ``PromptSession``."""
|
|
|
|
prompt_app: Any = None
|
|
"""The prompt-toolkit ``Application`` instance for this session.
|
|
|
|
Stored here (instead of accessed via ``get_app_or_none()``) so that worker-thread
|
|
slash commands (e.g. ``/theme``) can refresh styles via ``call_soon_threadsafe`` on
|
|
the main asyncio loop."""
|
|
|
|
main_loop: Any = None
|
|
"""The asyncio event loop for the main REPL coroutine.
|
|
|
|
Set once by ``InteractiveShellController.start_interactive_shell`` so worker-thread
|
|
code can schedule prompt-toolkit updates on the main thread."""
|
|
|
|
prompt_refresh_fn: Callable[[], None] | None = field(default=None, repr=False)
|
|
"""Loop-owned hook to apply pending prefill and redraw the active prompt."""
|
|
|
|
fleet_sampler_starter: Callable[[], None] | None = field(default=None, repr=False)
|
|
"""Loop-owned hook to lazily start the fleet sampler on first live ``/fleet`` use.
|
|
|
|
Set by the interactive-shell controller so the sampler (and its ``psutil`` dependency)
|
|
stays out of base REPL startup and only runs when fleet monitoring is actually
|
|
requested. Thread-safe: the starter marshals task creation onto the REPL event loop."""
|
|
|
|
pending_prompt_default: str | None = None
|
|
"""When set, the next interactive prompt is pre-filled with this string (then cleared)."""
|
|
|
|
pending_prompt_autosubmit: bool = False
|
|
"""When True alongside ``pending_prompt_default``, the prefilled prompt is
|
|
submitted automatically instead of waiting for the user to press Enter.
|
|
|
|
Used to auto-launch an interactive command the agent decided to run (e.g.
|
|
``/integrations setup sentry``) so it flows through the normal
|
|
exclusive-stdin dispatch path — the only place an interactive child process
|
|
gets clean stdin."""
|
|
|
|
exclusive_stdin_active: bool = False
|
|
"""True while a turn is running with exclusive stdin reserved (no live prompt).
|
|
|
|
Inline picker/wizard slash commands must dispatch immediately during these
|
|
turns instead of re-queueing via ``set_auto_command``, which would loop."""
|
|
|
|
agent_turn_executed_slashes: set[str] = field(default_factory=set, repr=False)
|
|
"""Slash command lines already executed during the current action-agent turn.
|
|
|
|
Prevents the tool-calling loop from re-dispatching the same literal slash
|
|
command when the model emits a duplicate ``slash_invoke`` on a later iteration."""
|
|
|
|
background_mode_enabled: bool = False
|
|
"""Whether new investigations should run as session-local background tasks."""
|
|
|
|
background_investigations: dict[str, BackgroundInvestigationRecord] = field(
|
|
default_factory=dict
|
|
)
|
|
"""Completed or in-flight background RCA summaries, keyed by task id."""
|
|
|
|
background_notification_preferences: BackgroundNotificationPreferences = field(
|
|
default_factory=BackgroundNotificationPreferences
|
|
)
|
|
"""Preferred notification channels for background RCA completion events."""
|
|
|
|
background_notices: list[str] = field(default_factory=list)
|
|
"""Thread-safe queue of Rich markup messages drained by the REPL main loop."""
|
|
|
|
_background_notices_lock: threading.Lock = field(
|
|
default_factory=threading.Lock, repr=False, compare=False
|
|
)
|
|
|
|
history_generation: int = 0
|
|
"""Incremented on /new so background synthetic watchers can skip stale history writes."""
|
|
|
|
metrics: TerminalMetrics = field(default_factory=TerminalMetrics)
|
|
"""Interactive-shell turn/intervention analytics counters (see ``/status``)."""
|
|
|
|
_turn_outcome_hint: str | None = field(default=None, repr=False, compare=False)
|
|
"""Optional structured outcome set by a terminal handler for analytics."""
|
|
|
|
_pending_turn_llm: Any | None = field(default=None, repr=False, compare=False)
|
|
"""LLM run metadata (an ``LlmRunInfo``) staged by a terminal handler for the
|
|
current turn's prompt-recorder flush. Consumed exactly once via
|
|
``pop_pending_turn_llm`` so it cannot leak into later turns."""
|
|
|
|
_pending_turn_error: tuple[str, str] | None = field(default=None, repr=False, compare=False)
|
|
"""Structured ``(error_kind, message)`` staged by a failing handler for the
|
|
current turn's prompt-recorder flush. Consumed exactly once via
|
|
``pop_pending_turn_error`` so it cannot leak into later turns."""
|
|
|
|
# ── behavior over the fields above (Session delegates via ``session.terminal``) ──
|
|
|
|
def pop_pending_prompt_default(self) -> str:
|
|
"""Return pre-filled text for the next prompt line, if any, and clear it."""
|
|
value = self.pending_prompt_default
|
|
self.pending_prompt_default = None
|
|
return value or ""
|
|
|
|
def pop_pending_autosubmit(self) -> bool:
|
|
"""Return whether the pending prefill should auto-submit, and clear the flag."""
|
|
value = self.pending_prompt_autosubmit
|
|
self.pending_prompt_autosubmit = False
|
|
return value
|
|
|
|
def set_auto_command(self, command: str) -> None:
|
|
"""Queue a command to run automatically on the next prompt iteration.
|
|
|
|
Prefills the input with ``command`` and marks it for auto-submit, then
|
|
refreshes the active prompt so the loop submits it without waiting for
|
|
Enter. Lets the agent launch an interactive command (setup/connect)
|
|
through the normal exclusive-stdin dispatch path rather than spawning it
|
|
mid-turn, where it would fight the live prompt for stdin.
|
|
"""
|
|
self.pending_prompt_default = command
|
|
self.pending_prompt_autosubmit = True
|
|
self.notify_prompt_changed()
|
|
|
|
def notify_prompt_changed(self) -> None:
|
|
"""Redraw the active prompt (placeholder state and pending prefill)."""
|
|
if self.prompt_refresh_fn is not None:
|
|
self.prompt_refresh_fn()
|
|
|
|
def ensure_fleet_sampler_started(self) -> None:
|
|
"""Request that the fleet sampler start (no-op if unwired or already running)."""
|
|
if self.fleet_sampler_starter is not None:
|
|
self.fleet_sampler_starter()
|
|
|
|
def enqueue_background_notice(self, message: str) -> None:
|
|
"""Queue a background-thread status line for the main REPL loop to print."""
|
|
with self._background_notices_lock:
|
|
self.background_notices.append(message)
|
|
self.notify_prompt_changed()
|
|
|
|
def drain_background_notices(self) -> list[str]:
|
|
"""Return and clear any queued background status lines."""
|
|
with self._background_notices_lock:
|
|
notices = list(self.background_notices)
|
|
self.background_notices.clear()
|
|
return notices
|
|
|
|
def set_turn_outcome_hint(self, hint: str | None) -> None:
|
|
"""Attach a structured outcome for the current terminal handler."""
|
|
self._turn_outcome_hint = hint.strip() if isinstance(hint, str) and hint.strip() else None
|
|
|
|
def pop_turn_outcome_hint(self) -> str | None:
|
|
"""Return and clear any structured outcome hint for this turn."""
|
|
hint = self._turn_outcome_hint
|
|
self._turn_outcome_hint = None
|
|
return hint
|
|
|
|
def set_pending_turn_llm(self, run: Any | None) -> None:
|
|
"""Stage LLM run metadata for this turn's prompt-recorder flush."""
|
|
self._pending_turn_llm = run
|
|
|
|
def pop_pending_turn_llm(self) -> Any | None:
|
|
"""Return and clear staged LLM run metadata for this turn."""
|
|
run = self._pending_turn_llm
|
|
self._pending_turn_llm = None
|
|
return run
|
|
|
|
def set_pending_turn_error(self, kind: str, message: str) -> None:
|
|
"""Stage a structured turn error for this turn's prompt-recorder flush."""
|
|
kind = kind.strip()
|
|
message = message.strip()
|
|
if kind or message:
|
|
self._pending_turn_error = (kind or "error", message)
|
|
|
|
def pop_pending_turn_error(self) -> tuple[str, str] | None:
|
|
"""Return and clear the staged structured turn error."""
|
|
error = self._pending_turn_error
|
|
self._pending_turn_error = None
|
|
return error
|