feat(plans): implement checkpoint listing and management CLI commands #8683
@@ -32,6 +32,7 @@ The format follows [Keep a Changelog](https://keepachangelog.com/en/1.1.0/).
|
||||
### Added
|
||||
|
||||
|
||||
- **Plan checkpoint management CLI commands** (#8683): Added `agents plan checkpoint-list <plan-id>` and `agents plan checkpoint-delete <checkpoint-id>` commands. Listing output now highlights checkpoint ID, type, created timestamp, reason, phase, and decision linkage with a concise field summary footer across rich/table/json/yaml formats. Deletion supports batch IDs, interactive confirmation (skip with `--yes`), and structured JSON/YAML responses for automation-friendly scripting.
|
||||
- **Invariant Remove CLI Command** (#8530): Implemented `agents invariant remove <id>` command that soft-deletes an invariant by ID. The command displays a confirmation prompt before removal (bypassable with `--yes`/`-y`), outputs the removed invariant ID on success, and shows a clear error message when the invariant ID does not exist. Supports `--format` flag for JSON and YAML output. Full BDD test coverage and Robot Framework integration tests included.
|
||||
- **TDD: MCPToolAdapter.infer_resource_slots() TypeError with null properties** (#10470):
|
||||
Added a TDD issue-capture Behave scenario that reproduces the bug where
|
||||
|
||||
@@ -0,0 +1,135 @@
|
||||
Feature: Checkpoint listing and management CLI commands
|
||||
As a user
|
||||
I want to list and delete checkpoints for a plan
|
||||
So that I can view and manage rollback points through the CLI
|
||||
|
||||
# ===================================================================
|
||||
# checkpoint-list: empty state
|
||||
# ===================================================================
|
||||
|
||||
Scenario: Listing checkpoints when none exist shows empty message
|
||||
Given a cp-cli mocked checkpoint service with no checkpoints for plan "01ARZ3NDEKTSV4RRFFQ69G5FAV"
|
||||
When I invoke cp-cli checkpoint-list for plan "01ARZ3NDEKTSV4RRFFQ69G5FAV" in default format
|
||||
Then the cp-cli command should succeed
|
||||
And the cp-cli output should contain "No checkpoints found"
|
||||
|
||||
Scenario: Listing checkpoints in json format when none exist returns empty list
|
||||
Given a cp-cli mocked checkpoint service with no checkpoints for plan "01ARZ3NDEKTSV4RRFFQ69G5FAV"
|
||||
When I invoke cp-cli checkpoint-list for plan "01ARZ3NDEKTSV4RRFFQ69G5FAV" with format "json"
|
||||
Then the cp-cli command should succeed
|
||||
And the cp-cli output should contain "[]"
|
||||
|
||||
# ===================================================================
|
||||
# checkpoint-list: populated state
|
||||
# ===================================================================
|
||||
|
||||
Scenario: Listing multiple checkpoints shows table with IDs and timestamps
|
||||
Given a cp-cli mocked checkpoint service with 3 checkpoints for plan "01ARZ3NDEKTSV4RRFFQ69G5FAV"
|
||||
When I invoke cp-cli checkpoint-list for plan "01ARZ3NDEKTSV4RRFFQ69G5FAV" in default format
|
||||
Then the cp-cli command should succeed
|
||||
And the cp-cli output should contain "Fields: checkpoint_id"
|
||||
|
||||
Scenario: Listing checkpoints in json format returns structured data
|
||||
Given a cp-cli mocked checkpoint service with 2 checkpoints for plan "01ARZ3NDEKTSV4RRFFQ69G5FAV"
|
||||
When I invoke cp-cli checkpoint-list for plan "01ARZ3NDEKTSV4RRFFQ69G5FAV" with format "json"
|
||||
Then the cp-cli command should succeed
|
||||
And the cp-cli output should contain "checkpoint_id"
|
||||
And the cp-cli output should contain "plan_id"
|
||||
|
||||
Scenario: Listing checkpoints in yaml format returns structured data
|
||||
Given a cp-cli mocked checkpoint service with 2 checkpoints for plan "01ARZ3NDEKTSV4RRFFQ69G5FAV"
|
||||
When I invoke cp-cli checkpoint-list for plan "01ARZ3NDEKTSV4RRFFQ69G5FAV" with format "yaml"
|
||||
Then the cp-cli command should succeed
|
||||
And the cp-cli output should contain "checkpoint_id"
|
||||
|
||||
Scenario: Listing checkpoints in table format returns structured data
|
||||
Given a cp-cli mocked checkpoint service with 2 checkpoints for plan "01ARZ3NDEKTSV4RRFFQ69G5FAV"
|
||||
When I invoke cp-cli checkpoint-list for plan "01ARZ3NDEKTSV4RRFFQ69G5FAV" with format "table"
|
||||
Then the cp-cli command should succeed
|
||||
And the cp-cli output should contain "checkpoint_id"
|
||||
|
||||
# ===================================================================
|
||||
# checkpoint-list: sort options
|
||||
# ===================================================================
|
||||
|
||||
Scenario: Listing checkpoints with sort desc returns newest first
|
||||
Given a cp-cli mocked checkpoint service with 3 checkpoints for plan "01ARZ3NDEKTSV4RRFFQ69G5FAV"
|
||||
When I invoke cp-cli checkpoint-list for plan "01ARZ3NDEKTSV4RRFFQ69G5FAV" with sort "desc"
|
||||
Then the cp-cli command should succeed
|
||||
|
||||
Scenario: Listing checkpoints with sort asc returns oldest first
|
||||
Given a cp-cli mocked checkpoint service with 3 checkpoints for plan "01ARZ3NDEKTSV4RRFFQ69G5FAV"
|
||||
When I invoke cp-cli checkpoint-list for plan "01ARZ3NDEKTSV4RRFFQ69G5FAV" with sort "asc"
|
||||
Then the cp-cli command should succeed
|
||||
|
||||
# ===================================================================
|
||||
# checkpoint-list: type filter
|
||||
# ===================================================================
|
||||
|
||||
Scenario: Listing checkpoints filtered by type manual
|
||||
Given a cp-cli mocked checkpoint service with checkpoints of mixed types for plan "01ARZ3NDEKTSV4RRFFQ69G5FAV"
|
||||
When I invoke cp-cli checkpoint-list for plan "01ARZ3NDEKTSV4RRFFQ69G5FAV" with type filter "manual"
|
||||
Then the cp-cli command should succeed
|
||||
|
||||
Scenario: Listing checkpoints filtered by type pre_write
|
||||
Given a cp-cli mocked checkpoint service with checkpoints of mixed types for plan "01ARZ3NDEKTSV4RRFFQ69G5FAV"
|
||||
When I invoke cp-cli checkpoint-list for plan "01ARZ3NDEKTSV4RRFFQ69G5FAV" with type filter "pre_write"
|
||||
Then the cp-cli command should succeed
|
||||
|
||||
# ===================================================================
|
||||
# checkpoint-list: error handling
|
||||
# ===================================================================
|
||||
|
||||
Scenario: Listing checkpoints when service raises CleverAgentsError aborts
|
||||
Given a cp-cli mocked checkpoint service that raises CleverAgentsError on list
|
||||
When I invoke cp-cli checkpoint-list for plan "01ARZ3NDEKTSV4RRFFQ69G5FAV" in default format
|
||||
Then the cp-cli command should abort
|
||||
And the cp-cli output should contain "Error"
|
||||
|
||||
# ===================================================================
|
||||
# checkpoint-delete: single deletion
|
||||
# ===================================================================
|
||||
|
||||
Scenario: Deleting a single checkpoint with --yes succeeds
|
||||
Given a cp-cli mocked checkpoint service that can delete checkpoints
|
||||
When I invoke cp-cli checkpoint-delete "01ARZ3NDEKTSV4RRFFQ69G5FAV" with yes flag
|
||||
Then the cp-cli command should succeed
|
||||
And the cp-cli output should contain "deleted"
|
||||
|
||||
Scenario: Deleting a single checkpoint in json format returns structured data
|
||||
Given a cp-cli mocked checkpoint service that can delete checkpoints
|
||||
When I invoke cp-cli checkpoint-delete "01ARZ3NDEKTSV4RRFFQ69G5FAV" with yes flag and format "json"
|
||||
Then the cp-cli command should succeed
|
||||
And the cp-cli output should contain "deleted_count"
|
||||
|
||||
# ===================================================================
|
||||
# checkpoint-delete: batch deletion
|
||||
# ===================================================================
|
||||
|
||||
Scenario: Deleting multiple checkpoints in batch with --yes succeeds
|
||||
Given a cp-cli mocked checkpoint service that can delete checkpoints
|
||||
When I invoke cp-cli checkpoint-delete multiple IDs with yes flag
|
||||
Then the cp-cli command should succeed
|
||||
And the cp-cli output should contain "deleted"
|
||||
|
||||
# ===================================================================
|
||||
# checkpoint-delete: error handling
|
||||
# ===================================================================
|
||||
|
||||
Scenario: Deleting a non-existent checkpoint shows error message
|
||||
Given a cp-cli mocked checkpoint service that raises ResourceNotFoundError on delete
|
||||
When I invoke cp-cli checkpoint-delete "01ARZ3NDEKTSV4RRFFQ69G5FAV" with yes flag
|
||||
Then the cp-cli command should abort
|
||||
And the cp-cli output should contain "not found"
|
||||
|
||||
Scenario: Deleting with no checkpoint IDs aborts
|
||||
Given a cp-cli mocked checkpoint service that can delete checkpoints
|
||||
When I invoke cp-cli checkpoint-delete with no IDs
|
||||
Then the cp-cli command should abort
|
||||
And the cp-cli output should contain "At least one checkpoint ID is required"
|
||||
|
||||
Scenario: Cancelling deletion confirmation aborts
|
||||
Given a cp-cli mocked checkpoint service that can delete checkpoints
|
||||
When I invoke cp-cli checkpoint-delete "01ARZ3NDEKTSV4RRFFQ69G5FAV" and decline confirmation
|
||||
Then the cp-cli command should abort
|
||||
And the cp-cli output should contain "Deletion cancelled"
|
||||
@@ -0,0 +1,260 @@
|
||||
"""Step definitions for checkpoint_cli_commands.feature."""
|
||||
|
||||
from __future__ import annotations
|
||||
|
||||
from datetime import UTC, datetime
|
||||
from unittest.mock import MagicMock, patch
|
||||
|
||||
from behave import given, then, when
|
||||
from behave.runner import Context
|
||||
from typer.testing import CliRunner
|
||||
|
||||
from cleveragents.cli.commands.plan import app as plan_app
|
||||
from cleveragents.core.exceptions import CleverAgentsError, ResourceNotFoundError
|
||||
from cleveragents.domain.models.core.checkpoint import Checkpoint, CheckpointMetadata
|
||||
|
||||
_PLAN_ID = "01ARZ3NDEKTSV4RRFFQ69G5FAV"
|
||||
_CP_ID_1 = "01ARZ3NDEKTSV4RRFFQ69G5FB1"
|
||||
_CP_ID_2 = "01ARZ3NDEKTSV4RRFFQ69G5FB2"
|
||||
_CP_ID_3 = "01ARZ3NDEKTSV4RRFFQ69G5FB3"
|
||||
|
||||
_PATCH_CONTAINER = "cleveragents.application.container.get_container"
|
||||
|
||||
|
||||
def _make_checkpoint(
|
||||
checkpoint_id: str = _CP_ID_1,
|
||||
plan_id: str = _PLAN_ID,
|
||||
checkpoint_type: str = "manual",
|
||||
reason: str = "test checkpoint",
|
||||
phase: str = "execute",
|
||||
created_at: datetime | None = None,
|
||||
) -> Checkpoint:
|
||||
"""Build a Checkpoint domain object for testing."""
|
||||
return Checkpoint(
|
||||
checkpoint_id=checkpoint_id,
|
||||
plan_id=plan_id,
|
||||
sandbox_ref="abc123",
|
||||
checkpoint_type=checkpoint_type,
|
||||
created_at=created_at or datetime(2026, 4, 1, 10, 0, 0, tzinfo=UTC),
|
||||
metadata=CheckpointMetadata(
|
||||
reason=reason,
|
||||
source_tool="test",
|
||||
phase=phase,
|
||||
),
|
||||
)
|
||||
|
||||
|
||||
# ----------------------------------------------------------------------
|
||||
# Given steps
|
||||
# ----------------------------------------------------------------------
|
||||
|
||||
|
||||
@given('a cp-cli mocked checkpoint service with no checkpoints for plan "{plan_id}"')
|
||||
def step_cp_cli_no_checkpoints(context: Context, plan_id: str) -> None:
|
||||
"""Set up a checkpoint service that returns no checkpoints."""
|
||||
context.cp_cli_runner = CliRunner()
|
||||
mock_container = MagicMock()
|
||||
mock_svc = MagicMock()
|
||||
mock_svc.list_checkpoints.return_value = []
|
||||
mock_container.checkpoint_service.return_value = mock_svc
|
||||
context.cp_cli_mock_container = mock_container
|
||||
|
||||
|
||||
@given(
|
||||
'a cp-cli mocked checkpoint service with {count:d} checkpoints for plan "{plan_id}"'
|
||||
)
|
||||
def step_cp_cli_n_checkpoints(context: Context, count: int, plan_id: str) -> None:
|
||||
"""Set up a checkpoint service that returns N checkpoints."""
|
||||
context.cp_cli_runner = CliRunner()
|
||||
checkpoints = [
|
||||
_make_checkpoint(
|
||||
checkpoint_id=f"01ARZ3NDEKTSV4RRFFQ69G5F{i:02d}",
|
||||
plan_id=plan_id,
|
||||
created_at=datetime(2026, 4, 1, 10, i, 0, tzinfo=UTC),
|
||||
)
|
||||
for i in range(1, count + 1)
|
||||
]
|
||||
mock_container = MagicMock()
|
||||
mock_svc = MagicMock()
|
||||
mock_svc.list_checkpoints.return_value = checkpoints
|
||||
mock_container.checkpoint_service.return_value = mock_svc
|
||||
context.cp_cli_mock_container = mock_container
|
||||
|
||||
|
||||
@given(
|
||||
'a cp-cli mocked checkpoint service with checkpoints of mixed types for plan "{plan_id}"'
|
||||
)
|
||||
def step_cp_cli_mixed_type_checkpoints(context: Context, plan_id: str) -> None:
|
||||
"""Set up a checkpoint service with checkpoints of different types."""
|
||||
context.cp_cli_runner = CliRunner()
|
||||
checkpoints = [
|
||||
_make_checkpoint(
|
||||
checkpoint_id=_CP_ID_1, plan_id=plan_id, checkpoint_type="manual"
|
||||
),
|
||||
_make_checkpoint(
|
||||
checkpoint_id=_CP_ID_2, plan_id=plan_id, checkpoint_type="pre_write"
|
||||
),
|
||||
_make_checkpoint(
|
||||
checkpoint_id=_CP_ID_3, plan_id=plan_id, checkpoint_type="post_step"
|
||||
),
|
||||
]
|
||||
mock_container = MagicMock()
|
||||
mock_svc = MagicMock()
|
||||
mock_svc.list_checkpoints.return_value = checkpoints
|
||||
mock_container.checkpoint_service.return_value = mock_svc
|
||||
context.cp_cli_mock_container = mock_container
|
||||
|
||||
|
||||
@given("a cp-cli mocked checkpoint service that raises CleverAgentsError on list")
|
||||
def step_cp_cli_list_error(context: Context) -> None:
|
||||
"""Set up a checkpoint service that raises CleverAgentsError on list."""
|
||||
context.cp_cli_runner = CliRunner()
|
||||
mock_container = MagicMock()
|
||||
mock_svc = MagicMock()
|
||||
mock_svc.list_checkpoints.side_effect = CleverAgentsError("service error")
|
||||
mock_container.checkpoint_service.return_value = mock_svc
|
||||
context.cp_cli_mock_container = mock_container
|
||||
|
||||
|
||||
@given("a cp-cli mocked checkpoint service that can delete checkpoints")
|
||||
def step_cp_cli_delete_ok(context: Context) -> None:
|
||||
"""Set up a checkpoint service that successfully deletes checkpoints."""
|
||||
context.cp_cli_runner = CliRunner()
|
||||
mock_container = MagicMock()
|
||||
mock_svc = MagicMock()
|
||||
mock_svc.delete_checkpoint.return_value = None
|
||||
mock_container.checkpoint_service.return_value = mock_svc
|
||||
context.cp_cli_mock_container = mock_container
|
||||
|
||||
|
||||
@given("a cp-cli mocked checkpoint service that raises ResourceNotFoundError on delete")
|
||||
def step_cp_cli_delete_not_found(context: Context) -> None:
|
||||
"""Set up a checkpoint service that raises ResourceNotFoundError on delete."""
|
||||
context.cp_cli_runner = CliRunner()
|
||||
mock_container = MagicMock()
|
||||
mock_svc = MagicMock()
|
||||
mock_svc.delete_checkpoint.side_effect = ResourceNotFoundError(
|
||||
resource_type="checkpoint", resource_id=_CP_ID_1
|
||||
)
|
||||
mock_container.checkpoint_service.return_value = mock_svc
|
||||
context.cp_cli_mock_container = mock_container
|
||||
|
||||
|
||||
# ----------------------------------------------------------------------
|
||||
# When steps
|
||||
# ----------------------------------------------------------------------
|
||||
|
||||
|
||||
@when('I invoke cp-cli checkpoint-list for plan "{plan_id}" in default format')
|
||||
def step_cp_cli_invoke_list(context: Context, plan_id: str) -> None:
|
||||
"""Invoke checkpoint-list command in default rich format."""
|
||||
with patch(_PATCH_CONTAINER, return_value=context.cp_cli_mock_container):
|
||||
context.cp_cli_result = context.cp_cli_runner.invoke(
|
||||
plan_app, ["checkpoint-list", plan_id]
|
||||
)
|
||||
|
||||
|
||||
@when('I invoke cp-cli checkpoint-list for plan "{plan_id}" with format "{fmt}"')
|
||||
def step_cp_cli_invoke_list_fmt(context: Context, plan_id: str, fmt: str) -> None:
|
||||
"""Invoke checkpoint-list command with a specific output format."""
|
||||
with patch(_PATCH_CONTAINER, return_value=context.cp_cli_mock_container):
|
||||
context.cp_cli_result = context.cp_cli_runner.invoke(
|
||||
plan_app, ["checkpoint-list", plan_id, "--format", fmt]
|
||||
)
|
||||
|
||||
|
||||
@when('I invoke cp-cli checkpoint-list for plan "{plan_id}" with sort "{sort}"')
|
||||
def step_cp_cli_invoke_list_sort(context: Context, plan_id: str, sort: str) -> None:
|
||||
"""Invoke checkpoint-list command with a sort option."""
|
||||
with patch(_PATCH_CONTAINER, return_value=context.cp_cli_mock_container):
|
||||
context.cp_cli_result = context.cp_cli_runner.invoke(
|
||||
plan_app, ["checkpoint-list", plan_id, "--sort", sort]
|
||||
)
|
||||
|
||||
|
||||
@when(
|
||||
'I invoke cp-cli checkpoint-list for plan "{plan_id}" with type filter "{cp_type}"'
|
||||
)
|
||||
def step_cp_cli_invoke_list_type(context: Context, plan_id: str, cp_type: str) -> None:
|
||||
"""Invoke checkpoint-list command with a type filter."""
|
||||
with patch(_PATCH_CONTAINER, return_value=context.cp_cli_mock_container):
|
||||
context.cp_cli_result = context.cp_cli_runner.invoke(
|
||||
plan_app, ["checkpoint-list", plan_id, "--type", cp_type]
|
||||
)
|
||||
|
||||
|
||||
@when('I invoke cp-cli checkpoint-delete "{cp_id}" with yes flag')
|
||||
def step_cp_cli_invoke_delete_yes(context: Context, cp_id: str) -> None:
|
||||
"""Invoke checkpoint-delete command with --yes flag."""
|
||||
with patch(_PATCH_CONTAINER, return_value=context.cp_cli_mock_container):
|
||||
context.cp_cli_result = context.cp_cli_runner.invoke(
|
||||
plan_app, ["checkpoint-delete", cp_id, "--yes"]
|
||||
)
|
||||
|
||||
|
||||
@when('I invoke cp-cli checkpoint-delete "{cp_id}" with yes flag and format "{fmt}"')
|
||||
def step_cp_cli_invoke_delete_yes_fmt(context: Context, cp_id: str, fmt: str) -> None:
|
||||
"""Invoke checkpoint-delete command with --yes flag and format."""
|
||||
with patch(_PATCH_CONTAINER, return_value=context.cp_cli_mock_container):
|
||||
context.cp_cli_result = context.cp_cli_runner.invoke(
|
||||
plan_app, ["checkpoint-delete", cp_id, "--yes", "--format", fmt]
|
||||
)
|
||||
|
||||
|
||||
@when("I invoke cp-cli checkpoint-delete multiple IDs with yes flag")
|
||||
def step_cp_cli_invoke_delete_multiple(context: Context) -> None:
|
||||
"""Invoke checkpoint-delete command with multiple checkpoint IDs."""
|
||||
with patch(_PATCH_CONTAINER, return_value=context.cp_cli_mock_container):
|
||||
context.cp_cli_result = context.cp_cli_runner.invoke(
|
||||
plan_app, ["checkpoint-delete", _CP_ID_1, _CP_ID_2, "--yes"]
|
||||
)
|
||||
|
||||
|
||||
@when("I invoke cp-cli checkpoint-delete with no IDs")
|
||||
def step_cp_cli_invoke_delete_no_ids(context: Context) -> None:
|
||||
"""Invoke checkpoint-delete command with no checkpoint IDs."""
|
||||
with patch(_PATCH_CONTAINER, return_value=context.cp_cli_mock_container):
|
||||
context.cp_cli_result = context.cp_cli_runner.invoke(
|
||||
plan_app, ["checkpoint-delete"]
|
||||
)
|
||||
|
||||
|
||||
@when('I invoke cp-cli checkpoint-delete "{cp_id}" and decline confirmation')
|
||||
def step_cp_cli_invoke_delete_decline(context: Context, cp_id: str) -> None:
|
||||
"""Invoke checkpoint-delete command and decline the confirmation prompt."""
|
||||
with patch(_PATCH_CONTAINER, return_value=context.cp_cli_mock_container):
|
||||
context.cp_cli_result = context.cp_cli_runner.invoke(
|
||||
plan_app, ["checkpoint-delete", cp_id], input="n\n"
|
||||
)
|
||||
|
||||
|
||||
# ----------------------------------------------------------------------
|
||||
# Then steps
|
||||
# ----------------------------------------------------------------------
|
||||
|
||||
|
||||
@then("the cp-cli command should succeed")
|
||||
def step_cp_cli_assert_success(context: Context) -> None:
|
||||
"""Assert the CLI exited with code 0."""
|
||||
assert context.cp_cli_result.exit_code == 0, (
|
||||
f"Expected exit_code=0, got {context.cp_cli_result.exit_code}. "
|
||||
f"Output: {context.cp_cli_result.output}"
|
||||
)
|
||||
|
||||
|
||||
@then("the cp-cli command should abort")
|
||||
def step_cp_cli_assert_abort(context: Context) -> None:
|
||||
"""Assert the CLI exited with a non-zero code."""
|
||||
assert context.cp_cli_result.exit_code != 0, (
|
||||
f"Expected non-zero exit_code, got {context.cp_cli_result.exit_code}. "
|
||||
f"Output: {context.cp_cli_result.output}"
|
||||
)
|
||||
|
||||
|
||||
@then('the cp-cli output should contain "{text}"')
|
||||
def step_cp_cli_assert_output_contains(context: Context, text: str) -> None:
|
||||
"""Assert the CLI output contains the expected text."""
|
||||
assert text in context.cp_cli_result.output, (
|
||||
f"Expected output to contain {text!r}. "
|
||||
f"Actual output: {context.cp_cli_result.output}"
|
||||
)
|
||||
@@ -5119,3 +5119,218 @@ def tree_decisions_cmd(
|
||||
expand=False,
|
||||
)
|
||||
)
|
||||
|
||||
|
||||
# ---------------------------------------------------------------------------
|
||||
# plan checkpoint-list / checkpoint-delete
|
||||
# ---------------------------------------------------------------------------
|
||||
|
||||
|
||||
@app.command("checkpoint-list")
|
||||
def checkpoint_list_cmd(
|
||||
plan_id: Annotated[
|
||||
str,
|
||||
typer.Argument(help="Plan ID (ULID) to list checkpoints for"),
|
||||
],
|
||||
sort: Annotated[
|
||||
str,
|
||||
typer.Option(
|
||||
"--sort",
|
||||
help="Sort order: asc (oldest first) or desc (newest first)",
|
||||
),
|
||||
] = "asc",
|
||||
checkpoint_type: Annotated[
|
||||
str | None,
|
||||
typer.Option(
|
||||
"--type",
|
||||
help="Filter by type: pre_write, post_step, manual, pre_decision",
|
||||
),
|
||||
] = None,
|
||||
fmt: Annotated[
|
||||
str,
|
||||
typer.Option(
|
||||
"--format",
|
||||
"-f",
|
||||
help=_FORMAT_HELP,
|
||||
),
|
||||
] = "rich",
|
||||
) -> None:
|
||||
"""List all checkpoints for a plan.
|
||||
|
||||
Displays checkpoint ID, timestamp, type, and state summary for each
|
||||
checkpoint associated with the given plan.
|
||||
|
||||
Examples::
|
||||
|
||||
agents plan checkpoint-list PLAN123
|
||||
agents plan checkpoint-list PLAN123 --sort desc
|
||||
agents plan checkpoint-list PLAN123 --type manual
|
||||
agents plan checkpoint-list PLAN123 --format json
|
||||
"""
|
||||
from cleveragents.application.container import get_container
|
||||
from cleveragents.core.exceptions import ResourceNotFoundError as RNF
|
||||
|
||||
try:
|
||||
container = get_container()
|
||||
svc = container.checkpoint_service()
|
||||
|
||||
checkpoints = svc.list_checkpoints(plan_id)
|
||||
|
||||
# Apply type filter
|
||||
if checkpoint_type is not None:
|
||||
checkpoints = [
|
||||
cp for cp in checkpoints if cp.checkpoint_type == checkpoint_type
|
||||
]
|
||||
|
||||
# Apply sort order
|
||||
reverse = sort.lower() == "desc"
|
||||
checkpoints = sorted(checkpoints, key=lambda cp: cp.created_at, reverse=reverse)
|
||||
|
||||
if fmt != OutputFormat.RICH.value:
|
||||
data: list[dict[str, object]] = [
|
||||
{
|
||||
"checkpoint_id": cp.checkpoint_id,
|
||||
"plan_id": cp.plan_id,
|
||||
"checkpoint_type": cp.checkpoint_type,
|
||||
"sandbox_ref": cp.sandbox_ref,
|
||||
"created_at": cp.created_at.isoformat(),
|
||||
"reason": cp.metadata.reason,
|
||||
"phase": cp.metadata.phase,
|
||||
"decision_id": cp.decision_id,
|
||||
}
|
||||
for cp in checkpoints
|
||||
]
|
||||
console.print(format_output(data, fmt))
|
||||
return
|
||||
|
||||
if not checkpoints:
|
||||
console.print(f"[dim]No checkpoints found for plan {plan_id}.[/dim]")
|
||||
return
|
||||
|
||||
table = Table(title=f"Checkpoints for Plan {plan_id}", show_header=True)
|
||||
table.add_column("Checkpoint ID", style="cyan", max_width=26)
|
||||
table.add_column("Checkpoint Type", style="yellow")
|
||||
table.add_column("Created", style="green")
|
||||
table.add_column("Reason")
|
||||
table.add_column("Phase")
|
||||
table.add_column("Decision ID", style="dim", max_width=26)
|
||||
|
||||
for cp in checkpoints:
|
||||
table.add_row(
|
||||
cp.checkpoint_id,
|
||||
cp.checkpoint_type,
|
||||
_format_relative_time(cp.created_at),
|
||||
cp.metadata.reason or "(none)",
|
||||
cp.metadata.phase or "(none)",
|
||||
cp.decision_id or "(none)",
|
||||
)
|
||||
|
||||
console.print(table)
|
||||
console.print(
|
||||
"[dim]Fields: checkpoint_id, checkpoint_type, created_at, reason, "
|
||||
"phase, decision_id[/dim]"
|
||||
)
|
||||
cp_word = "checkpoint" if len(checkpoints) == 1 else "checkpoints"
|
||||
console.print(
|
||||
f"[green bold]✓ OK[/green bold] {len(checkpoints)} {cp_word} listed"
|
||||
)
|
||||
|
||||
except RNF as e:
|
||||
console.print(f"[red]Not found:[/red] {e.message}")
|
||||
raise typer.Abort() from e
|
||||
except CleverAgentsError as e:
|
||||
console.print(f"[red]Error:[/red] {e.message}")
|
||||
raise typer.Abort() from e
|
||||
|
||||
|
||||
@app.command("checkpoint-delete")
|
||||
def checkpoint_delete_cmd(
|
||||
checkpoint_ids: Annotated[
|
||||
list[str] | None,
|
||||
typer.Argument(
|
||||
help="One or more checkpoint IDs to delete",
|
||||
metavar="CHECKPOINT_ID",
|
||||
),
|
||||
] = None,
|
||||
yes: Annotated[
|
||||
bool,
|
||||
typer.Option(
|
||||
"--yes",
|
||||
"-y",
|
||||
help="Skip confirmation prompt",
|
||||
),
|
||||
] = False,
|
||||
fmt: Annotated[
|
||||
str,
|
||||
typer.Option(
|
||||
"--format",
|
||||
"-f",
|
||||
help=_FORMAT_HELP,
|
||||
),
|
||||
] = "rich",
|
||||
) -> None:
|
||||
"""Delete one or more checkpoints by ID.
|
||||
|
||||
Accepts one or more checkpoint IDs as positional arguments.
|
||||
Prompts for confirmation unless --yes is supplied.
|
||||
|
||||
Examples::
|
||||
|
||||
agents plan checkpoint-delete CP123
|
||||
agents plan checkpoint-delete CP123 CP456 --yes
|
||||
agents plan checkpoint-delete CP123 --format json
|
||||
"""
|
||||
from cleveragents.application.container import get_container
|
||||
from cleveragents.core.exceptions import ResourceNotFoundError as RNF
|
||||
|
||||
ids: list[str] = list(checkpoint_ids or [])
|
||||
if not ids:
|
||||
console.print("[red]Error:[/red] At least one checkpoint ID is required.")
|
||||
raise typer.Abort()
|
||||
|
||||
if not yes:
|
||||
cp_word = "checkpoint" if len(ids) == 1 else "checkpoints"
|
||||
ids_display = ", ".join(ids)
|
||||
confirm = typer.confirm(f"Delete {len(ids)} {cp_word}: {ids_display}?")
|
||||
if not confirm:
|
||||
console.print("[yellow]Deletion cancelled.[/yellow]")
|
||||
raise typer.Abort()
|
||||
|
||||
container = get_container()
|
||||
svc = container.checkpoint_service()
|
||||
|
||||
deleted: list[str] = []
|
||||
errors: list[dict[str, str]] = []
|
||||
|
||||
for cp_id in ids:
|
||||
try:
|
||||
svc.delete_checkpoint(cp_id)
|
||||
deleted.append(cp_id)
|
||||
except RNF:
|
||||
errors.append({"checkpoint_id": cp_id, "error": "not found"})
|
||||
except CleverAgentsError as e:
|
||||
errors.append({"checkpoint_id": cp_id, "error": e.message})
|
||||
|
||||
if fmt != OutputFormat.RICH.value:
|
||||
result_data: dict[str, object] = {
|
||||
"deleted": deleted,
|
||||
"errors": errors,
|
||||
"deleted_count": len(deleted),
|
||||
"error_count": len(errors),
|
||||
}
|
||||
console.print(format_output(result_data, fmt))
|
||||
return
|
||||
|
||||
if deleted:
|
||||
cp_word = "checkpoint" if len(deleted) == 1 else "checkpoints"
|
||||
console.print(f"[green bold]✓ OK[/green bold] {len(deleted)} {cp_word} deleted")
|
||||
for cp_id in deleted:
|
||||
console.print(f" [dim]Deleted:[/dim] {cp_id}")
|
||||
|
||||
if errors:
|
||||
for err in errors:
|
||||
cp_id_val = err["checkpoint_id"]
|
||||
err_val = err["error"]
|
||||
console.print(f"[red]Error:[/red] {cp_id_val} — {err_val}")
|
||||
if not deleted:
|
||||
raise typer.Abort()
|
||||
|
||||
Reference in New Issue
Block a user