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

This commit is contained in:
wehub-resource-sync
2026-07-13 13:10:45 +08:00
commit 4b6817381b
3933 changed files with 525247 additions and 0 deletions
@@ -0,0 +1,13 @@
"""Core runtime engine for the interactive shell.
Reusable session state lives in ``core.agent_harness.session`` and terminal runtime
context lives in ``interactive_shell.runtime.context``. This package owns the
remaining runtime engine concerns (mutable runtime state, prompt manager,
turn detection).
"""
from __future__ import annotations
from platform.common.task_registry import TaskRegistry
__all__ = ["TaskRegistry"]
@@ -0,0 +1,39 @@
"""Prompt-mediated confirmation waiting for interactive-shell turns.
A turn worker thread parks here while it waits for the user to answer a
confirmation prompt rendered by the REPL prompt loop. The wait is
cancel-safe: it polls the dispatch cancel event and raises
:class:`DispatchCancelled` instead of silently auto-confirming.
"""
from __future__ import annotations
import threading
from surfaces.interactive_shell.runtime.core.state import PROMPT_REFRESH_INTERVAL_S, ReplState
class DispatchCancelled(Exception):
"""Raised when in-flight dispatch is cancelled during confirmation."""
def request_confirmation_via_prompt(state: ReplState, prompt_text: str) -> str:
response_event = threading.Event()
state.begin_confirmation(response_event, prompt_text)
try:
while not response_event.is_set():
cancel = state.current_cancel_event
if cancel is not None and cancel.is_set():
raise DispatchCancelled("cancelled while awaiting confirmation")
response_event.wait(timeout=PROMPT_REFRESH_INTERVAL_S)
if not state.confirm_response:
raise DispatchCancelled("cancelled while awaiting confirmation")
return state.confirm_response[0]
finally:
state.clear_confirmation()
__all__ = [
"DispatchCancelled",
"request_confirmation_via_prompt",
]
@@ -0,0 +1,128 @@
"""Prompt lifecycle and rendering glue for the interactive REPL loop."""
from __future__ import annotations
import asyncio
from collections.abc import Callable
from prompt_toolkit import PromptSession
from prompt_toolkit.application import Application
from prompt_toolkit.formatted_text import ANSI
from rich.console import Console
from surfaces.interactive_shell.runtime.core.state import (
PROMPT_REFRESH_INTERVAL_S,
ReplState,
SpinnerState,
)
from surfaces.interactive_shell.session import Session
from surfaces.interactive_shell.ui import input_prompt
from surfaces.interactive_shell.ui.components.cpr_stdin import (
drain_stale_cpr_bytes,
strip_cpr_sequences,
)
from surfaces.interactive_shell.ui.input_prompt import rendering as prompt_rendering
from surfaces.interactive_shell.ui.input_prompt.key_bindings import (
build_cancel_key_bindings,
install_session_key_bindings,
)
from surfaces.interactive_shell.ui.input_prompt.refresh import wire_prompt_refresh
from surfaces.interactive_shell.ui.input_prompt.style import refresh_prompt_theme
# Brief pause so a CPR reply still in flight lands in the stdin buffer before the
# non-blocking drain runs; without it the reply leaks into this prompt as literal bytes.
_CPR_SETTLE_SECONDS = 0.05
class PromptManager:
"""Own prompt-toolkit setup, prompt rendering, and prompt redraw hooks."""
def __init__(
self,
session: Session,
state: ReplState,
spinner: SpinnerState,
pt_session: PromptSession[str] | None = None,
) -> None:
self.session = session
self.state = state
self.spinner = spinner
self.pt_session = pt_session
self.pt_app: Application[str] | None = None
self.loop: asyncio.AbstractEventLoop | None = None
self._invalidate_prompt: Callable[[], None] | None = None
def setup(self) -> None:
if self.pt_session is None:
self.pt_session = input_prompt.build_prompt_session(self.session)
self.session.terminal.prompt_history_backend = self.pt_session.history
cancel_kb = build_cancel_key_bindings(self.state)
install_session_key_bindings(self.pt_session, cancel_kb)
self.pt_app = self.pt_session.app
self.loop = asyncio.get_running_loop()
self.session.terminal.prompt_app = self.pt_app
self.session.terminal.main_loop = self.loop
self.state.bind_loop(self.loop)
self._invalidate_prompt = wire_prompt_refresh(self.session, self.pt_app, self.loop)
@property
def invalidate_prompt(self) -> Callable[[], None]:
if self._invalidate_prompt is None:
raise RuntimeError("PromptManager.setup() must run before prompt invalidation")
return self._invalidate_prompt
def request_exit(self) -> None:
if self.pt_app is None or self.loop is None:
self.state.request_exit()
return
self.state.request_exit()
def _exit_prompt_app(attempts_left: int = 5) -> None:
if self.pt_app is not None and self.pt_app.is_running:
self.pt_app.exit()
return
if attempts_left > 0 and self.loop is not None:
self.loop.call_later(0.02, _exit_prompt_app, attempts_left - 1)
self.loop.call_soon_threadsafe(_exit_prompt_app)
def message_with_spinner(self) -> ANSI:
base = prompt_rendering._prompt_message(self.session).value
if self.state.is_awaiting_confirmation():
confirm_text = self.state.confirm_prompt_text
return ANSI(f"{confirm_text}\n{base}")
prefix = strip_cpr_sequences(
prompt_rendering.resolve_prompt_prefix_ansi(
inline_spinner=self.spinner.inline_spinner_ansi(),
idle_hint=prompt_rendering.resolve_idle_hint_ansi(self.session),
)
)
return ANSI(f"{prefix}\n{base}")
async def read_prompt_text(self) -> str:
if self.pt_session is None:
raise RuntimeError("PromptManager.setup() must run before reading prompts")
if self.session.terminal.pending_theme_refresh:
self.session.terminal.pending_theme_refresh = False
refresh_prompt_theme(self.session)
await asyncio.sleep(_CPR_SETTLE_SECONDS)
drain_stale_cpr_bytes()
prefilled = self.session.terminal.pop_pending_prompt_default()
if prefilled and self.session.terminal.pop_pending_autosubmit():
return prefilled
return await self.pt_session.prompt_async(
message=self.message_with_spinner,
bottom_toolbar=self.spinner.toolbar_ansi,
refresh_interval=PROMPT_REFRESH_INTERVAL_S,
placeholder=lambda: prompt_rendering.resolve_prompt_placeholder(self.session),
default=prefilled,
)
def render_submitted_prompt(self, console: Console, text: str) -> None:
prompt_rendering.render_submitted_prompt(console, self.session, text)
@@ -0,0 +1,268 @@
"""State models for the interactive shell UI runtime."""
from __future__ import annotations
import asyncio
import enum
import random
import threading
import time
from dataclasses import dataclass, field
from prompt_toolkit.application.current import get_app_or_none
from platform.terminal import theme as ui_theme
from surfaces.interactive_shell.ui.components.token_format import (
_CHARS_PER_TOKEN,
format_token_count_short,
)
# How often prompt-toolkit refreshes prompt callbacks and confirmation polling.
PROMPT_REFRESH_INTERVAL_S = 0.25
class TurnPhase(enum.Enum):
"""Explicit lifecycle phase of the current interactive-shell turn.
``phase`` is the declared turn intent and is authoritative for the
confirmation and cancelling states. ``is_dispatch_running()`` remains
derived from the asyncio task (the runtime truth of the in-flight turn),
because a task can settle on its own without an explicit transition.
"""
IDLE = "idle"
DISPATCHING = "dispatching"
AWAITING_CONFIRMATION = "awaiting_confirmation"
CANCELLING = "cancelling"
@dataclass
class ReplState:
"""Shared runtime state for prompt loop, queue worker, and cancel handlers.
Single source of truth for the active dispatch task, cancellation event,
confirmation lifecycle, exit request, and the explicit ``TurnPhase``.
Mutate turn state through the transition methods below rather than poking
raw fields, so ``phase`` stays consistent with the cancellation primitives.
"""
queue: asyncio.Queue[str] = field(default_factory=asyncio.Queue)
current_task: asyncio.Task[None] | None = None
current_cancel_event: threading.Event | None = None
loop: asyncio.AbstractEventLoop | None = None
exit_requested: bool = False
confirm_event: threading.Event | None = None
confirm_response: list[str] = field(default_factory=list)
confirm_prompt_text: str = ""
phase: TurnPhase = TurnPhase.IDLE
def is_dispatch_running(self) -> bool:
return self.current_task is not None and not self.current_task.done()
def is_awaiting_confirmation(self) -> bool:
return self.phase is TurnPhase.AWAITING_CONFIRMATION
def is_cancelling(self) -> bool:
return self.phase is TurnPhase.CANCELLING
def deliver_confirmation(self, answer: str) -> None:
if self.confirm_event is None:
return
self.confirm_response.append(answer)
self.confirm_event.set()
def bind_loop(self, loop: asyncio.AbstractEventLoop) -> None:
self.loop = loop
def request_exit(self) -> None:
self.exit_requested = True
def begin_confirmation(self, event: threading.Event, prompt_text: str = "") -> None:
# Reset the response list BEFORE publishing ``confirm_event`` so a
# concurrent ``deliver_confirmation`` cannot have its answer clobbered.
# ``phase`` is set before the publish so a parked worker is observable
# as awaiting confirmation the instant the event is visible.
self.confirm_response = []
self.confirm_prompt_text = prompt_text
self.phase = TurnPhase.AWAITING_CONFIRMATION
self.confirm_event = event
def clear_confirmation(self) -> None:
self.confirm_event = None
self.confirm_response = []
self.confirm_prompt_text = ""
# Only a normal confirmation completion returns to dispatching/idle; a
# cancel in progress must keep its CANCELLING phase.
if self.phase is TurnPhase.AWAITING_CONFIRMATION:
self.phase = TurnPhase.DISPATCHING if self.is_dispatch_running() else TurnPhase.IDLE
def start_dispatch(self, *, task: asyncio.Task[None], cancel_event: threading.Event) -> None:
self.current_task = task
self.current_cancel_event = cancel_event
self.phase = TurnPhase.DISPATCHING
def attach_turn_task(self, task: asyncio.Task[None]) -> None:
"""Mark a queued turn task as the active dispatch (queue worker entry)."""
self.current_task = task
self.phase = TurnPhase.DISPATCHING
def attach_cancel_event(self, cancel_event: threading.Event) -> None:
"""Park a cancel event for a dispatch that has no asyncio task."""
self.current_cancel_event = cancel_event
self.phase = TurnPhase.DISPATCHING
def clear_current_task(self, task: asyncio.Task[None] | None = None) -> None:
if task is None or self.current_task is task:
self.current_task = None
self.phase = TurnPhase.IDLE
def finish_dispatch(self, cancel_event: threading.Event) -> None:
if self.current_cancel_event is cancel_event:
self.current_cancel_event = None
self.phase = TurnPhase.IDLE
def cancel_current_dispatch(self) -> None:
# Mark the cancel intent first, but only when there is something to
# cancel, so an idle no-op call does not leave a stale CANCELLING phase.
if (
self.current_cancel_event is not None
or self.confirm_event is not None
or self.is_dispatch_running()
):
self.phase = TurnPhase.CANCELLING
if self.current_cancel_event is not None:
self.current_cancel_event.set()
if self.confirm_event is not None:
self.confirm_event.set()
task = self.current_task
if task is not None and not task.done():
if self.loop is not None:
self.loop.call_soon_threadsafe(task.cancel)
else:
task.cancel()
class SpinnerState:
"""Mutable state read by prompt callbacks for toolbar + inline spinner."""
_SPINNER_FRAMES = ("", "", "", "", "", "", "", "", "", "")
# One glyph advance per interval of *elapsed time*. The frame must be a
# pure function of the clock, never of how often the prompt message
# callback runs: prompt_toolkit evaluates the message several times per
# render pass (layout measurement + paint), so a per-call counter can land
# on the same frame every visible render and freeze the animation.
_FRAME_INTERVAL_S = 0.1
_THINKING_VERBS = (
"thinking",
"pondering",
"exploring",
"reasoning",
"considering",
"analysing",
"investigating",
"deliberating",
"ruminating",
"deducing",
"noodling",
)
def __init__(self) -> None:
self.streaming: bool = False
self.started_at: float = 0.0
self.bytes_in: int = 0
self._verb: str = self._THINKING_VERBS[0]
self.phase: str = ""
def start(self) -> None:
self.streaming = True
self.started_at = time.monotonic()
self.bytes_in = 0
self._verb = random.choice(self._THINKING_VERBS)
self.phase = ""
def set_phase(self, label: str) -> None:
"""Animate a caller-supplied phase label instead of a thinking verb.
Investigation stages (``/investigate``) dispatch deterministically, so
the turn-level "thinking" spinner never starts. The progress display
calls this to keep the prompt spinner cycling with the active pipeline
stage; it can be called repeatedly to advance the phase.
"""
if not self.streaming:
self.started_at = time.monotonic()
self._frame_idx = 0
self.streaming = True
self.phase = label
def stop(self) -> None:
self.streaming = False
self.phase = ""
def toolbar_ansi(self) -> str:
# Always return an empty string so prompt_toolkit's ConditionalContainer
# collapses the toolbar in every state. A visible toolbar causes
# prompt_toolkit to emit \033[6n (CPR) cursor-position queries on every
# refresh_interval; those responses leak into the vt100 input parser as
# literal keystrokes, corrupting the input field. Hiding the toolbar
# unconditionally also keeps its height at zero in both streaming and
# idle states, which prevents the one-row height delta that would cause
# prompt_toolkit to misplace the cursor and leave stale spinner lines on
# screen. Idle hints are surfaced through idle_hint_ansi() instead,
# which is rendered in the prompt message's reserved first line.
return ""
def idle_hint_ansi(self) -> str:
"""Dim hint line shown above the rule when no dispatch is running."""
hint = "/ for commands · ↑↓ history"
app = get_app_or_none()
if app is not None and app.current_buffer.text:
hint += " · esc to clear"
return f"{ui_theme.DIM_ANSI}{hint}{ui_theme.ANSI_RESET}"
def inline_spinner_ansi(self) -> str:
if not self.streaming:
return ""
elapsed = time.monotonic() - self.started_at
token_count = self.bytes_in // _CHARS_PER_TOKEN
frame_idx = int(elapsed / self._FRAME_INTERVAL_S)
glyph = self._SPINNER_FRAMES[frame_idx % len(self._SPINNER_FRAMES)]
if token_count > 0:
tokens_str = format_token_count_short(token_count)
suffix = f" ({elapsed:.0f}s · ↓ {tokens_str} tokens)"
else:
suffix = f" ({elapsed:.0f}s)"
label = self.phase or f"{self._verb}"
return (
f"{ui_theme.PROMPT_ACCENT_ANSI}{glyph} {label}{ui_theme.ANSI_RESET}"
f"{ui_theme.ANSI_DIM}{suffix} esc to cancel{ui_theme.ANSI_RESET}"
)
@dataclass(frozen=True)
class ReplMutableState:
"""Initial mutable state bundle shared by the interactive runtime."""
state: ReplState
spinner: SpinnerState
def create_repl_mutable_state(
*,
state: ReplState | None = None,
spinner: SpinnerState | None = None,
) -> ReplMutableState:
"""Return the canonical initial mutable state objects for a REPL runtime."""
return ReplMutableState(
state=state if state is not None else ReplState(),
spinner=spinner if spinner is not None else SpinnerState(),
)
__all__ = [
"PROMPT_REFRESH_INTERVAL_S",
"ReplMutableState",
"ReplState",
"SpinnerState",
"TurnPhase",
"create_repl_mutable_state",
]
@@ -0,0 +1,126 @@
"""Turn-result data model and the single owner of shell-turn accounting.
This module holds the shell's accounting side effects around the core
"facts only" turn-result models: action-agent analytics, terminal-turn
aggregate telemetry, prompt-recorder flushing, conversational-turn
persistence, and the final assistant-intent stamp.
"""
from __future__ import annotations
from dataclasses import dataclass
# The neutral "facts only" turn-result models live in the decoupled agent
# package; this module owns only the shell's accounting side effects over them.
from core.agent_harness.turns.turn_results import (
ShellTurnResult,
ToolCallingAccountingStatus,
ToolCallingTurnResult,
)
from platform.analytics.cli import capture_terminal_turn_summarized
from surfaces.interactive_shell.session import Session
from surfaces.interactive_shell.utils.telemetry import PromptRecorder
@dataclass
class ShellTurnAccounting:
"""Single owner of a shell turn's accounting side effects.
Separates "what happened" (decided by the turn flow) from "how it is
accounted for": action-agent analytics, terminal-turn aggregate telemetry,
prompt-recorder flushing, conversational-turn persistence, and the final
assistant-intent stamp.
"""
session: Session
text: str
recorder: PromptRecorder | None
def record_action_result(self, action_result: ToolCallingTurnResult) -> None:
"""Emit action-agent analytics and update terminal-turn aggregates."""
self._record_action_analytics(action_result)
self._record_terminal_turn(action_result)
def finalize(self, result: ShellTurnResult) -> ShellTurnResult:
"""Flush the recorder, persist the turn, and stamp the session intent."""
self._flush_prompt_recorder(result)
if result.llm_run is not None:
self.session.record("cli_agent", self.text)
self.session.last_assistant_intent = result.final_intent
return result
def _record_action_analytics(self, action_result: ToolCallingTurnResult) -> None:
from platform.analytics.cli import (
capture_repl_execution_policy_decision,
capture_terminal_actions_executed,
capture_terminal_actions_planned,
)
if action_result.accounting_status == "not_run":
capture_terminal_actions_executed(
planned_count=0,
executed_count=0,
executed_success_count=0,
)
return
capture_terminal_actions_planned(
planned_count=action_result.planned_count,
has_unhandled_clause=action_result.has_unhandled_clause,
)
capture_repl_execution_policy_decision(
{
"policy_stage": "shell_action_agent",
"policy_trace": (
"agent_tool_calls" if action_result.planned_count else "assistant_handoff"
),
"planned_count": action_result.planned_count,
"has_unhandled_clause": action_result.has_unhandled_clause,
}
)
capture_terminal_actions_executed(
planned_count=action_result.planned_count,
executed_count=action_result.executed_count,
executed_success_count=action_result.executed_success_count,
)
def _record_terminal_turn(self, action_result: ToolCallingTurnResult) -> None:
fallback_to_llm = not action_result.handled
snapshot = self.session.terminal.metrics.record_turn(
executed_count=action_result.executed_count,
executed_success_count=action_result.executed_success_count,
fallback_to_llm=fallback_to_llm,
)
capture_terminal_turn_summarized(
planned_count=action_result.planned_count,
executed_count=action_result.executed_count,
executed_success_count=action_result.executed_success_count,
fallback_to_llm=fallback_to_llm,
session_turn_index=snapshot.turn_index,
session_fallback_count=snapshot.fallback_count,
session_action_success_percent=snapshot.action_success_percent,
session_fallback_rate_percent=snapshot.fallback_rate_percent,
)
def _flush_prompt_recorder(self, result: ShellTurnResult) -> None:
# Pending turn LLM/error state is consumed unconditionally so a turn
# that stages it can never leak it into a later turn's flush.
pending_run = self.session.terminal.pop_pending_turn_llm()
pending_error = self.session.terminal.pop_pending_turn_error()
if self.recorder is None:
return
if pending_error is not None:
self.recorder.set_error(pending_error[0], pending_error[1])
self.recorder.set_response(
result.assistant_response_text,
result.llm_run if result.llm_run is not None else pending_run,
)
self.recorder.flush()
__all__ = [
"ShellTurnAccounting",
"ShellTurnResult",
"ToolCallingAccountingStatus",
"ToolCallingTurnResult",
]
@@ -0,0 +1,47 @@
"""Pure text classifiers for interactive-shell prompt turns."""
from __future__ import annotations
import re
_INTERVENTION_CORRECTION_RE = re.compile(
r"("
r"no(?=[,.!?]|$)"
r"|nope\b"
r"|nvm\b"
r"|nevermind\b|never\s*mind\b"
r"|wrong\b"
r"|wait(?=[,.!?]|$)"
r"|stop(?=[,.!?]|$)"
r"|actually\b"
r"|scratch\s+that\b"
r"|instead(?=[,.!?]|$)"
r"|(?:let'?s\s+)?do\s+[^.\n]{1,60}\s+instead\b"
r"|try\s+[^.\n]{1,60}\s+instead\b"
r")",
re.IGNORECASE,
)
_CONFIRMATION_TOKENS: frozenset[str] = frozenset({"", "y", "yes", "n", "no"})
_CANCEL_REQUEST_TOKENS: frozenset[str] = frozenset({"/cancel", "/stop", "/abort"})
def looks_like_confirmation_answer(text: str | None) -> bool:
return (text or "").strip().lower() in _CONFIRMATION_TOKENS
def looks_like_cancel_request(text: str | None) -> bool:
return (text or "").strip().lower() in _CANCEL_REQUEST_TOKENS
def looks_like_correction(text: str) -> bool:
stripped = text.lstrip()
if not stripped or stripped.startswith("```"):
return False
return _INTERVENTION_CORRECTION_RE.match(stripped[:80]) is not None
__all__ = [
"looks_like_cancel_request",
"looks_like_confirmation_answer",
"looks_like_correction",
]