Files
wehub-resource-sync 4b6817381b
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
chore: import upstream snapshot with attribution
2026-07-13 13:10:45 +08:00

191 lines
7.2 KiB
Python

from __future__ import annotations
import time
from typing import TYPE_CHECKING, Any, Protocol, TypeGuard, runtime_checkable
from rich.text import Text
from surfaces.interactive_shell.ui.output.environment import _safe_print
if TYPE_CHECKING:
from surfaces.interactive_shell.ui.output.events import DisplayProtocol
from surfaces.interactive_shell.ui.output.repl_display import _ReplEventLogDisplay
from surfaces.interactive_shell.ui.components.time_format import _elapsed_hms, _fmt_timing
from surfaces.interactive_shell.ui.output.tool_details import (
build_tool_call_line,
build_tool_detail_text,
make_tool_detail_record,
tool_detail_body,
)
from surfaces.interactive_shell.ui.output.tool_details import (
format_tool_summary as _format_tool_summary,
)
from surfaces.interactive_shell.ui.output.tool_details import (
record_tool_summary as _record_tool_summary,
)
from surfaces.shared.tool_labels import tool_short_label, tool_source_label
from tools.registry import resolve_tool_display_name
def _is_repl_display(display: object) -> TypeGuard[_ReplEventLogDisplay]:
from surfaces.interactive_shell.ui.output.repl_display import _ReplEventLogDisplay
return isinstance(display, _ReplEventLogDisplay)
@runtime_checkable
class ToolTrackingSupport(Protocol):
"""Interface that concrete classes must satisfy to use :class:`ToolTrackingMixin`."""
def update_subtext(self, node_name: str, text: str, duration: float = 4.0) -> None:
raise NotImplementedError
def print_above_renderable(self, renderable: Any) -> None:
raise NotImplementedError
class ToolTrackingMixin:
_silent: bool
_rich: bool
_t0: float
_display: DisplayProtocol | None
_tool_start_times: dict[str, float]
_tool_inputs: dict[str, Any]
_tool_details_visible: bool
_tool_detail_records: list[dict[str, Any]]
_printed_tool_detail_ids: set[int]
_tool_summary_counts: dict[str, dict[str, int]]
_tool_summary_order: list[tuple[str, str]]
def update_subtext(self, node_name: str, text: str, duration: float = 4.0) -> None:
raise NotImplementedError
def print_above_renderable(self, renderable: Any) -> None:
raise NotImplementedError
def record_tool_start(
self,
tool_name: str,
tool_input: Any = None,
*,
event_key: str | None = None,
) -> None:
key = event_key or tool_name
self._tool_start_times[key] = time.monotonic()
self._tool_inputs[key] = tool_input
if self._silent:
return
_record_tool_summary(tool_name, self._tool_summary_counts, self._tool_summary_order)
source = tool_source_label(tool_name)
label = tool_short_label(tool_name, source)
current = f"{source} · {label}" if label else source
self.update_subtext("investigation_agent", f"calling {current}...", duration=15.0)
self.update_subtext("investigate", f"calling {current}...", duration=15.0)
self._sync_tool_detail_view()
def record_tool_end(
self,
tool_name: str,
output: Any = None,
*,
event_key: str | None = None,
tool_input: Any = None,
) -> None:
key = event_key or tool_name
start = self._tool_start_times.pop(key, None)
elapsed_ms = int((time.monotonic() - start) * 1000) if start is not None else None
stored_input = self._tool_inputs.pop(key, None)
# UI tool tracking only; tool spans are emitted in core.execution.
if self._silent:
return
self._update_tool_summary_subtext()
self._record_tool_detail(
resolve_tool_display_name(tool_name),
tool_input if tool_input is not None else stored_input,
output,
elapsed=_fmt_timing(elapsed_ms) if elapsed_ms is not None else "",
)
if elapsed_ms is not None and not _is_repl_display(self._display):
# REPL investigations show an aggregate lap summary; one line per tool
# call floods scrollback during multi-lap ReAct loops.
self.print_above_renderable(
build_tool_call_line(tool_name, elapsed_ms, time.monotonic() - self._t0)
)
def print_status_hint(self, text: str) -> None:
if self._silent:
return
if _is_repl_display(self._display):
self._display.animate_hint(text)
return
t = Text()
t.append(f"{_elapsed_hms(time.monotonic() - self._t0)} ", style="dim")
t.append(" ↳ ", style="dim")
t.append(text, style="dim")
self.print_above_renderable(t)
def toggle_tool_details(self) -> None:
if self._silent:
return
self._tool_details_visible = not self._tool_details_visible
if self._rich and self._display is not None and not _is_repl_display(self._display):
self._sync_tool_detail_view(clear=True)
return
label = "shown" if self._tool_details_visible else "hidden"
_safe_print(f" Tool details {label} (ctrl+o)")
if self._tool_details_visible:
self._flush_tool_details()
def _sync_tool_detail_view(self, *, clear: bool = False) -> None:
if self._rich and self._display is not None and not _is_repl_display(self._display):
self._display.set_tool_details(
visible=self._tool_details_visible,
records=self._tool_detail_records,
summary=self.format_tool_summary(),
clear=clear,
)
def _update_tool_summary_subtext(self) -> None:
if summary := self.format_tool_summary():
self.update_subtext("investigation_agent", summary, duration=30.0)
self.update_subtext("investigate", summary, duration=30.0)
def format_tool_summary(self) -> str:
return _format_tool_summary(self._tool_summary_counts, self._tool_summary_order)
def _record_tool_detail(
self,
display: str,
tool_input: Any,
output: Any,
*,
elapsed: str = "",
) -> None:
record = make_tool_detail_record(display, tool_input, output, elapsed=elapsed)
if record is None:
return
self._tool_detail_records.append(record)
if not self._tool_details_visible:
return
if self._rich and self._display is not None and not _is_repl_display(self._display):
self._sync_tool_detail_view()
else:
self._print_tool_detail(record)
def _flush_tool_details(self) -> None:
for record in self._tool_detail_records:
if id(record) not in self._printed_tool_detail_ids:
self._print_tool_detail(record)
def _print_tool_detail(self, record: dict[str, Any]) -> None:
if self._rich:
self.print_above_renderable(build_tool_detail_text(record))
else:
display = str(record.get("display") or "tool")
elapsed = str(record.get("elapsed") or "")
suffix = f" {elapsed}" if elapsed else ""
_safe_print(f" Tool details: {display}{suffix}")
for line in tool_detail_body(record).splitlines():
_safe_print(f" {line}")
self._printed_tool_detail_ids.add(id(record))