Skip to content

Commit f0aecba

Browse files
committed
Refactor CLI into command-family package
1 parent 454476c commit f0aecba

13 files changed

Lines changed: 1297 additions & 1133 deletions

File tree

‎agentlab/cli.py‎

Lines changed: 0 additions & 1118 deletions
This file was deleted.

‎agentlab/cli/__init__.py‎

Lines changed: 57 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,57 @@
1+
from __future__ import annotations
2+
3+
from typing import Iterable
4+
5+
from agentlab.cli.agent_options import (
6+
_agent_factory,
7+
_add_claude_options,
8+
_claude_code_config_from_args,
9+
_codex_config_from_args,
10+
)
11+
from agentlab.cli.doctor import handle_doctor
12+
from agentlab.cli.parser import build_parser
13+
from agentlab.cli.reports import handle_report_capability_evidence_digest
14+
from agentlab.cli.review import handle_review
15+
from agentlab.cli.run import handle_run, _print_run_summaries
16+
from agentlab.cli.task import (
17+
handle_task_smoke_test,
18+
handle_task_validate,
19+
handle_task_verify_reference,
20+
)
21+
from agentlab.cli.trials import (
22+
handle_runs_list,
23+
handle_trials_archive_excluded,
24+
handle_trials_summarize,
25+
)
26+
27+
28+
def main(argv: Iterable[str] | None = None) -> int:
29+
parser = build_parser()
30+
args = parser.parse_args(list(argv) if argv is not None else None)
31+
32+
if not hasattr(args, "handler"):
33+
parser.print_help()
34+
return 2
35+
36+
return args.handler(args)
37+
38+
39+
__all__ = [
40+
"_agent_factory",
41+
"_add_claude_options",
42+
"_claude_code_config_from_args",
43+
"_codex_config_from_args",
44+
"_print_run_summaries",
45+
"build_parser",
46+
"handle_doctor",
47+
"handle_report_capability_evidence_digest",
48+
"handle_review",
49+
"handle_run",
50+
"handle_runs_list",
51+
"handle_task_smoke_test",
52+
"handle_task_validate",
53+
"handle_task_verify_reference",
54+
"handle_trials_archive_excluded",
55+
"handle_trials_summarize",
56+
"main",
57+
]

‎agentlab/cli/agent_options.py‎

