Skip to content

Commit 7679a53

Browse files
authored
Merge pull request #40 from abundant-ai/codex/add-oddish-ls
[codex] add oddish ls command
2 parents 28f223e + a063cd8 commit 7679a53

4 files changed

Lines changed: 198 additions & 1 deletion

File tree

DOCS.md

Lines changed: 24 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -21,6 +21,7 @@ export ODDISH_API_KEY="ok_..."
2121

2222
- `oddish run` - submit a job
2323
- `oddish upload` - register a task or upload existing trials
24+
- `oddish ls` - list uploaded tasks
2425
- `oddish status` - view progress
2526
- `oddish cancel` - stop in-flight trials for a task
2627
- `oddish pull` - download logs and artifacts
@@ -108,6 +109,29 @@ agents:
108109
n_trials: 3
109110
```
110111
112+
## List Tasks
113+
114+
Use `oddish ls` to browse uploaded tasks with their latest version, trial
115+
counts, reward summary, last run time, and linked experiments.
116+
117+
```bash
118+
oddish ls
119+
oddish ls --query django
120+
oddish ls --limit 50
121+
oddish ls --json
122+
```
123+
124+
<details>
125+
<summary>Options</summary>
126+
127+
- `--query`, `-q TEXT` - Filter tasks by name
128+
- `--limit`, `-n INTEGER` - Maximum number of tasks to show
129+
- `--offset INTEGER` - Number of tasks to skip
130+
- `--json` - Emit the raw task browser JSON response
131+
- `--api TEXT` - Override the API URL
132+
133+
</details>
134+
111135
## Check Progress
112136

113137
Use `oddish status` to inspect the system, a task, or an experiment.

oddish/README.md

Lines changed: 17 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -19,7 +19,8 @@ export ODDISH_API_KEY="ok_..."
1919
# Submit a run
2020
oddish run -d swebench@1.0 -a codex -m openai/gpt-5.2 --n-trials 3
2121

22-
# Watch progress
22+
# List and watch progress
23+
oddish ls
2324
oddish status
2425
oddish status <task_id> --watch
2526

@@ -63,6 +64,7 @@ Available commands:
6364

6465
- `oddish run` uploads a local task or dataset, downloads a registry dataset, or expands a sweep config into trials
6566
- `oddish upload` registers task bundles (no trials) or uploads off-oddish Harbor trial results (logs, rewards, tokens) onto an existing task
67+
- `oddish ls` lists uploaded tasks with version, trial, reward, and experiment summaries
6668
- `oddish status` shows system, task, or experiment status
6769
- `oddish cancel` stops all in-flight runs for a task
6870
- `oddish pull` downloads logs, results, trajectories, and artifact files for a trial, task, or experiment
@@ -250,6 +252,20 @@ Notes:
250252
- Experiments can be heterogeneous — one experiment can mix trials
251253
that ran on Oddish with trials that were imported.
252254

255+
### `oddish ls`
256+
257+
List uploaded tasks using the same latest-version task browser API as the
258+
dashboard.
259+
260+
Examples:
261+
262+
```bash
263+
oddish ls
264+
oddish ls --query django
265+
oddish ls --limit 50
266+
oddish ls --json
267+
```
268+
253269
### `oddish status`
254270

255271
Without arguments, `oddish status` shows recent experiments and API health. Use

oddish/src/oddish/cli/__init__.py

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -3,6 +3,7 @@
33
import typer
44
from oddish.cli.cancel import cancel
55
from oddish.cli.delete import delete
6+
from oddish.cli.ls import ls
67
from oddish.cli.pull import pull
78
from oddish.cli.run import run
89
from oddish.cli.status import status
@@ -15,6 +16,7 @@
1516

1617
app.command()(run)
1718
app.command()(upload)
19+
app.command(name="ls")(ls)
1820
app.command()(status)
1921
app.command()(cancel)
2022
app.command()(delete)

oddish/src/oddish/cli/ls.py

Lines changed: 155 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,155 @@
1+
from __future__ import annotations
2+
3+
import json
4+
from datetime import datetime
5+
from typing import Annotated, Any
6+
7+
import httpx
8+
import typer
9+
from rich.console import Console
10+
from rich.table import Table
11+
12+
from oddish.cli.config import get_api_url, get_auth_headers, require_api_key
13+
14+
console = Console()
15+
16+
17+
def _format_datetime(value: str | None) -> str:
18+
if not value:
19+
return "-"
20+
try:
21+
parsed = datetime.fromisoformat(value.replace("Z", "+00:00"))
22+
except ValueError:
23+
return value
24+
return parsed.strftime("%m-%d %H:%M")
25+
26+
27+
def _format_reward(row: dict[str, Any]) -> str:
28+
reward_total = int(row.get("reward_total") or 0)
29+
if reward_total == 0:
30+
return "-"
31+
32+
reward_success = int(row.get("reward_success") or 0)
33+
reward_sum = float(row.get("reward_sum") or 0)
34+
average = reward_sum / reward_total
35+
return f"{reward_success}/{reward_total} avg {average:.2f}"
36+
37+
38+
def _format_trials(row: dict[str, Any]) -> str:
39+
total = int(row.get("total_trials") or 0)
40+
completed = int(row.get("completed_trials") or 0)
41+
failed = int(row.get("failed_trials") or 0)
42+
if total == 0:
43+
return "-"
44+
if failed:
45+
label = "fail" if failed == 1 else "fails"
46+
return f"{completed}/{total} ({failed} {label})"
47+
return f"{completed}/{total}"
48+
49+
50+
def _format_experiments(row: dict[str, Any]) -> str:
51+
experiments = row.get("experiments") or []
52+
if not experiments:
53+
return "-"
54+
names = [
55+
experiment.get("name") or experiment.get("id") or "-"
56+
for experiment in experiments
57+
]
58+
return ", ".join(names[:2]) + (" +" if len(names) > 2 else "")
59+
60+
61+
def ls(
62+
query: Annotated[
63+
str | None,
64+
typer.Option(
65+
"--query",
66+
"-q",
67+
help="Filter tasks by name",
68+
),
69+
] = None,
70+
limit: Annotated[
71+
int,
72+
typer.Option(
73+
"--limit",
74+
"-n",
75+
min=1,
76+
max=100,
77+
help="Maximum number of tasks to show",
78+
),
79+
] = 25,
80+
offset: Annotated[
81+
int,
82+
typer.Option(
83+
"--offset",
84+
min=0,
85+
help="Number of tasks to skip",
86+
),
87+
] = 0,
88+
json_output: Annotated[
89+
bool,
90+
typer.Option(
91+
"--json",
92+
help="Emit the raw JSON response",
93+
),
94+
] = False,
95+
api_url: Annotated[
96+
str,
97+
typer.Option("--api", help="API URL"),
98+
] = "",
99+
) -> None:
100+
"""List uploaded tasks."""
101+
if not api_url:
102+
api_url = get_api_url()
103+
require_api_key(api_url)
104+
105+
params: dict[str, int | str] = {"limit": limit, "offset": offset}
106+
if query:
107+
params["query"] = query
108+
109+
try:
110+
with httpx.Client(timeout=30.0, headers=get_auth_headers(api_url)) as client:
111+
response = client.get(f"{api_url}/tasks/browse", params=params)
112+
except httpx.HTTPError as exc:
113+
console.print(f"[red]Failed to connect to API:[/red] {exc}")
114+
raise typer.Exit(1) from exc
115+
116+
if response.status_code != 200:
117+
console.print(f"[red]Failed to list tasks:[/red] {response.text}")
118+
raise typer.Exit(1)
119+
120+
result = response.json()
121+
if json_output:
122+
print(json.dumps(result, indent=2))
123+
return
124+
125+
tasks = result.get("items") or []
126+
if not tasks:
127+
console.print("[dim]No tasks found[/dim]")
128+
return
129+
130+
table = Table(title="Tasks", show_header=True)
131+
table.add_column("Task", style="cyan", no_wrap=True)
132+
table.add_column("Name")
133+
table.add_column("Ver", justify="right", no_wrap=True)
134+
table.add_column("Trials", justify="right", no_wrap=True)
135+
table.add_column("Reward", justify="right", no_wrap=True)
136+
table.add_column("Last", no_wrap=True)
137+
table.add_column("Exp")
138+
139+
for task in tasks:
140+
current_version = task.get("current_version")
141+
version = f"v{current_version}" if current_version is not None else "-"
142+
table.add_row(
143+
task.get("id", "-"),
144+
task.get("name") or "-",
145+
version,
146+
_format_trials(task),
147+
_format_reward(task),
148+
_format_datetime(task.get("last_run_at")),
149+
_format_experiments(task),
150+
)
151+
152+
console.print(table)
153+
if result.get("has_more"):
154+
next_offset = offset + limit
155+
console.print(f"[dim]More available: oddish ls --offset {next_offset}[/dim]")

0 commit comments

Comments
 (0)