Skip to content

Commit ff86090

Browse files
authored
Merge pull request #127 from WecoAI/feature/run-subcommands
Add weco run subcommands for inspecting and managing optimization runs
2 parents 416cce5 + fac6fd8 commit ff86090

14 files changed

Lines changed: 821 additions & 5 deletions

File tree

README.md

Lines changed: 38 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -186,14 +186,50 @@ For more advanced examples, including [Triton](/examples/triton/README.md), [CUD
186186

187187
| Command | Description | When to Use |
188188
|---------|-------------|-------------|
189-
| `weco run [options]` | Direct optimization execution | **For advanced users** - When you know exactly what to optimize and how |
189+
| `weco run [options]` | Start a new optimization | When you know what to optimize and how |
190190
| `weco resume <run-id>` | Resume an interrupted run | Continue from the last completed step |
191191
| `weco login` | Authenticate with Weco | First-time setup or switching accounts |
192-
| `weco logout` | Clear authentication credentials | To switch accounts or troubleshoot authentication issues |
192+
| `weco logout` | Clear authentication credentials | Switch accounts or troubleshoot auth |
193193
| `weco credits balance` | Check your current credit balance | Monitor usage |
194194
| `weco credits topup [amount]` | Purchase additional credits | When you need more credits (default: 10) |
195195
| `weco credits autotopup` | Configure automatic top-up | Set up automatic credit replenishment |
196196

197+
### Run Subcommands
198+
199+
Inspect and manage optimization runs. All output is JSON, designed for programmatic access (AI coding agents, scripts).
200+
201+
| Command | Description |
202+
|---------|-------------|
203+
| `weco run status <run-id>` | Run progress, pending nodes, review mode flag |
204+
| `weco run results <run-id>` | Results sorted by metric |
205+
| `weco run show <run-id> --step <N\|best>` | Single node detail with code |
206+
| `weco run diff <run-id> --step <N\|best>` | Unified code diff between steps |
207+
| `weco run stop <run-id>` | Graceful termination (tree preserved) |
208+
| `weco run instruct <run-id> "<text>"` | Update instructions mid-run |
209+
| `weco run review <run-id>` | List pending approval nodes (review mode) |
210+
| `weco run revise <run-id> --node <id> --source <file>` | Replace a node's code |
211+
| `weco run submit <run-id> --node <id>` | Evaluate and submit a node |
212+
213+
```bash
214+
# Check progress
215+
weco run status 0002e071-1b67-411f-a514-36947f0c4b31
216+
217+
# Top 5 results as JSON
218+
weco run results 0002e071-1b67-411f-a514-36947f0c4b31 --top 5
219+
220+
# Diff best solution against baseline
221+
weco run diff 0002e071-1b67-411f-a514-36947f0c4b31 --step best
222+
223+
# Review mode: inspect, optionally edit, and submit
224+
weco run review 0002e071-1b67-411f-a514-36947f0c4b31
225+
weco run submit 0002e071-1b67-411f-a514-36947f0c4b31 --node <node-id>
226+
227+
# Submit with your own code (explicit path mapping)
228+
weco run submit <run-id> --node <id> --source module.py=./my_version.py
229+
```
230+
231+
**Source path mapping:** When using `--source` with `revise` or `submit`, you can map local files to the run's source paths using `target_path=local_path` syntax (e.g., `--source module.py=./optimized.py`). Without an explicit mapping, files are matched positionally to the run's original source paths.
232+
197233
### Observe Commands
198234

199235
Track experiments from your own optimization loop (LLM agents, custom scripts, manual experiments) in the Weco dashboard:

weco/cli.py

Lines changed: 120 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -176,6 +176,72 @@ def configure_run_parser(run_parser: argparse.ArgumentParser) -> None:
176176
_load_backend(backend_name).register_args(run_parser)
177177

178178

179+
def _configure_run_subcommands(run_parser: argparse.ArgumentParser) -> None:
180+
"""Add subcommands under ``weco run`` for inspecting and managing existing runs."""
181+
subs = run_parser.add_subparsers(dest="run_subcommand")
182+
183+
# weco run status <run-id>
184+
p = subs.add_parser("status", help="Show run status and progress (JSON)")
185+
p.add_argument("run_id", type=str, help="Run UUID")
186+
187+
# weco run results <run-id>
188+
p = subs.add_parser("results", help="Show results sorted by metric")
189+
p.add_argument("run_id", type=str, help="Run UUID")
190+
p.add_argument("--top", type=int, default=None, help="Show only the top N results")
191+
p.add_argument("--format", type=str, choices=["json", "table", "csv"], default="json", help="Output format")
192+
p.add_argument("--plot", action="store_true", help="Show ASCII metric trajectory")
193+
p.add_argument("--include-code", action="store_true", help="Include full source code")
194+
195+
# weco run show <run-id> --step N
196+
p = subs.add_parser("show", help="Show details for a specific step")
197+
p.add_argument("run_id", type=str, help="Run UUID")
198+
p.add_argument("--step", type=str, required=True, help="Step number or 'best'")
199+
200+
# weco run diff <run-id> --step N
201+
p = subs.add_parser("diff", help="Show code diff between steps")
202+
p.add_argument("run_id", type=str, help="Run UUID")
203+
p.add_argument("--step", type=str, required=True, help="Step number or 'best'")
204+
p.add_argument("--against", type=str, default="baseline", help="'baseline' (default), 'parent', or step number")
205+
206+
# weco run stop <run-id>
207+
p = subs.add_parser("stop", help="Terminate a running optimization")
208+
p.add_argument("run_id", type=str, help="Run UUID")
209+
210+
# weco run instruct <run-id> <instructions>
211+
p = subs.add_parser("instruct", help="Update additional instructions for an active run")
212+
p.add_argument("run_id", type=str, help="Run UUID")
213+
p.add_argument("instructions", type=str, help="New instructions (text or path to file)")
214+
215+
# weco run review <run-id>
216+
p = subs.add_parser("review", help="Show pending approval nodes (review mode)")
217+
p.add_argument("run_id", type=str, help="Run UUID")
218+
219+
# weco run revise <run-id> --node <id> --source <file>
220+
p = subs.add_parser("revise", help="Replace a pending node's code with a new revision")
221+
p.add_argument("run_id", type=str, help="Run UUID")
222+
p.add_argument("--node", type=str, required=True, help="Node ID to revise")
223+
revise_source = p.add_mutually_exclusive_group(required=True)
224+
revise_source.add_argument("-s", "--source", type=str, help="Path to a single source file")
225+
revise_source.add_argument("--sources", nargs="+", type=str, help="Paths to multiple source files")
226+
227+
# weco run submit <run-id> --node <id>
228+
p = subs.add_parser("submit", help="Submit a pending node for evaluation (review mode)")
229+
p.add_argument("run_id", type=str, help="Run UUID")
230+
p.add_argument("--node", type=str, required=True, help="Node ID to submit")
231+
submit_source = p.add_mutually_exclusive_group()
232+
submit_source.add_argument("-s", "--source", type=str, help="Optional: source file (creates revision before submitting)")
233+
submit_source.add_argument(
234+
"--sources", nargs="+", type=str, help="Optional: source files (creates revision before submitting)"
235+
)
236+
p.add_argument(
237+
"-c",
238+
"--eval-command",
239+
type=str,
240+
default=None,
241+
help="Override the eval command (use when the stored command doesn't work in this environment)",
242+
)
243+
244+
179245
def configure_credits_parser(credits_parser: argparse.ArgumentParser) -> None:
180246
"""Configure the credits command parser and all its subcommands."""
181247
credits_subparsers = credits_parser.add_subparsers(dest="credits_command", help="Credit management commands")
@@ -278,6 +344,52 @@ def configure_resume_parser(resume_parser: argparse.ArgumentParser) -> None:
278344
)
279345