Lines changed: 210 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,210 @@
1+
from __future__ import annotations
2+
3+
import argparse
4+
from typing import Callable, Sequence
5+
6+
from agentlab.agents.claude_code import ClaudeCodeAdapter, ClaudeCodeConfig
7+
from agentlab.agents.codex_cli import CodexCliAdapter, CodexCliConfig
8+
from agentlab.agents.manual import ManualAgentAdapter
9+
10+
11+
AGENT_CHOICES = ("manual", "codex", "claude")
12+
PREFLIGHT_AGENT_CHOICES = ("codex", "claude")
13+
14+
15+
def add_agent_argument(
16+
parser: argparse.ArgumentParser,
17+
*,
18+
default: str,
19+
choices: Sequence[str],
20+
help: str,
21+
) -> None:
22+
parser.add_argument(
23+
"--agent",
24+
default=default,
25+
choices=list(choices),
26+
help=help,
27+
)
28+
29+
30+
def add_agent_adapter_options(
31+
parser: argparse.ArgumentParser,
32+
*,
33+
codex_timeout_default: int,
34+
codex_timeout_help: str = "Maximum wall time for `codex exec`.",
35+
claude_timeout_default: int,
36+
) -> None:
37+
add_codex_options(
38+
parser,
39+
timeout_default=codex_timeout_default,
40+
timeout_help=codex_timeout_help,
41+
)
42+
add_claude_options(parser, timeout_default=claude_timeout_default)
43+
44+
45+
def add_codex_options(
46+
parser: argparse.ArgumentParser,
47+
*,
48+
timeout_default: int,
49+
timeout_help: str,
50+
) -> None:
51+
parser.add_argument(
52+
"--codex-command",
53+
default="codex",
54+
help="Codex CLI executable to use when --agent codex.",
55+
)
56+
parser.add_argument(
57+
"--codex-model",
58+
default=None,
59+
help="Optional model passed to `codex exec --model`.",
60+
)
61+
parser.add_argument(
62+
"--codex-profile",
63+
default=None,
64+
help="Optional profile passed to `codex exec --profile`.",
65+
)
66+
parser.add_argument(
67+
"--codex-sandbox",
68+
default="workspace-write",
69+
choices=["read-only", "workspace-write", "danger-full-access"],
70+
help="Sandbox mode passed to `codex exec --sandbox`.",
71+
)
72+
parser.add_argument(
73+
"--codex-approval",
74+
default="never",
75+
choices=["untrusted", "on-failure", "on-request", "never"],
76+
help="Approval policy passed to `codex exec --ask-for-approval`.",
77+
)
78+
parser.add_argument(
79+
"--codex-timeout-seconds",
80+
type=int,
81+
default=timeout_default,
82+
help=timeout_help,
83+
)
84+
85+
86+
def add_claude_options(
87+
parser: argparse.ArgumentParser,
88+
*,
89+
timeout_default: int,
90+
) -> None:
91+
parser.add_argument(
92+
"--claude-command",
93+
default="claude",
94+
help="Claude Code CLI executable to use when --agent claude.",
95+
)
96+
parser.add_argument(
97+
"--claude-model",
98+
default=None,
99+
help="Optional model passed to `claude --model`.",
100+
)
101+
parser.add_argument(
102+
"--claude-permission-mode",
103+
default="acceptEdits",
104+
choices=[
105+
"default",
106+
"acceptEdits",
107+
"plan",
108+
"auto",
109+
"dontAsk",
110+
"bypassPermissions",
111+
],
112+
help="Permission mode passed to Claude Code.",
113+
)
114+
parser.add_argument(
115+
"--claude-output-format",
116+
default="stream-json",
117+
choices=["text", "json", "stream-json"],
118+
help="Print-mode output format passed to Claude Code.",
119+
)
120+
parser.add_argument(
121+
"--claude-max-turns",
122+
type=int,
123+
default=None,
124+
help="Optional maximum agentic turns for Claude Code print mode.",
125+
)
126+
parser.add_argument(
127+
"--claude-allowed-tool",
128+
action="append",
129+
default=[],
130+
help=(
131+
"Tool permission rule passed as `--allowedTools`. Can be repeated, "
132+
"for example `Read` or `Bash(pytest *)`."
133+
),
134+
)
135+
parser.add_argument(
136+
"--claude-disallowed-tool",
137+
action="append",
138+
default=[],
139+
help=(
140+
"Tool permission rule passed as `--disallowedTools`. Can be "
141+
"repeated, for example `Bash(git push *)`."
142+
),
143+
)
144+
parser.add_argument(
145+
"--claude-timeout-seconds",
146+
type=int,
147+
default=timeout_default,
148+
help="Maximum wall time for `claude -p`.",
149+
)
150+
parser.add_argument(
151+
"--claude-session-persistence",
152+
action="store_true",
153+
help=(
154+
"Allow Claude Code to persist its local session. By default "
155+
"agentlab passes `--no-session-persistence` for isolated trials."
156+
),
157+
)
158+
159+
160+
_add_claude_options = add_claude_options
161+
162+
163+
def _build_agent(args: argparse.Namespace, show_progress: bool = True) -> object:
164+
if args.agent == "manual":
165+
return ManualAgentAdapter(pause=not args.no_pause)
166+
if args.agent == "codex":
167+
return CodexCliAdapter(_codex_config_from_args(args, show_progress))
168+
if args.agent == "claude":
169+
return ClaudeCodeAdapter(_claude_code_config_from_args(args, show_progress))
170+
raise RuntimeError(f"unknown agent: {args.agent}")
171+
172+
173+
def _codex_config_from_args(
174+
args: argparse.Namespace,
175+
show_progress: bool = True,
176+
) -> CodexCliConfig:
177+
return CodexCliConfig(
178+
command=args.codex_command,
179+
model=args.codex_model,
180+
profile=args.codex_profile,
181+
sandbox=args.codex_sandbox,
182+
approval_policy=args.codex_approval,
183+
timeout_seconds=args.codex_timeout_seconds,
184+
show_progress=show_progress,
185+
)
186+
187+
188+
def _claude_code_config_from_args(
189+
args: argparse.Namespace,
190+
show_progress: bool = True,
191+
) -> ClaudeCodeConfig:
192+
return ClaudeCodeConfig(
193+
command=args.claude_command,
194+
model=args.claude_model,
195+
permission_mode=args.claude_permission_mode,
196+
output_format=args.claude_output_format,
197+
max_turns=args.claude_max_turns,
198+
allowed_tools=tuple(args.claude_allowed_tool),
199+
disallowed_tools=tuple(args.claude_disallowed_tool),
200+
timeout_seconds=args.claude_timeout_seconds,
201+
show_progress=show_progress,
202+
no_session_persistence=not args.claude_session_persistence,
203+
)
204+
205+
206+
def _agent_factory(args: argparse.Namespace) -> Callable[[bool], object]:
207+
def build(show_progress: bool) -> object:
208+
return _build_agent(args, show_progress=show_progress)
209+
210+
return build

‎agentlab/cli/doctor.py‎

