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
42 changes: 30 additions & 12 deletions agentdescent/cli.py
Original file line number Diff line number Diff line change
Expand Up @@ -2125,6 +2125,7 @@ def cmd_run(a: argparse.Namespace) -> int:
def status_payload(run_id: Optional[str], *, store: Optional[str] = None,
recent_rounds: int = 3) -> Any:
if run_id:
run_id = resolve_run_id(run_id, store=store)
rd = runstore.get(run_id, store=store)
st = rd.status().to_dict()
st["recent_rounds"] = rd.rounds()[-recent_rounds:]
Expand Down Expand Up @@ -2387,6 +2388,7 @@ def _partial_show(rd: "runstore.RunDir", st: Any) -> Dict[str, Any]:

def show_payload(run_id: str, *, store: Optional[str] = None, diff: bool = True,
max_chars: int = 40_000) -> Dict[str, Any]:
run_id = resolve_run_id(run_id, store=store) or run_id
rd = runstore.get(run_id, store=store)
st = rd.status()
result = rd.result()
Expand Down Expand Up @@ -2551,6 +2553,7 @@ def resolve_apply_dest(to: Optional[str], install_to: Optional[str]) -> Optional

def apply_payload(run_id: str, *, to: Optional[str] = None, store: Optional[str] = None,
dry_run: bool = False, backup: bool = True) -> Dict[str, Any]:
run_id = resolve_run_id(run_id, store=store) or run_id
rd = runstore.get(run_id, store=store)
result = rd.result()
if result is None:
Expand Down Expand Up @@ -5426,10 +5429,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).
#: Words that stand for "the newest run in the store" wherever a run id goes (#892, #896).
LATEST_RUN_ALIASES = ("latest", "last")


def resolve_run_id(run_id: Optional[str], *, store: Optional[str] = None
) -> Optional[str]:
"""Map ``latest`` / ``last`` to the newest run id.

A real run directory named ``latest`` or ``last`` still wins. An empty
store raises :class:`~agentdescent.runstore.RunStoreError` with the same
wording the CLI uses (#892), so MCP tools and payload helpers match
``agentdescent show latest`` instead of looking up a folder named
``latest`` (#896). Any other value, including ``None``, is returned
unchanged.
"""
if not isinstance(run_id, str) or run_id.lower() not in LATEST_RUN_ALIASES:
return run_id
if os.path.isdir(os.path.join(runstore.store_path(store), run_id)):
return run_id
runs = runstore.list_runs(store=store)
if not runs:
raise runstore.RunStoreError(
f"no runs under {runstore.store_path(store)} yet, so {run_id!r} has nothing "
"to point at; start one with `agentdescent demo` or `agentdescent evolve`")
return runs[0].run_id


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

Expand All @@ -5440,17 +5466,9 @@ def _resolve_latest_run(a: argparse.Namespace) -> None:
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
resolved = resolve_run_id(rid, store=getattr(a, "store", None))
if resolved is not None:
a.run_id = resolved


def main(argv: Optional[Sequence[str]] = None) -> int:
Expand Down
2 changes: 2 additions & 0 deletions agentdescent/diagnose.py
Original file line number Diff line number Diff line change
Expand Up @@ -183,6 +183,8 @@ def diagnose_payload(run_id: str, *, store: Optional[str] = None) -> DiagnosisRe
from .cli import show_payload

payload = show_payload(run_id, store=store, diff=False)
# show_payload maps latest/last (#896); report the resolved id.
run_id = str(payload.get("run_id") or run_id)
state = str(payload.get("state") or "")
if state == "cancelled":
return DiagnosisReport(
Expand Down
23 changes: 15 additions & 8 deletions agentdescent/mcp.py
Original file line number Diff line number Diff line change
Expand Up @@ -37,7 +37,7 @@
from . import __version__, runstore
from .cli import (NESTED_ENV, apply_from_payload, apply_payload, doctor_report,
plan_payload, require_cli_on_path, require_resume_host_on_path,
show_payload, status_payload)
resolve_run_id, show_payload, status_payload)
from .evolvespec import (EvolveSpec, SpecError, budget_round_overshoot_warning,
compose, estimate)

