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
2 changes: 1 addition & 1 deletion pyproject.toml
Original file line number Diff line number Diff line change
Expand Up @@ -8,7 +8,7 @@ name = "weco"
authors = [{ name = "Weco AI Team", email = "contact@weco.ai" }]
description = "Documentation for `weco`, a CLI for using Weco AI's code optimizer."
readme = "README.md"
version = "0.3.37"
version = "0.3.38"
license = { file = "LICENSE" }
requires-python = ">=3.10"
dependencies = [
Expand Down
35 changes: 31 additions & 4 deletions weco/cli.py
Original file line number Diff line number Diff line change
Expand Up @@ -215,6 +215,19 @@ def _configure_run_subcommands(run_parser: argparse.ArgumentParser) -> None:
# weco run status <run-id>
p = subs.add_parser("status", help="Show run status and progress (JSON)")
p.add_argument("run_id", type=str, help="Run UUID")
p.add_argument(
"--lineage",
action="store_true",
help="Report lineage-wide aggregates (status, best, steps) across all derived runs, not just this run.",
)

# weco run overview <run-id>
p = subs.add_parser(
"overview", help="Show the full lineage picture (tree, global best, all derived runs) — dashboard parity"
)
p.add_argument("run_id", type=str, help="Any run UUID in the lineage")
p.add_argument("--include-code", action="store_true", help="Include plan and full source code for each node")
p.add_argument("--plot", action="store_true", help="Show ASCII metric trajectory across the whole lineage")

# weco run results <run-id>
p = subs.add_parser("results", help="Show results sorted by metric")
Expand All @@ -223,16 +236,26 @@ def _configure_run_subcommands(run_parser: argparse.ArgumentParser) -> None:
p.add_argument("--format", type=str, choices=["json", "table", "csv"], default="json", help="Output format")
p.add_argument("--plot", action="store_true", help="Show ASCII metric trajectory")
p.add_argument("--include-code", action="store_true", help="Include full source code")
p.add_argument(
"--lineage", action="store_true", help="Rank results across all derived runs in the lineage, not just this run."
)

# weco run show <run-id> --step N
p = subs.add_parser("show", help="Show details for a specific step")
p.add_argument("run_id", type=str, help="Run UUID")
p.add_argument("--step", type=str, required=True, help="Step number or 'best'")
p.add_argument(
"--step",
type=str,
required=True,
help="Step number, 'best' (lineage-best, matches `derive --from-step best`), or 'run-best' (best in this run)",
)

# weco run diff <run-id> --step N
p = subs.add_parser("diff", help="Show code diff between steps")
p.add_argument("run_id", type=str, help="Run UUID")
p.add_argument("--step", type=str, required=True, help="Step number or 'best'")
p.add_argument(
"--step", type=str, required=True, help="Step number, 'best' (lineage-best), or 'run-best' (best in this run)"
)
p.add_argument("--against", type=str, default="baseline", help="'baseline' (default), 'parent', or step number")

# weco run stop <run-id>
Expand Down Expand Up @@ -453,7 +476,7 @@ def configure_resume_parser(resume_parser: argparse.ArgumentParser) -> None:

def _dispatch_run_subcommand(sub: str, args: argparse.Namespace) -> None:
"""Dispatch ``weco run <subcommand>`` to the appropriate handler."""
from .commands.run import status, results, show, diff, stop, instruct, review, revise, submit, derive
from .commands.run import status, overview, results, show, diff, stop, instruct, review, revise, submit, derive

def _collect_source_paths() -> list[str] | None:
if getattr(args, "sources", None):
Expand All @@ -463,13 +486,17 @@ def _collect_source_paths() -> list[str] | None:
return None

handlers = {
"status": lambda: status.handle(run_id=args.run_id, console=console),
"status": lambda: status.handle(run_id=args.run_id, lineage=args.lineage, console=console),
"overview": lambda: overview.handle(
run_id=args.run_id, include_code=args.include_code, plot=args.plot, console=console
),
"results": lambda: results.handle(
run_id=args.run_id,
top=args.top,
format=args.format,
plot=args.plot,
include_code=args.include_code,
lineage=args.lineage,
console=console,
),
"show": lambda: show.handle(run_id=args.run_id, step=args.step, console=console),
Expand Down
30 changes: 30 additions & 0 deletions weco/commands/__init__.py
Original file line number Diff line number Diff line change
Expand Up @@ -27,6 +27,36 @@ def fetch_run(client: WecoClient, run_id: str, include_history: bool = True) ->
sys.exit(1)


def resolve_lineage_id(client: WecoClient, run_id: str) -> str:
"""Resolve a run ID to its lineage ID (= root run ID).

A standalone (non-derived) run has no ``lineage_id`` of its own — it *is*
the lineage root, so we fall back to the run ID. Exits on fetch error.
"""
run = fetch_run(client, run_id, include_history=False)
return run.get("lineage_id") or run_id


def fetch_lineage(client: WecoClient, lineage_id: str) -> dict:
"""Fetch the full lineage tree via ``GET /lineages/{id}``, or exit on error."""
try:
return client.get_lineage(lineage_id)
except Exception as e:
print(json.dumps({"error": f"Failed to fetch lineage {lineage_id}: {e}"}))
sys.exit(1)


def fetch_lineage_nodes(
client: WecoClient, lineage_id: str, *, include_details: bool = False, status: str | None = None
) -> list[dict]:
"""Fetch all nodes across a lineage via ``GET /lineages/{id}/nodes``, or exit."""
try:
return client.list_lineage_nodes(lineage_id, include_details=include_details, status=status).get("nodes", [])
except Exception as e:
print(json.dumps({"error": f"Failed to fetch lineage nodes for {lineage_id}: {e}"}))
sys.exit(1)


def fetch_nodes(
client: WecoClient,
run_id: str,
Expand Down
37 changes: 29 additions & 8 deletions weco/commands/run/diff.py
Original file line number Diff line number Diff line change
Expand Up @@ -6,7 +6,7 @@

from rich.console import Console

from .. import make_client, fetch_nodes
from .. import make_client, fetch_nodes, resolve_lineage_id, fetch_lineage


def _get_node_by_step(client, run_id: str, step: int, include_code: bool = True) -> dict | None:
Expand All @@ -15,27 +15,48 @@ def _get_node_by_step(client, run_id: str, step: int, include_code: bool = True)
return nodes[0] if nodes else None


def _get_best_node(client, run_id: str) -> dict | None:
"""Fetch the best-scoring node."""
def _get_run_best_node(client, run_id: str) -> dict | None:
"""Fetch the best-scoring node within a single run."""
_meta, nodes = fetch_nodes(client, run_id, sort="metric", top=1)
return nodes[0] if nodes else None


def _get_lineage_best_node(client, run_id: str) -> tuple[dict | None, str | None]:
"""Resolve the lineage-global best node and the run that owns it — the same
node `derive --from-step best` branches from."""
lineage_id = resolve_lineage_id(client, run_id)
best = fetch_lineage(client, lineage_id).get("best")
if not best:
return None, None
best_run_id = best.get("run_id")
return _get_node_by_step(client, best_run_id, best.get("step")), best_run_id


def handle(run_id: str, step: str, against: str, console: Console) -> None:
"""Show code diff between steps."""
client = make_client(console)

# The run whose nodes we diff against. For lineage-best this may differ from
# the queried run, so baseline/parent/step comparisons resolve within the
# run that actually owns the best node.
base_run_id = run_id

# Resolve target node
if step == "best":
target_node = _get_best_node(client, run_id)
target_node, base_run_id = _get_lineage_best_node(client, run_id)
if not target_node:
print(json.dumps({"error": "No scored nodes found in lineage"}))
sys.exit(1)
elif step == "run-best":
target_node = _get_run_best_node(client, run_id)
if not target_node:
print(json.dumps({"error": "No scored nodes found"}))
sys.exit(1)
else:
try:
step_num = int(step)
except ValueError:
print(json.dumps({"error": f"Invalid step: {step}. Use an integer or 'best'"}))
print(json.dumps({"error": f"Invalid step: {step}. Use an integer, 'best', or 'run-best'"}))
sys.exit(1)
target_node = _get_node_by_step(client, run_id, step_num)
if not target_node:
Expand All @@ -46,7 +67,7 @@ def handle(run_id: str, step: str, against: str, console: Console) -> None:

# Resolve base node
if against == "baseline":
base_node = _get_node_by_step(client, run_id, 0)
base_node = _get_node_by_step(client, base_run_id, 0)
if not base_node:
print(json.dumps({"error": "No baseline node (step 0) found"}))
sys.exit(1)
Expand All @@ -56,7 +77,7 @@ def handle(run_id: str, step: str, against: str, console: Console) -> None:
if parent_step is None:
print(json.dumps({"error": f"Node at step {target_node.get('step')} has no parent"}))
sys.exit(1)
base_node = _get_node_by_step(client, run_id, parent_step)
base_node = _get_node_by_step(client, base_run_id, parent_step)
if not base_node:
print(json.dumps({"error": f"Parent node at step {parent_step} not found"}))
sys.exit(1)
Expand All @@ -67,7 +88,7 @@ def handle(run_id: str, step: str, against: str, console: Console) -> None:
except ValueError:
print(json.dumps({"error": f"Invalid --against value: {against}. Use 'baseline', 'parent', or a step number"}))
sys.exit(1)
base_node = _get_node_by_step(client, run_id, base_step)
base_node = _get_node_by_step(client, base_run_id, base_step)
if not base_node:
print(json.dumps({"error": f"No node found at step {base_step}"}))
sys.exit(1)
Expand Down
98 changes: 98 additions & 0 deletions weco/commands/run/overview.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,98 @@
"""``weco run overview <run-id>`` — lineage-wide run picture (dashboard parity).

The other read commands (``status``, ``results``, ``show``, ``diff``) are
single-run scoped. Once a run has been ``derive``d, the related runs form a
*lineage* — a tree of root + derived branches — and a per-run view can't show
the global best, the derived branches, or where each branched. This command
resolves a run to its lineage and returns the whole picture in one call,
matching what the dashboard run page shows.
"""

import json

from rich.console import Console

from .. import make_client, resolve_lineage_id, fetch_lineage, fetch_lineage_nodes
from .results import _sparkline


def handle(run_id: str, include_code: bool, plot: bool, console: Console) -> None:
"""Show the full lineage overview for the run as JSON."""
client = make_client(console)

lineage_id = resolve_lineage_id(client, run_id)
lineage = fetch_lineage(client, lineage_id)
nodes = fetch_lineage_nodes(client, lineage_id, include_details=include_code)

best = lineage.get("best")
members = []
for m in lineage.get("members", []):
members.append(
{
"run_id": m.get("id"),
"name": m.get("name"),
"status": m.get("status"),
"sub_run_index": m.get("sub_run_index"),
"best_metric": m.get("best_metric"),
"best_step": m.get("best_step"),
"current_step": m.get("current_step"),
"steps": m.get("steps"),
"derived_from": m.get("derived_from"),
"additional_instructions": m.get("additional_instructions"),
"children": m.get("children", []),
}
)

node_list = []
for n in nodes:
entry = {
"global_step": n.get("global_step"),
"run_id": n.get("run_id"),
"step": n.get("step"),
"node_id": n.get("id"),
"parent_id": n.get("parent_id"),
"metric": n.get("metric_value"),
"status": n.get("status"),
"is_buggy": n.get("is_buggy"),
"summary_title": n.get("summary_title"),
}
if include_code:
entry["plan"] = n.get("plan", "")
entry["code"] = n.get("code", {})
node_list.append(entry)

output = {
"lineage_id": lineage.get("id", lineage_id),
"root_run_id": lineage.get("root_run_id"),
"name": lineage.get("name"),
"metric_name": lineage.get("metric_name"),
"goal": "maximize" if lineage.get("maximize") else "minimize",
"status": lineage.get("status"),
"best_metric": lineage.get("best_metric"),
"current_step": lineage.get("current_step"),
"total_steps": lineage.get("total_steps"),
"member_count": lineage.get("member_count"),
"active_member_count": lineage.get("active_member_count"),
"best": best,
"members": members,
"nodes": node_list,
}

print(json.dumps(output, indent=2))

if plot:
# Lineage-wide trajectory in global-step order, scored nodes only.
scored = sorted(
(n for n in nodes if n.get("metric_value") is not None),
key=lambda n: n.get("global_step") if n.get("global_step") is not None else 0,
)
if scored:
values = [n["metric_value"] for n in scored]
best_val = lineage.get("best_metric", values[0])
best_step = best.get("step") if best else "?"
best_run = best.get("run_id") if best else "?"
spark = _sparkline(values)
print(
f"\nLineage steps 0-{len(scored)}: {values[0]:.4g} {spark} -> "
f"{best_val:.4g} (best at step {best_step} in run {best_run})"
)
Loading
Loading