fix(cli): implement missing actor context list, show, and clear commands #10913

Open
HAL9000 wants to merge 3 commits from feature/issue-4748-actor-context-list-show-clear into master
3 changed files with 384 additions and 4 deletions
+64 -2
View File
@@ -1,7 +1,7 @@
Feature: Actor context clear, remove, export, and import commands
Feature: Actor context list, clear, show, remove, export, and import commands
As a CleverAgents user
I want to manage actor contexts via the CLI
So that I can remove, export, and import conversation contexts
So that I can inspect, filter, remove, export, and import conversation contexts
Background:
Given a temporary context directory for actor context tests
1
@@ -135,3 +135,65 @@ Feature: Actor context clear, remove, export, and import commands
And I import the context from that JSON file as "roundtrip"
Then the context "roundtrip" should exist
And the imported context should have the same messages as the original
# ── context list ────────────────────────────────────────────
Scenario: List all actor contexts
Given an actor context named "docs" exists with messages
And an actor context named "notes" exists with messages
When I run actor context list
Then the actor context list command should succeed
Scenario: List actor contexts with matching regex filter
Given an actor context named "docs" exists with messages
And an actor context named "docs_backup" exists with messages
And an actor context named "notes" exists with messages
When I run actor context list "docs"
Then the actor context list command should succeed
Scenario: List empty when regex filter matches nothing
Given an actor context named "docs" exists with messages
And an actor context named "notes" exists with messages
When I run actor context list "^zzz_"
Then the actor context list command should succeed
And the output should contain valid JSON with key "context_list"
Scenario: List actor contexts with invalid regex fails
Given an actor context named "docs" exists with messages
When I run actor context list "[invalid("
Then the actor context list command should fail with exit code 1
Scenario: List empty when no contexts exist
When I run actor context list
Then the actor context list command should succeed
Scenario: List outputs JSON format
Given an actor context named "docs" exists with messages
And an actor context named "notes" exists with messages
When I run actor context list --format json
Then the actor context list command should succeed
And the output should contain valid JSON with key "context_list"
And the output JSON key "context_list" should contain keys "count, contexts"
Scenario: List outputs YAML format
Given an actor context named "docs" exists with messages
When I run actor context list --format yaml
Then the actor context list command should succeed
# ── context show ────────────────────────────────────────────
Scenario: Show a named actor context
Given an actor context named "docs" exists with messages
When I run actor context show "docs"
Then the actor context show command should succeed
Scenario: Show a named actor context outputs JSON format
Given an actor context named "docs" exists with messages
When I run actor context show "docs" --format json
Then the actor context show command should succeed
And the output should contain valid JSON with key "context_show"
And the output JSON key "context_show" should contain keys "name, message_count, total_size_kb, estimated_tokens, created_at"
Scenario: Show a non-existent context fails
When I run actor context show "nonexistent"
Then the actor context show command should fail with exit code 1
+113 -1
View File
@@ -1,5 +1,5 @@
# pyright: reportRedeclaration=false
"""Step definitions for actor context remove/export/import commands."""
"""Step definitions for actor context list, clear, show, remove, export, and import commands."""
from __future__ import annotations
@@ -576,3 +576,115 @@ def step_roundtrip_messages(context):
assert orig["content"] == imp["content"], (
f"Content mismatch: {orig['content']!r} vs {imp['content']!r}"
)
# ---------------------------------------------------------------------------
# When — list
# ---------------------------------------------------------------------------
@when("I run actor context list")
def step_list_all(context):
context.result = context.runner.invoke(
actor_context_app,
["list", "--context-dir", str(context.context_dir)],
)
@when('I run actor context list "{regex}"')
def step_list_regex(context, regex):
context.result = context.runner.invoke(
actor_context_app,
[
"list",
regex,
"--context-dir",
str(context.context_dir),
],
)
@when("I run actor context list --format {fmt}")
def step_list_format(context, fmt):
context.result = context.runner.invoke(
actor_context_app,
[
"list",
"--context-dir",
str(context.context_dir),
"--format",
fmt,
],
)
# ---------------------------------------------------------------------------
# When — show
# ---------------------------------------------------------------------------
@when('I run actor context show "{name}"')
def step_show(context, name):
args = [
"show",
name,
"--context-dir",
str(context.context_dir),
]
context.result = context.runner.invoke(actor_context_app, args)
@when('I run actor context show "{name}" --format {fmt}')
def step_show_format(context, name, fmt):
context.result = context.runner.invoke(
actor_context_app,
[
"show",
name,
"--context-dir",
str(context.context_dir),
"--format",
fmt,
],
)
# ---------------------------------------------------------------------------
# Then — list / show success and failure assertions
# ---------------------------------------------------------------------------
@then("the actor context list command should succeed")
def step_list_success(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 actor context list command should fail with exit code 1")
def step_list_fail(context):
assert context.result.exit_code == 1, (
f"Expected exit 1, got {context.result.exit_code}.\n"
f"stdout: {context.result.output}\n"
f"stderr: {getattr(context.result, 'stderr', '')}"
)
@then("the actor context show command should succeed")
def step_show_success(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 actor context show command should fail with exit code 1")
def step_show_fail(context):
assert context.result.exit_code == 1, (
f"Expected exit 1, got {context.result.exit_code}.\n"
f"stdout: {context.result.output}\n"
f"stderr: {getattr(context.result, 'stderr', '')}"
)
+207 -1
View File
@@ -1,6 +1,7 @@
"""Actor-scoped context management commands.
Implements ``agents actor context clear``, ``agents actor context remove``,
Implements ``agents actor context clear``, ``agents actor context list``,
``agents actor context remove``, ``agents actor context show``,
``agents actor context export``, and ``agents actor context import`` per the
Outdated
Review

Suggestion: The module-level docstring on line 1 references only remove, export, and import commands. It should be updated to mention the three new list, show, and clear commands for completeness.

Suggestion: The module-level docstring on line 1 references only `remove`, `export`, and `import` commands. It should be updated to mention the three new `list`, `show`, and `clear` commands for completeness.
v3 specification. These commands manage named conversation contexts stored
under ``~/.cleveragents/context/`` using the
@@ -12,6 +13,7 @@ from __future__ import annotations
import hashlib
import json
import re
import shutil
from pathlib import Path
from typing import Annotated, Any
1
@@ -380,6 +382,210 @@ def context_clear(
_render_output(data, fmt, rich_panels=panels, ok_message="Context cleared")
# ---------------------------------------------------------------------------
# New: list / show commands
# ---------------------------------------------------------------------------
@app.command("list")
def context_list(
regex: Annotated[
str | None,
typer.Argument(
help="Optional regex pattern to filter context names",
),
] = None,
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:
"""List actor contexts with an optional regex filter.
When a REGEX pattern is supplied only context names matching the pattern
are listed. Output can be rendered as a rich table, JSON, YAML, plain
text, color, or table format.
Examples::
agents actor context list
agents actor context list "docs"
agents actor context list "^proj_" --format json
"""
base = _default_context_base(context_dir)
all_names = _list_context_names(base)
if regex is not None:
try:
pattern = re.compile(regex)
except re.error as exc:
typer.echo(f"Error: Invalid regex: {exc}", err=True)
raise typer.Exit(code=1) from exc
names = [n for n in all_names if pattern.search(n)]
else:
names = list(all_names)
if not names and regex is not None:
data: dict[str, Any] = {
"context_list": {
"count": 0,
"contexts": [],
"filter": regex,
}
}
console.print(format_output(data, fmt))
return
if not names:
typer.echo("No contexts found.")
return
# Build per-context summary data
context_entries: list[dict[str, Any]] = []
for cname in names:
ctx_mgr = ContextManager(cname, context_dir)
size_kb = _context_size_kb(ctx_mgr)
msg_count = len(ctx_mgr.messages)
created_at = ctx_mgr.metadata.get("created_at", "")
context_entries.append(
{
"name": cname,
"messages": msg_count,
"size_kb": round(size_kb, 1),
"created_at": created_at,
}
)
table_data: dict[str, Any] = {
"context_list": {
"count": len(names),
"contexts": context_entries,
**({"filter": regex} if regex is not None else {}),
}
}
if fmt == OutputFormat.RICH.value or fmt == OutputFormat.TABLE.value:
from rich.table import Table as RichTable
table = RichTable(title=f"Actor Contexts ({len(names)} total)")
table.add_column("Name", style="cyan")
table.add_column("Messages", style="green")
table.add_column("Size (KB)", style="magenta")
table.add_column("Created", style="yellow")
for entry in context_entries:
table.add_row(
entry["name"],
str(entry["messages"]),
f"{entry['size_kb']}",
entry["created_at"] or "",
)
console.print(table)
elif fmt == OutputFormat.PLAIN.value or fmt == OutputFormat.COLOR.value:
for cname in names:
typer.echo(cname)
else:
# json / yaml
console.print(format_output(table_data, fmt))
@app.command("show")
def context_show(
name: Annotated[
str,
typer.Argument(help="Context name to show"),
],
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 a summary of a named actor context.
Displays message count, total storage size, estimated token usage, and
creation timestamp for the given context. Output supports rich, JSON,
YAML, plain, table, and color formats.
Examples::
agents actor context show docs
agents actor context show docs --format json
"""
base = _default_context_base(context_dir)
if not (base / name).exists():
typer.echo(f"Error: Context '{name}' does not exist.", err=True)
raise typer.Exit(code=1)
ctx_mgr = ContextManager(name, context_dir)
size_kb = _context_size_kb(ctx_mgr)
msg_count = len(ctx_mgr.messages)
created_at = ctx_mgr.metadata.get("created_at", "")
# Estimate tokens: rough rule of thumb is ~4 chars per token
total_chars = sum(len(str(msg.get("content", ""))) for msg in ctx_mgr.messages)
estimated_tokens = round(total_chars / 4)
data: dict[str, Any] = {
"context_show": {
"name": name,
"message_count": msg_count,
"total_size_kb": round(size_kb, 1),
"estimated_tokens": estimated_tokens,
"created_at": created_at,
}
}
if fmt == OutputFormat.RICH.value:
panels = [
(
f"Context: {name}",
(
f"[bold]Messages:[/bold] {msg_count}\n"
f"[bold]Size:[/bold] {round(size_kb, 1)} KB\n"
f"[bold]Est. Tokens:[/bold] {estimated_tokens}\n"
f"[bold]Created:[/bold] {created_at}"
),
)
]
_render_output(data, fmt, rich_panels=panels, ok_message="Context summary")
elif fmt == OutputFormat.TABLE.value:
from rich.table import Table as RichTable
table = RichTable(title=f"Context Summary: {name}")
table.add_column("Metric", style="cyan")
table.add_column("Value", style="green")
table.add_row("Messages", str(msg_count))
table.add_row("Size (KB)", f"{round(size_kb, 1)}")
table.add_row("Est. Tokens", str(estimated_tokens))
table.add_row("Created", created_at)
console.print(table)
elif fmt == OutputFormat.PLAIN.value or fmt == OutputFormat.COLOR.value:
typer.echo(f"Context: {name}")
typer.echo(f"Messages: {msg_count}")
typer.echo(f"Size: {round(size_kb, 1)} KB")
typer.echo(f"Est. Tokens: {estimated_tokens}")
typer.echo(f"Created: {created_at}")
else:
console.print(format_output(data, fmt))
@app.command("export")
def context_export(
name: Annotated[