Expand Down Expand Up @@ -147,24 +147,27 @@ def advanced_from_env(environ: Optional[Dict[str, str]] = None) -> bool:
"Progress of one run (round, best held-out reward, calls, dollars if priced, "
"state, the last few rounds, and `audit_store` when the run has one) or, with no "
"run_id, {store, runs} where `runs` is "
"every run newest first and is empty when there are none. Cheap; safe to poll. "
"every run newest first and is empty when there are none. `run_id` may be "
"`latest` or `last` for the newest run (same as the CLI). Cheap; safe to poll. "
"Summarise round-to-round deltas for the user rather than pasting JSON."),
"show": (
"The evolved artifact when a run is done: for a directory, the list of files, a "
"unified diff against the original, and the plan `apply` would execute (files "
"written, extra files, backups); for text, the evolved instruction. Also the "
"outcomes() histogram that says why proposals were committed or refused. Explain "
"what changed and why; do not paste the whole tree."),
"outcomes() histogram that says why proposals were committed or refused. Pass the "
"run_id from start, or `latest`/`last` for the newest run (same as the CLI). "
"Explain what changed and why; do not paste the whole tree."),
"diagnose": (
"Explain why a run ended where it did: solved, seed-already-solved, "
"partial-improvement, running, "
"cancelled, run-failed, reflector-error, scoring-mismatch, agent-failures, "
"gate-rejected, no-improvement or unknown, with bounded evidence and the next "
"step. Call this after `show` when the run learned little, instead of guessing "
"step. `run_id` may be `latest` or `last` for the newest run (same as the CLI). "
"Call this after `show` when the run learned little, instead of guessing "
"from the outcome histogram."),
"apply": (
"DESTRUCTIVE: write an evolved artifact back over a target. Pass `run_id` to "
"install a finished run's result (optional `to` / `install_to` overrides the run's target; `install_to` is the host-skills-root form for skill_library), OR "
"install a finished run's result (`latest`/`last` = newest run, same as the CLI; optional `to` / `install_to` overrides the run's target; `install_to` is the host-skills-root form for skill_library), OR "
"pass `from` + `to` to install an on-disk artifact such as a campaign "
"`final_target` -- same rules as CLI `apply --from` (from requires to; mutually "
"exclusive with run_id; symlinks refused). Backs the original up first unless "
Expand All @@ -173,11 +176,13 @@ def advanced_from_env(environ: Optional[Dict[str, str]] = None) -> bool:
"already matched, so nothing was written and no backup exists -- tell the user "
"nothing changed. Otherwise tell them the backup path afterwards."),
"cancel": (
"Stop a running evolution and every worker agent it started. The run keeps its "
"Stop a running evolution and every worker agent it started. `run_id` may be "
"`latest` or `last` for the newest run (same as the CLI). The run keeps its "
"ledger and can be resumed."),
"resume": (
"Continue a stopped, failed or cancelled run on its existing ledger (the engine "
"picks up where it left off). Returns the new status."),
"picks up where it left off). `run_id` may be `latest` or `last` for the newest "
"run (same as the CLI). Returns the new status."),
"audit_status": (
"Read a sparse-audit JSONL: how biased the run's cheap verifier is against ground "
"truth (`delta_hat`), how *scattered* that error is (`resid_sd` -- the number the "
Expand Down Expand Up @@ -680,6 +685,7 @@ def apply(self, run_id: Optional[str] = None, to: Optional[str] = None,

def cancel(self, run_id: str) -> Dict[str, Any]:
try:
run_id = resolve_run_id(run_id, store=self.store) or run_id
return runstore.cancel(run_id, store=self.store).to_dict()
except runstore.RunStoreError as e:
return {"error": str(e)}
Expand All @@ -689,6 +695,7 @@ def resume(self, run_id: str, budget_usd: Optional[float] = None,
try:
# A resumed run gets the *current* bridge: the session that started
# it is likely gone, and its address with it.
run_id = resolve_run_id(run_id, store=self.store) or run_id
runstore.require_budget_pair(budget_usd, usd_per_call)
# Same host gate as CLI resume: an absent plugin host must not
# relaunch and report running (#370).
Expand Down
2 changes: 2 additions & 0 deletions changelog.d/896-mcp-latest-run-alias.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,2 @@
Fixed
`show_payload` / `status_payload` / `apply_payload` and the MCP `show` / `status` / `apply` / `diagnose` / `cancel` / `resume` tools accept `latest` (or `last`) the same way the CLI does, so a host that asks to show the newest run no longer gets `no run 'latest'` (#896).
49 changes: 48 additions & 1 deletion tests/test_latest_run_alias.py
Original file line number Diff line number Diff line change
@@ -1,8 +1,10 @@
"""`latest` / `last` stand for the newest run id on run-id commands (#892)."""
"""`latest` / `last` stand for the newest run id on run-id commands (#892, #896)."""

import json
import os

import pytest

from agentdescent import cli, runstore


Expand Down Expand Up @@ -42,3 +44,48 @@ def test_other_ids_untouched(tmp_path, capsys):
_mk(store, "20260101-000000-aaaaaa")
assert cli.main(["--store", store, "status", "nope"]) == 2
assert "no run 'nope'" in capsys.readouterr().err


def test_payload_helpers_resolve_latest(tmp_path):
store = str(tmp_path / "runs")
_mk(store, "20260101-000000-aaaaaa")
_mk(store, "20260202-000000-bbbbbb")
shown = cli.show_payload("latest", store=store)
assert shown["run_id"] == "20260202-000000-bbbbbb"
st = cli.status_payload("LAST", store=store)
assert st["run_id"] == "20260202-000000-bbbbbb"
with pytest.raises(runstore.RunStoreError, match="20260202-000000-bbbbbb"):
cli.apply_payload("latest", store=store)


def test_mcp_tools_resolve_latest(tmp_path):
from agentdescent.mcp import Tools

store = str(tmp_path / "runs")
_mk(store, "20260101-000000-aaaaaa")
_mk(store, "20260202-000000-bbbbbb")
t = Tools(store)
assert t.show("latest")["run_id"] == "20260202-000000-bbbbbb"
assert t.status("last")["run_id"] == "20260202-000000-bbbbbb"
assert t.diagnose("LATEST")["run_id"] == "20260202-000000-bbbbbb"
err = t.apply("latest")
assert "error" in err
assert "20260202-000000-bbbbbb" in err["error"]
assert "no run 'latest'" not in err["error"]
cancelled = t.cancel("latest")
assert cancelled.get("run_id") == "20260202-000000-bbbbbb" or (
"error" in cancelled and "no run 'latest'" not in cancelled["error"])


def test_mcp_empty_store_latest_is_clear(tmp_path):
from agentdescent.mcp import Tools

store = str(tmp_path / "empty")
t = Tools(store)
for fn in (t.show, t.diagnose, t.cancel, t.resume, t.apply):
out = fn("latest")
assert "error" in out
assert "no runs under" in out["error"] and "agentdescent demo" in out["error"]
out = t.status("latest")
assert "error" in out
assert "no runs under" in out["error"]
Loading