280346

347+
def _dispatch_run_subcommand(sub: str, args: argparse.Namespace) -> None:
348+
"""Dispatch ``weco run <subcommand>`` to the appropriate handler."""
349+
from .commands.run import status, results, show, diff, stop, instruct, review, revise, submit
350+
351+
def _collect_source_paths() -> list[str] | None:
352+
if getattr(args, "sources", None):
353+
return args.sources
354+
if getattr(args, "source", None):
355+
return [args.source]
356+
return None
357+
358+
handlers = {
359+
"status": lambda: status.handle(run_id=args.run_id, console=console),
360+
"results": lambda: results.handle(
361+
run_id=args.run_id,
362+
top=args.top,
363+
format=args.format,
364+
plot=args.plot,
365+
include_code=args.include_code,
366+
console=console,
367+
),
368+
"show": lambda: show.handle(run_id=args.run_id, step=args.step, console=console),
369+
"diff": lambda: diff.handle(run_id=args.run_id, step=args.step, against=args.against, console=console),
370+
"stop": lambda: stop.handle(run_id=args.run_id, console=console),
371+
"instruct": lambda: instruct.handle(run_id=args.run_id, instructions=args.instructions, console=console),
372+
"review": lambda: review.handle(run_id=args.run_id, console=console),
373+
"revise": lambda: revise.handle(
374+
run_id=args.run_id, node_id=args.node, source_paths=_collect_source_paths(), console=console
375+
),
376+
"submit": lambda: submit.handle(
377+
run_id=args.run_id,
378+
node_id=args.node,
379+
source_paths=_collect_source_paths(),
380+
eval_command_override=getattr(args, "eval_command", None),
381+
console=console,
382+
),
383+
}
384+
385+
handler = handlers.get(sub)
386+
if handler is None:
387+
console.print(f"[bold red]Unknown run subcommand: {sub}[/]")
388+
sys.exit(1)
389+
handler()
390+
sys.exit(0)
391+
392+
281393
def execute_run_command(args: argparse.Namespace) -> None:
282394
"""Execute the 'weco run' command with all its logic."""
283395
from .optimizer import optimize
@@ -419,9 +531,10 @@ def _main() -> None:
419531

