Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
34 changes: 31 additions & 3 deletions agentdescent/cli.py
Original file line number Diff line number Diff line change
Expand Up @@ -4770,12 +4770,12 @@ def build_parser() -> argparse.ArgumentParser:
s.set_defaults(fn=cmd_status)

s = sub.add_parser("watch", help="follow a run's rounds until it ends")
s.add_argument("run_id")
s.add_argument("run_id", help="a run id, or `latest` for the newest run")
s.add_argument("--interval", type=float, default=5.0)
s.set_defaults(fn=cmd_watch)

s = sub.add_parser("show", help="the evolved artifact, and the diff against the original")
s.add_argument("run_id")
s.add_argument("run_id", help="a run id, or `latest` for the newest run")
s.add_argument("--diff", action="store_true", default=True)
s.add_argument("--no-diff", action="store_false", dest="diff")
s.set_defaults(fn=cmd_show)
Expand Down Expand Up @@ -5002,7 +5002,7 @@ def build_parser() -> argparse.ArgumentParser:
s2.set_defaults(fn=cmd_algorithms)

s = sub.add_parser("diagnose", help="why a run learned nothing (or stopped)")
s.add_argument("run_id")
s.add_argument("run_id", help="a run id, or `latest` for the newest run")
s.add_argument("--json", action="store_true", default=argparse.SUPPRESS,
help="machine-readable output")
s.set_defaults(fn=cmd_diagnose)
Expand Down Expand Up @@ -5424,6 +5424,33 @@ def _accept_json_after_verb(sub: "argparse._SubParsersAction") -> None:
help="machine-readable output")


#: Words that stand for "the newest run in the store" wherever a run id goes (#892).
LATEST_RUN_ALIASES = ("latest", "last")


def _resolve_latest_run(a: argparse.Namespace) -> None:
"""Swap ``latest`` / ``last`` for the newest run id on run-id commands.

A real run directory with that name still wins, and an empty store is a
clear error rather than ``no run 'latest'``.
"""
if getattr(a, "fn", None) not in (cmd_status, cmd_watch, cmd_show, cmd_apply,
cmd_cancel, cmd_resume, cmd_diagnose):
return
rid = getattr(a, "run_id", None)
if not isinstance(rid, str) or rid.lower() not in LATEST_RUN_ALIASES:
return
store = getattr(a, "store", None)
if os.path.isdir(os.path.join(runstore.store_path(store), rid)):
return
runs = runstore.list_runs(store=store)
if not runs:
raise runstore.RunStoreError(
f"no runs under {runstore.store_path(store)} yet, so {rid!r} has nothing "
"to point at; start one with `agentdescent demo` or `agentdescent evolve`")
a.run_id = runs[0].run_id


def main(argv: Optional[Sequence[str]] = None) -> int:
args = list(sys.argv[1:] if argv is None else argv)
_CliParser.json_errors = _wants_json(args)
Expand All @@ -5432,6 +5459,7 @@ def main(argv: Optional[Sequence[str]] = None) -> int:
finally:
_CliParser.json_errors = False
try:
_resolve_latest_run(a)
return a.fn(a)
except (SpecError, runstore.RunStoreError) as e:
# `--json` callers get the same object `_spec_error` prints (#658).
Expand Down
5 changes: 5 additions & 0 deletions changelog.d/892-latest-run-alias.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,5 @@
Added
`status`, `watch`, `show`, `apply`, `cancel`, `resume` and `diagnose` accept
`latest` (or `last`) in place of a run id and use the newest run in the store,
so `agentdescent show latest` works straight after a demo; an empty store
says so instead of `no run 'latest'` (#892).
44 changes: 44 additions & 0 deletions tests/test_latest_run_alias.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,44 @@
"""`latest` / `last` stand for the newest run id on run-id commands (#892)."""

import json
import os

from agentdescent import cli, runstore


def _mk(store, rid):
return runstore.create({"kind": "text", "target": "x.txt"}, run_id=rid, store=store)


def test_latest_resolves_to_newest_run(tmp_path, capsys):
store = str(tmp_path / "runs")
_mk(store, "20260101-000000-aaaaaa")
_mk(store, "20260202-000000-bbbbbb")
for word in ("latest", "last", "LATEST"):
assert cli.main(["--store", store, "--json", "status", word]) == 0
out = json.loads(capsys.readouterr().out)
assert out["run_id"] == "20260202-000000-bbbbbb"


def test_real_run_named_latest_wins(tmp_path, capsys):
store = str(tmp_path / "runs")
_mk(store, "latest")
_mk(store, "20990101-000000-cccccc")
assert cli.main(["--store", store, "--json", "status", "latest"]) == 0
assert json.loads(capsys.readouterr().out)["run_id"] == "latest"


def test_latest_on_empty_store_is_clear(tmp_path, capsys):
store = str(tmp_path / "empty")
assert cli.main(["--store", store, "--json", "show", "latest"]) == 2
out = json.loads(capsys.readouterr().out)
assert out["ok"] is False
assert "no runs under" in out["error"] and "agentdescent demo" in out["error"]
assert not os.path.exists(os.path.join(store, "latest"))


def test_other_ids_untouched(tmp_path, capsys):
store = str(tmp_path / "runs")
_mk(store, "20260101-000000-aaaaaa")
assert cli.main(["--store", store, "status", "nope"]) == 2
assert "no run 'nope'" in capsys.readouterr().err
Loading