zwarm 3.2.1__py3-none-any.whl → 3.6.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 +346 -30
- zwarm/cli/main.py +221 -90
- zwarm/cli/pilot.py +107 -9
- zwarm/core/config.py +26 -9
- zwarm/core/costs.py +55 -183
- zwarm/core/registry.py +329 -0
- zwarm/core/test_config.py +2 -3
- zwarm/orchestrator.py +17 -43
- zwarm/sessions/__init__.py +48 -9
- zwarm/sessions/base.py +501 -0
- zwarm/sessions/claude.py +481 -0
- zwarm/sessions/manager.py +233 -486
- zwarm/tools/delegation.py +93 -31
- {zwarm-3.2.1.dist-info → zwarm-3.6.0.dist-info}/METADATA +73 -21
- {zwarm-3.2.1.dist-info → zwarm-3.6.0.dist-info}/RECORD +17 -21
- zwarm/adapters/__init__.py +0 -21
- zwarm/adapters/base.py +0 -109
- zwarm/adapters/claude_code.py +0 -357
- zwarm/adapters/codex_mcp.py +0 -1262
- zwarm/adapters/registry.py +0 -69
- zwarm/adapters/test_codex_mcp.py +0 -274
- zwarm/adapters/test_registry.py +0 -68
- {zwarm-3.2.1.dist-info → zwarm-3.6.0.dist-info}/WHEEL +0 -0
- {zwarm-3.2.1.dist-info → zwarm-3.6.0.dist-info}/entry_points.txt +0 -0
zwarm/cli/interactive.py
CHANGED
|
@@ -153,11 +153,11 @@ STATUS_ICONS = {
|
|
|
153
153
|
def cmd_help():
|
|
154
154
|
"""Show help."""
|
|
155
155
|
table = Table(show_header=False, box=None, padding=(0, 2))
|
|
156
|
-
table.add_column("Command", style="cyan", width=
|
|
156
|
+
table.add_column("Command", style="cyan", width=35)
|
|
157
157
|
table.add_column("Description")
|
|
158
158
|
|
|
159
159
|
table.add_row("[bold]Session Lifecycle[/]", "")
|
|
160
|
-
table.add_row('spawn "task" [--
|
|
160
|
+
table.add_row('spawn "task" [--model M] [--adapter A]', "Start new session")
|
|
161
161
|
table.add_row('c ID "message"', "Continue conversation")
|
|
162
162
|
table.add_row("kill ID | all", "Stop session(s)")
|
|
163
163
|
table.add_row("rm ID | all", "Delete session(s)")
|
|
@@ -169,6 +169,12 @@ def cmd_help():
|
|
|
169
169
|
table.add_row("traj ID [--full]", "Show trajectory (steps taken)")
|
|
170
170
|
table.add_row("watch ID", "Live follow session output")
|
|
171
171
|
table.add_row("", "")
|
|
172
|
+
table.add_row("[bold]Configuration[/]", "")
|
|
173
|
+
table.add_row("models", "List available models and adapters")
|
|
174
|
+
table.add_row("", "")
|
|
175
|
+
table.add_row("[bold]Shell[/]", "")
|
|
176
|
+
table.add_row("!<command>", "Run shell command (e.g., !ls, !git status)")
|
|
177
|
+
table.add_row("", "")
|
|
172
178
|
table.add_row("[bold]Meta[/]", "")
|
|
173
179
|
table.add_row("help", "Show this help")
|
|
174
180
|
table.add_row("quit", "Exit")
|
|
@@ -176,6 +182,37 @@ def cmd_help():
|
|
|
176
182
|
console.print(table)
|
|
177
183
|
|
|
178
184
|
|
|
185
|
+
def cmd_models():
|
|
186
|
+
"""Show available models."""
|
|
187
|
+
from zwarm.core.registry import list_models, list_adapters
|
|
188
|
+
|
|
189
|
+
table = Table(title="Available Models", box=None)
|
|
190
|
+
table.add_column("Adapter", style="cyan")
|
|
191
|
+
table.add_column("Model", style="green")
|
|
192
|
+
table.add_column("Aliases", style="dim")
|
|
193
|
+
table.add_column("Price ($/1M)", justify="right")
|
|
194
|
+
table.add_column("Description")
|
|
195
|
+
|
|
196
|
+
for adapter in list_adapters():
|
|
197
|
+
first = True
|
|
198
|
+
for model in list_models(adapter):
|
|
199
|
+
default_mark = " *" if model.is_default else ""
|
|
200
|
+
price = f"{model.input_per_million:.2f}/{model.output_per_million:.2f}"
|
|
201
|
+
aliases = ", ".join(model.aliases)
|
|
202
|
+
table.add_row(
|
|
203
|
+
adapter if first else "",
|
|
204
|
+
f"{model.canonical}{default_mark}",
|
|
205
|
+
aliases,
|
|
206
|
+
price,
|
|
207
|
+
model.description,
|
|
208
|
+
)
|
|
209
|
+
first = False
|
|
210
|
+
|
|
211
|
+
console.print(table)
|
|
212
|
+
console.print("\n[dim]* = default for adapter. Price = input/output per 1M tokens.[/]")
|
|
213
|
+
console.print("[dim]Use --model <name> or --adapter <adapter> with spawn.[/]")
|
|
214
|
+
|
|
215
|
+
|
|
179
216
|
def cmd_ls(manager):
|
|
180
217
|
"""List all sessions."""
|
|
181
218
|
from zwarm.sessions import SessionStatus
|
|
@@ -221,23 +258,35 @@ def cmd_ls(manager):
|
|
|
221
258
|
table = Table(box=None, show_header=True, header_style="bold dim")
|
|
222
259
|
table.add_column("ID", style="cyan", width=10)
|
|
223
260
|
table.add_column("", width=2)
|
|
261
|
+
table.add_column("Model", width=12)
|
|
224
262
|
table.add_column("T", width=2)
|
|
225
|
-
table.add_column("Task", max_width=
|
|
263
|
+
table.add_column("Task", max_width=26)
|
|
226
264
|
table.add_column("Updated", justify="right", width=8)
|
|
227
|
-
table.add_column("Last Message", max_width=
|
|
265
|
+
table.add_column("Last Message", max_width=36)
|
|
228
266
|
|
|
229
267
|
for s in sessions:
|
|
230
268
|
icon = STATUS_ICONS.get(s.status.value, "?")
|
|
231
|
-
task_preview = s.task[:
|
|
269
|
+
task_preview = s.task[:23] + "..." if len(s.task) > 26 else s.task
|
|
232
270
|
updated = time_ago(s.updated_at)
|
|
233
271
|
|
|
272
|
+
# Short model name (e.g., "gpt-5.1-codex-mini" -> "codex-mini")
|
|
273
|
+
model_short = s.model or "?"
|
|
274
|
+
if "codex" in model_short.lower():
|
|
275
|
+
# Extract codex variant: gpt-5.1-codex-mini -> codex-mini
|
|
276
|
+
parts = model_short.split("-")
|
|
277
|
+
codex_idx = next((i for i, p in enumerate(parts) if "codex" in p.lower()), -1)
|
|
278
|
+
if codex_idx >= 0:
|
|
279
|
+
model_short = "-".join(parts[codex_idx:])
|
|
280
|
+
elif len(model_short) > 12:
|
|
281
|
+
model_short = model_short[:10] + ".."
|
|
282
|
+
|
|
234
283
|
# Get last assistant message
|
|
235
284
|
messages = manager.get_messages(s.id)
|
|
236
285
|
last_msg = ""
|
|
237
286
|
for msg in reversed(messages):
|
|
238
287
|
if msg.role == "assistant":
|
|
239
|
-
last_msg = msg.content.replace("\n", " ")[:
|
|
240
|
-
if len(msg.content) >
|
|
288
|
+
last_msg = msg.content.replace("\n", " ")[:33]
|
|
289
|
+
if len(msg.content) > 33:
|
|
241
290
|
last_msg += "..."
|
|
242
291
|
break
|
|
243
292
|
|
|
@@ -258,14 +307,110 @@ def cmd_ls(manager):
|
|
|
258
307
|
last_msg_styled = f"[green]{last_msg or '(done)'}[/]"
|
|
259
308
|
updated_styled = f"[dim]{updated}[/]"
|
|
260
309
|
elif s.status == SessionStatus.FAILED:
|
|
261
|
-
err = s.error[:
|
|
310
|
+
err = s.error[:33] if s.error else "(failed)"
|
|
262
311
|
last_msg_styled = f"[red]{err}...[/]"
|
|
263
312
|
updated_styled = f"[red]{updated}[/]"
|
|
264
313
|
else:
|
|
265
314
|
last_msg_styled = f"[dim]{last_msg or '-'}[/]"
|
|
266
315
|
updated_styled = f"[dim]{updated}[/]"
|
|
267
316
|
|
|
268
|
-
table.add_row(s.short_id, icon, str(s.turn), task_preview, updated_styled, last_msg_styled)
|
|
317
|
+
table.add_row(s.short_id, icon, f"[dim]{model_short}[/]", str(s.turn), task_preview, updated_styled, last_msg_styled)
|
|
318
|
+
|
|
319
|
+
console.print(table)
|
|
320
|
+
|
|
321
|
+
|
|
322
|
+
def cmd_ls_multi(sessions: list, managers: dict | None = None):
|
|
323
|
+
"""
|
|
324
|
+
List sessions from multiple managers.
|
|
325
|
+
|
|
326
|
+
Args:
|
|
327
|
+
sessions: List of Session objects
|
|
328
|
+
managers: Optional dict of adapter -> manager for getting messages
|
|
329
|
+
"""
|
|
330
|
+
from zwarm.sessions import SessionStatus
|
|
331
|
+
from zwarm.core.costs import estimate_session_cost, format_cost
|
|
332
|
+
|
|
333
|
+
if not sessions:
|
|
334
|
+
console.print(" [dim]No sessions. Use 'spawn \"task\"' to start one.[/]")
|
|
335
|
+
return
|
|
336
|
+
|
|
337
|
+
# Summary counts
|
|
338
|
+
running = sum(1 for s in sessions if s.status == SessionStatus.RUNNING)
|
|
339
|
+
completed = sum(1 for s in sessions if s.status == SessionStatus.COMPLETED)
|
|
340
|
+
failed = sum(1 for s in sessions if s.status == SessionStatus.FAILED)
|
|
341
|
+
killed = sum(1 for s in sessions if s.status == SessionStatus.KILLED)
|
|
342
|
+
|
|
343
|
+
# Total cost and tokens
|
|
344
|
+
total_cost = 0.0
|
|
345
|
+
total_tokens = 0
|
|
346
|
+
for s in sessions:
|
|
347
|
+
cost_info = estimate_session_cost(s.model, s.token_usage)
|
|
348
|
+
if cost_info["cost"] is not None:
|
|
349
|
+
total_cost += cost_info["cost"]
|
|
350
|
+
total_tokens += s.token_usage.get("total_tokens", 0)
|
|
351
|
+
|
|
352
|
+
parts = []
|
|
353
|
+
if running:
|
|
354
|
+
parts.append(f"[yellow]{running} running[/]")
|
|
355
|
+
if completed:
|
|
356
|
+
parts.append(f"[green]{completed} done[/]")
|
|
357
|
+
if failed:
|
|
358
|
+
parts.append(f"[red]{failed} failed[/]")
|
|
359
|
+
if killed:
|
|
360
|
+
parts.append(f"[dim]{killed} killed[/]")
|
|
361
|
+
parts.append(f"[cyan]{total_tokens:,} tokens[/]")
|
|
362
|
+
parts.append(f"[green]{format_cost(total_cost)}[/]")
|
|
363
|
+
if parts:
|
|
364
|
+
console.print(" | ".join(parts))
|
|
365
|
+
console.print()
|
|
366
|
+
|
|
367
|
+
# Table
|
|
368
|
+
table = Table(box=None, show_header=True, header_style="bold dim")
|
|
369
|
+
table.add_column("ID", style="cyan", width=10)
|
|
370
|
+
table.add_column("", width=2)
|
|
371
|
+
table.add_column("Adapter", width=7)
|
|
372
|
+
table.add_column("Model", width=12)
|
|
373
|
+
table.add_column("T", width=2)
|
|
374
|
+
table.add_column("Task", max_width=24)
|
|
375
|
+
table.add_column("Updated", justify="right", width=8)
|
|
376
|
+
|
|
377
|
+
for s in sessions:
|
|
378
|
+
icon = STATUS_ICONS.get(s.status.value, "?")
|
|
379
|
+
task_preview = s.task[:21] + "..." if len(s.task) > 24 else s.task
|
|
380
|
+
updated = time_ago(s.updated_at)
|
|
381
|
+
|
|
382
|
+
# Short model name
|
|
383
|
+
model_short = s.model or "?"
|
|
384
|
+
if "codex" in model_short.lower():
|
|
385
|
+
parts = model_short.split("-")
|
|
386
|
+
codex_idx = next((i for i, p in enumerate(parts) if "codex" in p.lower()), -1)
|
|
387
|
+
if codex_idx >= 0:
|
|
388
|
+
model_short = "-".join(parts[codex_idx:])
|
|
389
|
+
elif len(model_short) > 12:
|
|
390
|
+
model_short = model_short[:10] + ".."
|
|
391
|
+
|
|
392
|
+
# Adapter short name
|
|
393
|
+
adapter_short = getattr(s, "adapter", "?")[:7]
|
|
394
|
+
|
|
395
|
+
# Style based on status
|
|
396
|
+
if s.status == SessionStatus.RUNNING:
|
|
397
|
+
updated_styled = f"[yellow]{updated}[/]"
|
|
398
|
+
elif s.status == SessionStatus.COMPLETED:
|
|
399
|
+
try:
|
|
400
|
+
dt = datetime.fromisoformat(s.updated_at)
|
|
401
|
+
is_recent = (datetime.now() - dt).total_seconds() < 60
|
|
402
|
+
except Exception:
|
|
403
|
+
is_recent = False
|
|
404
|
+
if is_recent:
|
|
405
|
+
updated_styled = f"[green bold]{updated} ★[/]"
|
|
406
|
+
else:
|
|
407
|
+
updated_styled = f"[dim]{updated}[/]"
|
|
408
|
+
elif s.status == SessionStatus.FAILED:
|
|
409
|
+
updated_styled = f"[red]{updated}[/]"
|
|
410
|
+
else:
|
|
411
|
+
updated_styled = f"[dim]{updated}[/]"
|
|
412
|
+
|
|
413
|
+
table.add_row(s.short_id, icon, f"[dim]{adapter_short}[/]", f"[dim]{model_short}[/]", str(s.turn), task_preview, updated_styled)
|
|
269
414
|
|
|
270
415
|
console.print(table)
|
|
271
416
|
|
|
@@ -280,7 +425,7 @@ def cmd_peek(manager, session_id: str):
|
|
|
280
425
|
icon = STATUS_ICONS.get(session.status.value, "?")
|
|
281
426
|
console.print(f"\n{icon} [cyan]{session.short_id}[/] ({session.status.value})")
|
|
282
427
|
console.print(f" [dim]Task:[/] {session.task[:60]}...")
|
|
283
|
-
console.print(f" [dim]Turn:[/] {session.turn} | [dim]Updated:[/] {time_ago(session.updated_at)}")
|
|
428
|
+
console.print(f" [dim]Model:[/] {session.model} | [dim]Turn:[/] {session.turn} | [dim]Updated:[/] {time_ago(session.updated_at)}")
|
|
284
429
|
|
|
285
430
|
# Latest message
|
|
286
431
|
messages = manager.get_messages(session.id)
|
|
@@ -307,7 +452,7 @@ def cmd_show(manager, session_id: str):
|
|
|
307
452
|
icon = STATUS_ICONS.get(session.status.value, "?")
|
|
308
453
|
console.print(f"\n{icon} [bold cyan]{session.short_id}[/] - {session.status.value}")
|
|
309
454
|
console.print(f" [dim]Task:[/] {session.task}")
|
|
310
|
-
console.print(f" [dim]Turn:[/] {session.turn} | [dim]Runtime:[/] {session.runtime
|
|
455
|
+
console.print(f" [dim]Model:[/] {session.model} | [dim]Turn:[/] {session.turn} | [dim]Runtime:[/] {session.runtime}")
|
|
311
456
|
|
|
312
457
|
# Token usage with cost estimate
|
|
313
458
|
usage = session.token_usage
|
|
@@ -409,6 +554,7 @@ def cmd_watch(manager, session_id: str):
|
|
|
409
554
|
|
|
410
555
|
console.print(f"\n[bold]Watching {session.short_id}[/]...")
|
|
411
556
|
console.print(f" [dim]Task:[/] {session.task[:60]}...")
|
|
557
|
+
console.print(f" [dim]Model:[/] {session.model}")
|
|
412
558
|
console.print(f" [dim]Press Ctrl+C to stop watching[/]\n")
|
|
413
559
|
|
|
414
560
|
seen_steps = 0
|
|
@@ -471,20 +617,52 @@ def cmd_watch(manager, session_id: str):
|
|
|
471
617
|
console.print()
|
|
472
618
|
|
|
473
619
|
|
|
474
|
-
def cmd_spawn(
|
|
475
|
-
"""
|
|
620
|
+
def cmd_spawn(managers: dict, task: str, working_dir: Path, model: str, adapter: str | None = None):
|
|
621
|
+
"""
|
|
622
|
+
Spawn a new session.
|
|
623
|
+
|
|
624
|
+
Args:
|
|
625
|
+
managers: Dict of adapter name -> session manager
|
|
626
|
+
task: Task description
|
|
627
|
+
working_dir: Working directory
|
|
628
|
+
model: Model name or alias
|
|
629
|
+
adapter: Adapter override (auto-detected from model if None)
|
|
630
|
+
"""
|
|
631
|
+
from zwarm.core.registry import get_adapter_for_model, get_default_model, resolve_model
|
|
632
|
+
|
|
633
|
+
# Auto-detect adapter from model if not specified
|
|
634
|
+
if adapter is None:
|
|
635
|
+
detected = get_adapter_for_model(model)
|
|
636
|
+
if detected:
|
|
637
|
+
adapter = detected
|
|
638
|
+
else:
|
|
639
|
+
# Default to codex if model not recognized
|
|
640
|
+
adapter = "codex"
|
|
641
|
+
|
|
642
|
+
# Resolve model alias to canonical name if needed
|
|
643
|
+
model_info = resolve_model(model)
|
|
644
|
+
effective_model = model_info.canonical if model_info else model
|
|
645
|
+
|
|
646
|
+
# Get the right manager
|
|
647
|
+
if adapter not in managers:
|
|
648
|
+
console.print(f" [red]Unknown adapter:[/] {adapter}")
|
|
649
|
+
console.print(f" [dim]Available: {', '.join(managers.keys())}[/]")
|
|
650
|
+
return
|
|
651
|
+
|
|
652
|
+
manager = managers[adapter]
|
|
653
|
+
|
|
476
654
|
console.print(f"\n[dim]Spawning session...[/]")
|
|
655
|
+
console.print(f" [dim]Adapter:[/] {adapter}")
|
|
656
|
+
console.print(f" [dim]Model:[/] {effective_model}")
|
|
477
657
|
console.print(f" [dim]Dir:[/] {working_dir}")
|
|
478
|
-
console.print(f" [dim]Model:[/] {model}")
|
|
479
658
|
|
|
480
659
|
try:
|
|
481
660
|
session = manager.start_session(
|
|
482
661
|
task=task,
|
|
483
662
|
working_dir=working_dir,
|
|
484
|
-
model=
|
|
663
|
+
model=effective_model,
|
|
485
664
|
sandbox="workspace-write",
|
|
486
665
|
source="user",
|
|
487
|
-
adapter="codex",
|
|
488
666
|
)
|
|
489
667
|
|
|
490
668
|
console.print(f"\n[green]✓[/] Session: [cyan]{session.short_id}[/]")
|
|
@@ -613,13 +791,34 @@ def run_interactive(
|
|
|
613
791
|
working_dir: Default working directory for sessions
|
|
614
792
|
model: Default model for sessions
|
|
615
793
|
"""
|
|
616
|
-
from zwarm.sessions import
|
|
794
|
+
from zwarm.sessions import get_session_manager
|
|
795
|
+
from zwarm.core.registry import get_adapter_for_model, list_adapters
|
|
796
|
+
|
|
797
|
+
# Initialize managers for all adapters
|
|
798
|
+
state_dir = working_dir / ".zwarm"
|
|
799
|
+
managers = {}
|
|
800
|
+
for adapter in list_adapters():
|
|
801
|
+
try:
|
|
802
|
+
managers[adapter] = get_session_manager(adapter, str(state_dir))
|
|
803
|
+
except Exception:
|
|
804
|
+
pass # Adapter not available
|
|
617
805
|
|
|
618
|
-
|
|
806
|
+
if not managers:
|
|
807
|
+
console.print("[red]No adapters available. Run 'zwarm init' first.[/]")
|
|
808
|
+
return
|
|
809
|
+
|
|
810
|
+
# Primary manager for listing (aggregates across all adapters)
|
|
811
|
+
primary_adapter = get_adapter_for_model(model) or "codex"
|
|
812
|
+
if primary_adapter not in managers:
|
|
813
|
+
primary_adapter = list(managers.keys())[0]
|
|
619
814
|
|
|
620
815
|
# Setup prompt with autocomplete
|
|
621
816
|
def get_sessions():
|
|
622
|
-
|
|
817
|
+
# Aggregate sessions from all managers
|
|
818
|
+
all_sessions = []
|
|
819
|
+
for mgr in managers.values():
|
|
820
|
+
all_sessions.extend(mgr.list_sessions())
|
|
821
|
+
return all_sessions
|
|
623
822
|
|
|
624
823
|
completer = SessionCompleter(get_sessions)
|
|
625
824
|
style = Style.from_dict({
|
|
@@ -637,7 +836,8 @@ def run_interactive(
|
|
|
637
836
|
console.print("\n[bold cyan]zwarm interactive[/] - Session Manager\n")
|
|
638
837
|
console.print(f" [dim]Dir:[/] {working_dir.absolute()}")
|
|
639
838
|
console.print(f" [dim]Model:[/] {model}")
|
|
640
|
-
console.print(f"
|
|
839
|
+
console.print(f" [dim]Adapters:[/] {', '.join(managers.keys())}")
|
|
840
|
+
console.print(f"\n Type [cyan]help[/] for commands, [cyan]models[/] to see available models.")
|
|
641
841
|
console.print(f" [dim]Tab to autocomplete session IDs[/]\n")
|
|
642
842
|
|
|
643
843
|
# REPL
|
|
@@ -647,6 +847,29 @@ def run_interactive(
|
|
|
647
847
|
if not raw:
|
|
648
848
|
continue
|
|
649
849
|
|
|
850
|
+
# Bang command: !cmd runs shell command
|
|
851
|
+
if raw.startswith("!"):
|
|
852
|
+
import subprocess
|
|
853
|
+
shell_cmd = raw[1:].strip()
|
|
854
|
+
if shell_cmd:
|
|
855
|
+
try:
|
|
856
|
+
result = subprocess.run(
|
|
857
|
+
shell_cmd,
|
|
858
|
+
shell=True,
|
|
859
|
+
cwd=working_dir,
|
|
860
|
+
capture_output=True,
|
|
861
|
+
text=True,
|
|
862
|
+
)
|
|
863
|
+
if result.stdout:
|
|
864
|
+
console.print(result.stdout.rstrip())
|
|
865
|
+
if result.stderr:
|
|
866
|
+
console.print(f"[red]{result.stderr.rstrip()}[/]")
|
|
867
|
+
if result.returncode != 0:
|
|
868
|
+
console.print(f"[dim](exit code: {result.returncode})[/]")
|
|
869
|
+
except Exception as e:
|
|
870
|
+
console.print(f"[red]Error:[/] {e}")
|
|
871
|
+
continue
|
|
872
|
+
|
|
650
873
|
try:
|
|
651
874
|
parts = shlex.split(raw)
|
|
652
875
|
except ValueError:
|
|
@@ -655,6 +878,26 @@ def run_interactive(
|
|
|
655
878
|
cmd = parts[0].lower()
|
|
656
879
|
args = parts[1:]
|
|
657
880
|
|
|
881
|
+
# Helper to find session and return the correct manager for its adapter
|
|
882
|
+
def find_session(sid: str):
|
|
883
|
+
# First, find the session (any manager can load it)
|
|
884
|
+
session = None
|
|
885
|
+
for mgr in managers.values():
|
|
886
|
+
session = mgr.get_session(sid)
|
|
887
|
+
if session:
|
|
888
|
+
break
|
|
889
|
+
|
|
890
|
+
if not session:
|
|
891
|
+
return None, None
|
|
892
|
+
|
|
893
|
+
# Return the manager that matches the session's adapter
|
|
894
|
+
adapter = getattr(session, "adapter", "codex")
|
|
895
|
+
if adapter in managers:
|
|
896
|
+
return managers[adapter], session
|
|
897
|
+
else:
|
|
898
|
+
# Fallback to whichever manager found it
|
|
899
|
+
return mgr, session
|
|
900
|
+
|
|
658
901
|
# Dispatch
|
|
659
902
|
if cmd in ("q", "quit", "exit"):
|
|
660
903
|
console.print("\n[dim]Goodbye![/]\n")
|
|
@@ -663,20 +906,43 @@ def run_interactive(
|
|
|
663
906
|
elif cmd in ("h", "help"):
|
|
664
907
|
cmd_help()
|
|
665
908
|
|
|
909
|
+
elif cmd == "models":
|
|
910
|
+
cmd_models()
|
|
911
|
+
|
|
666
912
|
elif cmd in ("ls", "list"):
|
|
667
|
-
|
|
913
|
+
# Aggregate sessions from all managers
|
|
914
|
+
from zwarm.sessions import SessionStatus
|
|
915
|
+
from zwarm.core.costs import estimate_session_cost, format_cost
|
|
916
|
+
|
|
917
|
+
all_sessions = []
|
|
918
|
+
for mgr in managers.values():
|
|
919
|
+
all_sessions.extend(mgr.list_sessions())
|
|
920
|
+
|
|
921
|
+
if not all_sessions:
|
|
922
|
+
console.print(" [dim]No sessions. Use 'spawn \"task\"' to start one.[/]")
|
|
923
|
+
else:
|
|
924
|
+
# Use first manager's cmd_ls logic but with aggregated sessions
|
|
925
|
+
cmd_ls_multi(all_sessions, managers)
|
|
668
926
|
|
|
669
927
|
elif cmd in ("?", "peek"):
|
|
670
928
|
if not args:
|
|
671
929
|
console.print(" [red]Usage:[/] peek ID")
|
|
672
930
|
else:
|
|
673
|
-
|
|
931
|
+
mgr, _ = find_session(args[0])
|
|
932
|
+
if mgr:
|
|
933
|
+
cmd_peek(mgr, args[0])
|
|
934
|
+
else:
|
|
935
|
+
console.print(f" [red]Session not found:[/] {args[0]}")
|
|
674
936
|
|
|
675
937
|
elif cmd == "show":
|
|
676
938
|
if not args:
|
|
677
939
|
console.print(" [red]Usage:[/] show ID")
|
|
678
940
|
else:
|
|
679
|
-
|
|
941
|
+
mgr, _ = find_session(args[0])
|
|
942
|
+
if mgr:
|
|
943
|
+
cmd_show(mgr, args[0])
|
|
944
|
+
else:
|
|
945
|
+
console.print(f" [red]Session not found:[/] {args[0]}")
|
|
680
946
|
|
|
681
947
|
elif cmd in ("traj", "trajectory"):
|
|
682
948
|
if not args:
|
|
@@ -684,22 +950,31 @@ def run_interactive(
|
|
|
684
950
|
else:
|
|
685
951
|
full = "--full" in args
|
|
686
952
|
sid = [a for a in args if not a.startswith("-")][0]
|
|
687
|
-
|
|
953
|
+
mgr, _ = find_session(sid)
|
|
954
|
+
if mgr:
|
|
955
|
+
cmd_traj(mgr, sid, full=full)
|
|
956
|
+
else:
|
|
957
|
+
console.print(f" [red]Session not found:[/] {sid}")
|
|
688
958
|
|
|
689
959
|
elif cmd == "watch":
|
|
690
960
|
if not args:
|
|
691
961
|
console.print(" [red]Usage:[/] watch ID")
|
|
692
962
|
else:
|
|
693
|
-
|
|
963
|
+
mgr, _ = find_session(args[0])
|
|
964
|
+
if mgr:
|
|
965
|
+
cmd_watch(mgr, args[0])
|
|
966
|
+
else:
|
|
967
|
+
console.print(f" [red]Session not found:[/] {args[0]}")
|
|
694
968
|
|
|
695
969
|
elif cmd == "spawn":
|
|
696
970
|
if not args:
|
|
697
|
-
console.print(" [red]Usage:[/] spawn \"task\" [--
|
|
971
|
+
console.print(" [red]Usage:[/] spawn \"task\" [--model M] [--adapter A]")
|
|
698
972
|
else:
|
|
699
973
|
# Parse spawn args
|
|
700
974
|
task_parts = []
|
|
701
975
|
spawn_dir = working_dir
|
|
702
976
|
spawn_model = model
|
|
977
|
+
spawn_adapter = None
|
|
703
978
|
i = 0
|
|
704
979
|
while i < len(args):
|
|
705
980
|
if args[i] in ("--dir", "-d") and i + 1 < len(args):
|
|
@@ -708,13 +983,16 @@ def run_interactive(
|
|
|
708
983
|
elif args[i] in ("--model", "-m") and i + 1 < len(args):
|
|
709
984
|
spawn_model = args[i + 1]
|
|
710
985
|
i += 2
|
|
986
|
+
elif args[i] in ("--adapter", "-a") and i + 1 < len(args):
|
|
987
|
+
spawn_adapter = args[i + 1]
|
|
988
|
+
i += 2
|
|
711
989
|
else:
|
|
712
990
|
task_parts.append(args[i])
|
|
713
991
|
i += 1
|
|
714
992
|
|
|
715
993
|
task = " ".join(task_parts)
|
|
716
994
|
if task:
|
|
717
|
-
cmd_spawn(
|
|
995
|
+
cmd_spawn(managers, task, spawn_dir, spawn_model, spawn_adapter)
|
|
718
996
|
else:
|
|
719
997
|
console.print(" [red]Task required[/]")
|
|
720
998
|
|
|
@@ -722,19 +1000,57 @@ def run_interactive(
|
|
|
722
1000
|
if len(args) < 2:
|
|
723
1001
|
console.print(" [red]Usage:[/] c ID \"message\"")
|
|
724
1002
|
else:
|
|
725
|
-
|
|
1003
|
+
mgr, _ = find_session(args[0])
|
|
1004
|
+
if mgr:
|
|
1005
|
+
cmd_continue(mgr, args[0], " ".join(args[1:]))
|
|
1006
|
+
else:
|
|
1007
|
+
console.print(f" [red]Session not found:[/] {args[0]}")
|
|
726
1008
|
|
|
727
1009
|
elif cmd == "kill":
|
|
728
1010
|
if not args:
|
|
729
1011
|
console.print(" [red]Usage:[/] kill ID | all")
|
|
1012
|
+
elif args[0].lower() == "all":
|
|
1013
|
+
# Kill all running across all managers
|
|
1014
|
+
killed = 0
|
|
1015
|
+
for mgr in managers.values():
|
|
1016
|
+
from zwarm.sessions import SessionStatus
|
|
1017
|
+
for s in mgr.list_sessions(status=SessionStatus.RUNNING):
|
|
1018
|
+
if mgr.kill_session(s.id):
|
|
1019
|
+
killed += 1
|
|
1020
|
+
console.print(f" [green]✓[/] Killed {s.short_id}")
|
|
1021
|
+
if killed:
|
|
1022
|
+
console.print(f"\n[green]Killed {killed} session(s)[/]")
|
|
1023
|
+
else:
|
|
1024
|
+
console.print(" [dim]No running sessions[/]")
|
|
730
1025
|
else:
|
|
731
|
-
|
|
1026
|
+
mgr, _ = find_session(args[0])
|
|
1027
|
+
if mgr:
|
|
1028
|
+
cmd_kill(mgr, args[0])
|
|
1029
|
+
else:
|
|
1030
|
+
console.print(f" [red]Session not found:[/] {args[0]}")
|
|
732
1031
|
|
|
733
1032
|
elif cmd in ("rm", "delete"):
|
|
734
1033
|
if not args:
|
|
735
1034
|
console.print(" [red]Usage:[/] rm ID | all")
|
|
1035
|
+
elif args[0].lower() == "all":
|
|
1036
|
+
# Delete all non-running across all managers
|
|
1037
|
+
deleted = 0
|
|
1038
|
+
for mgr in managers.values():
|
|
1039
|
+
from zwarm.sessions import SessionStatus
|
|
1040
|
+
for s in mgr.list_sessions():
|
|
1041
|
+
if s.status != SessionStatus.RUNNING:
|
|
1042
|
+
if mgr.delete_session(s.id):
|
|
1043
|
+
deleted += 1
|
|
1044
|
+
if deleted:
|
|
1045
|
+
console.print(f"[green]✓[/] Deleted {deleted} session(s)")
|
|
1046
|
+
else:
|
|
1047
|
+
console.print(" [dim]Nothing to delete[/]")
|
|
736
1048
|
else:
|
|
737
|
-
|
|
1049
|
+
mgr, _ = find_session(args[0])
|
|
1050
|
+
if mgr:
|
|
1051
|
+
cmd_rm(mgr, args[0])
|
|
1052
|
+
else:
|
|
1053
|
+
console.print(f" [red]Session not found:[/] {args[0]}")
|
|
738
1054
|
|
|
739
1055
|
else:
|
|
740
1056
|
console.print(f" [yellow]Unknown command:[/] {cmd}")
|