Lines changed: 76 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,76 @@
1+
from __future__ import annotations
2+
3+
import argparse
4+
import shlex
5+
import sys
6+
7+
from agentlab.agents.claude_code import run_claude_code_preflight
8+
from agentlab.agents.codex_cli import run_codex_preflight
9+
from agentlab.cli.agent_options import (
10+
PREFLIGHT_AGENT_CHOICES,
11+
add_agent_adapter_options,
12+
add_agent_argument,
13+
_claude_code_config_from_args,
14+
_codex_config_from_args,
15+
)
16+
from agentlab.cli.output import _trim_cli_output
17+
from agentlab.terminal import print_error
18+
19+
20+
def add_doctor_command(subcommands: argparse._SubParsersAction) -> None:
21+
doctor_parser = subcommands.add_parser(
22+
"doctor",
23+
help="Check local agent harness prerequisites before running trials.",
24+
)
25+
add_agent_argument(
26+
doctor_parser,
27+
default="codex",
28+
choices=PREFLIGHT_AGENT_CHOICES,
29+
help="Agent harness to check.",
30+
)
31+
add_agent_adapter_options(
32+
doctor_parser,
33+
codex_timeout_default=15,
34+
codex_timeout_help="Maximum wall time for each Codex preflight command.",
35+
claude_timeout_default=15,
36+
)
37+
doctor_parser.set_defaults(handler=handle_doctor)
38+
39+
40+
def handle_doctor(args: argparse.Namespace) -> int:
41+
if args.agent == "codex":
42+
result = run_codex_preflight(
43+
_codex_config_from_args(args, show_progress=False),
44+
timeout_seconds=args.codex_timeout_seconds,
45+
)
46+
elif args.agent == "claude":
47+
result = run_claude_code_preflight(
48+
_claude_code_config_from_args(args, show_progress=False),
49+
timeout_seconds=args.claude_timeout_seconds,
50+
)
51+
else:
52+
print_error(f"unknown agent: {args.agent}")
53+
return 1
54+
55+
_print_preflight_result(result)
56+
return 0 if result.passed else 1
57+
58+
59+
def _print_preflight_result(result: object) -> None:
60+
print(f"Doctor: {result.agent_name}", flush=True)
61+
for check in result.checks:
62+
if check.passed:
63+
print(f"OK {check.name}: {check.message}")
64+
continue
65+
66+
print_error(f"{check.name}: {check.message}")
67+
if check.command:
68+
print(f" Command: {shlex.join(check.command)}", file=sys.stderr)
69+
output = _trim_cli_output(check.stderr or check.stdout)
70+
if output:
71+
print(output, file=sys.stderr)
72+
73+
if result.passed:
74+
print("Preflight passed.")
75+
else:
76+
print("Preflight failed.", file=sys.stderr)

‎agentlab/cli/output.py‎

Lines changed: 21 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,21 @@
1+
from __future__ import annotations
2+
3+
from typing import List
4+
5+
6+
def _print_table(headers: List[str], rows: List[List[str]]) -> None:
7+
widths = [
8+
max(len(str(row[column])) for row in [headers] + rows)
9+
for column in range(len(headers))
10+
]
11+
print(" ".join(header.ljust(widths[index]) for index, header in enumerate(headers)))
12+
print(" ".join("-" * width for width in widths))
13+
for row in rows:
14+
print(" ".join(str(cell).ljust(widths[index]) for index, cell in enumerate(row)))
15+
16+
17+
def _trim_cli_output(output: str, max_chars: int = 1000) -> str:
18+
output = output.strip()
19+
if len(output) <= max_chars:
20+
return output
21+
return output[-max_chars:]

‎agentlab/cli/parser.py‎

Lines changed: 27 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,27 @@
1+
from __future__ import annotations
2+
3+
import argparse
4+
5+
from agentlab.cli.doctor import add_doctor_command
6+
from agentlab.cli.reports import add_report_commands
7+
from agentlab.cli.review import add_review_command
8+
from agentlab.cli.run import add_run_command
9+
from agentlab.cli.task import add_task_commands
10+
from agentlab.cli.trials import add_trial_commands
11+
12+
13+
def build_parser() -> argparse.ArgumentParser:
14+
parser = argparse.ArgumentParser(
15+
prog="agentlab",
16+
description="Evaluate coding agents on reproducible software tasks.",
17+
)
18+
subcommands = parser.add_subparsers(dest="command")
19+
20+
add_task_commands(subcommands)
21+
add_doctor_command(subcommands)
22+
add_run_command(subcommands)
23+
add_trial_commands(subcommands)
24+
add_report_commands(subcommands)
25+
add_review_command(subcommands)
26+
27+
return parser

0 commit comments

Comments
 (0)