Compare commits
2 Commits
| Author | SHA1 | Date | |
|---|---|---|---|
| 9502c6684a | |||
| 5af054cafb |
@@ -3,8 +3,6 @@ name: CI
|
||||
on:
|
||||
push:
|
||||
branches: [master, develop]
|
||||
pull_request:
|
||||
branches: [master, develop]
|
||||
|
||||
vars:
|
||||
docker_prefix: "http://harbor.cleverthis.com/docker/"
|
||||
|
||||
@@ -5,6 +5,15 @@ The format follows [Keep a Changelog](https://keepachangelog.com/en/1.1.0/).
|
||||
|
||||
## [Unreleased]
|
||||
|
||||
### Added
|
||||
|
||||
- **`agents actor context show` CLI command** (#9586): Added a new ``show`` subcommand for
|
||||
the actor context management module that displays message count, role distribution,
|
||||
storage size, and the last N messages (default: 5) per context. Supports single-context
|
||||
display (*``agents actor context show <name>``*) and bulk listing of all contexts via
|
||||
``--all`` flag. Output is rendered in Rich panels by default and can be switched to
|
||||
machine-readable formats (JSON, YAML, plain) with ``--format``.
|
||||
|
||||
### Fixed
|
||||
|
||||
- **Cross-actor subgraph cycle detection reads actor_ref field** (#1431): Fixed
|
||||
|
||||
@@ -28,6 +28,7 @@ Below are some of the specific details of various contributions.
|
||||
* HAL 9000 has contributed the git worktree TOCTOU race condition fix (PR #8178 / issue #7507): replaced the unsafe mkdtemp() + rmdir() pattern with a parent-directory approach to eliminate the race window in concurrent git worktree operations.
|
||||
* HAL 9000 has contributed the git_tools TOCTOU race condition fix (PR #8255 / issue #7619): eliminated the Time-Of-Check-To-Time-Of-Use race in `_get_base_env()` by adding double-checked locking with a module-level `threading.Lock`, preventing concurrent threads from writing conflicting environment snapshots.
|
||||
* HAL 9000 has contributed the mandatory PR compliance checklist to `implementation-supervisor.md` (#9824): added an 8-item checklist to the worker prompt body with concrete items covering CHANGELOG.md, CONTRIBUTORS.md, commit footer, CI verification, BDD tests, Epic reference, labels, and milestone assignment to eliminate systemic PR merge blockers.
|
||||
* HAL 9000 has contributed the context show CLI command (#9586): implemented ``agents actor context show`` for displaying message count, role distribution, storage size, and recent messages within a named context, enabling users to inspect context state at a glance.
|
||||
* HAL 9000 has contributed comprehensive milestone documentation for v3.6.0 (Advanced Concepts & Deferred Features) and v3.7.0 (TUI Implementation) (PR #9903): split into sub-documents covering context strategies, LLM backends, resource types, A2A rename, container tool execution, scope chain resolution, cost/safety budgets, E2E workflow tests, code review examples, plugin architecture, TUI layout, persona system, reference/command input, session management, configuration, and TuiMaterializer integration.
|
||||
* HAL 9000 has contributed the LLMTraceRepository data-integrity fix (PR #8185 / issue #7505): replaced the unconditional `session.commit()` in `LLMTraceRepository.save()` with a dual-path implementation that respects the UnitOfWork pattern — flushing only when an external session is provided, and flushing + committing + closing when operating standalone. This eliminates premature transaction commits, loss of rollback capability, and a docstring/implementation mismatch.
|
||||
* HAL 9000 has contributed the ACMS Index Data Model and File Traversal Engine (PR #9664 / issue #9579): foundational data structures for indexed context entries with hot/warm/cold/archive storage tier classification, tag system, and a timeout-safe chunked file traversal engine for large projects with 10,000+ files.
|
||||
|
||||
@@ -0,0 +1,54 @@
|
||||
Feature: Actor context show CLI command
|
||||
As a CleverAgents user
|
||||
I want to view the contents and metadata of actor contexts via the CLI
|
||||
So that I can inspect conversation state without loading it into a plan
|
||||
|
||||
Background:
|
||||
Given a temporary context directory for actor context tests
|
||||
|
||||
# ── Single context show ────────────────────────────────────────────────
|
||||
|
||||
Scenario: Show existing context displays summary
|
||||
Given an actor context named "docs" exists with messages
|
||||
When I run actor context show "docs"
|
||||
Then the show command returns 0
|
||||
And the show output includes "Context:"
|
||||
And the show output includes "Messages:"
|
||||
|
||||
Scenario: Show non-existent context fails
|
||||
When I run actor context show "ghost"
|
||||
Then the show command returns error
|
||||
|
||||
Scenario: Show with --all flag lists all contexts
|
||||
Given an actor context named "alpha" exists with messages
|
||||
And an actor context named "beta" exists with messages
|
||||
When I run actor context show --all
|
||||
Then the show command returns 0
|
||||
And the show output includes "Context:"
|
||||
|
||||
Scenario: Show with --head option truncates messages
|
||||
Given an actor context named "long" exists with many messages
|
||||
When I run actor context show "long" with --head 2
|
||||
Then the show command returns 0
|
||||
AND the show output includes "Last 2 message(s)"
|
||||
|
||||
Scenario: Show empty context displays no messages indicator
|
||||
Given an actor context named "empty" exists
|
||||
When I run actor context show "empty"
|
||||
Then the show command returns 0
|
||||
AND the show output includes "(no messages)"
|
||||
|
||||
# ── Output format variants ─────────────────────────────────────────────
|
||||
|
||||
Scenario: Show outputs JSON format
|
||||
Given an actor context named "json-test" exists with messages
|
||||
When I run actor context show in json format for "json-test"
|
||||
Then the show command returns 0
|
||||
AND the show output is valid JSON
|
||||
AND the show JSON output includes key "contexts"
|
||||
|
||||
Scenario: Show outputs YAML format
|
||||
Given an actor context named "yaml-test" exists with messages
|
||||
When I run actor context show in yaml format for "yaml-test"
|
||||
Then the show command returns 0
|
||||
AND the show output is valid YAML
|
||||
@@ -0,0 +1,145 @@
|
||||
# pyright: reportRedeclaration=false
|
||||
"""Step definitions for actor context show CLI command."""
|
||||
|
||||
from __future__ import annotations
|
||||
|
||||
import json
|
||||
from pathlib import Path
|
||||
from typing import Any
|
||||
|
||||
import yaml
|
||||
from behave import given, then, when
|
||||
from typer.testing import CliRunner
|
||||
|
||||
from cleveragents.cli.commands.actor_context import app as actor_context_app
|
||||
|
||||
|
||||
# ---------------------------------------------------------------------------
|
||||
# Givens — context creation helpers (background step is shared with
|
||||
# actor_context_cmds_steps.py which defines "a temporary context directory")
|
||||
# ---------------------------------------------------------------------------
|
||||
|
||||
# Note: steps for 'an actor context named "{name}" exists with messages' and
|
||||
# 'an actor context named "{name}" exists' are already defined in
|
||||
# actor_context_cmds_steps.py (lines 59 and 53 respectively).
|
||||
|
||||
|
||||
@given("an actor context named \"long\" exists with many messages")
|
||||
def step_long_context(context):
|
||||
"""Create a context with 50+ messages to test --head truncation."""
|
||||
from cleveragents.reactive.context_manager import ContextManager
|
||||
|
||||
mgr = ContextManager("long", context.context_dir)
|
||||
for i in range(50):
|
||||
role = "user" if i % 2 == 0 else "assistant"
|
||||
mgr.add_message(role, f"This is message number {i} of the conversation.")
|
||||
|
||||
|
||||
# ---------------------------------------------------------------------------
|
||||
# When
|
||||
# ---------------------------------------------------------------------------
|
||||
|
||||
|
||||
@when('I run actor context show "{name}"')
|
||||
def step_show_single(context, name):
|
||||
context.result = context.runner.invoke(
|
||||
actor_context_app,
|
||||
["show", name, "--context-dir", str(context.context_dir)],
|
||||
)
|
||||
|
||||
|
||||
@when('I run actor context show "{name}" with --head {n}')
|
||||
def step_show_single_head(context, name, n):
|
||||
context.result = context.runner.invoke(
|
||||
actor_context_app,
|
||||
[
|
||||
"show",
|
||||
name,
|
||||
"--context-dir",
|
||||
str(context.context_dir),
|
||||
"--head",
|
||||
n,
|
||||
],
|
||||
)
|
||||
|
||||
|
||||
@when("I run actor context show --all")
|
||||
def step_show_all(context):
|
||||
context.result = context.runner.invoke(
|
||||
actor_context_app,
|
||||
["show", "--all", "--context-dir", str(context.context_dir)],
|
||||
)
|
||||
|
||||
|
||||
@when('I run actor context show in {fmt} format for "{name}"')
|
||||
def step_show_format(context, fmt, name):
|
||||
context.result = context.runner.invoke(
|
||||
actor_context_app,
|
||||
[
|
||||
"show",
|
||||
name,
|
||||
"--context-dir",
|
||||
str(context.context_dir),
|
||||
"--format",
|
||||
fmt,
|
||||
],
|
||||
)
|
||||
|
||||
|
||||
# ---------------------------------------------------------------------------
|
||||
# Then — success / failure (unique prefixes to avoid ambiguity with
|
||||
# project_context_cli_steps.py and other step modules)
|
||||
# ---------------------------------------------------------------------------
|
||||
|
||||
|
||||
@then("the show command returns 0")
|
||||
def step_show_returns_zero(context):
|
||||
assert context.result.exit_code == 0, (
|
||||
f"Expected exit 0, got {context.result.exit_code}.\n"
|
||||
f"stdout: {context.result.output}\n"
|
||||
f"stderr: {getattr(context.result, 'stderr', '')}"
|
||||
)
|
||||
|
||||
|
||||
@then("the show command returns error")
|
||||
def step_show_returns_error(context):
|
||||
assert context.result.exit_code != 0, (
|
||||
f"Expected non-zero exit code, got {context.result.exit_code}.\n"
|
||||
f"stdout: {context.result.output}\n"
|
||||
f"stderr: {getattr(context.result, 'stderr', '')}"
|
||||
)
|
||||
|
||||
|
||||
# ---------------------------------------------------------------------------
|
||||
# Then — output assertions (unique prefix to avoid ambiguity with other
|
||||
# 'output should contain' steps defined in auto_debug_cli_coverage_steps.py)
|
||||
# ---------------------------------------------------------------------------
|
||||
|
||||
|
||||
@then('the show output includes "{text}"')
|
||||
def step_show_output_contains(context, text):
|
||||
assert text in context.result.output, (
|
||||
f'Expected "{text}" in output.\n'
|
||||
f"Actual output:\n{context.result.output}"
|
||||
)
|
||||
|
||||
|
||||
@then("the show output is valid JSON")
|
||||
def step_show_output_valid_json(context):
|
||||
parsed = json.loads(context.result.output)
|
||||
assert isinstance(parsed, dict), "Output is not a JSON object"
|
||||
|
||||
|
||||
@then('the show JSON output includes key "{key}"')
|
||||
def step_show_json_key(context, key):
|
||||
parsed = json.loads(context.result.output)
|
||||
assert key in parsed, f'Key "{key}" not found: {list(parsed.keys())}'
|
||||
|
||||
|
||||
@then("the show output is valid YAML")
|
||||
def step_show_output_valid_yaml(context):
|
||||
try:
|
||||
parsed = yaml.safe_load(context.result.output)
|
||||
assert isinstance(parsed, dict), "Output is not a YAML mapping"
|
||||
except yaml.YAMLError as e:
|
||||
raise AssertionError(f"Output is not valid YAML: {e}\nActual:\n{context.result.output}")
|
||||
@@ -106,6 +106,157 @@ def _render_output(
|
||||
# ---------------------------------------------------------------------------
|
||||
|
||||
|
||||
@app.command("show")
|
||||
def context_show(
|
||||
name: Annotated[
|
||||
str | None,
|
||||
typer.Argument(help="Context name to show (all contexts if omitted)"),
|
||||
] = None,
|
||||
all_contexts: Annotated[
|
||||
bool,
|
||||
typer.Option("--all", "-a", help="Show all contexts"),
|
||||
] = False,
|
||||
head_n: Annotated[
|
||||
int,
|
||||
typer.Option(
|
||||
"--head",
|
||||
"-n",
|
||||
min=1,
|
||||
help="Number of recent messages to display per context (default: 5)",
|
||||
),
|
||||
] = 5,
|
||||
context_dir: Annotated[
|
||||
Path | None,
|
||||
typer.Option(
|
||||
"--context-dir",
|
||||
help="Directory where contexts are stored",
|
||||
resolve_path=True,
|
||||
),
|
||||
] = None,
|
||||
fmt: Annotated[
|
||||
str,
|
||||
typer.Option("--format", "-f", help=_FORMAT_HELP),
|
||||
] = "rich",
|
||||
) -> None:
|
||||
"""Show information about a named actor context or all contexts.
|
||||
|
||||
Displays the message count, storage size, and the last N messages
|
||||
(configurable via ``--head`` / ``-n``) for each context. Use ``--all``
|
||||
to list every context at once.
|
||||
|
||||
Examples::
|
||||
|
||||
agents actor context show docs
|
||||
agents actor context show docs --head 20
|
||||
agents actor context show --all -n 3
|
||||
"""
|
||||
base = _default_context_base(context_dir)
|
||||
|
||||
# Determine which contexts to show
|
||||
contexts: list[str]
|
||||
if all_contexts:
|
||||
contexts = _list_context_names(base)
|
||||
elif name is not None:
|
||||
target = base / name
|
||||
if not target.exists():
|
||||
typer.echo(f"Error: Context '{name}' does not exist.", err=True)
|
||||
raise typer.Exit(code=1)
|
||||
contexts = [name]
|
||||
else:
|
||||
# No --all and no name: show all available contexts
|
||||
contexts = _list_context_names(base)
|
||||
|
||||
if not contexts:
|
||||
typer.echo("No contexts found.")
|
||||
return
|
||||
|
||||
panels: list[tuple[str, str]] = []
|
||||
messages_list: list[dict[str, Any]] = []
|
||||
|
||||
for cname in contexts:
|
||||
mgr = ContextManager(cname, context_dir)
|
||||
|
||||
# Message role breakdown
|
||||
role_counts: dict[str, int] = {}
|
||||
for msg in mgr.messages:
|
||||
role = msg.get("role", "unknown")
|
||||
role_counts[role] = role_counts.get(role, 0) + 1
|
||||
|
||||
size_kb = _context_size_kb(mgr)
|
||||
|
||||
# Build the panel body
|
||||
lines = [
|
||||
f"[bold]Context:[/bold] {cname}",
|
||||
f"[bold]Messages:[/bold] {len(mgr.messages)} "
|
||||
f"({' '.join(f'{r}: {c}' for r, c in sorted(role_counts.items()))})",
|
||||
f"[bold]Storage:[/bold] {size_kb} KB",
|
||||
]
|
||||
|
||||
metadata = mgr.metadata
|
||||
if "created_at" in metadata:
|
||||
lines.append(f"[bold]Created:[/bold] {metadata['created_at']}")
|
||||
last_up = metadata.get("last_updated", "")
|
||||
if last_up and last_up != metadata.get("created_at"):
|
||||
lines.append(f"[bold]Last Updated:[/bold] {metadata['last_updated']}")
|
||||
|
||||
# Show last N messages
|
||||
recent = mgr.messages[-head_n:] if mgr.messages else []
|
||||
if recent:
|
||||
lines.append("")
|
||||
lines.append(f"[bold]--- Last {len(recent)} message(s) ---[/bold]")
|
||||
for msg in recent:
|
||||
role = msg.get("role", "?")
|
||||
content = msg.get("content", "")
|
||||
# Truncate long messages for display
|
||||
if len(content) > 200:
|
||||
content = content[:200] + "..."
|
||||
timestamp = msg.get("timestamp", "")
|
||||
lines.append(
|
||||
f"\n[dim]{role}[/dim] "
|
||||
f"([italic]{timestamp}[/italic])\n[green]{content}[/green]"
|
||||
)
|
||||
else:
|
||||
lines.append("[dim](no messages)[/dim]")
|
||||
|
||||
panels.append((cname, "\n".join(lines)))
|
||||
|
||||
# Collect all recent messages for envelope data
|
||||
recent_entries = [dict(m) for m in recent]
|
||||
messages_list.append(
|
||||
{"context": cname, "messages": recent_entries, "count": len(recent)}
|
||||
)
|
||||
|
||||
if fmt == OutputFormat.RICH.value:
|
||||
for title, body in panels:
|
||||
console.print(Panel(body, title=f"Context: {title}", expand=False))
|
||||
console.print()
|
||||
else:
|
||||
data: dict[str, Any] = {}
|
||||
for entry in messages_list:
|
||||
data[entry["context"]] = {
|
||||
"message_count": entry["count"],
|
||||
"recent_messages": entry["messages"][:head_n],
|
||||
}
|
||||
# Also include metadata summary
|
||||
summary_data: list[dict[str, Any]] = []
|
||||
for cname in contexts:
|
||||
mgr = ContextManager(cname, context_dir)
|
||||
role_counts: dict[str, int] = {}
|
||||
for msg in mgr.messages:
|
||||
role = msg.get("role", "unknown")
|
||||
role_counts[role] = role_counts.get(role, 0) + 1
|
||||
summary_data.append(
|
||||
{
|
||||
"context": cname,
|
||||
"total_messages": len(mgr.messages),
|
||||
"role_distribution": role_counts,
|
||||
"storage_kb": _context_size_kb(mgr),
|
||||
}
|
||||
)
|
||||
output_data = {"contexts": summary_data}
|
||||
print(format_output(output_data, fmt))
|
||||
|
||||
|
||||
@app.command("remove")
|
||||
def context_remove(
|
||||
name: Annotated[
|
||||
|
||||
Reference in New Issue
Block a user