420532
# --- Run Command Parser Setup ---
421533
run_parser = subparsers.add_parser(
422-
"run", help="Run code optimization", formatter_class=argparse.RawTextHelpFormatter, allow_abbrev=False
534+
"run", help="Run and manage optimizations", formatter_class=argparse.RawTextHelpFormatter, allow_abbrev=False
423535
)
424-
configure_run_parser(run_parser) # Use the helper to add arguments
536+
configure_run_parser(run_parser) # Flags for starting a new optimization
537+
_configure_run_subcommands(run_parser) # Subcommands for inspecting/managing runs
425538

426539
# --- Login Command Parser Setup ---
427540
_ = subparsers.add_parser("login", help="Log in to Weco and save your API key.")
@@ -500,7 +613,11 @@ def _main() -> None:
500613
clear_api_key()
501614
sys.exit(0)
502615
elif args.command == "run":
503-
execute_run_command(args)
616+
sub = getattr(args, "run_subcommand", None)
617+
if sub is not None:
618+
_dispatch_run_subcommand(sub, args)
619+
else:
620+
execute_run_command(args)
504621
elif args.command == "credits":
505622
from .credits import handle_credits_command
506623

weco/commands/__init__.py

Lines changed: 108 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,108 @@
1+
"""Shared helpers for ``weco run`` subcommand handlers."""
2+
3+
import json
4+
import pathlib
5+
import sys
6+
7+
from rich.console import Console
8+
9+
from ..core.api import WecoClient
10+
from ..auth import handle_authentication
11+
12+
13+
def make_client(console: Console) -> WecoClient:
14+
"""Authenticate and return a ``WecoClient``, or exit on failure."""
15+
_, auth_headers = handle_authentication(console)
16+
if not auth_headers:
17+
sys.exit(1)
18+
return WecoClient(auth_headers)
19+
20+
21+
def fetch_run(client: WecoClient, run_id: str, include_history: bool = True) -> dict:
22+
"""Fetch full run data via ``GET /runs/{run_id}``, or exit on error."""
23+
try:
24+
return client.get_run_status(run_id, include_history=include_history)
25+
except Exception as e:
26+
print(json.dumps({"error": f"Failed to fetch run {run_id}: {e}"}))
27+
sys.exit(1)
28+
29+
30+
def fetch_nodes(
31+
client: WecoClient,
32+
run_id: str,
33+
*,
34+
step: int | None = None,
35+
status: str | None = None,
36+
top: int | None = None,
37+
sort: str | None = None,
38+
include_code: bool = True,
39+
) -> tuple[dict, list[dict]]:
40+
"""Fetch nodes via ``GET /runs/{run_id}/nodes``, or exit on error.
41+
42+
Returns:
43+
A ``(run_metadata, nodes)`` tuple where ``run_metadata`` contains
44+
lightweight run info (status, metric_name, best_metric, etc.) and
45+
``nodes`` is the filtered/sorted list of node dicts.
46+
"""
47+
try:
48+
data = client.list_nodes(run_id, step=step, status=status, top=top, sort=sort, include_code=include_code)
49+
return data.get("run", {}), data.get("nodes", [])
50+
except Exception as e:
51+
print(json.dumps({"error": f"Failed to fetch nodes for run {run_id}: {e}"}))
52+
sys.exit(1)
53+
54+
55+
def read_source_code(source_args: list[str], run_code_keys: list[str] | None = None) -> dict[str, str]:
56+
"""Read source files and map them to run code keys.
57+
58+
Each entry in *source_args* is either:
59+
60+
* ``target_path=local_path`` — explicit mapping (store content of
61+
*local_path* under the key *target_path*)
62+
* ``local_path`` — no mapping; falls back to positional matching
63+
against *run_code_keys* if available, otherwise uses the local path.
64+
65+
Args:
66+
source_args: Raw ``--source`` / ``--sources`` values from argparse.
67+
run_code_keys: The run's original source file keys (from baseline
68+
node). Used for positional fallback when no explicit mapping.
69+
70+
Returns:
71+
Dict mapping target path → file content.
72+
"""
73+
explicit: dict[str, str] = {}
74+
unmapped_paths: list[str] = []
75+
unmapped_contents: list[str] = []
76+
77+
for arg in source_args:
78+
if "=" in arg:
79+
target, local = arg.split("=", 1)
80+
target, local = target.strip(), local.strip()
81+
if not target or not local:
82+
print(json.dumps({"error": f"Invalid source mapping: '{arg}'. Use target_path=local_path"}))
83+
sys.exit(1)
84+
p = pathlib.Path(local)
85+
if not p.exists():
86+
print(json.dumps({"error": f"Source file not found: {local}"}))
87+
sys.exit(1)
88+
explicit[target] = p.read_text()
89+
else:
90+
p = pathlib.Path(arg)
91+
if not p.exists():
92+
print(json.dumps({"error": f"Source file not found: {arg}"}))
93+
sys.exit(1)
94+
unmapped_paths.append(arg)
95+
unmapped_contents.append(p.read_text())
96+
97+
# Resolve unmapped files
98+
if unmapped_contents:
99+
if run_code_keys and len(unmapped_contents) == len(run_code_keys):
100+
# Positional match against the run's source keys
101+
for key, content in zip(run_code_keys, unmapped_contents):
102+
explicit[key] = content
103+
else:
104+
# Use local paths as-is
105+
for path, content in zip(unmapped_paths, unmapped_contents):
106+
explicit[str(pathlib.Path(path))] = content
107+
108+
return explicit

weco/commands/run/__init__.py

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1 @@
1+
"""Commands under ``weco run <subcommand>``."""

0 commit comments

Comments
 (0)