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

361 lines
13 KiB
Python

"""Reference text for OpenSRE interactive-shell CLI answers."""
from __future__ import annotations
import logging
import time
from collections.abc import Callable, Mapping
from typing import Any
import click
from core.agent_harness.grounding.diagnostics import (
GroundingSource,
log_grounding_cache_diagnostics,
)
from core.agent_harness.grounding.models import CacheStats
from core.agent_harness.prompts.prompt_context import DefaultPromptContextProvider
_logger = logging.getLogger(__name__)
_MAX_REFERENCE_CHARS = 28_000
# Heuristic: truncated or failed reference output must not be cached or the
# assistant would keep an empty reference for the whole process.
_MIN_CACHEABLE_CLI_REFERENCE_CHARS = 80
_CLI_REFERENCE_SENTINEL = "=== opensre --help ==="
SlashCommandProvider = Callable[[], Mapping[str, Any]]
CommandGroupProvider = Callable[[], click.Command | None]
def _is_cacheable_cli_reference(text: str) -> bool:
stripped = text.strip()
if len(stripped) < _MIN_CACHEABLE_CLI_REFERENCE_CHARS:
return False
return _CLI_REFERENCE_SENTINEL in text
def _slash_command_names(provider: SlashCommandProvider | None) -> list[str]:
if provider is None:
return []
return sorted(provider().keys())
def _current_cli_signature(
cli_group: click.Command | None,
slash_provider: SlashCommandProvider | None = None,
) -> str:
"""Stable signature of the CLI command surface and interactive slash commands.
Bumps cache when subcommands change, slash-command metadata changes, or the
installed package version changes.
"""
from config.version import get_opensre_version
if isinstance(cli_group, click.Group):
cmd_names = ",".join(sorted(cli_group.commands.keys()))
else:
cmd_names = ""
slash_names = ",".join(_slash_command_names(slash_provider))
return f"opensre={get_opensre_version()}|commands={cmd_names}|slash={slash_names}"
def _format_param(param: click.Parameter) -> str:
"""Return a compact, side-effect-free description of a Click parameter."""
if isinstance(param, click.Option):
names = ", ".join((*param.opts, *param.secondary_opts))
if not names:
names = param.name or "(option)"
value_hint = ""
if not param.is_flag and not param.count:
value_hint = " " + (param.metavar or param.name or "VALUE").upper()
default = ""
if param.show_default and param.default not in (None, "", ()):
default = f" [default: {param.default}]"
help_text = (param.help or "").strip()
return f" {names}{value_hint} - {help_text}{default}".rstrip()
if isinstance(param, click.Argument):
name = (param.name or "ARG").upper()
required = "" if param.required else " (optional)"
return f" {name}{required}"
return f" {param.name or type(param).__name__}"
def _format_command_reference(
command: click.Command,
*,
path: str,
include_subcommands: bool = True,
) -> str:
"""Render Click command metadata without invoking help callbacks.
``click.Command.get_help()`` eventually calls ``format_help()``. The root
OpenSRE group overrides that path to render via Rich's live console, which
can leak into the interactive terminal when the assistant builds grounding
context. This renderer inspects command objects directly instead.
"""
lines = [f"Usage: {path} [OPTIONS]"]
if isinstance(command, click.Group):
lines[0] += " COMMAND [ARGS]..."
elif command.params:
arg_names = [
(param.name or "ARG").upper()
for param in command.params
if isinstance(param, click.Argument)
]
if arg_names:
lines[0] += " " + " ".join(arg_names)
help_text = (command.help or command.short_help or "").strip()
if help_text:
lines.extend(["", help_text])
params = [param for param in command.params if not getattr(param, "hidden", False)]
if params:
lines.extend(["", "Options/Arguments:"])
lines.extend(_format_param(param) for param in params)
if include_subcommands and isinstance(command, click.Group):
command_rows: list[tuple[str, str]] = []
with click.Context(command, info_name=path.rsplit(" ", 1)[-1]) as ctx:
for name in command.list_commands(ctx):
subcommand = command.get_command(ctx, name)
if subcommand is None or subcommand.hidden:
continue
command_rows.append((name, subcommand.get_short_help_str(limit=160)))
if command_rows:
lines.extend(["", "Commands:"])
lines.extend(f" {name} - {summary}".rstrip() for name, summary in command_rows)
return "\n".join(lines).rstrip() + "\n"
def _build_cli_reference_text_uncached(
cli_group: click.Command | None,
slash_provider: SlashCommandProvider | None = None,
) -> str:
"""Build a side-effect-free CLI reference without invoking Click commands."""
parts: list[str] = []
if cli_group is None:
parts.append("=== opensre --help ===\n")
parts.append(
"The CLI command surface is not available in this runtime.\n"
"Launch the OpenSRE CLI (`opensre`) for command-level help.\n"
)
else:
parts.append("=== opensre --help ===\n")
parts.append(_format_command_reference(cli_group, path="opensre"))
if isinstance(cli_group, click.Group):
with click.Context(cli_group, info_name="opensre") as ctx:
for name in sorted(cli_group.commands.keys()):
command = cli_group.get_command(ctx, name)
if command is None or command.hidden:
continue
parts.append(f"\n=== opensre {name} --help ===\n")
parts.append(_format_command_reference(command, path=f"opensre {name}"))
parts.append("\n=== Interactive-shell slash commands ===\n")
parts.append(_interactive_shell_slash_hints(slash_provider))
text = "".join(parts)
if len(text) > _MAX_REFERENCE_CHARS:
return text[:_MAX_REFERENCE_CHARS] + "\n\n[... reference truncated ...]\n"
return text
def _interactive_shell_slash_hints(provider: SlashCommandProvider | None = None) -> str:
lines = [
"In the interactive shell, describe an incident or paste alert JSON to run "
+ "a investigation pipeline, or chat with the terminal assistant for CLI help.",
"Alpha mode runs every shell command with no guardrails: plain commands are parsed to "
+ "argv and run without a shell, while pipes, redirects, command substitution, and a "
+ "leading ! all run through a full shell. There is no read-only allowlist or blocked "
+ "command list.",
"Slash commands:",
"",
]
if provider is not None:
for cmd in provider().values():
name = getattr(cmd, "name", "")
description = getattr(cmd, "description", "")
if name:
lines.append(f" {name} - {description}".rstrip())
else:
lines.append(" (slash command registry is supplied by the interactive shell runtime)")
lines.extend(
[
"",
"Non-interactive investigation: `opensre investigate` with stdin, file, or flags.",
"Launch the interactive shell: `opensre` (requires a TTY).",
]
)
return "\n".join(lines)
class CliReference:
"""Session-scoped cache for assembled CLI help reference text.
Holds its cache as instance state so each REPL session (via
:func:`session_cli_reference`) owns an isolated cache with no module-level
mutable globals.
"""
name = "cli"
def __init__(self) -> None:
self._signature: str | None = None
self._text: str | None = None
self._created_at_monotonic: float = 0.0
self._hits: int = 0
self._misses: int = 0
self._slash_commands_provider: SlashCommandProvider | None = None
self._command_group_provider: CommandGroupProvider | None = None
def set_slash_commands_provider(self, provider: SlashCommandProvider | None) -> None:
"""Set the shell-owned slash command source used for grounding text."""
self._slash_commands_provider = provider
self.invalidate()
def set_command_group_provider(self, provider: CommandGroupProvider | None) -> None:
"""Bind a surface-owned callable that returns the root Click command group."""
self._command_group_provider = provider
self.invalidate()
def _resolve_command_group(self) -> click.Command | None:
provider = self._command_group_provider
if provider is None:
return None
try:
return provider()
except Exception:
_logger.debug("Command group provider raised; skipping CLI reference", exc_info=True)
return None
def build_text(self) -> str:
"""Assemble ``opensre`` and subcommand ``--help`` output for LLM grounding.
Cached on this instance while the command registry signature matches.
"""
slash_provider = self._slash_commands_provider
cli_group = self._resolve_command_group()
sig = _current_cli_signature(cli_group, slash_provider)
if self._text is not None and self._signature == sig:
self._hits += 1
return self._text
self._misses += 1
text = _build_cli_reference_text_uncached(cli_group, slash_provider)
if cli_group is not None and _is_cacheable_cli_reference(text):
self._signature = sig
self._text = text
self._created_at_monotonic = time.monotonic()
else:
self._signature = None
self._text = None
self._created_at_monotonic = 0.0
_logger.warning(
"CLI reference build produced non-cacheable output (%d chars); skipping cache",
len(text),
)
return text
def invalidate(self) -> None:
"""Drop cached CLI reference text (for tests or forced refresh)."""
self._signature = None
self._text = None
self._created_at_monotonic = 0.0
self._hits = 0
self._misses = 0
def stats(self) -> CacheStats:
"""Debug counters for grounding cache hit/miss and last signature."""
return CacheStats(
name=self.name,
hits=self._hits,
misses=self._misses,
cached=self._text is not None,
signature=self._signature,
created_at_monotonic=self._created_at_monotonic,
)
def as_grounding_source(self) -> GroundingSource:
return GroundingSource(name=self.name, stats_fn=self.stats)
def _slash_commands() -> Mapping[str, object]:
from surfaces.interactive_shell.command_registry import SLASH_COMMANDS
return SLASH_COMMANDS
def _cli_command_group() -> click.Command | None:
from surfaces.cli.__main__ import cli
return cli
_SESSION_CLI_REFERENCE_ATTR = "_shell_cli_reference"
def session_cli_reference(session: Any) -> CliReference:
"""Return the session-scoped CLI catalog cache, creating it on first use."""
cached = getattr(session, _SESSION_CLI_REFERENCE_ATTR, None)
if isinstance(cached, CliReference):
return cached
cli = CliReference()
cli.set_command_group_provider(_cli_command_group)
cli.set_slash_commands_provider(_slash_commands)
setattr(session, _SESSION_CLI_REFERENCE_ATTR, cli)
return cli
def shell_prompt_context_provider(session: Any) -> ShellPromptContextProvider:
"""Build a prompt provider that reuses the session's CLI catalog cache."""
return ShellPromptContextProvider(session)
class ShellPromptContextProvider:
""":class:`core.agent_harness.ports.PromptContextProvider` for the interactive shell.
Owns CLI catalog assembly (``surfaces.cli`` + slash commands) and delegates
repo-level grounding (AGENTS.md, environment block) to
:class:`~core.agent_harness.prompts.prompt_context.DefaultPromptContextProvider`.
"""
def __init__(self, session: Any) -> None:
self._base = DefaultPromptContextProvider(session)
self._cli = session_cli_reference(session)
def cli_reference(self) -> str:
return self._cli.build_text()
def agents_md(self) -> str:
return self._base.agents_md()
def investigation_flow(self) -> str:
return self._base.investigation_flow()
def environment_block(self) -> str:
return self._base.environment_block()
def suggested_synthetic_prompt(self) -> str:
return self._base.suggested_synthetic_prompt()
def log_diagnostics(self, reason: str) -> None:
self._base.log_diagnostics(reason)
log_grounding_cache_diagnostics([self._cli.as_grounding_source()], reason)
__all__ = [
"CliReference",
"CommandGroupProvider",
"ShellPromptContextProvider",
"SlashCommandProvider",
"session_cli_reference",
"shell_prompt_context_provider",
]