zwarm 3.4.0__py3-none-any.whl → 3.7.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 +420 -52
- zwarm/cli/main.py +127 -14
- zwarm/cli/pilot.py +52 -4
- zwarm/core/costs.py +55 -183
- zwarm/core/environment.py +55 -1
- zwarm/core/registry.py +329 -0
- zwarm/orchestrator.py +64 -12
- zwarm/sessions/__init__.py +48 -9
- zwarm/sessions/base.py +501 -0
- zwarm/sessions/claude.py +481 -0
- zwarm/sessions/manager.py +85 -458
- zwarm/tools/delegation.py +126 -61
- {zwarm-3.4.0.dist-info → zwarm-3.7.0.dist-info}/METADATA +70 -21
- {zwarm-3.4.0.dist-info → zwarm-3.7.0.dist-info}/RECORD +16 -13
- {zwarm-3.4.0.dist-info → zwarm-3.7.0.dist-info}/WHEEL +0 -0
- {zwarm-3.4.0.dist-info → zwarm-3.7.0.dist-info}/entry_points.txt +0 -0
zwarm/cli/interactive.py
CHANGED
|
@@ -153,22 +153,28 @@ 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)")
|
|
164
164
|
table.add_row("", "")
|
|
165
165
|
table.add_row("[bold]Viewing[/]", "")
|
|
166
166
|
table.add_row("ls", "Dashboard of all sessions")
|
|
167
|
-
table.add_row("? ID / peek ID", "Quick peek (status + latest)")
|
|
168
|
-
table.add_row("show ID", "Full
|
|
169
|
-
table.add_row("traj ID [--full]", "
|
|
167
|
+
table.add_row("? ID / peek ID", "Quick peek (status + latest preview)")
|
|
168
|
+
table.add_row("show ID [-v]", "Full response from agent (-v: verbose)")
|
|
169
|
+
table.add_row("traj ID [--full]", "Trajectory (--full: all data)")
|
|
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)
|
|
@@ -294,8 +439,15 @@ def cmd_peek(manager, session_id: str):
|
|
|
294
439
|
console.print()
|
|
295
440
|
|
|
296
441
|
|
|
297
|
-
def cmd_show(manager, session_id: str):
|
|
298
|
-
"""
|
|
442
|
+
def cmd_show(manager, session_id: str, verbose: bool = False):
|
|
443
|
+
"""
|
|
444
|
+
Full session details with messages.
|
|
445
|
+
|
|
446
|
+
Args:
|
|
447
|
+
manager: Session manager
|
|
448
|
+
session_id: Session to show
|
|
449
|
+
verbose: If True, show everything including full system messages
|
|
450
|
+
"""
|
|
299
451
|
from zwarm.core.costs import estimate_session_cost
|
|
300
452
|
|
|
301
453
|
session = manager.get_session(session_id)
|
|
@@ -306,8 +458,8 @@ def cmd_show(manager, session_id: str):
|
|
|
306
458
|
# Header
|
|
307
459
|
icon = STATUS_ICONS.get(session.status.value, "?")
|
|
308
460
|
console.print(f"\n{icon} [bold cyan]{session.short_id}[/] - {session.status.value}")
|
|
309
|
-
console.print(f" [dim]Task:[/] {session.task}")
|
|
310
|
-
console.print(f" [dim]Turn:[/] {session.turn} | [dim]Runtime:[/] {session.runtime}")
|
|
461
|
+
console.print(f" [dim]Task:[/] {session.task[:100]}..." if len(session.task) > 100 else f" [dim]Task:[/] {session.task}")
|
|
462
|
+
console.print(f" [dim]Model:[/] {session.model} | [dim]Turn:[/] {session.turn} | [dim]Runtime:[/] {session.runtime}")
|
|
311
463
|
|
|
312
464
|
# Token usage with cost estimate
|
|
313
465
|
usage = session.token_usage
|
|
@@ -323,28 +475,40 @@ def cmd_show(manager, session_id: str):
|
|
|
323
475
|
if session.error:
|
|
324
476
|
console.print(f" [red]Error:[/] {session.error}")
|
|
325
477
|
|
|
326
|
-
# Messages
|
|
478
|
+
# Messages - show FULL assistant response (that's the point of show)
|
|
327
479
|
messages = manager.get_messages(session.id)
|
|
328
480
|
if messages:
|
|
329
481
|
console.print(f"\n[bold]Messages ({len(messages)}):[/]")
|
|
330
482
|
for msg in messages:
|
|
331
483
|
role = msg.role
|
|
332
|
-
content = msg.content
|
|
333
|
-
if len(msg.content) > 200:
|
|
334
|
-
content += "..."
|
|
484
|
+
content = msg.content
|
|
335
485
|
|
|
336
486
|
if role == "user":
|
|
487
|
+
# User messages (task) can be truncated unless verbose
|
|
488
|
+
if not verbose and len(content) > 200:
|
|
489
|
+
content = content[:200] + "..."
|
|
337
490
|
console.print(f" [blue]USER:[/] {content}")
|
|
338
491
|
elif role == "assistant":
|
|
492
|
+
# FULL assistant response - this is what users need to see
|
|
339
493
|
console.print(f" [green]ASSISTANT:[/] {content}")
|
|
340
494
|
else:
|
|
341
|
-
|
|
495
|
+
# System/other messages truncated unless verbose
|
|
496
|
+
if not verbose and len(content) > 100:
|
|
497
|
+
content = content[:100] + "..."
|
|
498
|
+
console.print(f" [dim]{role.upper()}:[/] {content}")
|
|
342
499
|
|
|
343
500
|
console.print()
|
|
344
501
|
|
|
345
502
|
|
|
346
503
|
def cmd_traj(manager, session_id: str, full: bool = False):
|
|
347
|
-
"""
|
|
504
|
+
"""
|
|
505
|
+
Show session trajectory.
|
|
506
|
+
|
|
507
|
+
Args:
|
|
508
|
+
manager: Session manager
|
|
509
|
+
session_id: Session to show trajectory for
|
|
510
|
+
full: If True, show full untruncated content for all steps
|
|
511
|
+
"""
|
|
348
512
|
session = manager.get_session(session_id)
|
|
349
513
|
if not session:
|
|
350
514
|
console.print(f" [red]Session not found:[/] {session_id}")
|
|
@@ -352,7 +516,8 @@ def cmd_traj(manager, session_id: str, full: bool = False):
|
|
|
352
516
|
|
|
353
517
|
trajectory = manager.get_trajectory(session_id, full=full)
|
|
354
518
|
|
|
355
|
-
|
|
519
|
+
mode_str = "[bold green](FULL)[/]" if full else "[dim](summary - use --full for complete)[/]"
|
|
520
|
+
console.print(f"\n[bold]Trajectory for {session.short_id}[/] ({len(trajectory)} steps) {mode_str}")
|
|
356
521
|
console.print(f" [dim]Task:[/] {session.task[:60]}...")
|
|
357
522
|
console.print()
|
|
358
523
|
|
|
@@ -363,33 +528,63 @@ def cmd_traj(manager, session_id: str, full: bool = False):
|
|
|
363
528
|
text = step.get("full_text") if full else step.get("summary", "")
|
|
364
529
|
console.print(f" [dim]{i+1}.[/] [magenta]💭 thinking[/]")
|
|
365
530
|
if text:
|
|
366
|
-
|
|
531
|
+
if full:
|
|
532
|
+
# Full mode: show everything, handle multiline
|
|
533
|
+
for line in text.split("\n"):
|
|
534
|
+
console.print(f" {line}")
|
|
535
|
+
else:
|
|
536
|
+
console.print(f" {text[:150]}{'...' if len(text) > 150 else ''}")
|
|
367
537
|
|
|
368
538
|
elif step_type == "command":
|
|
369
539
|
cmd = step.get("command", "")
|
|
370
540
|
output = step.get("output", "")
|
|
371
541
|
exit_code = step.get("exit_code", 0)
|
|
372
542
|
console.print(f" [dim]{i+1}.[/] [yellow]$ {cmd}[/]")
|
|
373
|
-
if output
|
|
374
|
-
|
|
543
|
+
if output:
|
|
544
|
+
if full:
|
|
545
|
+
# Full mode: show complete output
|
|
546
|
+
for line in output.split("\n")[:50]: # Cap at 50 lines for sanity
|
|
547
|
+
console.print(f" {line}")
|
|
548
|
+
if output.count("\n") > 50:
|
|
549
|
+
console.print(f" [dim]... ({output.count(chr(10)) - 50} more lines)[/]")
|
|
550
|
+
else:
|
|
551
|
+
console.print(f" {output[:100]}{'...' if len(output) > 100 else ''}")
|
|
375
552
|
if exit_code and exit_code != 0:
|
|
376
553
|
console.print(f" [red](exit: {exit_code})[/]")
|
|
377
554
|
|
|
378
555
|
elif step_type == "tool_call":
|
|
379
556
|
tool = step.get("tool", "unknown")
|
|
380
|
-
|
|
381
|
-
|
|
557
|
+
if full and step.get("full_args"):
|
|
558
|
+
import json
|
|
559
|
+
args_str = json.dumps(step["full_args"], indent=2)
|
|
560
|
+
console.print(f" [dim]{i+1}.[/] [cyan]🔧 {tool}[/]")
|
|
561
|
+
for line in args_str.split("\n"):
|
|
562
|
+
console.print(f" {line}")
|
|
563
|
+
else:
|
|
564
|
+
args_preview = step.get("args_preview", "")
|
|
565
|
+
console.print(f" [dim]{i+1}.[/] [cyan]🔧 {tool}[/]({args_preview})")
|
|
382
566
|
|
|
383
567
|
elif step_type == "tool_output":
|
|
384
568
|
output = step.get("output", "")
|
|
385
|
-
|
|
386
|
-
|
|
569
|
+
if full:
|
|
570
|
+
# Full mode: show complete output
|
|
571
|
+
for line in output.split("\n")[:30]:
|
|
572
|
+
console.print(f" [dim]→ {line}[/]")
|
|
573
|
+
if output.count("\n") > 30:
|
|
574
|
+
console.print(f" [dim]... ({output.count(chr(10)) - 30} more lines)[/]")
|
|
575
|
+
else:
|
|
576
|
+
console.print(f" [dim]→ {output[:100]}{'...' if len(output) > 100 else ''}[/]")
|
|
387
577
|
|
|
388
578
|
elif step_type == "message":
|
|
389
579
|
text = step.get("full_text") if full else step.get("summary", "")
|
|
390
580
|
console.print(f" [dim]{i+1}.[/] [green]💬 response[/]")
|
|
391
581
|
if text:
|
|
392
|
-
|
|
582
|
+
if full:
|
|
583
|
+
# Full mode: show everything
|
|
584
|
+
for line in text.split("\n"):
|
|
585
|
+
console.print(f" {line}")
|
|
586
|
+
else:
|
|
587
|
+
console.print(f" {text[:150]}{'...' if len(text) > 150 else ''}")
|
|
393
588
|
|
|
394
589
|
console.print()
|
|
395
590
|
|
|
@@ -409,6 +604,7 @@ def cmd_watch(manager, session_id: str):
|
|
|
409
604
|
|
|
410
605
|
console.print(f"\n[bold]Watching {session.short_id}[/]...")
|
|
411
606
|
console.print(f" [dim]Task:[/] {session.task[:60]}...")
|
|
607
|
+
console.print(f" [dim]Model:[/] {session.model}")
|
|
412
608
|
console.print(f" [dim]Press Ctrl+C to stop watching[/]\n")
|
|
413
609
|
|
|
414
610
|
seen_steps = 0
|
|
@@ -471,20 +667,52 @@ def cmd_watch(manager, session_id: str):
|
|
|
471
667
|
console.print()
|
|
472
668
|
|
|
473
669
|
|
|
474
|
-
def cmd_spawn(
|
|
475
|
-
"""
|
|
670
|
+
def cmd_spawn(managers: dict, task: str, working_dir: Path, model: str, adapter: str | None = None):
|
|
671
|
+
"""
|
|
672
|
+
Spawn a new session.
|
|
673
|
+
|
|
674
|
+
Args:
|
|
675
|
+
managers: Dict of adapter name -> session manager
|
|
676
|
+
task: Task description
|
|
677
|
+
working_dir: Working directory
|
|
678
|
+
model: Model name or alias
|
|
679
|
+
adapter: Adapter override (auto-detected from model if None)
|
|
680
|
+
"""
|
|
681
|
+
from zwarm.core.registry import get_adapter_for_model, get_default_model, resolve_model
|
|
682
|
+
|
|
683
|
+
# Auto-detect adapter from model if not specified
|
|
684
|
+
if adapter is None:
|
|
685
|
+
detected = get_adapter_for_model(model)
|
|
686
|
+
if detected:
|
|
687
|
+
adapter = detected
|
|
688
|
+
else:
|
|
689
|
+
# Default to codex if model not recognized
|
|
690
|
+
adapter = "codex"
|
|
691
|
+
|
|
692
|
+
# Resolve model alias to canonical name if needed
|
|
693
|
+
model_info = resolve_model(model)
|
|
694
|
+
effective_model = model_info.canonical if model_info else model
|
|
695
|
+
|
|
696
|
+
# Get the right manager
|
|
697
|
+
if adapter not in managers:
|
|
698
|
+
console.print(f" [red]Unknown adapter:[/] {adapter}")
|
|
699
|
+
console.print(f" [dim]Available: {', '.join(managers.keys())}[/]")
|
|
700
|
+
return
|
|
701
|
+
|
|
702
|
+
manager = managers[adapter]
|
|
703
|
+
|
|
476
704
|
console.print(f"\n[dim]Spawning session...[/]")
|
|
705
|
+
console.print(f" [dim]Adapter:[/] {adapter}")
|
|
706
|
+
console.print(f" [dim]Model:[/] {effective_model}")
|
|
477
707
|
console.print(f" [dim]Dir:[/] {working_dir}")
|
|
478
|
-
console.print(f" [dim]Model:[/] {model}")
|
|
479
708
|
|
|
480
709
|
try:
|
|
481
710
|
session = manager.start_session(
|
|
482
711
|
task=task,
|
|
483
712
|
working_dir=working_dir,
|
|
484
|
-
model=
|
|
713
|
+
model=effective_model,
|
|
485
714
|
sandbox="workspace-write",
|
|
486
715
|
source="user",
|
|
487
|
-
adapter="codex",
|
|
488
716
|
)
|
|
489
717
|
|
|
490
718
|
console.print(f"\n[green]✓[/] Session: [cyan]{session.short_id}[/]")
|
|
@@ -613,13 +841,34 @@ def run_interactive(
|
|
|
613
841
|
working_dir: Default working directory for sessions
|
|
614
842
|
model: Default model for sessions
|
|
615
843
|
"""
|
|
616
|
-
from zwarm.sessions import
|
|
844
|
+
from zwarm.sessions import get_session_manager
|
|
845
|
+
from zwarm.core.registry import get_adapter_for_model, list_adapters
|
|
617
846
|
|
|
618
|
-
|
|
847
|
+
# Initialize managers for all adapters
|
|
848
|
+
state_dir = working_dir / ".zwarm"
|
|
849
|
+
managers = {}
|
|
850
|
+
for adapter in list_adapters():
|
|
851
|
+
try:
|
|
852
|
+
managers[adapter] = get_session_manager(adapter, str(state_dir))
|
|
853
|
+
except Exception:
|
|
854
|
+
pass # Adapter not available
|
|
855
|
+
|
|
856
|
+
if not managers:
|
|
857
|
+
console.print("[red]No adapters available. Run 'zwarm init' first.[/]")
|
|
858
|
+
return
|
|
859
|
+
|
|
860
|
+
# Primary manager for listing (aggregates across all adapters)
|
|
861
|
+
primary_adapter = get_adapter_for_model(model) or "codex"
|
|
862
|
+
if primary_adapter not in managers:
|
|
863
|
+
primary_adapter = list(managers.keys())[0]
|
|
619
864
|
|
|
620
865
|
# Setup prompt with autocomplete
|
|
621
866
|
def get_sessions():
|
|
622
|
-
|
|
867
|
+
# Aggregate sessions from all managers
|
|
868
|
+
all_sessions = []
|
|
869
|
+
for mgr in managers.values():
|
|
870
|
+
all_sessions.extend(mgr.list_sessions())
|
|
871
|
+
return all_sessions
|
|
623
872
|
|
|
624
873
|
completer = SessionCompleter(get_sessions)
|
|
625
874
|
style = Style.from_dict({
|
|
@@ -637,7 +886,8 @@ def run_interactive(
|
|
|
637
886
|
console.print("\n[bold cyan]zwarm interactive[/] - Session Manager\n")
|
|
638
887
|
console.print(f" [dim]Dir:[/] {working_dir.absolute()}")
|
|
639
888
|
console.print(f" [dim]Model:[/] {model}")
|
|
640
|
-
console.print(f"
|
|
889
|
+
console.print(f" [dim]Adapters:[/] {', '.join(managers.keys())}")
|
|
890
|
+
console.print(f"\n Type [cyan]help[/] for commands, [cyan]models[/] to see available models.")
|
|
641
891
|
console.print(f" [dim]Tab to autocomplete session IDs[/]\n")
|
|
642
892
|
|
|
643
893
|
# REPL
|
|
@@ -647,6 +897,29 @@ def run_interactive(
|
|
|
647
897
|
if not raw:
|
|
648
898
|
continue
|
|
649
899
|
|
|
900
|
+
# Bang command: !cmd runs shell command
|
|
901
|
+
if raw.startswith("!"):
|
|
902
|
+
import subprocess
|
|
903
|
+
shell_cmd = raw[1:].strip()
|
|
904
|
+
if shell_cmd:
|
|
905
|
+
try:
|
|
906
|
+
result = subprocess.run(
|
|
907
|
+
shell_cmd,
|
|
908
|
+
shell=True,
|
|
909
|
+
cwd=working_dir,
|
|
910
|
+
capture_output=True,
|
|
911
|
+
text=True,
|
|
912
|
+
)
|
|
913
|
+
if result.stdout:
|
|
914
|
+
console.print(result.stdout.rstrip())
|
|
915
|
+
if result.stderr:
|
|
916
|
+
console.print(f"[red]{result.stderr.rstrip()}[/]")
|
|
917
|
+
if result.returncode != 0:
|
|
918
|
+
console.print(f"[dim](exit code: {result.returncode})[/]")
|
|
919
|
+
except Exception as e:
|
|
920
|
+
console.print(f"[red]Error:[/] {e}")
|
|
921
|
+
continue
|
|
922
|
+
|
|
650
923
|
try:
|
|
651
924
|
parts = shlex.split(raw)
|
|
652
925
|
except ValueError:
|
|
@@ -655,6 +928,26 @@ def run_interactive(
|
|
|
655
928
|
cmd = parts[0].lower()
|
|
656
929
|
args = parts[1:]
|
|
657
930
|
|
|
931
|
+
# Helper to find session and return the correct manager for its adapter
|
|
932
|
+
def find_session(sid: str):
|
|
933
|
+
# First, find the session (any manager can load it)
|
|
934
|
+
session = None
|
|
935
|
+
for mgr in managers.values():
|
|
936
|
+
session = mgr.get_session(sid)
|
|
937
|
+
if session:
|
|
938
|
+
break
|
|
939
|
+
|
|
940
|
+
if not session:
|
|
941
|
+
return None, None
|
|
942
|
+
|
|
943
|
+
# Return the manager that matches the session's adapter
|
|
944
|
+
adapter = getattr(session, "adapter", "codex")
|
|
945
|
+
if adapter in managers:
|
|
946
|
+
return managers[adapter], session
|
|
947
|
+
else:
|
|
948
|
+
# Fallback to whichever manager found it
|
|
949
|
+
return mgr, session
|
|
950
|
+
|
|
658
951
|
# Dispatch
|
|
659
952
|
if cmd in ("q", "quit", "exit"):
|
|
660
953
|
console.print("\n[dim]Goodbye![/]\n")
|
|
@@ -663,20 +956,45 @@ def run_interactive(
|
|
|
663
956
|
elif cmd in ("h", "help"):
|
|
664
957
|
cmd_help()
|
|
665
958
|
|
|
959
|
+
elif cmd == "models":
|
|
960
|
+
cmd_models()
|
|
961
|
+
|
|
666
962
|
elif cmd in ("ls", "list"):
|
|
667
|
-
|
|
963
|
+
# Aggregate sessions from all managers
|
|
964
|
+
from zwarm.sessions import SessionStatus
|
|
965
|
+
from zwarm.core.costs import estimate_session_cost, format_cost
|
|
966
|
+
|
|
967
|
+
all_sessions = []
|
|
968
|
+
for mgr in managers.values():
|
|
969
|
+
all_sessions.extend(mgr.list_sessions())
|
|
970
|
+
|
|
971
|
+
if not all_sessions:
|
|
972
|
+
console.print(" [dim]No sessions. Use 'spawn \"task\"' to start one.[/]")
|
|
973
|
+
else:
|
|
974
|
+
# Use first manager's cmd_ls logic but with aggregated sessions
|
|
975
|
+
cmd_ls_multi(all_sessions, managers)
|
|
668
976
|
|
|
669
977
|
elif cmd in ("?", "peek"):
|
|
670
978
|
if not args:
|
|
671
979
|
console.print(" [red]Usage:[/] peek ID")
|
|
672
980
|
else:
|
|
673
|
-
|
|
981
|
+
mgr, _ = find_session(args[0])
|
|
982
|
+
if mgr:
|
|
983
|
+
cmd_peek(mgr, args[0])
|
|
984
|
+
else:
|
|
985
|
+
console.print(f" [red]Session not found:[/] {args[0]}")
|
|
674
986
|
|
|
675
987
|
elif cmd == "show":
|
|
676
988
|
if not args:
|
|
677
|
-
console.print(" [red]Usage:[/] show ID")
|
|
989
|
+
console.print(" [red]Usage:[/] show ID [-v]")
|
|
678
990
|
else:
|
|
679
|
-
|
|
991
|
+
verbose = "-v" in args or "--verbose" in args
|
|
992
|
+
sid = [a for a in args if not a.startswith("-")][0]
|
|
993
|
+
mgr, _ = find_session(sid)
|
|
994
|
+
if mgr:
|
|
995
|
+
cmd_show(mgr, sid, verbose=verbose)
|
|
996
|
+
else:
|
|
997
|
+
console.print(f" [red]Session not found:[/] {sid}")
|
|
680
998
|
|
|
681
999
|
elif cmd in ("traj", "trajectory"):
|
|
682
1000
|
if not args:
|
|
@@ -684,22 +1002,31 @@ def run_interactive(
|
|
|
684
1002
|
else:
|
|
685
1003
|
full = "--full" in args
|
|
686
1004
|
sid = [a for a in args if not a.startswith("-")][0]
|
|
687
|
-
|
|
1005
|
+
mgr, _ = find_session(sid)
|
|
1006
|
+
if mgr:
|
|
1007
|
+
cmd_traj(mgr, sid, full=full)
|
|
1008
|
+
else:
|
|
1009
|
+
console.print(f" [red]Session not found:[/] {sid}")
|
|
688
1010
|
|
|
689
1011
|
elif cmd == "watch":
|
|
690
1012
|
if not args:
|
|
691
1013
|
console.print(" [red]Usage:[/] watch ID")
|
|
692
1014
|
else:
|
|
693
|
-
|
|
1015
|
+
mgr, _ = find_session(args[0])
|
|
1016
|
+
if mgr:
|
|
1017
|
+
cmd_watch(mgr, args[0])
|
|
1018
|
+
else:
|
|
1019
|
+
console.print(f" [red]Session not found:[/] {args[0]}")
|
|
694
1020
|
|
|
695
1021
|
elif cmd == "spawn":
|
|
696
1022
|
if not args:
|
|
697
|
-
console.print(" [red]Usage:[/] spawn \"task\" [--
|
|
1023
|
+
console.print(" [red]Usage:[/] spawn \"task\" [--model M] [--adapter A]")
|
|
698
1024
|
else:
|
|
699
1025
|
# Parse spawn args
|
|
700
1026
|
task_parts = []
|
|
701
1027
|
spawn_dir = working_dir
|
|
702
1028
|
spawn_model = model
|
|
1029
|
+
spawn_adapter = None
|
|
703
1030
|
i = 0
|
|
704
1031
|
while i < len(args):
|
|
705
1032
|
if args[i] in ("--dir", "-d") and i + 1 < len(args):
|
|
@@ -708,13 +1035,16 @@ def run_interactive(
|
|
|
708
1035
|
elif args[i] in ("--model", "-m") and i + 1 < len(args):
|
|
709
1036
|
spawn_model = args[i + 1]
|
|
710
1037
|
i += 2
|
|
1038
|
+
elif args[i] in ("--adapter", "-a") and i + 1 < len(args):
|
|
1039
|
+
spawn_adapter = args[i + 1]
|
|
1040
|
+
i += 2
|
|
711
1041
|
else:
|
|
712
1042
|
task_parts.append(args[i])
|
|
713
1043
|
i += 1
|
|
714
1044
|
|
|
715
1045
|
task = " ".join(task_parts)
|
|
716
1046
|
if task:
|
|
717
|
-
cmd_spawn(
|
|
1047
|
+
cmd_spawn(managers, task, spawn_dir, spawn_model, spawn_adapter)
|
|
718
1048
|
else:
|
|
719
1049
|
console.print(" [red]Task required[/]")
|
|
720
1050
|
|
|
@@ -722,19 +1052,57 @@ def run_interactive(
|
|
|
722
1052
|
if len(args) < 2:
|
|
723
1053
|
console.print(" [red]Usage:[/] c ID \"message\"")
|
|
724
1054
|
else:
|
|
725
|
-
|
|
1055
|
+
mgr, _ = find_session(args[0])
|
|
1056
|
+
if mgr:
|
|
1057
|
+
cmd_continue(mgr, args[0], " ".join(args[1:]))
|
|
1058
|
+
else:
|
|
1059
|
+
console.print(f" [red]Session not found:[/] {args[0]}")
|
|
726
1060
|
|
|
727
1061
|
elif cmd == "kill":
|
|
728
1062
|
if not args:
|
|
729
1063
|
console.print(" [red]Usage:[/] kill ID | all")
|
|
1064
|
+
elif args[0].lower() == "all":
|
|
1065
|
+
# Kill all running across all managers
|
|
1066
|
+
killed = 0
|
|
1067
|
+
for mgr in managers.values():
|
|
1068
|
+
from zwarm.sessions import SessionStatus
|
|
1069
|
+
for s in mgr.list_sessions(status=SessionStatus.RUNNING):
|
|
1070
|
+
if mgr.kill_session(s.id):
|
|
1071
|
+
killed += 1
|
|
1072
|
+
console.print(f" [green]✓[/] Killed {s.short_id}")
|
|
1073
|
+
if killed:
|
|
1074
|
+
console.print(f"\n[green]Killed {killed} session(s)[/]")
|
|
1075
|
+
else:
|
|
1076
|
+
console.print(" [dim]No running sessions[/]")
|
|
730
1077
|
else:
|
|
731
|
-
|
|
1078
|
+
mgr, _ = find_session(args[0])
|
|
1079
|
+
if mgr:
|
|
1080
|
+
cmd_kill(mgr, args[0])
|
|
1081
|
+
else:
|
|
1082
|
+
console.print(f" [red]Session not found:[/] {args[0]}")
|
|
732
1083
|
|
|
733
1084
|
elif cmd in ("rm", "delete"):
|
|
734
1085
|
if not args:
|
|
735
1086
|
console.print(" [red]Usage:[/] rm ID | all")
|
|
1087
|
+
elif args[0].lower() == "all":
|
|
1088
|
+
# Delete all non-running across all managers
|
|
1089
|
+
deleted = 0
|
|
1090
|
+
for mgr in managers.values():
|
|
1091
|
+
from zwarm.sessions import SessionStatus
|
|
1092
|
+
for s in mgr.list_sessions():
|
|
1093
|
+
if s.status != SessionStatus.RUNNING:
|
|
1094
|
+
if mgr.delete_session(s.id):
|
|
1095
|
+
deleted += 1
|
|
1096
|
+
if deleted:
|
|
1097
|
+
console.print(f"[green]✓[/] Deleted {deleted} session(s)")
|
|
1098
|
+
else:
|
|
1099
|
+
console.print(" [dim]Nothing to delete[/]")
|
|
736
1100
|
else:
|
|
737
|
-
|
|
1101
|
+
mgr, _ = find_session(args[0])
|
|
1102
|
+
if mgr:
|
|
1103
|
+
cmd_rm(mgr, args[0])
|
|
1104
|
+
else:
|
|
1105
|
+
console.print(f" [red]Session not found:[/] {args[0]}")
|
|
738
1106
|
|
|
739
1107
|
else:
|
|
740
1108
|
console.print(f" [yellow]Unknown command:[/] {cmd}")
|