zwarm 3.0__py3-none-any.whl → 3.2.0__py3-none-any.whl
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- zwarm/cli/interactive.py +749 -0
- zwarm/cli/main.py +207 -854
- zwarm/cli/pilot.py +293 -151
- zwarm/core/__init__.py +20 -0
- zwarm/core/checkpoints.py +216 -0
- zwarm/core/costs.py +199 -0
- zwarm/tools/delegation.py +18 -161
- {zwarm-3.0.dist-info → zwarm-3.2.0.dist-info}/METADATA +2 -1
- {zwarm-3.0.dist-info → zwarm-3.2.0.dist-info}/RECORD +11 -8
- {zwarm-3.0.dist-info → zwarm-3.2.0.dist-info}/WHEEL +0 -0
- {zwarm-3.0.dist-info → zwarm-3.2.0.dist-info}/entry_points.txt +0 -0
zwarm/cli/main.py
CHANGED
|
@@ -740,8 +740,14 @@ def init(
|
|
|
740
740
|
[bold]Creates:[/]
|
|
741
741
|
[cyan].zwarm/[/] State directory for sessions and events
|
|
742
742
|
[cyan].zwarm/config.toml[/] Runtime settings (weave, adapter, watchers)
|
|
743
|
+
[cyan].zwarm/codex.toml[/] Codex CLI settings (model, reasoning effort)
|
|
743
744
|
[cyan]zwarm.yaml[/] Project config (optional, with --with-project)
|
|
744
745
|
|
|
746
|
+
[bold]Configuration relationship:[/]
|
|
747
|
+
config.toml → Controls zwarm itself (tracing, which watchers run)
|
|
748
|
+
codex.toml → Controls the Codex CLI that runs executor sessions
|
|
749
|
+
zwarm.yaml → Project-specific context injected into orchestrator
|
|
750
|
+
|
|
745
751
|
[bold]Examples:[/]
|
|
746
752
|
[dim]# Interactive setup[/]
|
|
747
753
|
$ zwarm init
|
|
@@ -788,6 +794,9 @@ def init(
|
|
|
788
794
|
create_project_config = with_project
|
|
789
795
|
project_description = ""
|
|
790
796
|
project_context = ""
|
|
797
|
+
# Codex settings
|
|
798
|
+
codex_model = "gpt-5.1-codex-mini"
|
|
799
|
+
codex_reasoning = "high"
|
|
791
800
|
|
|
792
801
|
if not non_interactive:
|
|
793
802
|
console.print("[bold]Configuration[/]\n")
|
|
@@ -806,6 +815,42 @@ def init(
|
|
|
806
815
|
type=str,
|
|
807
816
|
)
|
|
808
817
|
|
|
818
|
+
# Codex model settings
|
|
819
|
+
console.print("\n [bold]Codex Model Settings[/] (.zwarm/codex.toml)")
|
|
820
|
+
console.print(" [dim]These control the underlying Codex CLI that runs executor sessions[/]\n")
|
|
821
|
+
|
|
822
|
+
console.print(" Available models:")
|
|
823
|
+
console.print(" [cyan]1[/] gpt-5.1-codex-mini [dim]- Fast, cheap, good for most tasks (Recommended)[/]")
|
|
824
|
+
console.print(" [cyan]2[/] gpt-5.1-codex [dim]- Balanced speed and capability[/]")
|
|
825
|
+
console.print(" [cyan]3[/] gpt-5.1-codex-max [dim]- Most capable, 400k context, expensive[/]")
|
|
826
|
+
|
|
827
|
+
model_choice = typer.prompt(
|
|
828
|
+
" Select model (1-3)",
|
|
829
|
+
default="1",
|
|
830
|
+
type=str,
|
|
831
|
+
)
|
|
832
|
+
model_map = {
|
|
833
|
+
"1": "gpt-5.1-codex-mini",
|
|
834
|
+
"2": "gpt-5.1-codex",
|
|
835
|
+
"3": "gpt-5.1-codex-max",
|
|
836
|
+
}
|
|
837
|
+
codex_model = model_map.get(model_choice, model_choice)
|
|
838
|
+
if model_choice not in model_map:
|
|
839
|
+
console.print(f" [dim]Using custom model: {codex_model}[/]")
|
|
840
|
+
|
|
841
|
+
console.print("\n Reasoning effort (how much the model \"thinks\" before responding):")
|
|
842
|
+
console.print(" [cyan]1[/] low [dim]- Minimal reasoning, fastest responses[/]")
|
|
843
|
+
console.print(" [cyan]2[/] medium [dim]- Balanced reasoning[/]")
|
|
844
|
+
console.print(" [cyan]3[/] high [dim]- Maximum reasoning, best for complex tasks (Recommended)[/]")
|
|
845
|
+
|
|
846
|
+
reasoning_choice = typer.prompt(
|
|
847
|
+
" Select reasoning effort (1-3)",
|
|
848
|
+
default="3",
|
|
849
|
+
type=str,
|
|
850
|
+
)
|
|
851
|
+
reasoning_map = {"1": "low", "2": "medium", "3": "high"}
|
|
852
|
+
codex_reasoning = reasoning_map.get(reasoning_choice, "high")
|
|
853
|
+
|
|
809
854
|
# Watchers
|
|
810
855
|
console.print("\n [bold]Watchers[/] (trajectory aligners)")
|
|
811
856
|
available_watchers = ["progress", "budget", "delegation", "delegation_reminder", "scope", "pattern", "quality"]
|
|
@@ -857,10 +902,20 @@ def init(
|
|
|
857
902
|
|
|
858
903
|
# Create codex.toml for isolated codex configuration
|
|
859
904
|
codex_toml_path = state_dir / "codex.toml"
|
|
860
|
-
|
|
861
|
-
|
|
905
|
+
write_codex_toml = True
|
|
906
|
+
if codex_toml_path.exists():
|
|
907
|
+
if not non_interactive:
|
|
908
|
+
overwrite_codex = typer.confirm(" .zwarm/codex.toml exists. Overwrite?", default=False)
|
|
909
|
+
if not overwrite_codex:
|
|
910
|
+
write_codex_toml = False
|
|
911
|
+
console.print(" [dim]Skipping codex.toml[/]")
|
|
912
|
+
else:
|
|
913
|
+
write_codex_toml = False # Don't overwrite in non-interactive mode
|
|
914
|
+
|
|
915
|
+
if write_codex_toml:
|
|
916
|
+
codex_content = _generate_codex_toml(model=codex_model, reasoning_effort=codex_reasoning)
|
|
862
917
|
codex_toml_path.write_text(codex_content)
|
|
863
|
-
console.print(f" [green]✓[/] Created .zwarm/codex.toml
|
|
918
|
+
console.print(f" [green]✓[/] Created .zwarm/codex.toml")
|
|
864
919
|
|
|
865
920
|
# Create zwarm.yaml
|
|
866
921
|
if create_project_config:
|
|
@@ -880,11 +935,23 @@ def init(
|
|
|
880
935
|
|
|
881
936
|
# Summary
|
|
882
937
|
console.print("\n[bold green]Done![/] zwarm is ready.\n")
|
|
938
|
+
|
|
939
|
+
# Explain config files
|
|
940
|
+
console.print("[bold]Configuration files:[/]")
|
|
941
|
+
console.print(" [cyan].zwarm/config.toml[/] - Runtime settings (Weave tracing, watchers)")
|
|
942
|
+
console.print(" [cyan].zwarm/codex.toml[/] - Codex CLI settings (model, reasoning effort)")
|
|
943
|
+
if create_project_config:
|
|
944
|
+
console.print(" [cyan]zwarm.yaml[/] - Project context and constraints")
|
|
945
|
+
console.print()
|
|
946
|
+
console.print(" [dim]Edit these files to customize behavior. Run 'zwarm init' again to reconfigure.[/]\n")
|
|
947
|
+
|
|
883
948
|
console.print("[bold]Next steps:[/]")
|
|
884
949
|
console.print(" [dim]# Run the orchestrator[/]")
|
|
885
950
|
console.print(" $ zwarm orchestrate --task \"Your task here\"\n")
|
|
886
951
|
console.print(" [dim]# Or test an executor directly[/]")
|
|
887
952
|
console.print(" $ zwarm exec --task \"What is 2+2?\"\n")
|
|
953
|
+
console.print(" [dim]# Interactive session management[/]")
|
|
954
|
+
console.print(" $ zwarm interactive\n")
|
|
888
955
|
|
|
889
956
|
|
|
890
957
|
def _generate_config_toml(
|
|
@@ -1240,897 +1307,183 @@ def clean(
|
|
|
1240
1307
|
def interactive(
|
|
1241
1308
|
default_dir: Annotated[Path, typer.Option("--dir", "-d", help="Default working directory")] = Path("."),
|
|
1242
1309
|
model: Annotated[Optional[str], typer.Option("--model", help="Default model override")] = None,
|
|
1243
|
-
adapter: Annotated[str, typer.Option("--adapter", "-a", help="Executor adapter")] = "codex_mcp",
|
|
1244
|
-
state_dir: Annotated[Path, typer.Option("--state-dir", help="State directory for persistence")] = Path(".zwarm"),
|
|
1245
1310
|
):
|
|
1246
1311
|
"""
|
|
1247
|
-
|
|
1248
|
-
|
|
1249
|
-
Spawn multiple agents across different directories, manage them interactively,
|
|
1250
|
-
and view their outputs. You are the orchestrator.
|
|
1312
|
+
Interactive REPL for session management.
|
|
1251
1313
|
|
|
1252
|
-
|
|
1253
|
-
|
|
1314
|
+
A clean, autocomplete-enabled interface for managing codex sessions.
|
|
1315
|
+
Tab-complete session IDs, watch live output, and manage the session lifecycle.
|
|
1254
1316
|
|
|
1255
1317
|
[bold]Commands:[/]
|
|
1256
|
-
[cyan]spawn[/] "task"
|
|
1257
|
-
[cyan]
|
|
1258
|
-
[cyan]
|
|
1259
|
-
[cyan]
|
|
1260
|
-
[cyan]
|
|
1261
|
-
[cyan]
|
|
1262
|
-
[cyan]c[/]
|
|
1263
|
-
[cyan]kill[/] ID
|
|
1264
|
-
[cyan]rm[/] ID
|
|
1265
|
-
[cyan]killall[/] Stop all running sessions
|
|
1266
|
-
[cyan]clean[/] Remove old sessions (>7 days)
|
|
1267
|
-
[cyan]q[/] / [cyan]quit[/] Exit
|
|
1268
|
-
|
|
1269
|
-
[bold]Spawn Options:[/]
|
|
1270
|
-
spawn "task" --dir ~/project --model gpt-5.1-codex-max --async
|
|
1318
|
+
[cyan]spawn[/] "task" Start a new session
|
|
1319
|
+
[cyan]ls[/] Dashboard of all sessions
|
|
1320
|
+
[cyan]peek[/] ID / [cyan]?[/] ID Quick status check
|
|
1321
|
+
[cyan]show[/] ID Full session details
|
|
1322
|
+
[cyan]traj[/] ID Show trajectory (steps taken)
|
|
1323
|
+
[cyan]watch[/] ID Live follow session output
|
|
1324
|
+
[cyan]c[/] ID "msg" Continue conversation
|
|
1325
|
+
[cyan]kill[/] ID | all Stop session(s)
|
|
1326
|
+
[cyan]rm[/] ID | all Delete session(s)
|
|
1271
1327
|
|
|
1272
1328
|
[bold]Examples:[/]
|
|
1273
1329
|
$ zwarm interactive
|
|
1274
|
-
> spawn "Build auth module"
|
|
1275
|
-
>
|
|
1276
|
-
> c abc123 "Now add
|
|
1330
|
+
> spawn "Build auth module"
|
|
1331
|
+
> watch abc123
|
|
1332
|
+
> c abc123 "Now add tests"
|
|
1277
1333
|
> ls
|
|
1278
|
-
> ? abc123
|
|
1279
1334
|
"""
|
|
1280
|
-
from zwarm.
|
|
1281
|
-
from zwarm.core.models import ConversationSession, SessionStatus, SessionMode
|
|
1282
|
-
import argparse
|
|
1335
|
+
from zwarm.cli.interactive import run_interactive
|
|
1283
1336
|
|
|
1284
|
-
# Initialize adapter (same as orchestrator uses)
|
|
1285
1337
|
default_model = model or "gpt-5.1-codex-mini"
|
|
1286
|
-
|
|
1287
|
-
|
|
1288
|
-
# Config path for isolated codex configuration
|
|
1289
|
-
codex_config_path = state_dir / "codex.toml"
|
|
1290
|
-
if not codex_config_path.exists():
|
|
1291
|
-
# Try relative to working dir
|
|
1292
|
-
codex_config_path = default_dir / ".zwarm" / "codex.toml"
|
|
1293
|
-
if not codex_config_path.exists():
|
|
1294
|
-
codex_config_path = None # Fall back to overrides
|
|
1295
|
-
|
|
1296
|
-
# Session tracking - same pattern as orchestrator
|
|
1297
|
-
sessions: dict[str, ConversationSession] = {}
|
|
1298
|
-
adapters_cache: dict[str, Any] = {}
|
|
1299
|
-
|
|
1300
|
-
def get_or_create_adapter(adapter_name: str, session_model: str | None = None) -> Any:
|
|
1301
|
-
"""Get or create an adapter instance with isolated config."""
|
|
1302
|
-
cache_key = f"{adapter_name}:{session_model or default_model}"
|
|
1303
|
-
if cache_key not in adapters_cache:
|
|
1304
|
-
adapters_cache[cache_key] = get_adapter(
|
|
1305
|
-
adapter_name,
|
|
1306
|
-
model=session_model or default_model,
|
|
1307
|
-
config_path=codex_config_path,
|
|
1308
|
-
)
|
|
1309
|
-
return adapters_cache[cache_key]
|
|
1310
|
-
|
|
1311
|
-
def find_session(query: str) -> tuple[ConversationSession | None, str | None]:
|
|
1312
|
-
"""Find session by full or partial ID."""
|
|
1313
|
-
if query in sessions:
|
|
1314
|
-
return sessions[query], query
|
|
1315
|
-
for sid, session in sessions.items():
|
|
1316
|
-
if sid.startswith(query):
|
|
1317
|
-
return session, sid
|
|
1318
|
-
return None, None
|
|
1319
|
-
|
|
1320
|
-
console.print("\n[bold cyan]zwarm interactive[/] - Multi-Agent Command Center\n")
|
|
1321
|
-
console.print(f" Working dir: {default_dir.absolute()}")
|
|
1322
|
-
console.print(f" Model: [cyan]{default_model}[/]")
|
|
1323
|
-
console.print(f" Adapter: [cyan]{default_adapter}[/]")
|
|
1324
|
-
if codex_config_path:
|
|
1325
|
-
console.print(f" Config: [green]{codex_config_path}[/] (isolated)")
|
|
1326
|
-
else:
|
|
1327
|
-
console.print(f" Config: [yellow]using fallback overrides[/] (run 'zwarm init' for isolation)")
|
|
1328
|
-
console.print(f" Available: {', '.join(list_adapters())}")
|
|
1329
|
-
console.print("\n Type [cyan]help[/] for commands, [cyan]quit[/] to exit.\n")
|
|
1330
|
-
|
|
1331
|
-
def show_help():
|
|
1332
|
-
help_table = Table(show_header=False, box=None, padding=(0, 2))
|
|
1333
|
-
help_table.add_column("Command", style="cyan", width=35)
|
|
1334
|
-
help_table.add_column("Description")
|
|
1335
|
-
help_table.add_row('spawn "task" [options]', "Start session (waits for completion)")
|
|
1336
|
-
help_table.add_row(" --dir PATH", "Working directory")
|
|
1337
|
-
help_table.add_row(" --model NAME", "Model override")
|
|
1338
|
-
help_table.add_row(" --async", "Background mode (don't wait)")
|
|
1339
|
-
help_table.add_row("", "")
|
|
1340
|
-
help_table.add_row("ls / list", "Dashboard of all sessions")
|
|
1341
|
-
help_table.add_row("? ID / peek ID", "Quick peek (status + latest message)")
|
|
1342
|
-
help_table.add_row("show ID", "Full session details & messages")
|
|
1343
|
-
help_table.add_row("traj ID [--full]", "Show trajectory (all steps taken)")
|
|
1344
|
-
help_table.add_row('c ID "msg"', "Continue conversation (wait for response)")
|
|
1345
|
-
help_table.add_row('ca ID "msg"', "Continue async (fire-and-forget)")
|
|
1346
|
-
help_table.add_row("check ID", "Check session status")
|
|
1347
|
-
help_table.add_row("kill ID", "Stop a running session")
|
|
1348
|
-
help_table.add_row("rm ID", "Delete session entirely")
|
|
1349
|
-
help_table.add_row("killall", "Stop all running sessions")
|
|
1350
|
-
help_table.add_row("clean", "Remove old completed sessions")
|
|
1351
|
-
help_table.add_row("q / quit", "Exit")
|
|
1352
|
-
console.print(help_table)
|
|
1353
|
-
|
|
1354
|
-
def show_sessions():
|
|
1355
|
-
"""List all sessions from CodexSessionManager (same as orchestrator)."""
|
|
1356
|
-
from zwarm.sessions import CodexSessionManager, SessionStatus as SessStatus
|
|
1357
|
-
from datetime import datetime
|
|
1358
|
-
|
|
1359
|
-
manager = CodexSessionManager(default_dir / ".zwarm")
|
|
1360
|
-
all_sessions = manager.list_sessions()
|
|
1361
|
-
|
|
1362
|
-
if not all_sessions:
|
|
1363
|
-
console.print(" [dim]No sessions. Use 'spawn \"task\"' to start one.[/]")
|
|
1364
|
-
return
|
|
1365
|
-
|
|
1366
|
-
# Status summary
|
|
1367
|
-
running = sum(1 for s in all_sessions if s.status == SessStatus.RUNNING)
|
|
1368
|
-
completed = sum(1 for s in all_sessions if s.status == SessStatus.COMPLETED)
|
|
1369
|
-
failed = sum(1 for s in all_sessions if s.status == SessStatus.FAILED)
|
|
1370
|
-
killed = sum(1 for s in all_sessions if s.status == SessStatus.KILLED)
|
|
1371
|
-
|
|
1372
|
-
summary_parts = []
|
|
1373
|
-
if running:
|
|
1374
|
-
summary_parts.append(f"[yellow]{running} running[/]")
|
|
1375
|
-
if completed:
|
|
1376
|
-
summary_parts.append(f"[green]{completed} done[/]")
|
|
1377
|
-
if failed:
|
|
1378
|
-
summary_parts.append(f"[red]{failed} failed[/]")
|
|
1379
|
-
if killed:
|
|
1380
|
-
summary_parts.append(f"[dim]{killed} killed[/]")
|
|
1381
|
-
if summary_parts:
|
|
1382
|
-
console.print(" | ".join(summary_parts))
|
|
1383
|
-
console.print()
|
|
1384
|
-
|
|
1385
|
-
def time_ago(iso_str: str) -> str:
|
|
1386
|
-
"""Convert ISO timestamp to human-readable 'X ago' format."""
|
|
1387
|
-
try:
|
|
1388
|
-
dt = datetime.fromisoformat(iso_str)
|
|
1389
|
-
delta = datetime.now() - dt
|
|
1390
|
-
secs = delta.total_seconds()
|
|
1391
|
-
if secs < 60:
|
|
1392
|
-
return f"{int(secs)}s"
|
|
1393
|
-
elif secs < 3600:
|
|
1394
|
-
return f"{int(secs/60)}m"
|
|
1395
|
-
elif secs < 86400:
|
|
1396
|
-
return f"{secs/3600:.1f}h"
|
|
1397
|
-
else:
|
|
1398
|
-
return f"{secs/86400:.1f}d"
|
|
1399
|
-
except:
|
|
1400
|
-
return "?"
|
|
1401
|
-
|
|
1402
|
-
table = Table(box=None, show_header=True, header_style="bold dim")
|
|
1403
|
-
table.add_column("ID", style="cyan", width=10)
|
|
1404
|
-
table.add_column("", width=2) # Status icon
|
|
1405
|
-
table.add_column("T", width=2) # Turn
|
|
1406
|
-
table.add_column("Task", max_width=30)
|
|
1407
|
-
table.add_column("Updated", justify="right", width=8)
|
|
1408
|
-
table.add_column("Last Message", max_width=40)
|
|
1409
|
-
|
|
1410
|
-
status_icons = {
|
|
1411
|
-
"running": "[yellow]●[/]",
|
|
1412
|
-
"completed": "[green]✓[/]",
|
|
1413
|
-
"failed": "[red]✗[/]",
|
|
1414
|
-
"killed": "[dim]○[/]",
|
|
1415
|
-
"pending": "[dim]◌[/]",
|
|
1416
|
-
}
|
|
1417
|
-
|
|
1418
|
-
for s in all_sessions:
|
|
1419
|
-
icon = status_icons.get(s.status.value, "?")
|
|
1420
|
-
task_preview = s.task[:27] + "..." if len(s.task) > 30 else s.task
|
|
1421
|
-
updated = time_ago(s.updated_at)
|
|
1422
|
-
|
|
1423
|
-
# Get last assistant message preview
|
|
1424
|
-
messages = manager.get_messages(s.id)
|
|
1425
|
-
last_msg = ""
|
|
1426
|
-
for msg in reversed(messages):
|
|
1427
|
-
if msg.role == "assistant":
|
|
1428
|
-
last_msg = msg.content.replace("\n", " ")[:37]
|
|
1429
|
-
if len(msg.content) > 37:
|
|
1430
|
-
last_msg += "..."
|
|
1431
|
-
break
|
|
1432
|
-
|
|
1433
|
-
# Style the last message based on recency
|
|
1434
|
-
if s.status == SessStatus.RUNNING:
|
|
1435
|
-
last_msg_styled = f"[yellow]{last_msg or '(working...)'}[/]"
|
|
1436
|
-
updated_styled = f"[yellow]{updated}[/]"
|
|
1437
|
-
elif s.status == SessStatus.COMPLETED:
|
|
1438
|
-
# Highlight if recently completed (< 60s)
|
|
1439
|
-
try:
|
|
1440
|
-
dt = datetime.fromisoformat(s.updated_at)
|
|
1441
|
-
is_recent = (datetime.now() - dt).total_seconds() < 60
|
|
1442
|
-
except:
|
|
1443
|
-
is_recent = False
|
|
1444
|
-
if is_recent:
|
|
1445
|
-
last_msg_styled = f"[green bold]{last_msg or '(done)'}[/]"
|
|
1446
|
-
updated_styled = f"[green bold]{updated} ★[/]"
|
|
1447
|
-
else:
|
|
1448
|
-
last_msg_styled = f"[green]{last_msg or '(done)'}[/]"
|
|
1449
|
-
updated_styled = f"[dim]{updated}[/]"
|
|
1450
|
-
elif s.status == SessStatus.FAILED:
|
|
1451
|
-
last_msg_styled = f"[red]{s.error[:37] if s.error else '(failed)'}...[/]"
|
|
1452
|
-
updated_styled = f"[red]{updated}[/]"
|
|
1453
|
-
else:
|
|
1454
|
-
last_msg_styled = f"[dim]{last_msg or '-'}[/]"
|
|
1455
|
-
updated_styled = f"[dim]{updated}[/]"
|
|
1456
|
-
|
|
1457
|
-
table.add_row(
|
|
1458
|
-
s.short_id,
|
|
1459
|
-
icon,
|
|
1460
|
-
str(s.turn),
|
|
1461
|
-
task_preview,
|
|
1462
|
-
updated_styled,
|
|
1463
|
-
last_msg_styled,
|
|
1464
|
-
)
|
|
1465
|
-
|
|
1466
|
-
console.print(table)
|
|
1467
|
-
|
|
1468
|
-
def parse_spawn_args(args: list[str]) -> dict:
|
|
1469
|
-
"""Parse spawn command arguments."""
|
|
1470
|
-
parser = argparse.ArgumentParser(add_help=False)
|
|
1471
|
-
parser.add_argument("task", nargs="*")
|
|
1472
|
-
parser.add_argument("--dir", "-d", type=Path, default=None)
|
|
1473
|
-
parser.add_argument("--model", "-m", default=None)
|
|
1474
|
-
parser.add_argument("--async", dest="async_mode", action="store_true", default=False)
|
|
1475
|
-
|
|
1476
|
-
try:
|
|
1477
|
-
parsed, _ = parser.parse_known_args(args)
|
|
1478
|
-
return {
|
|
1479
|
-
"task": " ".join(parsed.task) if parsed.task else "",
|
|
1480
|
-
"dir": parsed.dir,
|
|
1481
|
-
"model": parsed.model,
|
|
1482
|
-
"async_mode": parsed.async_mode,
|
|
1483
|
-
}
|
|
1484
|
-
except SystemExit:
|
|
1485
|
-
return {"error": "Invalid spawn arguments"}
|
|
1486
|
-
|
|
1487
|
-
def do_spawn(args: list[str]):
|
|
1488
|
-
"""Spawn a new coding agent session using CodexSessionManager (same as orchestrator)."""
|
|
1489
|
-
from zwarm.sessions import CodexSessionManager, SessionStatus as SessStatus
|
|
1490
|
-
import time
|
|
1491
|
-
|
|
1492
|
-
parsed = parse_spawn_args(args)
|
|
1493
|
-
|
|
1494
|
-
if "error" in parsed:
|
|
1495
|
-
console.print(f" [red]{parsed['error']}[/]")
|
|
1496
|
-
console.print(" [dim]Usage: spawn \"task\" [--dir PATH] [--model NAME] [--async][/]")
|
|
1497
|
-
return
|
|
1498
|
-
|
|
1499
|
-
if not parsed["task"]:
|
|
1500
|
-
console.print(" [red]Task required[/]")
|
|
1501
|
-
console.print(" [dim]Usage: spawn \"task\" [--dir PATH] [--model NAME] [--async][/]")
|
|
1502
|
-
return
|
|
1503
|
-
|
|
1504
|
-
task = parsed["task"]
|
|
1505
|
-
work_dir = (parsed["dir"] or default_dir).absolute()
|
|
1506
|
-
session_model = parsed["model"] or default_model
|
|
1507
|
-
is_async = parsed.get("async_mode", False)
|
|
1508
|
-
|
|
1509
|
-
if not work_dir.exists():
|
|
1510
|
-
console.print(f" [red]Directory not found:[/] {work_dir}")
|
|
1511
|
-
return
|
|
1512
|
-
|
|
1513
|
-
mode_str = "async" if is_async else "sync"
|
|
1514
|
-
console.print(f"\n[dim]Spawning {mode_str} session...[/]")
|
|
1515
|
-
console.print(f" [dim]Dir: {work_dir}[/]")
|
|
1516
|
-
console.print(f" [dim]Model: {session_model}[/]")
|
|
1517
|
-
|
|
1518
|
-
try:
|
|
1519
|
-
# Use CodexSessionManager - SAME as orchestrator's delegate()
|
|
1520
|
-
manager = CodexSessionManager(work_dir / ".zwarm")
|
|
1521
|
-
session = manager.start_session(
|
|
1522
|
-
task=task,
|
|
1523
|
-
working_dir=work_dir,
|
|
1524
|
-
model=session_model,
|
|
1525
|
-
sandbox="workspace-write",
|
|
1526
|
-
source="user",
|
|
1527
|
-
adapter="codex",
|
|
1528
|
-
)
|
|
1529
|
-
|
|
1530
|
-
console.print(f"\n[green]✓[/] Session: [cyan]{session.short_id}[/]")
|
|
1531
|
-
console.print(f" [dim]PID: {session.pid}[/]")
|
|
1532
|
-
|
|
1533
|
-
# For sync mode, wait for completion and show response
|
|
1534
|
-
if not is_async:
|
|
1535
|
-
console.print(f"\n[dim]Waiting for completion...[/]")
|
|
1536
|
-
timeout = 300.0
|
|
1537
|
-
start = time.time()
|
|
1538
|
-
while time.time() - start < timeout:
|
|
1539
|
-
# get_session() auto-updates status based on output completion
|
|
1540
|
-
session = manager.get_session(session.id)
|
|
1541
|
-
if session.status != SessStatus.RUNNING:
|
|
1542
|
-
break
|
|
1543
|
-
time.sleep(1.0)
|
|
1544
|
-
|
|
1545
|
-
# Get all assistant responses
|
|
1546
|
-
messages = manager.get_messages(session.id)
|
|
1547
|
-
assistant_msgs = [m for m in messages if m.role == "assistant"]
|
|
1548
|
-
if assistant_msgs:
|
|
1549
|
-
console.print(f"\n[bold]Response ({len(assistant_msgs)} message{'s' if len(assistant_msgs) > 1 else ''}):[/]")
|
|
1550
|
-
for msg in assistant_msgs:
|
|
1551
|
-
preview = msg.content[:300]
|
|
1552
|
-
if len(msg.content) > 300:
|
|
1553
|
-
preview += "..."
|
|
1554
|
-
console.print(preview)
|
|
1555
|
-
if len(assistant_msgs) > 1:
|
|
1556
|
-
console.print() # Blank line between multiple messages
|
|
1557
|
-
|
|
1558
|
-
console.print(f"\n[dim]Use 'show {session.short_id}' to see full details[/]")
|
|
1559
|
-
console.print(f"[dim]Use 'c {session.short_id} \"message\"' to continue[/]")
|
|
1560
|
-
|
|
1561
|
-
except Exception as e:
|
|
1562
|
-
console.print(f" [red]Error:[/] {e}")
|
|
1563
|
-
import traceback
|
|
1564
|
-
console.print(f" [dim]{traceback.format_exc()}[/]")
|
|
1565
|
-
|
|
1566
|
-
def do_orchestrate(args: list[str]):
|
|
1567
|
-
"""Spawn an orchestrator agent that delegates to sub-sessions."""
|
|
1568
|
-
parsed = parse_spawn_args(args)
|
|
1569
|
-
|
|
1570
|
-
if "error" in parsed:
|
|
1571
|
-
console.print(f" [red]{parsed['error']}[/]")
|
|
1572
|
-
console.print(" [dim]Usage: orchestrate \"task\" [--dir PATH][/]")
|
|
1573
|
-
return
|
|
1338
|
+
run_interactive(working_dir=default_dir.absolute(), model=default_model)
|
|
1574
1339
|
|
|
1575
|
-
if not parsed["task"]:
|
|
1576
|
-
console.print(" [red]Task required[/]")
|
|
1577
|
-
console.print(" [dim]Usage: orchestrate \"task\" [--dir PATH][/]")
|
|
1578
|
-
return
|
|
1579
|
-
|
|
1580
|
-
task = parsed["task"]
|
|
1581
|
-
work_dir = (parsed["dir"] or default_dir).absolute()
|
|
1582
|
-
|
|
1583
|
-
if not work_dir.exists():
|
|
1584
|
-
console.print(f" [red]Directory not found:[/] {work_dir}")
|
|
1585
|
-
return
|
|
1586
1340
|
|
|
1587
|
-
|
|
1588
|
-
|
|
1589
|
-
|
|
1341
|
+
@app.command()
|
|
1342
|
+
def sessions(
|
|
1343
|
+
working_dir: Annotated[Path, typer.Option("--dir", "-d", help="Working directory")] = Path("."),
|
|
1344
|
+
clean: Annotated[bool, typer.Option("--clean", "-c", help="Delete all non-running sessions")] = False,
|
|
1345
|
+
kill_all: Annotated[bool, typer.Option("--kill-all", "-k", help="Kill all running sessions")] = False,
|
|
1346
|
+
rm: Annotated[Optional[str], typer.Option("--rm", help="Delete specific session ID")] = None,
|
|
1347
|
+
kill: Annotated[Optional[str], typer.Option("--kill", help="Kill specific session ID")] = None,
|
|
1348
|
+
):
|
|
1349
|
+
"""
|
|
1350
|
+
Quick session management.
|
|
1590
1351
|
|
|
1591
|
-
|
|
1592
|
-
from uuid import uuid4
|
|
1352
|
+
By default, lists all sessions. Use flags for quick actions:
|
|
1593
1353
|
|
|
1594
|
-
|
|
1595
|
-
|
|
1354
|
+
[bold]Examples:[/]
|
|
1355
|
+
[dim]# List all sessions[/]
|
|
1356
|
+
$ zwarm sessions
|
|
1596
1357
|
|
|
1597
|
-
#
|
|
1598
|
-
|
|
1599
|
-
|
|
1600
|
-
"--task", task,
|
|
1601
|
-
"--working-dir", str(work_dir),
|
|
1602
|
-
"--instance", instance_id,
|
|
1603
|
-
]
|
|
1358
|
+
[dim]# Delete all completed/failed sessions[/]
|
|
1359
|
+
$ zwarm sessions --clean
|
|
1360
|
+
$ zwarm sessions -c
|
|
1604
1361
|
|
|
1605
|
-
#
|
|
1606
|
-
|
|
1607
|
-
|
|
1608
|
-
log_file = log_dir / f"{instance_id}.log"
|
|
1362
|
+
[dim]# Kill all running sessions[/]
|
|
1363
|
+
$ zwarm sessions --kill-all
|
|
1364
|
+
$ zwarm sessions -k
|
|
1609
1365
|
|
|
1610
|
-
|
|
1611
|
-
|
|
1612
|
-
proc = subprocess.Popen(
|
|
1613
|
-
cmd,
|
|
1614
|
-
cwd=work_dir,
|
|
1615
|
-
stdout=f,
|
|
1616
|
-
stderr=subprocess.STDOUT,
|
|
1617
|
-
start_new_session=True,
|
|
1618
|
-
)
|
|
1366
|
+
[dim]# Delete a specific session[/]
|
|
1367
|
+
$ zwarm sessions --rm abc123
|
|
1619
1368
|
|
|
1620
|
-
|
|
1621
|
-
|
|
1622
|
-
|
|
1623
|
-
|
|
1624
|
-
|
|
1625
|
-
|
|
1626
|
-
except Exception as e:
|
|
1627
|
-
console.print(f" [red]Error starting orchestrator:[/] {e}")
|
|
1628
|
-
|
|
1629
|
-
def do_peek(session_id: str):
|
|
1630
|
-
"""Quick peek at session - just status + latest message (for fast polling)."""
|
|
1631
|
-
from zwarm.sessions import CodexSessionManager, SessionStatus as SessStatus
|
|
1369
|
+
[dim]# Kill a specific session[/]
|
|
1370
|
+
$ zwarm sessions --kill abc123
|
|
1371
|
+
"""
|
|
1372
|
+
from zwarm.sessions import CodexSessionManager, SessionStatus
|
|
1373
|
+
from zwarm.core.costs import estimate_session_cost, format_cost
|
|
1632
1374
|
|
|
1633
|
-
|
|
1634
|
-
session = manager.get_session(session_id)
|
|
1375
|
+
manager = CodexSessionManager(working_dir / ".zwarm")
|
|
1635
1376
|
|
|
1377
|
+
# Handle --kill (specific session)
|
|
1378
|
+
if kill:
|
|
1379
|
+
session = manager.get_session(kill)
|
|
1636
1380
|
if not session:
|
|
1637
|
-
console.print(f"
|
|
1638
|
-
|
|
1639
|
-
|
|
1640
|
-
|
|
1641
|
-
icon = {
|
|
1642
|
-
"running": "[yellow]●[/]",
|
|
1643
|
-
"completed": "[green]✓[/]",
|
|
1644
|
-
"failed": "[red]✗[/]",
|
|
1645
|
-
"killed": "[dim]○[/]",
|
|
1646
|
-
}.get(session.status.value, "?")
|
|
1647
|
-
|
|
1648
|
-
# Get latest assistant message
|
|
1649
|
-
messages = manager.get_messages(session.id)
|
|
1650
|
-
latest = None
|
|
1651
|
-
for msg in reversed(messages):
|
|
1652
|
-
if msg.role == "assistant":
|
|
1653
|
-
latest = msg.content.replace("\n", " ")
|
|
1654
|
-
break
|
|
1655
|
-
|
|
1656
|
-
if session.status == SessStatus.RUNNING:
|
|
1657
|
-
console.print(f"{icon} [cyan]{session.short_id}[/] [yellow](running...)[/]")
|
|
1658
|
-
elif latest:
|
|
1659
|
-
# Truncate for one-liner
|
|
1660
|
-
preview = latest[:120] + "..." if len(latest) > 120 else latest
|
|
1661
|
-
console.print(f"{icon} [cyan]{session.short_id}[/] {preview}")
|
|
1662
|
-
elif session.status == SessStatus.FAILED:
|
|
1663
|
-
error = (session.error or "unknown")[:80]
|
|
1664
|
-
console.print(f"{icon} [cyan]{session.short_id}[/] [red]{error}[/]")
|
|
1381
|
+
console.print(f"[red]Session not found:[/] {kill}")
|
|
1382
|
+
raise typer.Exit(1)
|
|
1383
|
+
if manager.kill_session(session.id):
|
|
1384
|
+
console.print(f"[green]✓[/] Killed {session.short_id}")
|
|
1665
1385
|
else:
|
|
1666
|
-
console.print(f"
|
|
1667
|
-
|
|
1668
|
-
def do_show(session_id: str):
|
|
1669
|
-
"""Show full session details and messages using CodexSessionManager."""
|
|
1670
|
-
from zwarm.sessions import CodexSessionManager, SessionStatus as SessStatus
|
|
1671
|
-
|
|
1672
|
-
manager = CodexSessionManager(default_dir / ".zwarm")
|
|
1673
|
-
session = manager.get_session(session_id)
|
|
1674
|
-
|
|
1675
|
-
if not session:
|
|
1676
|
-
console.print(f" [red]Session not found:[/] {session_id}")
|
|
1677
|
-
console.print(f" [dim]Use 'ls' to see available sessions[/]")
|
|
1678
|
-
return
|
|
1679
|
-
|
|
1680
|
-
# Status styling
|
|
1681
|
-
status_display = {
|
|
1682
|
-
"running": "[yellow]● running[/]",
|
|
1683
|
-
"completed": "[green]✓ completed[/]",
|
|
1684
|
-
"failed": "[red]✗ failed[/]",
|
|
1685
|
-
"killed": "[dim]○ killed[/]",
|
|
1686
|
-
"pending": "[dim]◌ pending[/]",
|
|
1687
|
-
}.get(session.status.value, str(session.status.value))
|
|
1688
|
-
|
|
1689
|
-
console.print()
|
|
1690
|
-
console.print(f"[bold cyan]Session {session.short_id}[/] {status_display}")
|
|
1691
|
-
console.print(f"[dim]Adapter:[/] {session.adapter} [dim]│[/] [dim]Model:[/] {session.model}")
|
|
1692
|
-
console.print(f"[dim]Task:[/] {session.task}")
|
|
1693
|
-
console.print(f"[dim]Dir:[/] {session.working_dir} [dim]│[/] [dim]Turn:[/] {session.turn}")
|
|
1694
|
-
console.print(f"[dim]Source:[/] {session.source_display} [dim]│[/] [dim]Runtime:[/] {session.runtime}")
|
|
1695
|
-
if session.pid:
|
|
1696
|
-
console.print(f"[dim]PID:[/] {session.pid}")
|
|
1697
|
-
|
|
1698
|
-
# Show log file path
|
|
1699
|
-
log_path = default_dir / ".zwarm" / "sessions" / session.id / "turns" / f"turn_{session.turn}.jsonl"
|
|
1700
|
-
console.print(f"[dim]Log:[/] {log_path}")
|
|
1701
|
-
console.print()
|
|
1702
|
-
|
|
1703
|
-
# Get messages from manager
|
|
1704
|
-
messages = manager.get_messages(session.id)
|
|
1705
|
-
|
|
1706
|
-
if not messages:
|
|
1707
|
-
if session.is_running:
|
|
1708
|
-
console.print("[yellow]Session is still running...[/]")
|
|
1709
|
-
else:
|
|
1710
|
-
console.print("[dim]No messages captured.[/]")
|
|
1711
|
-
return
|
|
1712
|
-
|
|
1713
|
-
# Display messages
|
|
1714
|
-
for msg in messages:
|
|
1715
|
-
if msg.role == "user":
|
|
1716
|
-
console.print(Panel(msg.content, title="[bold blue]User[/]", border_style="blue"))
|
|
1717
|
-
elif msg.role == "assistant":
|
|
1718
|
-
content = msg.content
|
|
1719
|
-
if len(content) > 2000:
|
|
1720
|
-
content = content[:2000] + "\n\n[dim]... (truncated)[/]"
|
|
1721
|
-
console.print(Panel(content, title="[bold green]Assistant[/]", border_style="green"))
|
|
1722
|
-
elif msg.role == "tool":
|
|
1723
|
-
console.print(f" [dim]Tool: {msg.content[:100]}[/]")
|
|
1724
|
-
|
|
1725
|
-
console.print()
|
|
1726
|
-
if session.token_usage:
|
|
1727
|
-
tokens = session.token_usage
|
|
1728
|
-
console.print(f"[dim]Tokens: {tokens.get('input_tokens', 0):,} in / {tokens.get('output_tokens', 0):,} out[/]")
|
|
1729
|
-
|
|
1730
|
-
if session.error:
|
|
1731
|
-
console.print(f"[red]Error:[/] {session.error}")
|
|
1732
|
-
|
|
1733
|
-
def do_trajectory(session_id: str, full: bool = False):
|
|
1734
|
-
"""Show the full trajectory of a session - all steps in order."""
|
|
1735
|
-
from zwarm.sessions import CodexSessionManager
|
|
1736
|
-
|
|
1737
|
-
manager = CodexSessionManager(default_dir / ".zwarm")
|
|
1738
|
-
session = manager.get_session(session_id)
|
|
1739
|
-
|
|
1740
|
-
if not session:
|
|
1741
|
-
console.print(f" [red]Session not found:[/] {session_id}")
|
|
1742
|
-
return
|
|
1743
|
-
|
|
1744
|
-
trajectory = manager.get_trajectory(session_id, full=full)
|
|
1745
|
-
|
|
1746
|
-
if not trajectory:
|
|
1747
|
-
console.print("[dim]No trajectory data available.[/]")
|
|
1748
|
-
return
|
|
1749
|
-
|
|
1750
|
-
mode = "[bold](full)[/] " if full else ""
|
|
1751
|
-
console.print(f"\n[bold cyan]Trajectory: {session.short_id}[/] {mode}({len(trajectory)} steps)")
|
|
1752
|
-
console.print(f"[dim]Task: {session.task[:60]}{'...' if len(session.task) > 60 else ''}[/]")
|
|
1753
|
-
console.print()
|
|
1754
|
-
|
|
1755
|
-
# Display each step
|
|
1756
|
-
for step in trajectory:
|
|
1757
|
-
turn = step.get("turn", 1)
|
|
1758
|
-
step_num = step.get("step", 0)
|
|
1759
|
-
step_type = step.get("type", "unknown")
|
|
1760
|
-
|
|
1761
|
-
prefix = f"[dim]T{turn}.{step_num:02d}[/]"
|
|
1762
|
-
|
|
1763
|
-
if step_type == "reasoning":
|
|
1764
|
-
if full and step.get("full_text"):
|
|
1765
|
-
console.print(f"{prefix} [yellow]thinking:[/]")
|
|
1766
|
-
console.print(f" {step['full_text']}")
|
|
1767
|
-
else:
|
|
1768
|
-
summary = step.get("summary", "")
|
|
1769
|
-
console.print(f"{prefix} [yellow]thinking:[/] {summary}")
|
|
1770
|
-
|
|
1771
|
-
elif step_type == "command":
|
|
1772
|
-
cmd = step.get("command", "")
|
|
1773
|
-
output = step.get("output", "")
|
|
1774
|
-
exit_code = step.get("exit_code", "?")
|
|
1775
|
-
# Show command
|
|
1776
|
-
console.print(f"{prefix} [cyan]$ {cmd}[/]")
|
|
1777
|
-
if output:
|
|
1778
|
-
if full:
|
|
1779
|
-
# Show all output
|
|
1780
|
-
for line in output.split("\n"):
|
|
1781
|
-
console.print(f" [dim]{line}[/]")
|
|
1782
|
-
else:
|
|
1783
|
-
# Indent output, max 5 lines
|
|
1784
|
-
for line in output.split("\n")[:5]:
|
|
1785
|
-
console.print(f" [dim]{line}[/]")
|
|
1786
|
-
if output.count("\n") > 5:
|
|
1787
|
-
console.print(f" [dim]... ({output.count(chr(10))} lines)[/]")
|
|
1788
|
-
if exit_code != 0 and exit_code is not None:
|
|
1789
|
-
console.print(f" [red]exit: {exit_code}[/]")
|
|
1790
|
-
|
|
1791
|
-
elif step_type == "tool_call":
|
|
1792
|
-
tool = step.get("tool", "unknown")
|
|
1793
|
-
if full and step.get("full_args"):
|
|
1794
|
-
import json
|
|
1795
|
-
console.print(f"{prefix} [magenta]tool:[/] {tool}")
|
|
1796
|
-
console.print(f" {json.dumps(step['full_args'], indent=2)}")
|
|
1797
|
-
else:
|
|
1798
|
-
args = step.get("args_preview", "")
|
|
1799
|
-
console.print(f"{prefix} [magenta]tool:[/] {tool}({args})")
|
|
1800
|
-
|
|
1801
|
-
elif step_type == "tool_output":
|
|
1802
|
-
output = step.get("output", "")
|
|
1803
|
-
if not full:
|
|
1804
|
-
output = output[:100]
|
|
1805
|
-
console.print(f"{prefix} [dim]→ {output}[/]")
|
|
1806
|
-
|
|
1807
|
-
elif step_type == "message":
|
|
1808
|
-
if full and step.get("full_text"):
|
|
1809
|
-
console.print(f"{prefix} [green]response:[/]")
|
|
1810
|
-
console.print(f" {step['full_text']}")
|
|
1811
|
-
else:
|
|
1812
|
-
summary = step.get("summary", "")
|
|
1813
|
-
full_len = step.get("full_length", 0)
|
|
1814
|
-
console.print(f"{prefix} [green]response:[/] {summary}")
|
|
1815
|
-
if full_len > 200:
|
|
1816
|
-
console.print(f" [dim]({full_len} chars total)[/]")
|
|
1817
|
-
|
|
1818
|
-
console.print()
|
|
1819
|
-
|
|
1820
|
-
def do_continue(session_id: str, message: str, wait: bool = True):
|
|
1821
|
-
"""
|
|
1822
|
-
Continue a conversation using CodexSessionManager.inject_message().
|
|
1823
|
-
|
|
1824
|
-
Works for both sync and async sessions:
|
|
1825
|
-
- If session was sync (or wait=True): wait for response
|
|
1826
|
-
- If session was async (or wait=False): fire-and-forget, check later with '?'
|
|
1827
|
-
"""
|
|
1828
|
-
from zwarm.sessions import CodexSessionManager, SessionStatus as SessStatus
|
|
1829
|
-
import time
|
|
1830
|
-
|
|
1831
|
-
manager = CodexSessionManager(default_dir / ".zwarm")
|
|
1832
|
-
session = manager.get_session(session_id)
|
|
1833
|
-
|
|
1834
|
-
if not session:
|
|
1835
|
-
console.print(f" [red]Session not found:[/] {session_id}")
|
|
1836
|
-
console.print(f" [dim]Use 'ls' to see available sessions[/]")
|
|
1837
|
-
return
|
|
1838
|
-
|
|
1839
|
-
if session.status == SessStatus.RUNNING:
|
|
1840
|
-
console.print("[yellow]Session is still running.[/]")
|
|
1841
|
-
console.print("[dim]Wait for it to complete, or use '?' to check progress.[/]")
|
|
1842
|
-
return
|
|
1843
|
-
|
|
1844
|
-
if session.status == SessStatus.KILLED:
|
|
1845
|
-
console.print("[yellow]Session was killed.[/]")
|
|
1846
|
-
console.print("[dim]Start a new session with 'spawn'.[/]")
|
|
1847
|
-
return
|
|
1848
|
-
|
|
1849
|
-
# Determine if we should wait based on session source
|
|
1850
|
-
# Sessions from 'user' with --async flag have source='user'
|
|
1851
|
-
# We'll use wait parameter to control this
|
|
1852
|
-
should_wait = wait
|
|
1853
|
-
|
|
1854
|
-
console.print(f"\n[dim]Sending message to {session.short_id}...[/]")
|
|
1855
|
-
|
|
1856
|
-
try:
|
|
1857
|
-
# Inject message - spawns new background process
|
|
1858
|
-
updated_session = manager.inject_message(session.id, message)
|
|
1859
|
-
|
|
1860
|
-
if not updated_session:
|
|
1861
|
-
console.print(f" [red]Failed to inject message[/]")
|
|
1862
|
-
return
|
|
1863
|
-
|
|
1864
|
-
console.print(f"[green]✓[/] Message sent (turn {updated_session.turn})")
|
|
1865
|
-
|
|
1866
|
-
if should_wait:
|
|
1867
|
-
# Sync mode: wait for completion
|
|
1868
|
-
console.print(f"[dim]Waiting for response...[/]")
|
|
1869
|
-
timeout = 300.0
|
|
1870
|
-
start = time.time()
|
|
1871
|
-
while time.time() - start < timeout:
|
|
1872
|
-
# get_session() auto-updates status based on output completion
|
|
1873
|
-
session = manager.get_session(session.id)
|
|
1874
|
-
if session.status != SessStatus.RUNNING:
|
|
1875
|
-
break
|
|
1876
|
-
time.sleep(1.0)
|
|
1877
|
-
|
|
1878
|
-
# Get the response (last assistant message)
|
|
1879
|
-
messages = manager.get_messages(session.id)
|
|
1880
|
-
response = ""
|
|
1881
|
-
for msg in reversed(messages):
|
|
1882
|
-
if msg.role == "assistant":
|
|
1883
|
-
response = msg.content
|
|
1884
|
-
break
|
|
1885
|
-
|
|
1886
|
-
console.print(f"\n[bold]Response:[/]")
|
|
1887
|
-
if len(response) > 500:
|
|
1888
|
-
console.print(response[:500] + "...")
|
|
1889
|
-
console.print(f"\n[dim]Use 'show {session.short_id}' to see full response[/]")
|
|
1890
|
-
else:
|
|
1891
|
-
console.print(response or "(no response captured)")
|
|
1892
|
-
else:
|
|
1893
|
-
# Async mode: return immediately
|
|
1894
|
-
console.print(f"[dim]Running in background (PID: {updated_session.pid})[/]")
|
|
1895
|
-
console.print(f"[dim]Use '? {session.short_id}' to check progress[/]")
|
|
1896
|
-
|
|
1897
|
-
except Exception as e:
|
|
1898
|
-
console.print(f" [red]Error:[/] {e}")
|
|
1899
|
-
import traceback
|
|
1900
|
-
console.print(f" [dim]{traceback.format_exc()}[/]")
|
|
1901
|
-
|
|
1902
|
-
def do_check(session_id: str):
|
|
1903
|
-
"""Check status of a session using CodexSessionManager (same as orchestrator)."""
|
|
1904
|
-
from zwarm.sessions import CodexSessionManager
|
|
1905
|
-
|
|
1906
|
-
manager = CodexSessionManager(default_dir / ".zwarm")
|
|
1907
|
-
session = manager.get_session(session_id)
|
|
1908
|
-
|
|
1909
|
-
if not session:
|
|
1910
|
-
console.print(f" [red]Session not found:[/] {session_id}")
|
|
1911
|
-
console.print(f" [dim]Use 'ls' to see available sessions[/]")
|
|
1912
|
-
return
|
|
1913
|
-
|
|
1914
|
-
status_icon = {
|
|
1915
|
-
"running": "●",
|
|
1916
|
-
"completed": "✓",
|
|
1917
|
-
"failed": "✗",
|
|
1918
|
-
"killed": "○",
|
|
1919
|
-
"pending": "◌",
|
|
1920
|
-
}.get(session.status.value, "?")
|
|
1921
|
-
|
|
1922
|
-
console.print(f"\n[bold]Session {session.short_id}[/]: {status_icon} {session.status.value}")
|
|
1923
|
-
console.print(f" [dim]Task: {session.task[:60]}{'...' if len(session.task) > 60 else ''}[/]")
|
|
1924
|
-
console.print(f" [dim]Turn: {session.turn} | Runtime: {session.runtime}[/]")
|
|
1925
|
-
|
|
1926
|
-
if session.status.value == "completed":
|
|
1927
|
-
messages = manager.get_messages(session.id)
|
|
1928
|
-
for msg in reversed(messages):
|
|
1929
|
-
if msg.role == "assistant":
|
|
1930
|
-
console.print(f"\n[bold]Response:[/]")
|
|
1931
|
-
if len(msg.content) > 500:
|
|
1932
|
-
console.print(msg.content[:500] + "...")
|
|
1933
|
-
else:
|
|
1934
|
-
console.print(msg.content)
|
|
1935
|
-
break
|
|
1936
|
-
elif session.status.value == "failed":
|
|
1937
|
-
console.print(f"[red]Error:[/] {session.error or 'Unknown error'}")
|
|
1938
|
-
elif session.status.value == "running":
|
|
1939
|
-
console.print("[dim]Session is still running...[/]")
|
|
1940
|
-
console.print(f" [dim]PID: {session.pid}[/]")
|
|
1941
|
-
|
|
1942
|
-
def do_kill(session_id: str):
|
|
1943
|
-
"""Kill a running session using CodexSessionManager (same as orchestrator)."""
|
|
1944
|
-
from zwarm.sessions import CodexSessionManager
|
|
1945
|
-
|
|
1946
|
-
manager = CodexSessionManager(default_dir / ".zwarm")
|
|
1947
|
-
session = manager.get_session(session_id)
|
|
1386
|
+
console.print(f"[yellow]Session not running or already stopped[/]")
|
|
1387
|
+
return
|
|
1948
1388
|
|
|
1389
|
+
# Handle --rm (specific session)
|
|
1390
|
+
if rm:
|
|
1391
|
+
session = manager.get_session(rm)
|
|
1949
1392
|
if not session:
|
|
1950
|
-
console.print(f"
|
|
1951
|
-
|
|
1952
|
-
|
|
1953
|
-
|
|
1954
|
-
|
|
1955
|
-
|
|
1956
|
-
|
|
1957
|
-
try:
|
|
1958
|
-
killed = manager.kill_session(session.id)
|
|
1959
|
-
if killed:
|
|
1960
|
-
console.print(f"[green]✓[/] Stopped session {session.short_id}")
|
|
1961
|
-
else:
|
|
1962
|
-
console.print(f"[red]Failed to stop session[/]")
|
|
1963
|
-
except Exception as e:
|
|
1964
|
-
console.print(f"[red]Failed to stop session:[/] {e}")
|
|
1965
|
-
|
|
1966
|
-
def do_killall():
|
|
1967
|
-
"""Kill all running sessions using CodexSessionManager."""
|
|
1968
|
-
from zwarm.sessions import CodexSessionManager, SessionStatus as SessStatus
|
|
1969
|
-
|
|
1970
|
-
manager = CodexSessionManager(default_dir / ".zwarm")
|
|
1971
|
-
running_sessions = manager.list_sessions(status=SessStatus.RUNNING)
|
|
1393
|
+
console.print(f"[red]Session not found:[/] {rm}")
|
|
1394
|
+
raise typer.Exit(1)
|
|
1395
|
+
if manager.delete_session(session.id):
|
|
1396
|
+
console.print(f"[green]✓[/] Deleted {session.short_id}")
|
|
1397
|
+
else:
|
|
1398
|
+
console.print(f"[red]Failed to delete[/]")
|
|
1399
|
+
return
|
|
1972
1400
|
|
|
1973
|
-
|
|
1974
|
-
|
|
1401
|
+
# Handle --kill-all
|
|
1402
|
+
if kill_all:
|
|
1403
|
+
running = manager.list_sessions(status=SessionStatus.RUNNING)
|
|
1404
|
+
if not running:
|
|
1405
|
+
console.print("[dim]No running sessions[/]")
|
|
1975
1406
|
return
|
|
1976
|
-
|
|
1977
1407
|
killed = 0
|
|
1978
|
-
for
|
|
1979
|
-
|
|
1980
|
-
|
|
1981
|
-
|
|
1982
|
-
|
|
1983
|
-
|
|
1984
|
-
|
|
1985
|
-
console.print(f"[green]✓[/] Killed {killed} sessions")
|
|
1986
|
-
|
|
1987
|
-
def do_clean():
|
|
1988
|
-
"""Remove old completed/failed sessions from disk."""
|
|
1989
|
-
from zwarm.sessions import CodexSessionManager
|
|
1990
|
-
|
|
1991
|
-
manager = CodexSessionManager(default_dir / ".zwarm")
|
|
1992
|
-
cleaned = manager.cleanup_completed(keep_days=7)
|
|
1993
|
-
console.print(f"[green]✓[/] Cleaned {cleaned} old sessions")
|
|
1994
|
-
|
|
1995
|
-
def do_delete(session_id: str):
|
|
1996
|
-
"""Delete a session entirely (removes from disk and ls)."""
|
|
1997
|
-
from zwarm.sessions import CodexSessionManager
|
|
1998
|
-
|
|
1999
|
-
manager = CodexSessionManager(default_dir / ".zwarm")
|
|
2000
|
-
session = manager.get_session(session_id)
|
|
1408
|
+
for s in running:
|
|
1409
|
+
if manager.kill_session(s.id):
|
|
1410
|
+
console.print(f" [green]✓[/] Killed {s.short_id}")
|
|
1411
|
+
killed += 1
|
|
1412
|
+
console.print(f"\n[green]Killed {killed} session(s)[/]")
|
|
1413
|
+
return
|
|
2001
1414
|
|
|
2002
|
-
|
|
2003
|
-
|
|
1415
|
+
# Handle --clean
|
|
1416
|
+
if clean:
|
|
1417
|
+
all_sessions = manager.list_sessions()
|
|
1418
|
+
to_delete = [s for s in all_sessions if s.status != SessionStatus.RUNNING]
|
|
1419
|
+
if not to_delete:
|
|
1420
|
+
console.print("[dim]Nothing to clean[/]")
|
|
2004
1421
|
return
|
|
1422
|
+
deleted = 0
|
|
1423
|
+
for s in to_delete:
|
|
1424
|
+
if manager.delete_session(s.id):
|
|
1425
|
+
deleted += 1
|
|
1426
|
+
console.print(f"[green]✓[/] Deleted {deleted} session(s)")
|
|
1427
|
+
return
|
|
2005
1428
|
|
|
2006
|
-
|
|
2007
|
-
|
|
2008
|
-
else:
|
|
2009
|
-
console.print(f"[red]Failed to delete session[/]")
|
|
2010
|
-
|
|
2011
|
-
# REPL loop
|
|
2012
|
-
import shlex
|
|
2013
|
-
|
|
2014
|
-
while True:
|
|
2015
|
-
try:
|
|
2016
|
-
raw_input = console.input("[bold cyan]>[/] ").strip()
|
|
2017
|
-
if not raw_input:
|
|
2018
|
-
continue
|
|
2019
|
-
|
|
2020
|
-
# Parse command
|
|
2021
|
-
try:
|
|
2022
|
-
parts = shlex.split(raw_input)
|
|
2023
|
-
except ValueError:
|
|
2024
|
-
parts = raw_input.split()
|
|
2025
|
-
|
|
2026
|
-
cmd = parts[0].lower()
|
|
2027
|
-
args = parts[1:]
|
|
2028
|
-
|
|
2029
|
-
if cmd in ("q", "quit", "exit"):
|
|
2030
|
-
active = [s for s in sessions.values() if s.status == SessionStatus.ACTIVE]
|
|
2031
|
-
if active:
|
|
2032
|
-
console.print(f" [yellow]Warning:[/] {len(active)} sessions still active")
|
|
2033
|
-
console.print(" [dim]Use 'killall' first, or they'll continue in background[/]")
|
|
2034
|
-
|
|
2035
|
-
# Cleanup adapters
|
|
2036
|
-
for adapter_instance in adapters_cache.values():
|
|
2037
|
-
try:
|
|
2038
|
-
asyncio.run(adapter_instance.cleanup())
|
|
2039
|
-
except Exception:
|
|
2040
|
-
pass
|
|
2041
|
-
|
|
2042
|
-
console.print("\n[dim]Goodbye![/]\n")
|
|
2043
|
-
break
|
|
2044
|
-
|
|
2045
|
-
elif cmd in ("h", "help"):
|
|
2046
|
-
show_help()
|
|
2047
|
-
|
|
2048
|
-
elif cmd in ("ls", "list"):
|
|
2049
|
-
show_sessions()
|
|
2050
|
-
|
|
2051
|
-
elif cmd == "spawn":
|
|
2052
|
-
do_spawn(args)
|
|
2053
|
-
|
|
2054
|
-
elif cmd == "async":
|
|
2055
|
-
# Async spawn shorthand
|
|
2056
|
-
do_spawn(args + ["--async"])
|
|
2057
|
-
|
|
2058
|
-
elif cmd == "orchestrate":
|
|
2059
|
-
do_orchestrate(args)
|
|
2060
|
-
|
|
2061
|
-
elif cmd in ("?", "peek"):
|
|
2062
|
-
if not args:
|
|
2063
|
-
console.print(" [red]Usage:[/] ? SESSION_ID")
|
|
2064
|
-
else:
|
|
2065
|
-
do_peek(args[0])
|
|
2066
|
-
|
|
2067
|
-
elif cmd in ("show", "check"):
|
|
2068
|
-
if not args:
|
|
2069
|
-
console.print(" [red]Usage:[/] show SESSION_ID")
|
|
2070
|
-
else:
|
|
2071
|
-
do_show(args[0])
|
|
2072
|
-
|
|
2073
|
-
elif cmd in ("traj", "trajectory"):
|
|
2074
|
-
if not args:
|
|
2075
|
-
console.print(" [red]Usage:[/] traj SESSION_ID [--full]")
|
|
2076
|
-
else:
|
|
2077
|
-
full_mode = "--full" in args
|
|
2078
|
-
session_arg = [a for a in args if a != "--full"]
|
|
2079
|
-
if session_arg:
|
|
2080
|
-
do_trajectory(session_arg[0], full=full_mode)
|
|
2081
|
-
else:
|
|
2082
|
-
console.print(" [red]Usage:[/] traj SESSION_ID [--full]")
|
|
2083
|
-
|
|
2084
|
-
elif cmd in ("c", "continue"):
|
|
2085
|
-
# Sync continue - waits for response
|
|
2086
|
-
if len(args) < 2:
|
|
2087
|
-
console.print(" [red]Usage:[/] c SESSION_ID \"message\"")
|
|
2088
|
-
else:
|
|
2089
|
-
do_continue(args[0], " ".join(args[1:]), wait=True)
|
|
1429
|
+
# Default: list all sessions
|
|
1430
|
+
all_sessions = manager.list_sessions()
|
|
2090
1431
|
|
|
2091
|
-
|
|
2092
|
-
|
|
2093
|
-
|
|
2094
|
-
|
|
2095
|
-
console.print(" [dim]Sends message and returns immediately (check with '?')[/]")
|
|
2096
|
-
else:
|
|
2097
|
-
do_continue(args[0], " ".join(args[1:]), wait=False)
|
|
1432
|
+
if not all_sessions:
|
|
1433
|
+
console.print("[dim]No sessions found.[/]")
|
|
1434
|
+
console.print("[dim]Start one with:[/] zwarm session start \"your task\"")
|
|
1435
|
+
return
|
|
2098
1436
|
|
|
2099
|
-
|
|
2100
|
-
|
|
2101
|
-
|
|
2102
|
-
|
|
2103
|
-
|
|
1437
|
+
# Summary counts
|
|
1438
|
+
running = sum(1 for s in all_sessions if s.status == SessionStatus.RUNNING)
|
|
1439
|
+
completed = sum(1 for s in all_sessions if s.status == SessionStatus.COMPLETED)
|
|
1440
|
+
failed = sum(1 for s in all_sessions if s.status == SessionStatus.FAILED)
|
|
1441
|
+
killed_count = sum(1 for s in all_sessions if s.status == SessionStatus.KILLED)
|
|
1442
|
+
|
|
1443
|
+
parts = []
|
|
1444
|
+
if running:
|
|
1445
|
+
parts.append(f"[yellow]⟳ {running} running[/]")
|
|
1446
|
+
if completed:
|
|
1447
|
+
parts.append(f"[green]✓ {completed} completed[/]")
|
|
1448
|
+
if failed:
|
|
1449
|
+
parts.append(f"[red]✗ {failed} failed[/]")
|
|
1450
|
+
if killed_count:
|
|
1451
|
+
parts.append(f"[dim]⊘ {killed_count} killed[/]")
|
|
1452
|
+
|
|
1453
|
+
console.print(" │ ".join(parts))
|
|
1454
|
+
console.print()
|
|
2104
1455
|
|
|
2105
|
-
|
|
2106
|
-
|
|
2107
|
-
|
|
2108
|
-
|
|
2109
|
-
|
|
1456
|
+
# Table
|
|
1457
|
+
table = Table(box=None, show_header=True, header_style="bold dim")
|
|
1458
|
+
table.add_column("ID", style="cyan")
|
|
1459
|
+
table.add_column("", width=2)
|
|
1460
|
+
table.add_column("Task", max_width=45)
|
|
1461
|
+
table.add_column("Tokens", justify="right", style="dim")
|
|
1462
|
+
table.add_column("Cost", justify="right", style="dim")
|
|
2110
1463
|
|
|
2111
|
-
|
|
2112
|
-
|
|
1464
|
+
status_icons = {
|
|
1465
|
+
SessionStatus.RUNNING: "[yellow]⟳[/]",
|
|
1466
|
+
SessionStatus.COMPLETED: "[green]✓[/]",
|
|
1467
|
+
SessionStatus.FAILED: "[red]✗[/]",
|
|
1468
|
+
SessionStatus.KILLED: "[dim]⊘[/]",
|
|
1469
|
+
SessionStatus.PENDING: "[dim]○[/]",
|
|
1470
|
+
}
|
|
2113
1471
|
|
|
2114
|
-
|
|
2115
|
-
|
|
1472
|
+
for session in all_sessions:
|
|
1473
|
+
icon = status_icons.get(session.status, "?")
|
|
1474
|
+
task = session.task[:42] + "..." if len(session.task) > 45 else session.task
|
|
1475
|
+
tokens = session.token_usage.get("total_tokens", 0)
|
|
1476
|
+
tokens_str = f"{tokens:,}" if tokens else "-"
|
|
2116
1477
|
|
|
2117
|
-
|
|
2118
|
-
|
|
2119
|
-
|
|
2120
|
-
else:
|
|
2121
|
-
do_delete(args[0])
|
|
1478
|
+
# Cost estimate
|
|
1479
|
+
cost_info = estimate_session_cost(session.model, session.token_usage)
|
|
1480
|
+
cost_str = format_cost(cost_info.get("cost"))
|
|
2122
1481
|
|
|
2123
|
-
|
|
2124
|
-
console.print(f" [yellow]Unknown command:[/] {cmd}")
|
|
2125
|
-
console.print(" [dim]Type 'help' for available commands[/]")
|
|
1482
|
+
table.add_row(session.short_id, icon, task, tokens_str, cost_str)
|
|
2126
1483
|
|
|
2127
|
-
|
|
2128
|
-
|
|
2129
|
-
|
|
2130
|
-
console.print("\n[dim]Goodbye![/]\n")
|
|
2131
|
-
break
|
|
2132
|
-
except Exception as e:
|
|
2133
|
-
console.print(f" [red]Error:[/] {e}")
|
|
1484
|
+
console.print(table)
|
|
1485
|
+
console.print()
|
|
1486
|
+
console.print("[dim]Quick actions: --clean (-c) delete old │ --kill-all (-k) stop running[/]")
|
|
2134
1487
|
|
|
2135
1488
|
|
|
2136
1489
|
# =============================================================================
|