Skip to content

Commit 7c08651

Browse files
authored
feat: polish cli and tui (#35)
1 parent ba839a7 commit 7c08651

File tree

7 files changed

+69
-47
lines changed

7 files changed

+69
-47
lines changed

src/cli.py

Lines changed: 5 additions & 25 deletions
Original file line numberDiff line numberDiff line change
@@ -267,11 +267,7 @@ def create_parser(cls) -> argparse.ArgumentParser:
267267
g_limits.add_argument(
268268
"--timeout", type=int, default=3600, help="Timeout per instance (seconds)"
269269
)
270-
g_limits.add_argument(
271-
"--parallel",
272-
choices=["conservative", "balanced", "aggressive"],
273-
help="Preset resources: 1C/2G, 2C/4G, 3C/6-8G",
274-
)
270+
# Legacy parallel presets removed; use --max-parallel or auto
275271
g_limits.add_argument(
276272
"--allow-global-session-volume",
277273
action="store_true",
@@ -1573,12 +1569,9 @@ async def run_orchestrator(self, args: argparse.Namespace) -> int:
15731569
if args.show_run:
15741570
return await self.run_show_run(args)
15751571

1576-
# Check for prompt or resume
1577-
if not args.prompt and not args.resume and not args.resume_fresh:
1578-
self.console.print(
1579-
"[red]Error: Either provide a prompt or use --resume/--resume-fresh[/red]"
1580-
)
1581-
return 1
1572+
# Normalize missing prompt to empty string and let strategies validate if needed
1573+
if not args.resume and not args.resume_fresh:
1574+
args.prompt = getattr(args, "prompt", "") or ""
15821575

15831576
# Perform pre-flight checks for new runs
15841577
if not args.resume and not args.resume_fresh:
@@ -1786,20 +1779,7 @@ def _red(k, v):
17861779
# Store allow flags for constructor
17871780
allow_overwrite = bool(getattr(args, "allow_overwrite_protected_refs", False))
17881781

1789-
# Apply parallel preset before building limits
1790-
preset = getattr(args, "parallel", None)
1791-
if preset:
1792-
runner = full_config.setdefault("runner", {})
1793-
if preset == "conservative":
1794-
runner["cpu_limit"] = 1
1795-
runner["memory_limit"] = "2g"
1796-
elif preset == "balanced":
1797-
runner["cpu_limit"] = 2
1798-
runner["memory_limit"] = "4g"
1799-
elif preset == "aggressive":
1800-
runner["cpu_limit"] = 3
1801-
# Allow operator to override memory via env/config; default 6g
1802-
runner["memory_limit"] = runner.get("memory_limit", "6g")
1782+
# Parallel presets removed. Concurrency is either auto (default) or explicitly set via --max-parallel.
18031783

18041784
# Configure container limits from merged config
18051785
memory_limit_str = full_config["runner"]["memory_limit"]

src/orchestration/orchestrator.py

Lines changed: 27 additions & 11 deletions
Original file line numberDiff line numberDiff line change
@@ -74,6 +74,7 @@ def __init__(
7474
default_docker_image: Optional[str] = None,
7575
default_agent_cli_args: Optional[List[str]] = None,
7676
force_commit: bool = False,
77+
explicit_max_parallel: bool = False,
7778
):
7879
"""
7980
Initialize orchestrator.
@@ -107,6 +108,8 @@ def __init__(
107108
self.default_agent_cli_args = []
108109
# Runner behavior: force a commit if workspace has changes
109110
self.force_commit: bool = bool(force_commit)
111+
# Whether operator explicitly set max_parallel (override guards/policies)
112+
self._explicit_max_parallel = bool(explicit_max_parallel)
110113
# Load model mapping checksum for handshake (single-process still validates equality)
111114
try:
112115
from ..utils.model_mapping import load_model_mapping
@@ -251,9 +254,11 @@ async def initialize(self) -> None:
251254
logger.info(
252255
f"Parallelism(auto): host_cpu={host_cpu}, per_container={per_container} -> max_parallel_instances={adaptive}"
253256
)
254-
# Oversubscription warning
257+
# Oversubscription warning only when not explicit
255258
try:
256-
if (int(self.max_parallel_instances) * per_container) > host_cpu:
259+
if (
260+
int(self.max_parallel_instances) * per_container
261+
) > host_cpu and not getattr(self, "_explicit_max_parallel", False):
257262
logger.warning(
258263
f"Configured parallelism may oversubscribe CPU: max_parallel_instances={self.max_parallel_instances}, per_container_cpu={per_container}, host_cpu={host_cpu}"
259264
)
@@ -266,8 +271,11 @@ async def initialize(self) -> None:
266271
self._resource_pool = asyncio.Semaphore(int(self.max_parallel_instances or 1))
267272

268273
# Start multiple background executors for true parallel execution
269-
# Start a reasonable number of executors (min of max_parallel and 10)
270-
num_executors = min(int(self.max_parallel_instances or 1), 10)
274+
# If explicit max-parallel set, honor fully; otherwise cap at 10
275+
if getattr(self, "_explicit_max_parallel", False):
276+
num_executors = int(self.max_parallel_instances or 1)
277+
else:
278+
num_executors = min(int(self.max_parallel_instances or 1), 10)
271279
for i in range(num_executors):
272280
task = asyncio.create_task(self._instance_executor())
273281
self._executor_tasks.append(task)
@@ -1359,13 +1367,21 @@ async def _admission_wait(self, cpu_need: int, mem_need_gb: int) -> None:
13591367
slope = 0.0
13601368

13611369
async with self._admission_lock:
1362-
cpu_ok = (self._cpu_in_use + cpu_need) <= self._host_cpu
1363-
mem_ok = (self._mem_in_use_gb + mem_need_gb) <= int(
1364-
self._host_mem_gb * self._mem_guard_pct
1365-
)
1366-
disk_ok = (free_gb >= self._disk_min_free_gb) and (
1367-
slope <= self._pack_max_slope_mib_per_min
1368-
)
1370+
if getattr(self, "_explicit_max_parallel", False):
1371+
# Honor operator's explicit parallel setting: bypass CPU/memory guard; keep disk guard
1372+
disk_ok = (free_gb >= self._disk_min_free_gb) and (
1373+
slope <= self._pack_max_slope_mib_per_min
1374+
)
1375+
cpu_ok = True
1376+
mem_ok = True
1377+
else:
1378+
cpu_ok = (self._cpu_in_use + cpu_need) <= self._host_cpu
1379+
mem_ok = (self._mem_in_use_gb + mem_need_gb) <= int(
1380+
self._host_mem_gb * self._mem_guard_pct
1381+
)
1382+
disk_ok = (free_gb >= self._disk_min_free_gb) and (
1383+
slope <= self._pack_max_slope_mib_per_min
1384+
)
13691385

13701386
if cpu_ok and mem_ok and disk_ok:
13711387
self._cpu_in_use += cpu_need

src/orchestration/strategies/best_of_n.py

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -11,6 +11,7 @@
1111
from typing import List, TYPE_CHECKING
1212

1313
from ...shared import InstanceResult
14+
from ...exceptions import StrategyError
1415
from .base import Strategy, StrategyConfig
1516
from .scoring import ScoringStrategy
1617

@@ -76,6 +77,8 @@ async def execute(
7677
Returns:
7778
List containing the best scoring result
7879
"""
80+
if not (prompt or "").strip():
81+
raise StrategyError("best-of-n: prompt is required (non-empty)")
7982
config = self.create_config()
8083
logger.info(f"BestOfNStrategy starting with n={config.n}")
8184

src/orchestration/strategies/iterative.py

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -9,6 +9,7 @@
99
from typing import List, TYPE_CHECKING
1010

1111
from ...shared import InstanceResult
12+
from ...exceptions import StrategyError
1213
from .base import Strategy, StrategyConfig
1314

1415
if TYPE_CHECKING:
@@ -65,6 +66,8 @@ async def execute(
6566
Returns:
6667
List containing the final refined result
6768
"""
69+
if not (prompt or "").strip():
70+
raise StrategyError("iterative: prompt is required (non-empty)")
6871
config = self.create_config()
6972

7073
# Start with the initial implementation

src/orchestration/strategies/scoring.py

Lines changed: 3 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -10,8 +10,8 @@
1010
from typing import List, TYPE_CHECKING
1111

1212
from ...shared import InstanceResult
13-
from .base import Strategy, StrategyConfig
1413
from ...exceptions import StrategyError
14+
from .base import Strategy, StrategyConfig
1515

1616
if TYPE_CHECKING:
1717
from ..strategy_context import StrategyContext
@@ -67,6 +67,8 @@ async def execute(
6767
Returns:
6868
List containing the scored instance result
6969
"""
70+
if not (prompt or "").strip():
71+
raise StrategyError("scoring: prompt is required (non-empty)")
7072
config = self.create_config()
7173

7274
# Phase 1: Generate solution

src/orchestration/strategies/simple.py

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -9,6 +9,7 @@
99
from typing import List, TYPE_CHECKING
1010

1111
from ...shared import InstanceResult
12+
from ...exceptions import StrategyError
1213
from .base import Strategy, StrategyConfig
1314

1415
if TYPE_CHECKING:
@@ -50,6 +51,8 @@ async def execute(
5051
Returns:
5152
List containing the single instance result
5253
"""
54+
if not (prompt or "").strip():
55+
raise StrategyError("simple: prompt is required (non-empty)")
5356
logger.info(f"SimpleStrategy.execute called with prompt: {prompt[:50]}...")
5457

5558
# Durable run with key

src/tui/adaptive.py

Lines changed: 25 additions & 10 deletions
Original file line numberDiff line numberDiff line change
@@ -15,7 +15,7 @@
1515
from rich.console import Group
1616

1717
from .models import RunDisplay, InstanceDisplay, InstanceStatus
18-
from datetime import datetime
18+
from datetime import datetime, timezone
1919

2020

2121
class AdaptiveDisplay:
@@ -30,6 +30,14 @@ def __init__(self):
3030
}
3131
self._color_scheme = "default" # default|accessible
3232

33+
def _aware(self, dt: datetime | None) -> datetime:
34+
"""Return a timezone-aware datetime (UTC) for safe comparisons/sorts."""
35+
if dt is None:
36+
return datetime.now(timezone.utc)
37+
if dt.tzinfo is None:
38+
return dt.replace(tzinfo=timezone.utc)
39+
return dt
40+
3341
def set_color_scheme(self, scheme: str) -> None:
3442
s = (scheme or "default").strip().lower()
3543
if s not in ("default", "accessible"):
@@ -108,7 +116,7 @@ def _render_detailed(self, run: RunDisplay, frame_now=None) -> RenderableType:
108116
strategy_instances,
109117
key=lambda i: (
110118
status_order.get(i.status, 3),
111-
i.started_at or datetime.now(),
119+
self._aware(i.started_at),
112120
),
113121
)
114122
# Create instance panels
@@ -333,7 +341,7 @@ def _render_compact(self, run: RunDisplay, frame_now=None) -> RenderableType:
333341
key=lambda i: (
334342
i.strategy_name,
335343
status_order.get(i.status, 3),
336-
i.started_at or datetime.now(),
344+
self._aware(i.started_at),
337345
),
338346
)
339347

@@ -434,7 +442,7 @@ def _render_compact(self, run: RunDisplay, frame_now=None) -> RenderableType:
434442

435443
return Panel(table, border_style="blue", padding=(0, 1))
436444

437-
def _render_dense(self, run: RunDisplay) -> RenderableType:
445+
def _render_dense(self, run: RunDisplay, frame_now=None) -> RenderableType:
438446
"""Render dense view for >30 instances with minimal per-instance lines (phase/runtime)."""
439447
panels = []
440448
try:
@@ -463,7 +471,7 @@ def _render_dense(self, run: RunDisplay) -> RenderableType:
463471
items,
464472
key=lambda i: (
465473
status_order.get(i.status, 3),
466-
i.started_at or datetime.now(),
474+
self._aware(i.started_at),
467475
),
468476
)
469477
table = Table(show_header=False, expand=True, padding=(0, 1))
@@ -492,11 +500,18 @@ def _render_dense(self, run: RunDisplay) -> RenderableType:
492500
try:
493501
secs = inst.duration_seconds or 0
494502
if inst.status == InstanceStatus.RUNNING and inst.started_at:
495-
now = (
496-
datetime.now(inst.started_at.tzinfo)
497-
if inst.started_at.tzinfo
498-
else datetime.now()
499-
)
503+
if frame_now is not None:
504+
now = (
505+
frame_now
506+
if inst.started_at.tzinfo
507+
else frame_now.replace(tzinfo=None)
508+
)
509+
else:
510+
now = (
511+
datetime.now(inst.started_at.tzinfo)
512+
if inst.started_at.tzinfo
513+
else datetime.now()
514+
)
500515
secs = max(secs, (now - inst.started_at).total_seconds())
501516
if secs > 0:
502517
dur = self._format_duration(secs)

0 commit comments

Comments
 (0)