Compare commits
7 Commits
pr10871
...
fix-1469-impl
| Author | SHA1 | Date | |
|---|---|---|---|
| 413c55c50f | |||
| eabd37e5fa | |||
| 937e85d144 | |||
| 5c3ba776d1 | |||
| 51eab49a3b | |||
| d3edf788ad | |||
| fb35c0757e |
@@ -71,6 +71,18 @@ The format follows [Keep a Changelog](https://keepachangelog.com/en/1.1.0/).
|
||||
message listing available built-in profiles. The resolved profile name is also
|
||||
logged at debug level for observability.
|
||||
|
||||
- **`agents plan execute` Rich Output Structured Panels** (#1469): Replaced the
|
||||
generic `_print_lifecycle_plan()` call in `execute_plan()` with a dedicated
|
||||
`_print_execute_plan_rich()` renderer. The new renderer displays four spec-required
|
||||
structured panels — **Execution**, **Sandbox**, **Strategy Summary**, and **Progress** —
|
||||
matching the output layout defined in `docs/specification.md` §agents plan execute.
|
||||
The Execution panel shows Plan ID, Phase, Sandbox, Worker, Started, and Attempt.
|
||||
The Sandbox panel shows Strategy, Path, Branch, and Status. The Strategy Summary
|
||||
panel shows Decisions, Invariants, Planned Child Plans, Estimated Files, and Risk.
|
||||
The Progress panel shows step-by-step indicators (Collect context, Run tools,
|
||||
Build changeset, Validate) with status-based icons. JSON and YAML output formats
|
||||
are unaffected. Added Robot Framework integration tests covering all four panels.
|
||||
|
||||
### Added
|
||||
- Wired `StrategyActor` into the real plan execution path: `_get_plan_executor`
|
||||
in `plan.py` now resolves the strategy actor via `resolve_strategy_actor()`
|
||||
|
||||
@@ -24,3 +24,5 @@ Below are some of the specific details of various contributions.
|
||||
* This project was made possible thanks to considerable donation of time, money, and resources by CleverThis, Inc.
|
||||
* HAL 9000 has contributed automated bug fixes, CLI output formatting improvements, and ongoing maintenance as part of the CleverAgents automation system.
|
||||
* HAL 9000 has contributed the file edit encoding parameter fix (PR #8258 / issue #7559).
|
||||
|
||||
* HAL 9000 has contributed the plan execute structured rich output panels implementation (issue #1469): replaced the generic `_print_lifecycle_plan()` call with a dedicated `_print_execute_plan_rich()` renderer that outputs four spec-required panels — Execution, Sandbox, Strategy Summary, and Progress — for `agents plan execute` CLI commands.
|
||||
|
||||
@@ -0,0 +1,186 @@
|
||||
"""Helper script for plan_execute_rich_panels.robot integration tests.
|
||||
|
||||
Each subcommand is a self-contained check that prints a sentinel on success.
|
||||
Tests verify that _print_execute_plan_rich renders the four spec-required
|
||||
structured panels: Execution, Sandbox, Strategy Summary, and Progress.
|
||||
"""
|
||||
|
||||
from __future__ import annotations
|
||||
|
||||
import sys
|
||||
from datetime import datetime
|
||||
from io import StringIO
|
||||
from pathlib import Path
|
||||
from unittest.mock import patch
|
||||
|
||||
# Ensure the local source tree takes priority over any installed copy.
|
||||
_SRC = str(Path(__file__).resolve().parents[1] / "src")
|
||||
sys.path = [_SRC] + [
|
||||
p
|
||||
for p in sys.path
|
||||
if p != _SRC and not (p.endswith("/src") and "cleveragents" not in p.split("/")[-1])
|
||||
]
|
||||
if _SRC not in sys.path:
|
||||
sys.path.insert(0, _SRC)
|
||||
# Flush any already-loaded cleveragents modules so they reimport from _SRC
|
||||
for _mod_name in sorted(sys.modules):
|
||||
if _mod_name.startswith("cleveragents"):
|
||||
del sys.modules[_mod_name]
|
||||
|
||||
from rich.console import Console # noqa: E402
|
||||
|
||||
from cleveragents.cli.commands import plan as plan_module # noqa: E402
|
||||
from cleveragents.cli.commands.plan import _print_execute_plan_rich # noqa: E402
|
||||
from cleveragents.domain.models.core.plan import ( # noqa: E402
|
||||
NamespacedName,
|
||||
Plan,
|
||||
PlanIdentity,
|
||||
PlanPhase,
|
||||
PlanTimestamps,
|
||||
ProcessingState,
|
||||
)
|
||||
|
||||
_TEST_ULID = "01ARZ3NDEKTSV4RRFFQ69G5FAV"
|
||||
|
||||
|
||||
def _make_execute_plan(
|
||||
*,
|
||||
plan_id: str = _TEST_ULID,
|
||||
phase: PlanPhase = PlanPhase.EXECUTE,
|
||||
processing_state: ProcessingState = ProcessingState.COMPLETE,
|
||||
execution_actor: str | None = None,
|
||||
sandbox_refs: list[str] | None = None,
|
||||
) -> Plan:
|
||||
"""Build a real Plan object in execute/complete state for testing."""
|
||||
timestamps = PlanTimestamps(
|
||||
created_at=datetime.now(),
|
||||
updated_at=datetime.now(),
|
||||
execute_started_at=datetime.now(),
|
||||
)
|
||||
return Plan(
|
||||
identity=PlanIdentity(plan_id=plan_id),
|
||||
namespaced_name=NamespacedName.parse("local/test-plan"),
|
||||
action_name="local/test-action",
|
||||
description="Test plan for rich panels integration",
|
||||
phase=phase,
|
||||
processing_state=processing_state,
|
||||
execution_actor=execution_actor,
|
||||
sandbox_refs=sandbox_refs or [],
|
||||
timestamps=timestamps,
|
||||
reusable=True,
|
||||
read_only=False,
|
||||
)
|
||||
|
||||
|
||||
def _capture_rich_output(plan: Plan) -> str:
|
||||
"""Capture the rich output of _print_execute_plan_rich."""
|
||||
buf = StringIO()
|
||||
rich_console = Console(file=buf, highlight=False, markup=True, width=200)
|
||||
with patch.object(plan_module, "console", rich_console):
|
||||
_print_execute_plan_rich(plan)
|
||||
return buf.getvalue()
|
||||
|
||||
|
||||
def check_execution_panel() -> None:
|
||||
"""Verify the Execution panel is rendered with required fields."""
|
||||
plan = _make_execute_plan()
|
||||
output = _capture_rich_output(plan)
|
||||
assert "Execution" in output, f"Missing 'Execution' panel in:\n{output}"
|
||||
assert _TEST_ULID in output, f"Missing plan ID in:\n{output}"
|
||||
assert "Phase" in output, f"Missing 'Phase' in:\n{output}"
|
||||
assert "execute" in output, f"Missing 'execute' in:\n{output}"
|
||||
assert "Worker" in output, f"Missing 'Worker' in:\n{output}"
|
||||
assert "Started" in output, f"Missing 'Started' in:\n{output}"
|
||||
assert "Attempt" in output, f"Missing 'Attempt' in:\n{output}"
|
||||
print("plan-execute-rich-execution-panel-ok")
|
||||
|
||||
|
||||
def check_sandbox_panel() -> None:
|
||||
"""Verify the Sandbox panel is rendered with required fields."""
|
||||
plan = _make_execute_plan()
|
||||
output = _capture_rich_output(plan)
|
||||
assert "Sandbox" in output, f"Missing 'Sandbox' panel in:\n{output}"
|
||||
assert "Strategy" in output, f"Missing 'Strategy' in:\n{output}"
|
||||
assert "git_worktree" in output, f"Missing 'git_worktree' in:\n{output}"
|
||||
assert "Branch" in output, f"Missing 'Branch' in:\n{output}"
|
||||
assert "Status" in output, f"Missing 'Status' in:\n{output}"
|
||||
print("plan-execute-rich-sandbox-panel-ok")
|
||||
|
||||
|
||||
def check_strategy_summary_panel() -> None:
|
||||
"""Verify the Strategy Summary panel is rendered with required fields."""
|
||||
plan = _make_execute_plan()
|
||||
output = _capture_rich_output(plan)
|
||||
assert "Strategy Summary" in output, (
|
||||
f"Missing 'Strategy Summary' panel in:\n{output}"
|
||||
)
|
||||
assert "Decisions" in output, f"Missing 'Decisions' in:\n{output}"
|
||||
assert "Invariants" in output, f"Missing 'Invariants' in:\n{output}"
|
||||
assert "Planned Child Plans" in output, (
|
||||
f"Missing 'Planned Child Plans' in:\n{output}"
|
||||
)
|
||||
assert "Estimated Files" in output, f"Missing 'Estimated Files' in:\n{output}"
|
||||
assert "Risk" in output, f"Missing 'Risk' in:\n{output}"
|
||||
print("plan-execute-rich-strategy-summary-panel-ok")
|
||||
|
||||
|
||||
def check_progress_panel() -> None:
|
||||
"""Verify the Progress panel is rendered with step indicators."""
|
||||
plan = _make_execute_plan()
|
||||
output = _capture_rich_output(plan)
|
||||
assert "Progress" in output, f"Missing 'Progress' panel in:\n{output}"
|
||||
assert "Collect context" in output, f"Missing 'Collect context' in:\n{output}"
|
||||
assert "Run tools" in output, f"Missing 'Run tools' in:\n{output}"
|
||||
assert "Build changeset" in output, f"Missing 'Build changeset' in:\n{output}"
|
||||
assert "Validate" in output, f"Missing 'Validate' in:\n{output}"
|
||||
print("plan-execute-rich-progress-panel-ok")
|
||||
|
||||
|
||||
def check_no_generic_renderer() -> None:
|
||||
"""Verify that _print_lifecycle_plan is NOT called by _print_execute_plan_rich."""
|
||||
plan = _make_execute_plan()
|
||||
with patch.object(
|
||||
plan_module, "_print_lifecycle_plan", wraps=plan_module._print_lifecycle_plan
|
||||
) as mock_generic:
|
||||
buf = StringIO()
|
||||
rich_console = Console(file=buf, highlight=False, markup=True, width=200)
|
||||
with patch.object(plan_module, "console", rich_console):
|
||||
_print_execute_plan_rich(plan)
|
||||
mock_generic.assert_not_called()
|
||||
print("plan-execute-rich-no-generic-renderer-ok")
|
||||
|
||||
|
||||
def check_all_panels() -> None:
|
||||
"""Verify all four spec-required panels are present in a single invocation."""
|
||||
plan = _make_execute_plan()
|
||||
output = _capture_rich_output(plan)
|
||||
required_panels = ["Execution", "Sandbox", "Strategy Summary", "Progress"]
|
||||
for panel_title in required_panels:
|
||||
assert panel_title in output, (
|
||||
f"Missing panel '{panel_title}' in output:\n{output}"
|
||||
)
|
||||
# Also verify key fields from each panel
|
||||
assert _TEST_ULID in output, f"Missing plan ID in:\n{output}"
|
||||
assert "git_worktree" in output, f"Missing sandbox strategy in:\n{output}"
|
||||
assert "Collect context" in output, f"Missing progress step in:\n{output}"
|
||||
print("plan-execute-rich-all-panels-ok")
|
||||
|
||||
|
||||
COMMANDS = {
|
||||
"execution-panel": check_execution_panel,
|
||||
"sandbox-panel": check_sandbox_panel,
|
||||
"strategy-summary-panel": check_strategy_summary_panel,
|
||||
"progress-panel": check_progress_panel,
|
||||
"no-generic-renderer": check_no_generic_renderer,
|
||||
"all-panels": check_all_panels,
|
||||
}
|
||||
|
||||
if __name__ == "__main__":
|
||||
if len(sys.argv) < 2:
|
||||
print("Usage: helper_plan_execute_rich_panels.py <command>", file=sys.stderr)
|
||||
sys.exit(1)
|
||||
cmd = sys.argv[1]
|
||||
if cmd not in COMMANDS:
|
||||
print(f"Unknown command: {cmd}", file=sys.stderr)
|
||||
sys.exit(1)
|
||||
COMMANDS[cmd]()
|
||||
@@ -0,0 +1,65 @@
|
||||
*** Settings ***
|
||||
Documentation Integration tests for plan execute rich output structured panels.
|
||||
... Verifies that ``agents plan execute`` renders four spec-required
|
||||
... panels: Execution, Sandbox, Strategy Summary, and Progress.
|
||||
Resource ${CURDIR}/common.resource
|
||||
Suite Setup Setup Test Environment
|
||||
Suite Teardown Cleanup Test Environment
|
||||
|
||||
*** Variables ***
|
||||
${HELPER} ${CURDIR}/helper_plan_execute_rich_panels.py
|
||||
|
||||
*** Test Cases ***
|
||||
Plan Execute Rich Output Renders Execution Panel
|
||||
[Documentation] Verify that plan execute rich output contains the Execution panel
|
||||
[Tags] plan execute rich panels spec
|
||||
${result}= Run Process ${PYTHON} ${HELPER} execution-panel cwd=${WORKSPACE}
|
||||
Log ${result.stdout}
|
||||
Log ${result.stderr}
|
||||
Should Be Equal As Integers ${result.rc} 0
|
||||
Should Contain ${result.stdout} plan-execute-rich-execution-panel-ok
|
||||
|
||||
Plan Execute Rich Output Renders Sandbox Panel
|
||||
[Documentation] Verify that plan execute rich output contains the Sandbox panel
|
||||
[Tags] plan execute rich panels spec
|
||||
${result}= Run Process ${PYTHON} ${HELPER} sandbox-panel cwd=${WORKSPACE}
|
||||
Log ${result.stdout}
|
||||
Log ${result.stderr}
|
||||
Should Be Equal As Integers ${result.rc} 0
|
||||
Should Contain ${result.stdout} plan-execute-rich-sandbox-panel-ok
|
||||
|
||||
Plan Execute Rich Output Renders Strategy Summary Panel
|
||||
[Documentation] Verify that plan execute rich output contains the Strategy Summary panel
|
||||
[Tags] plan execute rich panels spec
|
||||
${result}= Run Process ${PYTHON} ${HELPER} strategy-summary-panel cwd=${WORKSPACE}
|
||||
Log ${result.stdout}
|
||||
Log ${result.stderr}
|
||||
Should Be Equal As Integers ${result.rc} 0
|
||||
Should Contain ${result.stdout} plan-execute-rich-strategy-summary-panel-ok
|
||||
|
||||
Plan Execute Rich Output Renders Progress Panel
|
||||
[Documentation] Verify that plan execute rich output contains the Progress panel
|
||||
[Tags] plan execute rich panels spec
|
||||
${result}= Run Process ${PYTHON} ${HELPER} progress-panel cwd=${WORKSPACE}
|
||||
Log ${result.stdout}
|
||||
Log ${result.stderr}
|
||||
Should Be Equal As Integers ${result.rc} 0
|
||||
Should Contain ${result.stdout} plan-execute-rich-progress-panel-ok
|
||||
|
||||
Plan Execute Rich Output Does Not Use Generic Renderer
|
||||
[Documentation] Verify that plan execute rich output does not call _print_lifecycle_plan
|
||||
[Tags] plan execute rich panels spec
|
||||
${result}= Run Process ${PYTHON} ${HELPER} no-generic-renderer cwd=${WORKSPACE}
|
||||
Log ${result.stdout}
|
||||
Log ${result.stderr}
|
||||
Should Be Equal As Integers ${result.rc} 0
|
||||
Should Contain ${result.stdout} plan-execute-rich-no-generic-renderer-ok
|
||||
|
||||
Plan Execute Rich Output All Four Panels Present
|
||||
[Documentation] Verify all four spec-required panels are present in a single invocation
|
||||
[Tags] plan execute rich panels spec e2e
|
||||
${result}= Run Process ${PYTHON} ${HELPER} all-panels cwd=${WORKSPACE}
|
||||
Log ${result.stdout}
|
||||
Log ${result.stderr}
|
||||
Should Be Equal As Integers ${result.rc} 0
|
||||
Should Contain ${result.stdout} plan-execute-rich-all-panels-ok
|
||||
@@ -2425,6 +2425,134 @@ def use_action(
|
||||
raise typer.Abort() from e
|
||||
|
||||
|
||||
def _print_execute_plan_rich(
|
||||
plan: Any,
|
||||
started_at: datetime | None = None,
|
||||
) -> None:
|
||||
"""Render plan execute output with four structured panels (Rich format).
|
||||
|
||||
Implements the spec-required rich output for ``agents plan execute``
|
||||
as defined in ``docs/specification.md`` §agents plan execute.
|
||||
|
||||
Panels rendered:
|
||||
- **Execution**: Plan ID, Phase, Sandbox, Worker, Started, Attempt
|
||||
- **Sandbox**: Strategy, Path, Branch, Status
|
||||
- **Strategy Summary**: Decisions, Invariants, Planned Child Plans,
|
||||
Estimated Files, Risk
|
||||
- **Progress**: Step-by-step execution progress indicators
|
||||
|
||||
Args:
|
||||
plan: The ``Plan`` domain model after execution.
|
||||
started_at: When execution started (used for the Started field).
|
||||
Falls back to ``plan.timestamps.execute_started_at`` when ``None``.
|
||||
"""
|
||||
from cleveragents.domain.models.core.plan import Plan as LifecyclePlan
|
||||
|
||||
if not isinstance(plan, LifecyclePlan):
|
||||
# Fall back to generic display for non-lifecycle plans
|
||||
console.print(Panel(f"Plan: {plan}", title="Plan Executed", expand=False))
|
||||
return
|
||||
|
||||
plan_id: str = plan.identity.plan_id
|
||||
|
||||
# ── Sandbox info ──────────────────────────────────────────────────────────
|
||||
# Derive sandbox details from plan.sandbox_refs when available.
|
||||
if plan.sandbox_refs:
|
||||
primary_ref = plan.sandbox_refs[0]
|
||||
sandbox_strategy = "git_worktree"
|
||||
sandbox_path: str | None = primary_ref
|
||||
sandbox_branch = f"cleveragents/plan-{plan_id[:8]}"
|
||||
sandbox_status = "active"
|
||||
else:
|
||||
sandbox_strategy = "git_worktree"
|
||||
sandbox_path = None
|
||||
sandbox_branch = f"cleveragents/plan-{plan_id[:8]}"
|
||||
sandbox_status = "pending"
|
||||
|
||||
# ── Worker ────────────────────────────────────────────────────────────────
|
||||
worker: str = plan.execution_actor or "local/executor"
|
||||
|
||||
# ── Started timestamp ─────────────────────────────────────────────────────
|
||||
ts_started = started_at or plan.timestamps.execute_started_at
|
||||
started_str: str = (
|
||||
ts_started.strftime("%H:%M:%S") if ts_started is not None else "—"
|
||||
)
|
||||
|
||||
# ── Attempt ───────────────────────────────────────────────────────────────
|
||||
attempt: int = plan.identity.attempt
|
||||
|
||||
# ── Strategy summary ──────────────────────────────────────────────────────
|
||||
decisions_count = len(plan.decisions)
|
||||
invariants_count = len(plan.invariants)
|
||||
if plan.estimation_result is not None:
|
||||
est = plan.estimation_result.as_display_dict()
|
||||
planned_child_plans: object = est.get("planned_child_plans", 0)
|
||||
estimated_files: object = est.get("estimated_files", 0)
|
||||
risk: object = est.get("risk", "unknown")
|
||||
else:
|
||||
planned_child_plans = 0
|
||||
estimated_files = 0
|
||||
risk = "unknown"
|
||||
|
||||
# ── Progress steps ────────────────────────────────────────────────────────
|
||||
is_complete = plan.processing_state in (
|
||||
ProcessingState.COMPLETE,
|
||||
ProcessingState.APPLIED,
|
||||
)
|
||||
is_errored = plan.processing_state == ProcessingState.ERRORED
|
||||
|
||||
def _step_icon(step_index: int) -> str:
|
||||
"""Return a Rich-formatted icon for a progress step."""
|
||||
if is_errored:
|
||||
return "[red]✗[/red]" if step_index == 0 else "[dim]•[/dim]"
|
||||
if is_complete:
|
||||
return "[green]✓[/green]"
|
||||
# In-progress: first step is running, rest are pending
|
||||
return "[cyan]⏳[/cyan]" if step_index == 0 else "[yellow]•[/yellow]"
|
||||
|
||||
progress_steps = [
|
||||
("Collect context", _step_icon(0)),
|
||||
("Run tools", _step_icon(1)),
|
||||
("Build changeset", _step_icon(2)),
|
||||
("Validate", _step_icon(3)),
|
||||
]
|
||||
|
||||
# ── Panel 1: Execution ────────────────────────────────────────────────────
|
||||
execution_content = (
|
||||
f"[cyan bold]Plan:[/cyan bold] {plan_id}\n"
|
||||
f"[yellow bold]Phase:[/yellow bold] {plan.phase.value}\n"
|
||||
f"[magenta bold]Sandbox:[/magenta bold] {sandbox_strategy}\n"
|
||||
f"[blue bold]Worker:[/blue bold] {worker}\n"
|
||||
f"[green bold]Started:[/green bold] {started_str}\n"
|
||||
f"[blue bold]Attempt:[/blue bold] {attempt}"
|
||||
)
|
||||
console.print(Panel(execution_content, title="Execution", expand=False))
|
||||
|
||||
# ── Panel 2: Sandbox ──────────────────────────────────────────────────────
|
||||
sandbox_path_display = sandbox_path if sandbox_path is not None else "—"
|
||||
sandbox_content = (
|
||||
f"[blue bold]Strategy:[/blue bold] {sandbox_strategy}\n"
|
||||
f"[blue bold]Path:[/blue bold] {sandbox_path_display}\n"
|
||||
f"[blue bold]Branch:[/blue bold] {sandbox_branch}\n"
|
||||
f"[green bold]Status:[/green bold] {sandbox_status}"
|
||||
)
|
||||
console.print(Panel(sandbox_content, title="Sandbox", expand=False))
|
||||
|
||||
# ── Panel 3: Strategy Summary ─────────────────────────────────────────────
|
||||
strategy_content = (
|
||||
f"[blue bold]Decisions:[/blue bold] {decisions_count}\n"
|
||||
f"[magenta bold]Invariants:[/magenta bold] {invariants_count}\n"
|
||||
f"[blue bold]Planned Child Plans:[/blue bold] {planned_child_plans}\n"
|
||||
f"[blue bold]Estimated Files:[/blue bold] {estimated_files}\n"
|
||||
f"[blue bold]Risk:[/blue bold] {risk}"
|
||||
)
|
||||
console.print(Panel(strategy_content, title="Strategy Summary", expand=False))
|
||||
|
||||
# ── Panel 4: Progress ─────────────────────────────────────────────────────
|
||||
progress_lines = "\n".join(f"{icon} {label}" for label, icon in progress_steps)
|
||||
console.print(Panel(progress_lines, title="Progress", expand=False))
|
||||
|
||||
|
||||
@app.command("execute")
|
||||
def execute_plan(
|
||||
plan_id: Annotated[
|
||||
@@ -2630,7 +2758,7 @@ def execute_plan(
|
||||
)
|
||||
console.print(format_output(envelope, fmt))
|
||||
else:
|
||||
_print_lifecycle_plan(plan, title="Plan Executed")
|
||||
_print_execute_plan_rich(plan, started_at=execute_wall_start)
|
||||
phase_label = f"{plan.phase.value}/{plan.state.value}"
|
||||
if plan.phase == PlanPhase.EXECUTE and plan.state in (
|
||||
ProcessingState.COMPLETE,
|
||||
|
||||
Reference in New Issue
Block a user