zwarm 2.3.5__py3-none-any.whl → 3.2.1__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 +314 -854
- zwarm/cli/pilot.py +1142 -0
- zwarm/core/__init__.py +20 -0
- zwarm/core/checkpoints.py +216 -0
- zwarm/core/costs.py +199 -0
- zwarm/prompts/__init__.py +3 -0
- zwarm/prompts/orchestrator.py +36 -29
- zwarm/prompts/pilot.py +147 -0
- zwarm/tools/delegation.py +73 -172
- zwarm-3.2.1.dist-info/METADATA +393 -0
- {zwarm-2.3.5.dist-info → zwarm-3.2.1.dist-info}/RECORD +14 -9
- zwarm-2.3.5.dist-info/METADATA +0 -309
- {zwarm-2.3.5.dist-info → zwarm-3.2.1.dist-info}/WHEEL +0 -0
- {zwarm-2.3.5.dist-info → zwarm-3.2.1.dist-info}/entry_points.txt +0 -0
zwarm/cli/main.py
CHANGED
|
@@ -79,6 +79,7 @@ app = typer.Typer(
|
|
|
79
79
|
[cyan]init[/] Initialize zwarm (creates .zwarm/ with config)
|
|
80
80
|
[cyan]reset[/] Reset state and optionally config files
|
|
81
81
|
[cyan]orchestrate[/] Start orchestrator to delegate tasks to executors
|
|
82
|
+
[cyan]pilot[/] Conversational orchestrator REPL (interactive)
|
|
82
83
|
[cyan]exec[/] Run a single executor directly (for testing)
|
|
83
84
|
[cyan]status[/] Show current state (sessions, tasks, events)
|
|
84
85
|
[cyan]history[/] Show event history log
|
|
@@ -274,6 +275,112 @@ def orchestrate(
|
|
|
274
275
|
sys.exit(1)
|
|
275
276
|
|
|
276
277
|
|
|
278
|
+
class PilotLM(str, Enum):
|
|
279
|
+
"""LM options for pilot mode."""
|
|
280
|
+
gpt5_mini = "gpt5-mini" # GPT5MiniTester - fast, cheap, good for testing
|
|
281
|
+
gpt5 = "gpt5" # GPT5Large - standard
|
|
282
|
+
gpt5_verbose = "gpt5-verbose" # GPT5LargeVerbose - with extended thinking
|
|
283
|
+
|
|
284
|
+
|
|
285
|
+
@app.command()
|
|
286
|
+
def pilot(
|
|
287
|
+
task: Annotated[Optional[str], typer.Option("--task", "-t", help="Initial task (optional)")] = None,
|
|
288
|
+
task_file: Annotated[Optional[Path], typer.Option("--task-file", "-f", help="Read task from file")] = None,
|
|
289
|
+
config: Annotated[Optional[Path], typer.Option("--config", "-c", help="Path to config YAML")] = None,
|
|
290
|
+
overrides: Annotated[Optional[list[str]], typer.Option("--set", help="Override config (key=value)")] = None,
|
|
291
|
+
working_dir: Annotated[Path, typer.Option("--working-dir", "-w", help="Working directory")] = Path("."),
|
|
292
|
+
instance: Annotated[Optional[str], typer.Option("--instance", "-i", help="Instance ID (for isolation)")] = None,
|
|
293
|
+
instance_name: Annotated[Optional[str], typer.Option("--name", "-n", help="Human-readable instance name")] = None,
|
|
294
|
+
model: Annotated[PilotLM, typer.Option("--model", "-m", help="LM to use")] = PilotLM.gpt5_verbose,
|
|
295
|
+
):
|
|
296
|
+
"""
|
|
297
|
+
Interactive conversational orchestrator REPL.
|
|
298
|
+
|
|
299
|
+
Like 'orchestrate' but conversational: give instructions, watch the
|
|
300
|
+
orchestrator work, course-correct in real-time, time-travel to checkpoints.
|
|
301
|
+
|
|
302
|
+
[bold]Features:[/]
|
|
303
|
+
- Streaming display of orchestrator thinking and tool calls
|
|
304
|
+
- Turn-by-turn execution with checkpoints
|
|
305
|
+
- Time travel (:goto T1) to return to previous states
|
|
306
|
+
- Session visibility (:sessions) and state inspection (:state)
|
|
307
|
+
|
|
308
|
+
[bold]Commands:[/]
|
|
309
|
+
:help Show help
|
|
310
|
+
:history [N|all] Show turn checkpoints
|
|
311
|
+
:goto <turn|root> Time travel (e.g., :goto T1)
|
|
312
|
+
:state Show orchestrator state
|
|
313
|
+
:sessions Show active executor sessions
|
|
314
|
+
:reasoning on|off Toggle reasoning display
|
|
315
|
+
:quit Exit
|
|
316
|
+
|
|
317
|
+
[bold]LM Options:[/]
|
|
318
|
+
gpt5-mini GPT5MiniTester - fast/cheap, good for testing
|
|
319
|
+
gpt5 GPT5Large - standard model
|
|
320
|
+
gpt5-verbose GPT5LargeVerbose - with extended thinking (default)
|
|
321
|
+
|
|
322
|
+
[bold]Examples:[/]
|
|
323
|
+
[dim]# Start fresh, give instructions interactively[/]
|
|
324
|
+
$ zwarm pilot
|
|
325
|
+
|
|
326
|
+
[dim]# Start with an initial task[/]
|
|
327
|
+
$ zwarm pilot --task "Build user authentication"
|
|
328
|
+
|
|
329
|
+
[dim]# Use faster model for testing[/]
|
|
330
|
+
$ zwarm pilot --model gpt5-mini
|
|
331
|
+
|
|
332
|
+
[dim]# Named instance[/]
|
|
333
|
+
$ zwarm pilot --name my-feature
|
|
334
|
+
"""
|
|
335
|
+
from zwarm.cli.pilot import run_pilot, build_pilot_orchestrator
|
|
336
|
+
|
|
337
|
+
# Resolve task (optional for pilot)
|
|
338
|
+
resolved_task = _resolve_task(task, task_file)
|
|
339
|
+
|
|
340
|
+
console.print(f"[bold]Starting pilot session...[/]")
|
|
341
|
+
console.print(f" Working dir: {working_dir.absolute()}")
|
|
342
|
+
console.print(f" Model: {model.value}")
|
|
343
|
+
if resolved_task:
|
|
344
|
+
console.print(f" Initial task: {resolved_task[:60]}...")
|
|
345
|
+
if instance:
|
|
346
|
+
console.print(f" Instance: {instance}" + (f" ({instance_name})" if instance_name else ""))
|
|
347
|
+
console.print()
|
|
348
|
+
|
|
349
|
+
orchestrator = None
|
|
350
|
+
try:
|
|
351
|
+
orchestrator = build_pilot_orchestrator(
|
|
352
|
+
config_path=config,
|
|
353
|
+
working_dir=working_dir.absolute(),
|
|
354
|
+
overrides=list(overrides or []),
|
|
355
|
+
instance_id=instance,
|
|
356
|
+
instance_name=instance_name,
|
|
357
|
+
lm_choice=model.value,
|
|
358
|
+
)
|
|
359
|
+
|
|
360
|
+
# Show instance ID if auto-generated
|
|
361
|
+
if orchestrator.instance_id and not instance:
|
|
362
|
+
console.print(f" [dim]Instance: {orchestrator.instance_id[:8]}[/]")
|
|
363
|
+
|
|
364
|
+
# Run the pilot REPL
|
|
365
|
+
run_pilot(orchestrator, initial_task=resolved_task)
|
|
366
|
+
|
|
367
|
+
# Save state on exit
|
|
368
|
+
orchestrator.save_state()
|
|
369
|
+
console.print("\n[dim]State saved.[/]")
|
|
370
|
+
|
|
371
|
+
except KeyboardInterrupt:
|
|
372
|
+
console.print("\n\n[yellow]Interrupted.[/]")
|
|
373
|
+
if orchestrator:
|
|
374
|
+
orchestrator.save_state()
|
|
375
|
+
console.print("[dim]State saved.[/]")
|
|
376
|
+
sys.exit(1)
|
|
377
|
+
except Exception as e:
|
|
378
|
+
console.print(f"\n[red]Error:[/] {e}")
|
|
379
|
+
import traceback
|
|
380
|
+
traceback.print_exc()
|
|
381
|
+
sys.exit(1)
|
|
382
|
+
|
|
383
|
+
|
|
277
384
|
@app.command()
|
|
278
385
|
def exec(
|
|
279
386
|
task: Annotated[str, typer.Option("--task", "-t", help="Task to execute")],
|
|
@@ -633,8 +740,14 @@ def init(
|
|
|
633
740
|
[bold]Creates:[/]
|
|
634
741
|
[cyan].zwarm/[/] State directory for sessions and events
|
|
635
742
|
[cyan].zwarm/config.toml[/] Runtime settings (weave, adapter, watchers)
|
|
743
|
+
[cyan].zwarm/codex.toml[/] Codex CLI settings (model, reasoning effort)
|
|
636
744
|
[cyan]zwarm.yaml[/] Project config (optional, with --with-project)
|
|
637
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
|
+
|
|
638
751
|
[bold]Examples:[/]
|
|
639
752
|
[dim]# Interactive setup[/]
|
|
640
753
|
$ zwarm init
|
|
@@ -681,6 +794,9 @@ def init(
|
|
|
681
794
|
create_project_config = with_project
|
|
682
795
|
project_description = ""
|
|
683
796
|
project_context = ""
|
|
797
|
+
# Codex settings
|
|
798
|
+
codex_model = "gpt-5.1-codex-mini"
|
|
799
|
+
codex_reasoning = "high"
|
|
684
800
|
|
|
685
801
|
if not non_interactive:
|
|
686
802
|
console.print("[bold]Configuration[/]\n")
|
|
@@ -699,6 +815,42 @@ def init(
|
|
|
699
815
|
type=str,
|
|
700
816
|
)
|
|
701
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
|
+
|
|
702
854
|
# Watchers
|
|
703
855
|
console.print("\n [bold]Watchers[/] (trajectory aligners)")
|
|
704
856
|
available_watchers = ["progress", "budget", "delegation", "delegation_reminder", "scope", "pattern", "quality"]
|
|
@@ -750,10 +902,20 @@ def init(
|
|
|
750
902
|
|
|
751
903
|
# Create codex.toml for isolated codex configuration
|
|
752
904
|
codex_toml_path = state_dir / "codex.toml"
|
|
753
|
-
|
|
754
|
-
|
|
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)
|
|
755
917
|
codex_toml_path.write_text(codex_content)
|
|
756
|
-
console.print(f" [green]✓[/] Created .zwarm/codex.toml
|
|
918
|
+
console.print(f" [green]✓[/] Created .zwarm/codex.toml")
|
|
757
919
|
|
|
758
920
|
# Create zwarm.yaml
|
|
759
921
|
if create_project_config:
|
|
@@ -773,11 +935,23 @@ def init(
|
|
|
773
935
|
|
|
774
936
|
# Summary
|
|
775
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
|
+
|
|
776
948
|
console.print("[bold]Next steps:[/]")
|
|
777
949
|
console.print(" [dim]# Run the orchestrator[/]")
|
|
778
950
|
console.print(" $ zwarm orchestrate --task \"Your task here\"\n")
|
|
779
951
|
console.print(" [dim]# Or test an executor directly[/]")
|
|
780
952
|
console.print(" $ zwarm exec --task \"What is 2+2?\"\n")
|
|
953
|
+
console.print(" [dim]# Interactive session management[/]")
|
|
954
|
+
console.print(" $ zwarm interactive\n")
|
|
781
955
|
|
|
782
956
|
|
|
783
957
|
def _generate_config_toml(
|
|
@@ -1133,897 +1307,183 @@ def clean(
|
|
|
1133
1307
|
def interactive(
|
|
1134
1308
|
default_dir: Annotated[Path, typer.Option("--dir", "-d", help="Default working directory")] = Path("."),
|
|
1135
1309
|
model: Annotated[Optional[str], typer.Option("--model", help="Default model override")] = None,
|
|
1136
|
-
adapter: Annotated[str, typer.Option("--adapter", "-a", help="Executor adapter")] = "codex_mcp",
|
|
1137
|
-
state_dir: Annotated[Path, typer.Option("--state-dir", help="State directory for persistence")] = Path(".zwarm"),
|
|
1138
1310
|
):
|
|
1139
1311
|
"""
|
|
1140
|
-
|
|
1312
|
+
Interactive REPL for session management.
|
|
1141
1313
|
|
|
1142
|
-
|
|
1143
|
-
|
|
1144
|
-
|
|
1145
|
-
This uses the SAME code path as `zwarm orchestrate` - the adapter layer.
|
|
1146
|
-
Interactive is the human-in-the-loop version, orchestrate is the LLM version.
|
|
1314
|
+
A clean, autocomplete-enabled interface for managing codex sessions.
|
|
1315
|
+
Tab-complete session IDs, watch live output, and manage the session lifecycle.
|
|
1147
1316
|
|
|
1148
1317
|
[bold]Commands:[/]
|
|
1149
|
-
[cyan]spawn[/] "task"
|
|
1150
|
-
[cyan]
|
|
1151
|
-
[cyan]
|
|
1152
|
-
[cyan]
|
|
1153
|
-
[cyan]
|
|
1154
|
-
[cyan]
|
|
1155
|
-
[cyan]c[/]
|
|
1156
|
-
[cyan]kill[/] ID
|
|
1157
|
-
[cyan]rm[/] ID
|
|
1158
|
-
[cyan]killall[/] Stop all running sessions
|
|
1159
|
-
[cyan]clean[/] Remove old sessions (>7 days)
|
|
1160
|
-
[cyan]q[/] / [cyan]quit[/] Exit
|
|
1161
|
-
|
|
1162
|
-
[bold]Spawn Options:[/]
|
|
1163
|
-
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)
|
|
1164
1327
|
|
|
1165
1328
|
[bold]Examples:[/]
|
|
1166
1329
|
$ zwarm interactive
|
|
1167
|
-
> spawn "Build auth module"
|
|
1168
|
-
>
|
|
1169
|
-
> c abc123 "Now add
|
|
1330
|
+
> spawn "Build auth module"
|
|
1331
|
+
> watch abc123
|
|
1332
|
+
> c abc123 "Now add tests"
|
|
1170
1333
|
> ls
|
|
1171
|
-
> ? abc123
|
|
1172
1334
|
"""
|
|
1173
|
-
from zwarm.
|
|
1174
|
-
from zwarm.core.models import ConversationSession, SessionStatus, SessionMode
|
|
1175
|
-
import argparse
|
|
1335
|
+
from zwarm.cli.interactive import run_interactive
|
|
1176
1336
|
|
|
1177
|
-
# Initialize adapter (same as orchestrator uses)
|
|
1178
1337
|
default_model = model or "gpt-5.1-codex-mini"
|
|
1179
|
-
|
|
1180
|
-
|
|
1181
|
-
# Config path for isolated codex configuration
|
|
1182
|
-
codex_config_path = state_dir / "codex.toml"
|
|
1183
|
-
if not codex_config_path.exists():
|
|
1184
|
-
# Try relative to working dir
|
|
1185
|
-
codex_config_path = default_dir / ".zwarm" / "codex.toml"
|
|
1186
|
-
if not codex_config_path.exists():
|
|
1187
|
-
codex_config_path = None # Fall back to overrides
|
|
1188
|
-
|
|
1189
|
-
# Session tracking - same pattern as orchestrator
|
|
1190
|
-
sessions: dict[str, ConversationSession] = {}
|
|
1191
|
-
adapters_cache: dict[str, Any] = {}
|
|
1192
|
-
|
|
1193
|
-
def get_or_create_adapter(adapter_name: str, session_model: str | None = None) -> Any:
|
|
1194
|
-
"""Get or create an adapter instance with isolated config."""
|
|
1195
|
-
cache_key = f"{adapter_name}:{session_model or default_model}"
|
|
1196
|
-
if cache_key not in adapters_cache:
|
|
1197
|
-
adapters_cache[cache_key] = get_adapter(
|
|
1198
|
-
adapter_name,
|
|
1199
|
-
model=session_model or default_model,
|
|
1200
|
-
config_path=codex_config_path,
|
|
1201
|
-
)
|
|
1202
|
-
return adapters_cache[cache_key]
|
|
1203
|
-
|
|
1204
|
-
def find_session(query: str) -> tuple[ConversationSession | None, str | None]:
|
|
1205
|
-
"""Find session by full or partial ID."""
|
|
1206
|
-
if query in sessions:
|
|
1207
|
-
return sessions[query], query
|
|
1208
|
-
for sid, session in sessions.items():
|
|
1209
|
-
if sid.startswith(query):
|
|
1210
|
-
return session, sid
|
|
1211
|
-
return None, None
|
|
1212
|
-
|
|
1213
|
-
console.print("\n[bold cyan]zwarm interactive[/] - Multi-Agent Command Center\n")
|
|
1214
|
-
console.print(f" Working dir: {default_dir.absolute()}")
|
|
1215
|
-
console.print(f" Model: [cyan]{default_model}[/]")
|
|
1216
|
-
console.print(f" Adapter: [cyan]{default_adapter}[/]")
|
|
1217
|
-
if codex_config_path:
|
|
1218
|
-
console.print(f" Config: [green]{codex_config_path}[/] (isolated)")
|
|
1219
|
-
else:
|
|
1220
|
-
console.print(f" Config: [yellow]using fallback overrides[/] (run 'zwarm init' for isolation)")
|
|
1221
|
-
console.print(f" Available: {', '.join(list_adapters())}")
|
|
1222
|
-
console.print("\n Type [cyan]help[/] for commands, [cyan]quit[/] to exit.\n")
|
|
1223
|
-
|
|
1224
|
-
def show_help():
|
|
1225
|
-
help_table = Table(show_header=False, box=None, padding=(0, 2))
|
|
1226
|
-
help_table.add_column("Command", style="cyan", width=35)
|
|
1227
|
-
help_table.add_column("Description")
|
|
1228
|
-
help_table.add_row('spawn "task" [options]', "Start session (waits for completion)")
|
|
1229
|
-
help_table.add_row(" --dir PATH", "Working directory")
|
|
1230
|
-
help_table.add_row(" --model NAME", "Model override")
|
|
1231
|
-
help_table.add_row(" --async", "Background mode (don't wait)")
|
|
1232
|
-
help_table.add_row("", "")
|
|
1233
|
-
help_table.add_row("ls / list", "Dashboard of all sessions")
|
|
1234
|
-
help_table.add_row("? ID / peek ID", "Quick peek (status + latest message)")
|
|
1235
|
-
help_table.add_row("show ID", "Full session details & messages")
|
|
1236
|
-
help_table.add_row("traj ID [--full]", "Show trajectory (all steps taken)")
|
|
1237
|
-
help_table.add_row('c ID "msg"', "Continue conversation (wait for response)")
|
|
1238
|
-
help_table.add_row('ca ID "msg"', "Continue async (fire-and-forget)")
|
|
1239
|
-
help_table.add_row("check ID", "Check session status")
|
|
1240
|
-
help_table.add_row("kill ID", "Stop a running session")
|
|
1241
|
-
help_table.add_row("rm ID", "Delete session entirely")
|
|
1242
|
-
help_table.add_row("killall", "Stop all running sessions")
|
|
1243
|
-
help_table.add_row("clean", "Remove old completed sessions")
|
|
1244
|
-
help_table.add_row("q / quit", "Exit")
|
|
1245
|
-
console.print(help_table)
|
|
1246
|
-
|
|
1247
|
-
def show_sessions():
|
|
1248
|
-
"""List all sessions from CodexSessionManager (same as orchestrator)."""
|
|
1249
|
-
from zwarm.sessions import CodexSessionManager, SessionStatus as SessStatus
|
|
1250
|
-
from datetime import datetime
|
|
1251
|
-
|
|
1252
|
-
manager = CodexSessionManager(default_dir / ".zwarm")
|
|
1253
|
-
all_sessions = manager.list_sessions()
|
|
1254
|
-
|
|
1255
|
-
if not all_sessions:
|
|
1256
|
-
console.print(" [dim]No sessions. Use 'spawn \"task\"' to start one.[/]")
|
|
1257
|
-
return
|
|
1258
|
-
|
|
1259
|
-
# Status summary
|
|
1260
|
-
running = sum(1 for s in all_sessions if s.status == SessStatus.RUNNING)
|
|
1261
|
-
completed = sum(1 for s in all_sessions if s.status == SessStatus.COMPLETED)
|
|
1262
|
-
failed = sum(1 for s in all_sessions if s.status == SessStatus.FAILED)
|
|
1263
|
-
killed = sum(1 for s in all_sessions if s.status == SessStatus.KILLED)
|
|
1264
|
-
|
|
1265
|
-
summary_parts = []
|
|
1266
|
-
if running:
|
|
1267
|
-
summary_parts.append(f"[yellow]{running} running[/]")
|
|
1268
|
-
if completed:
|
|
1269
|
-
summary_parts.append(f"[green]{completed} done[/]")
|
|
1270
|
-
if failed:
|
|
1271
|
-
summary_parts.append(f"[red]{failed} failed[/]")
|
|
1272
|
-
if killed:
|
|
1273
|
-
summary_parts.append(f"[dim]{killed} killed[/]")
|
|
1274
|
-
if summary_parts:
|
|
1275
|
-
console.print(" | ".join(summary_parts))
|
|
1276
|
-
console.print()
|
|
1277
|
-
|
|
1278
|
-
def time_ago(iso_str: str) -> str:
|
|
1279
|
-
"""Convert ISO timestamp to human-readable 'X ago' format."""
|
|
1280
|
-
try:
|
|
1281
|
-
dt = datetime.fromisoformat(iso_str)
|
|
1282
|
-
delta = datetime.now() - dt
|
|
1283
|
-
secs = delta.total_seconds()
|
|
1284
|
-
if secs < 60:
|
|
1285
|
-
return f"{int(secs)}s"
|
|
1286
|
-
elif secs < 3600:
|
|
1287
|
-
return f"{int(secs/60)}m"
|
|
1288
|
-
elif secs < 86400:
|
|
1289
|
-
return f"{secs/3600:.1f}h"
|
|
1290
|
-
else:
|
|
1291
|
-
return f"{secs/86400:.1f}d"
|
|
1292
|
-
except:
|
|
1293
|
-
return "?"
|
|
1294
|
-
|
|
1295
|
-
table = Table(box=None, show_header=True, header_style="bold dim")
|
|
1296
|
-
table.add_column("ID", style="cyan", width=10)
|
|
1297
|
-
table.add_column("", width=2) # Status icon
|
|
1298
|
-
table.add_column("T", width=2) # Turn
|
|
1299
|
-
table.add_column("Task", max_width=30)
|
|
1300
|
-
table.add_column("Updated", justify="right", width=8)
|
|
1301
|
-
table.add_column("Last Message", max_width=40)
|
|
1302
|
-
|
|
1303
|
-
status_icons = {
|
|
1304
|
-
"running": "[yellow]●[/]",
|
|
1305
|
-
"completed": "[green]✓[/]",
|
|
1306
|
-
"failed": "[red]✗[/]",
|
|
1307
|
-
"killed": "[dim]○[/]",
|
|
1308
|
-
"pending": "[dim]◌[/]",
|
|
1309
|
-
}
|
|
1310
|
-
|
|
1311
|
-
for s in all_sessions:
|
|
1312
|
-
icon = status_icons.get(s.status.value, "?")
|
|
1313
|
-
task_preview = s.task[:27] + "..." if len(s.task) > 30 else s.task
|
|
1314
|
-
updated = time_ago(s.updated_at)
|
|
1315
|
-
|
|
1316
|
-
# Get last assistant message preview
|
|
1317
|
-
messages = manager.get_messages(s.id)
|
|
1318
|
-
last_msg = ""
|
|
1319
|
-
for msg in reversed(messages):
|
|
1320
|
-
if msg.role == "assistant":
|
|
1321
|
-
last_msg = msg.content.replace("\n", " ")[:37]
|
|
1322
|
-
if len(msg.content) > 37:
|
|
1323
|
-
last_msg += "..."
|
|
1324
|
-
break
|
|
1325
|
-
|
|
1326
|
-
# Style the last message based on recency
|
|
1327
|
-
if s.status == SessStatus.RUNNING:
|
|
1328
|
-
last_msg_styled = f"[yellow]{last_msg or '(working...)'}[/]"
|
|
1329
|
-
updated_styled = f"[yellow]{updated}[/]"
|
|
1330
|
-
elif s.status == SessStatus.COMPLETED:
|
|
1331
|
-
# Highlight if recently completed (< 60s)
|
|
1332
|
-
try:
|
|
1333
|
-
dt = datetime.fromisoformat(s.updated_at)
|
|
1334
|
-
is_recent = (datetime.now() - dt).total_seconds() < 60
|
|
1335
|
-
except:
|
|
1336
|
-
is_recent = False
|
|
1337
|
-
if is_recent:
|
|
1338
|
-
last_msg_styled = f"[green bold]{last_msg or '(done)'}[/]"
|
|
1339
|
-
updated_styled = f"[green bold]{updated} ★[/]"
|
|
1340
|
-
else:
|
|
1341
|
-
last_msg_styled = f"[green]{last_msg or '(done)'}[/]"
|
|
1342
|
-
updated_styled = f"[dim]{updated}[/]"
|
|
1343
|
-
elif s.status == SessStatus.FAILED:
|
|
1344
|
-
last_msg_styled = f"[red]{s.error[:37] if s.error else '(failed)'}...[/]"
|
|
1345
|
-
updated_styled = f"[red]{updated}[/]"
|
|
1346
|
-
else:
|
|
1347
|
-
last_msg_styled = f"[dim]{last_msg or '-'}[/]"
|
|
1348
|
-
updated_styled = f"[dim]{updated}[/]"
|
|
1349
|
-
|
|
1350
|
-
table.add_row(
|
|
1351
|
-
s.short_id,
|
|
1352
|
-
icon,
|
|
1353
|
-
str(s.turn),
|
|
1354
|
-
task_preview,
|
|
1355
|
-
updated_styled,
|
|
1356
|
-
last_msg_styled,
|
|
1357
|
-
)
|
|
1358
|
-
|
|
1359
|
-
console.print(table)
|
|
1360
|
-
|
|
1361
|
-
def parse_spawn_args(args: list[str]) -> dict:
|
|
1362
|
-
"""Parse spawn command arguments."""
|
|
1363
|
-
parser = argparse.ArgumentParser(add_help=False)
|
|
1364
|
-
parser.add_argument("task", nargs="*")
|
|
1365
|
-
parser.add_argument("--dir", "-d", type=Path, default=None)
|
|
1366
|
-
parser.add_argument("--model", "-m", default=None)
|
|
1367
|
-
parser.add_argument("--async", dest="async_mode", action="store_true", default=False)
|
|
1368
|
-
|
|
1369
|
-
try:
|
|
1370
|
-
parsed, _ = parser.parse_known_args(args)
|
|
1371
|
-
return {
|
|
1372
|
-
"task": " ".join(parsed.task) if parsed.task else "",
|
|
1373
|
-
"dir": parsed.dir,
|
|
1374
|
-
"model": parsed.model,
|
|
1375
|
-
"async_mode": parsed.async_mode,
|
|
1376
|
-
}
|
|
1377
|
-
except SystemExit:
|
|
1378
|
-
return {"error": "Invalid spawn arguments"}
|
|
1379
|
-
|
|
1380
|
-
def do_spawn(args: list[str]):
|
|
1381
|
-
"""Spawn a new coding agent session using CodexSessionManager (same as orchestrator)."""
|
|
1382
|
-
from zwarm.sessions import CodexSessionManager, SessionStatus as SessStatus
|
|
1383
|
-
import time
|
|
1384
|
-
|
|
1385
|
-
parsed = parse_spawn_args(args)
|
|
1386
|
-
|
|
1387
|
-
if "error" in parsed:
|
|
1388
|
-
console.print(f" [red]{parsed['error']}[/]")
|
|
1389
|
-
console.print(" [dim]Usage: spawn \"task\" [--dir PATH] [--model NAME] [--async][/]")
|
|
1390
|
-
return
|
|
1391
|
-
|
|
1392
|
-
if not parsed["task"]:
|
|
1393
|
-
console.print(" [red]Task required[/]")
|
|
1394
|
-
console.print(" [dim]Usage: spawn \"task\" [--dir PATH] [--model NAME] [--async][/]")
|
|
1395
|
-
return
|
|
1396
|
-
|
|
1397
|
-
task = parsed["task"]
|
|
1398
|
-
work_dir = (parsed["dir"] or default_dir).absolute()
|
|
1399
|
-
session_model = parsed["model"] or default_model
|
|
1400
|
-
is_async = parsed.get("async_mode", False)
|
|
1401
|
-
|
|
1402
|
-
if not work_dir.exists():
|
|
1403
|
-
console.print(f" [red]Directory not found:[/] {work_dir}")
|
|
1404
|
-
return
|
|
1405
|
-
|
|
1406
|
-
mode_str = "async" if is_async else "sync"
|
|
1407
|
-
console.print(f"\n[dim]Spawning {mode_str} session...[/]")
|
|
1408
|
-
console.print(f" [dim]Dir: {work_dir}[/]")
|
|
1409
|
-
console.print(f" [dim]Model: {session_model}[/]")
|
|
1410
|
-
|
|
1411
|
-
try:
|
|
1412
|
-
# Use CodexSessionManager - SAME as orchestrator's delegate()
|
|
1413
|
-
manager = CodexSessionManager(work_dir / ".zwarm")
|
|
1414
|
-
session = manager.start_session(
|
|
1415
|
-
task=task,
|
|
1416
|
-
working_dir=work_dir,
|
|
1417
|
-
model=session_model,
|
|
1418
|
-
sandbox="workspace-write",
|
|
1419
|
-
source="user",
|
|
1420
|
-
adapter="codex",
|
|
1421
|
-
)
|
|
1422
|
-
|
|
1423
|
-
console.print(f"\n[green]✓[/] Session: [cyan]{session.short_id}[/]")
|
|
1424
|
-
console.print(f" [dim]PID: {session.pid}[/]")
|
|
1425
|
-
|
|
1426
|
-
# For sync mode, wait for completion and show response
|
|
1427
|
-
if not is_async:
|
|
1428
|
-
console.print(f"\n[dim]Waiting for completion...[/]")
|
|
1429
|
-
timeout = 300.0
|
|
1430
|
-
start = time.time()
|
|
1431
|
-
while time.time() - start < timeout:
|
|
1432
|
-
# get_session() auto-updates status based on output completion
|
|
1433
|
-
session = manager.get_session(session.id)
|
|
1434
|
-
if session.status != SessStatus.RUNNING:
|
|
1435
|
-
break
|
|
1436
|
-
time.sleep(1.0)
|
|
1437
|
-
|
|
1438
|
-
# Get all assistant responses
|
|
1439
|
-
messages = manager.get_messages(session.id)
|
|
1440
|
-
assistant_msgs = [m for m in messages if m.role == "assistant"]
|
|
1441
|
-
if assistant_msgs:
|
|
1442
|
-
console.print(f"\n[bold]Response ({len(assistant_msgs)} message{'s' if len(assistant_msgs) > 1 else ''}):[/]")
|
|
1443
|
-
for msg in assistant_msgs:
|
|
1444
|
-
preview = msg.content[:300]
|
|
1445
|
-
if len(msg.content) > 300:
|
|
1446
|
-
preview += "..."
|
|
1447
|
-
console.print(preview)
|
|
1448
|
-
if len(assistant_msgs) > 1:
|
|
1449
|
-
console.print() # Blank line between multiple messages
|
|
1450
|
-
|
|
1451
|
-
console.print(f"\n[dim]Use 'show {session.short_id}' to see full details[/]")
|
|
1452
|
-
console.print(f"[dim]Use 'c {session.short_id} \"message\"' to continue[/]")
|
|
1453
|
-
|
|
1454
|
-
except Exception as e:
|
|
1455
|
-
console.print(f" [red]Error:[/] {e}")
|
|
1456
|
-
import traceback
|
|
1457
|
-
console.print(f" [dim]{traceback.format_exc()}[/]")
|
|
1458
|
-
|
|
1459
|
-
def do_orchestrate(args: list[str]):
|
|
1460
|
-
"""Spawn an orchestrator agent that delegates to sub-sessions."""
|
|
1461
|
-
parsed = parse_spawn_args(args)
|
|
1462
|
-
|
|
1463
|
-
if "error" in parsed:
|
|
1464
|
-
console.print(f" [red]{parsed['error']}[/]")
|
|
1465
|
-
console.print(" [dim]Usage: orchestrate \"task\" [--dir PATH][/]")
|
|
1466
|
-
return
|
|
1467
|
-
|
|
1468
|
-
if not parsed["task"]:
|
|
1469
|
-
console.print(" [red]Task required[/]")
|
|
1470
|
-
console.print(" [dim]Usage: orchestrate \"task\" [--dir PATH][/]")
|
|
1471
|
-
return
|
|
1472
|
-
|
|
1473
|
-
task = parsed["task"]
|
|
1474
|
-
work_dir = (parsed["dir"] or default_dir).absolute()
|
|
1475
|
-
|
|
1476
|
-
if not work_dir.exists():
|
|
1477
|
-
console.print(f" [red]Directory not found:[/] {work_dir}")
|
|
1478
|
-
return
|
|
1479
|
-
|
|
1480
|
-
console.print(f"\n[dim]Starting orchestrator...[/]")
|
|
1481
|
-
console.print(f" [dim]Dir: {work_dir}[/]")
|
|
1482
|
-
console.print(f" [dim]Task: {task[:60]}{'...' if len(task) > 60 else ''}[/]")
|
|
1338
|
+
run_interactive(working_dir=default_dir.absolute(), model=default_model)
|
|
1483
1339
|
|
|
1484
|
-
import subprocess
|
|
1485
|
-
from uuid import uuid4
|
|
1486
1340
|
|
|
1487
|
-
|
|
1488
|
-
|
|
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.
|
|
1489
1351
|
|
|
1490
|
-
|
|
1491
|
-
cmd = [
|
|
1492
|
-
sys.executable, "-m", "zwarm.cli.main", "orchestrate",
|
|
1493
|
-
"--task", task,
|
|
1494
|
-
"--working-dir", str(work_dir),
|
|
1495
|
-
"--instance", instance_id,
|
|
1496
|
-
]
|
|
1352
|
+
By default, lists all sessions. Use flags for quick actions:
|
|
1497
1353
|
|
|
1498
|
-
|
|
1499
|
-
|
|
1500
|
-
|
|
1501
|
-
log_file = log_dir / f"{instance_id}.log"
|
|
1354
|
+
[bold]Examples:[/]
|
|
1355
|
+
[dim]# List all sessions[/]
|
|
1356
|
+
$ zwarm sessions
|
|
1502
1357
|
|
|
1503
|
-
|
|
1504
|
-
|
|
1505
|
-
|
|
1506
|
-
cmd,
|
|
1507
|
-
cwd=work_dir,
|
|
1508
|
-
stdout=f,
|
|
1509
|
-
stderr=subprocess.STDOUT,
|
|
1510
|
-
start_new_session=True,
|
|
1511
|
-
)
|
|
1358
|
+
[dim]# Delete all completed/failed sessions[/]
|
|
1359
|
+
$ zwarm sessions --clean
|
|
1360
|
+
$ zwarm sessions -c
|
|
1512
1361
|
|
|
1513
|
-
|
|
1514
|
-
|
|
1515
|
-
|
|
1516
|
-
console.print(f"\n[dim]Delegated sessions will appear with source 'orch:{instance_id[:4]}'[/]")
|
|
1517
|
-
console.print(f"[dim]Use 'ls' to monitor progress[/]")
|
|
1362
|
+
[dim]# Kill all running sessions[/]
|
|
1363
|
+
$ zwarm sessions --kill-all
|
|
1364
|
+
$ zwarm sessions -k
|
|
1518
1365
|
|
|
1519
|
-
|
|
1520
|
-
|
|
1366
|
+
[dim]# Delete a specific session[/]
|
|
1367
|
+
$ zwarm sessions --rm abc123
|
|
1521
1368
|
|
|
1522
|
-
|
|
1523
|
-
|
|
1524
|
-
|
|
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
|
|
1525
1374
|
|
|
1526
|
-
|
|
1527
|
-
session = manager.get_session(session_id)
|
|
1375
|
+
manager = CodexSessionManager(working_dir / ".zwarm")
|
|
1528
1376
|
|
|
1377
|
+
# Handle --kill (specific session)
|
|
1378
|
+
if kill:
|
|
1379
|
+
session = manager.get_session(kill)
|
|
1529
1380
|
if not session:
|
|
1530
|
-
console.print(f"
|
|
1531
|
-
|
|
1532
|
-
|
|
1533
|
-
|
|
1534
|
-
icon = {
|
|
1535
|
-
"running": "[yellow]●[/]",
|
|
1536
|
-
"completed": "[green]✓[/]",
|
|
1537
|
-
"failed": "[red]✗[/]",
|
|
1538
|
-
"killed": "[dim]○[/]",
|
|
1539
|
-
}.get(session.status.value, "?")
|
|
1540
|
-
|
|
1541
|
-
# Get latest assistant message
|
|
1542
|
-
messages = manager.get_messages(session.id)
|
|
1543
|
-
latest = None
|
|
1544
|
-
for msg in reversed(messages):
|
|
1545
|
-
if msg.role == "assistant":
|
|
1546
|
-
latest = msg.content.replace("\n", " ")
|
|
1547
|
-
break
|
|
1548
|
-
|
|
1549
|
-
if session.status == SessStatus.RUNNING:
|
|
1550
|
-
console.print(f"{icon} [cyan]{session.short_id}[/] [yellow](running...)[/]")
|
|
1551
|
-
elif latest:
|
|
1552
|
-
# Truncate for one-liner
|
|
1553
|
-
preview = latest[:120] + "..." if len(latest) > 120 else latest
|
|
1554
|
-
console.print(f"{icon} [cyan]{session.short_id}[/] {preview}")
|
|
1555
|
-
elif session.status == SessStatus.FAILED:
|
|
1556
|
-
error = (session.error or "unknown")[:80]
|
|
1557
|
-
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}")
|
|
1558
1385
|
else:
|
|
1559
|
-
console.print(f"
|
|
1560
|
-
|
|
1561
|
-
def do_show(session_id: str):
|
|
1562
|
-
"""Show full session details and messages using CodexSessionManager."""
|
|
1563
|
-
from zwarm.sessions import CodexSessionManager, SessionStatus as SessStatus
|
|
1564
|
-
|
|
1565
|
-
manager = CodexSessionManager(default_dir / ".zwarm")
|
|
1566
|
-
session = manager.get_session(session_id)
|
|
1567
|
-
|
|
1568
|
-
if not session:
|
|
1569
|
-
console.print(f" [red]Session not found:[/] {session_id}")
|
|
1570
|
-
console.print(f" [dim]Use 'ls' to see available sessions[/]")
|
|
1571
|
-
return
|
|
1572
|
-
|
|
1573
|
-
# Status styling
|
|
1574
|
-
status_display = {
|
|
1575
|
-
"running": "[yellow]● running[/]",
|
|
1576
|
-
"completed": "[green]✓ completed[/]",
|
|
1577
|
-
"failed": "[red]✗ failed[/]",
|
|
1578
|
-
"killed": "[dim]○ killed[/]",
|
|
1579
|
-
"pending": "[dim]◌ pending[/]",
|
|
1580
|
-
}.get(session.status.value, str(session.status.value))
|
|
1581
|
-
|
|
1582
|
-
console.print()
|
|
1583
|
-
console.print(f"[bold cyan]Session {session.short_id}[/] {status_display}")
|
|
1584
|
-
console.print(f"[dim]Adapter:[/] {session.adapter} [dim]│[/] [dim]Model:[/] {session.model}")
|
|
1585
|
-
console.print(f"[dim]Task:[/] {session.task}")
|
|
1586
|
-
console.print(f"[dim]Dir:[/] {session.working_dir} [dim]│[/] [dim]Turn:[/] {session.turn}")
|
|
1587
|
-
console.print(f"[dim]Source:[/] {session.source_display} [dim]│[/] [dim]Runtime:[/] {session.runtime}")
|
|
1588
|
-
if session.pid:
|
|
1589
|
-
console.print(f"[dim]PID:[/] {session.pid}")
|
|
1590
|
-
|
|
1591
|
-
# Show log file path
|
|
1592
|
-
log_path = default_dir / ".zwarm" / "sessions" / session.id / "turns" / f"turn_{session.turn}.jsonl"
|
|
1593
|
-
console.print(f"[dim]Log:[/] {log_path}")
|
|
1594
|
-
console.print()
|
|
1595
|
-
|
|
1596
|
-
# Get messages from manager
|
|
1597
|
-
messages = manager.get_messages(session.id)
|
|
1598
|
-
|
|
1599
|
-
if not messages:
|
|
1600
|
-
if session.is_running:
|
|
1601
|
-
console.print("[yellow]Session is still running...[/]")
|
|
1602
|
-
else:
|
|
1603
|
-
console.print("[dim]No messages captured.[/]")
|
|
1604
|
-
return
|
|
1605
|
-
|
|
1606
|
-
# Display messages
|
|
1607
|
-
for msg in messages:
|
|
1608
|
-
if msg.role == "user":
|
|
1609
|
-
console.print(Panel(msg.content, title="[bold blue]User[/]", border_style="blue"))
|
|
1610
|
-
elif msg.role == "assistant":
|
|
1611
|
-
content = msg.content
|
|
1612
|
-
if len(content) > 2000:
|
|
1613
|
-
content = content[:2000] + "\n\n[dim]... (truncated)[/]"
|
|
1614
|
-
console.print(Panel(content, title="[bold green]Assistant[/]", border_style="green"))
|
|
1615
|
-
elif msg.role == "tool":
|
|
1616
|
-
console.print(f" [dim]Tool: {msg.content[:100]}[/]")
|
|
1617
|
-
|
|
1618
|
-
console.print()
|
|
1619
|
-
if session.token_usage:
|
|
1620
|
-
tokens = session.token_usage
|
|
1621
|
-
console.print(f"[dim]Tokens: {tokens.get('input_tokens', 0):,} in / {tokens.get('output_tokens', 0):,} out[/]")
|
|
1622
|
-
|
|
1623
|
-
if session.error:
|
|
1624
|
-
console.print(f"[red]Error:[/] {session.error}")
|
|
1625
|
-
|
|
1626
|
-
def do_trajectory(session_id: str, full: bool = False):
|
|
1627
|
-
"""Show the full trajectory of a session - all steps in order."""
|
|
1628
|
-
from zwarm.sessions import CodexSessionManager
|
|
1629
|
-
|
|
1630
|
-
manager = CodexSessionManager(default_dir / ".zwarm")
|
|
1631
|
-
session = manager.get_session(session_id)
|
|
1632
|
-
|
|
1633
|
-
if not session:
|
|
1634
|
-
console.print(f" [red]Session not found:[/] {session_id}")
|
|
1635
|
-
return
|
|
1636
|
-
|
|
1637
|
-
trajectory = manager.get_trajectory(session_id, full=full)
|
|
1638
|
-
|
|
1639
|
-
if not trajectory:
|
|
1640
|
-
console.print("[dim]No trajectory data available.[/]")
|
|
1641
|
-
return
|
|
1642
|
-
|
|
1643
|
-
mode = "[bold](full)[/] " if full else ""
|
|
1644
|
-
console.print(f"\n[bold cyan]Trajectory: {session.short_id}[/] {mode}({len(trajectory)} steps)")
|
|
1645
|
-
console.print(f"[dim]Task: {session.task[:60]}{'...' if len(session.task) > 60 else ''}[/]")
|
|
1646
|
-
console.print()
|
|
1647
|
-
|
|
1648
|
-
# Display each step
|
|
1649
|
-
for step in trajectory:
|
|
1650
|
-
turn = step.get("turn", 1)
|
|
1651
|
-
step_num = step.get("step", 0)
|
|
1652
|
-
step_type = step.get("type", "unknown")
|
|
1653
|
-
|
|
1654
|
-
prefix = f"[dim]T{turn}.{step_num:02d}[/]"
|
|
1655
|
-
|
|
1656
|
-
if step_type == "reasoning":
|
|
1657
|
-
if full and step.get("full_text"):
|
|
1658
|
-
console.print(f"{prefix} [yellow]thinking:[/]")
|
|
1659
|
-
console.print(f" {step['full_text']}")
|
|
1660
|
-
else:
|
|
1661
|
-
summary = step.get("summary", "")
|
|
1662
|
-
console.print(f"{prefix} [yellow]thinking:[/] {summary}")
|
|
1663
|
-
|
|
1664
|
-
elif step_type == "command":
|
|
1665
|
-
cmd = step.get("command", "")
|
|
1666
|
-
output = step.get("output", "")
|
|
1667
|
-
exit_code = step.get("exit_code", "?")
|
|
1668
|
-
# Show command
|
|
1669
|
-
console.print(f"{prefix} [cyan]$ {cmd}[/]")
|
|
1670
|
-
if output:
|
|
1671
|
-
if full:
|
|
1672
|
-
# Show all output
|
|
1673
|
-
for line in output.split("\n"):
|
|
1674
|
-
console.print(f" [dim]{line}[/]")
|
|
1675
|
-
else:
|
|
1676
|
-
# Indent output, max 5 lines
|
|
1677
|
-
for line in output.split("\n")[:5]:
|
|
1678
|
-
console.print(f" [dim]{line}[/]")
|
|
1679
|
-
if output.count("\n") > 5:
|
|
1680
|
-
console.print(f" [dim]... ({output.count(chr(10))} lines)[/]")
|
|
1681
|
-
if exit_code != 0 and exit_code is not None:
|
|
1682
|
-
console.print(f" [red]exit: {exit_code}[/]")
|
|
1683
|
-
|
|
1684
|
-
elif step_type == "tool_call":
|
|
1685
|
-
tool = step.get("tool", "unknown")
|
|
1686
|
-
if full and step.get("full_args"):
|
|
1687
|
-
import json
|
|
1688
|
-
console.print(f"{prefix} [magenta]tool:[/] {tool}")
|
|
1689
|
-
console.print(f" {json.dumps(step['full_args'], indent=2)}")
|
|
1690
|
-
else:
|
|
1691
|
-
args = step.get("args_preview", "")
|
|
1692
|
-
console.print(f"{prefix} [magenta]tool:[/] {tool}({args})")
|
|
1693
|
-
|
|
1694
|
-
elif step_type == "tool_output":
|
|
1695
|
-
output = step.get("output", "")
|
|
1696
|
-
if not full:
|
|
1697
|
-
output = output[:100]
|
|
1698
|
-
console.print(f"{prefix} [dim]→ {output}[/]")
|
|
1699
|
-
|
|
1700
|
-
elif step_type == "message":
|
|
1701
|
-
if full and step.get("full_text"):
|
|
1702
|
-
console.print(f"{prefix} [green]response:[/]")
|
|
1703
|
-
console.print(f" {step['full_text']}")
|
|
1704
|
-
else:
|
|
1705
|
-
summary = step.get("summary", "")
|
|
1706
|
-
full_len = step.get("full_length", 0)
|
|
1707
|
-
console.print(f"{prefix} [green]response:[/] {summary}")
|
|
1708
|
-
if full_len > 200:
|
|
1709
|
-
console.print(f" [dim]({full_len} chars total)[/]")
|
|
1710
|
-
|
|
1711
|
-
console.print()
|
|
1712
|
-
|
|
1713
|
-
def do_continue(session_id: str, message: str, wait: bool = True):
|
|
1714
|
-
"""
|
|
1715
|
-
Continue a conversation using CodexSessionManager.inject_message().
|
|
1716
|
-
|
|
1717
|
-
Works for both sync and async sessions:
|
|
1718
|
-
- If session was sync (or wait=True): wait for response
|
|
1719
|
-
- If session was async (or wait=False): fire-and-forget, check later with '?'
|
|
1720
|
-
"""
|
|
1721
|
-
from zwarm.sessions import CodexSessionManager, SessionStatus as SessStatus
|
|
1722
|
-
import time
|
|
1723
|
-
|
|
1724
|
-
manager = CodexSessionManager(default_dir / ".zwarm")
|
|
1725
|
-
session = manager.get_session(session_id)
|
|
1726
|
-
|
|
1727
|
-
if not session:
|
|
1728
|
-
console.print(f" [red]Session not found:[/] {session_id}")
|
|
1729
|
-
console.print(f" [dim]Use 'ls' to see available sessions[/]")
|
|
1730
|
-
return
|
|
1731
|
-
|
|
1732
|
-
if session.status == SessStatus.RUNNING:
|
|
1733
|
-
console.print("[yellow]Session is still running.[/]")
|
|
1734
|
-
console.print("[dim]Wait for it to complete, or use '?' to check progress.[/]")
|
|
1735
|
-
return
|
|
1736
|
-
|
|
1737
|
-
if session.status == SessStatus.KILLED:
|
|
1738
|
-
console.print("[yellow]Session was killed.[/]")
|
|
1739
|
-
console.print("[dim]Start a new session with 'spawn'.[/]")
|
|
1740
|
-
return
|
|
1741
|
-
|
|
1742
|
-
# Determine if we should wait based on session source
|
|
1743
|
-
# Sessions from 'user' with --async flag have source='user'
|
|
1744
|
-
# We'll use wait parameter to control this
|
|
1745
|
-
should_wait = wait
|
|
1746
|
-
|
|
1747
|
-
console.print(f"\n[dim]Sending message to {session.short_id}...[/]")
|
|
1748
|
-
|
|
1749
|
-
try:
|
|
1750
|
-
# Inject message - spawns new background process
|
|
1751
|
-
updated_session = manager.inject_message(session.id, message)
|
|
1752
|
-
|
|
1753
|
-
if not updated_session:
|
|
1754
|
-
console.print(f" [red]Failed to inject message[/]")
|
|
1755
|
-
return
|
|
1756
|
-
|
|
1757
|
-
console.print(f"[green]✓[/] Message sent (turn {updated_session.turn})")
|
|
1758
|
-
|
|
1759
|
-
if should_wait:
|
|
1760
|
-
# Sync mode: wait for completion
|
|
1761
|
-
console.print(f"[dim]Waiting for response...[/]")
|
|
1762
|
-
timeout = 300.0
|
|
1763
|
-
start = time.time()
|
|
1764
|
-
while time.time() - start < timeout:
|
|
1765
|
-
# get_session() auto-updates status based on output completion
|
|
1766
|
-
session = manager.get_session(session.id)
|
|
1767
|
-
if session.status != SessStatus.RUNNING:
|
|
1768
|
-
break
|
|
1769
|
-
time.sleep(1.0)
|
|
1770
|
-
|
|
1771
|
-
# Get the response (last assistant message)
|
|
1772
|
-
messages = manager.get_messages(session.id)
|
|
1773
|
-
response = ""
|
|
1774
|
-
for msg in reversed(messages):
|
|
1775
|
-
if msg.role == "assistant":
|
|
1776
|
-
response = msg.content
|
|
1777
|
-
break
|
|
1778
|
-
|
|
1779
|
-
console.print(f"\n[bold]Response:[/]")
|
|
1780
|
-
if len(response) > 500:
|
|
1781
|
-
console.print(response[:500] + "...")
|
|
1782
|
-
console.print(f"\n[dim]Use 'show {session.short_id}' to see full response[/]")
|
|
1783
|
-
else:
|
|
1784
|
-
console.print(response or "(no response captured)")
|
|
1785
|
-
else:
|
|
1786
|
-
# Async mode: return immediately
|
|
1787
|
-
console.print(f"[dim]Running in background (PID: {updated_session.pid})[/]")
|
|
1788
|
-
console.print(f"[dim]Use '? {session.short_id}' to check progress[/]")
|
|
1789
|
-
|
|
1790
|
-
except Exception as e:
|
|
1791
|
-
console.print(f" [red]Error:[/] {e}")
|
|
1792
|
-
import traceback
|
|
1793
|
-
console.print(f" [dim]{traceback.format_exc()}[/]")
|
|
1794
|
-
|
|
1795
|
-
def do_check(session_id: str):
|
|
1796
|
-
"""Check status of a session using CodexSessionManager (same as orchestrator)."""
|
|
1797
|
-
from zwarm.sessions import CodexSessionManager
|
|
1798
|
-
|
|
1799
|
-
manager = CodexSessionManager(default_dir / ".zwarm")
|
|
1800
|
-
session = manager.get_session(session_id)
|
|
1801
|
-
|
|
1802
|
-
if not session:
|
|
1803
|
-
console.print(f" [red]Session not found:[/] {session_id}")
|
|
1804
|
-
console.print(f" [dim]Use 'ls' to see available sessions[/]")
|
|
1805
|
-
return
|
|
1806
|
-
|
|
1807
|
-
status_icon = {
|
|
1808
|
-
"running": "●",
|
|
1809
|
-
"completed": "✓",
|
|
1810
|
-
"failed": "✗",
|
|
1811
|
-
"killed": "○",
|
|
1812
|
-
"pending": "◌",
|
|
1813
|
-
}.get(session.status.value, "?")
|
|
1814
|
-
|
|
1815
|
-
console.print(f"\n[bold]Session {session.short_id}[/]: {status_icon} {session.status.value}")
|
|
1816
|
-
console.print(f" [dim]Task: {session.task[:60]}{'...' if len(session.task) > 60 else ''}[/]")
|
|
1817
|
-
console.print(f" [dim]Turn: {session.turn} | Runtime: {session.runtime}[/]")
|
|
1818
|
-
|
|
1819
|
-
if session.status.value == "completed":
|
|
1820
|
-
messages = manager.get_messages(session.id)
|
|
1821
|
-
for msg in reversed(messages):
|
|
1822
|
-
if msg.role == "assistant":
|
|
1823
|
-
console.print(f"\n[bold]Response:[/]")
|
|
1824
|
-
if len(msg.content) > 500:
|
|
1825
|
-
console.print(msg.content[:500] + "...")
|
|
1826
|
-
else:
|
|
1827
|
-
console.print(msg.content)
|
|
1828
|
-
break
|
|
1829
|
-
elif session.status.value == "failed":
|
|
1830
|
-
console.print(f"[red]Error:[/] {session.error or 'Unknown error'}")
|
|
1831
|
-
elif session.status.value == "running":
|
|
1832
|
-
console.print("[dim]Session is still running...[/]")
|
|
1833
|
-
console.print(f" [dim]PID: {session.pid}[/]")
|
|
1834
|
-
|
|
1835
|
-
def do_kill(session_id: str):
|
|
1836
|
-
"""Kill a running session using CodexSessionManager (same as orchestrator)."""
|
|
1837
|
-
from zwarm.sessions import CodexSessionManager
|
|
1838
|
-
|
|
1839
|
-
manager = CodexSessionManager(default_dir / ".zwarm")
|
|
1840
|
-
session = manager.get_session(session_id)
|
|
1386
|
+
console.print(f"[yellow]Session not running or already stopped[/]")
|
|
1387
|
+
return
|
|
1841
1388
|
|
|
1389
|
+
# Handle --rm (specific session)
|
|
1390
|
+
if rm:
|
|
1391
|
+
session = manager.get_session(rm)
|
|
1842
1392
|
if not session:
|
|
1843
|
-
console.print(f"
|
|
1844
|
-
|
|
1845
|
-
|
|
1846
|
-
|
|
1847
|
-
|
|
1848
|
-
|
|
1849
|
-
|
|
1850
|
-
try:
|
|
1851
|
-
killed = manager.kill_session(session.id)
|
|
1852
|
-
if killed:
|
|
1853
|
-
console.print(f"[green]✓[/] Stopped session {session.short_id}")
|
|
1854
|
-
else:
|
|
1855
|
-
console.print(f"[red]Failed to stop session[/]")
|
|
1856
|
-
except Exception as e:
|
|
1857
|
-
console.print(f"[red]Failed to stop session:[/] {e}")
|
|
1858
|
-
|
|
1859
|
-
def do_killall():
|
|
1860
|
-
"""Kill all running sessions using CodexSessionManager."""
|
|
1861
|
-
from zwarm.sessions import CodexSessionManager, SessionStatus as SessStatus
|
|
1862
|
-
|
|
1863
|
-
manager = CodexSessionManager(default_dir / ".zwarm")
|
|
1864
|
-
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
|
|
1865
1400
|
|
|
1866
|
-
|
|
1867
|
-
|
|
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[/]")
|
|
1868
1406
|
return
|
|
1869
|
-
|
|
1870
1407
|
killed = 0
|
|
1871
|
-
for
|
|
1872
|
-
|
|
1873
|
-
|
|
1874
|
-
|
|
1875
|
-
|
|
1876
|
-
|
|
1877
|
-
|
|
1878
|
-
console.print(f"[green]✓[/] Killed {killed} sessions")
|
|
1879
|
-
|
|
1880
|
-
def do_clean():
|
|
1881
|
-
"""Remove old completed/failed sessions from disk."""
|
|
1882
|
-
from zwarm.sessions import CodexSessionManager
|
|
1883
|
-
|
|
1884
|
-
manager = CodexSessionManager(default_dir / ".zwarm")
|
|
1885
|
-
cleaned = manager.cleanup_completed(keep_days=7)
|
|
1886
|
-
console.print(f"[green]✓[/] Cleaned {cleaned} old sessions")
|
|
1887
|
-
|
|
1888
|
-
def do_delete(session_id: str):
|
|
1889
|
-
"""Delete a session entirely (removes from disk and ls)."""
|
|
1890
|
-
from zwarm.sessions import CodexSessionManager
|
|
1891
|
-
|
|
1892
|
-
manager = CodexSessionManager(default_dir / ".zwarm")
|
|
1893
|
-
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
|
|
1894
1414
|
|
|
1895
|
-
|
|
1896
|
-
|
|
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[/]")
|
|
1897
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
|
|
1898
1428
|
|
|
1899
|
-
|
|
1900
|
-
|
|
1901
|
-
else:
|
|
1902
|
-
console.print(f"[red]Failed to delete session[/]")
|
|
1903
|
-
|
|
1904
|
-
# REPL loop
|
|
1905
|
-
import shlex
|
|
1906
|
-
|
|
1907
|
-
while True:
|
|
1908
|
-
try:
|
|
1909
|
-
raw_input = console.input("[bold cyan]>[/] ").strip()
|
|
1910
|
-
if not raw_input:
|
|
1911
|
-
continue
|
|
1912
|
-
|
|
1913
|
-
# Parse command
|
|
1914
|
-
try:
|
|
1915
|
-
parts = shlex.split(raw_input)
|
|
1916
|
-
except ValueError:
|
|
1917
|
-
parts = raw_input.split()
|
|
1918
|
-
|
|
1919
|
-
cmd = parts[0].lower()
|
|
1920
|
-
args = parts[1:]
|
|
1921
|
-
|
|
1922
|
-
if cmd in ("q", "quit", "exit"):
|
|
1923
|
-
active = [s for s in sessions.values() if s.status == SessionStatus.ACTIVE]
|
|
1924
|
-
if active:
|
|
1925
|
-
console.print(f" [yellow]Warning:[/] {len(active)} sessions still active")
|
|
1926
|
-
console.print(" [dim]Use 'killall' first, or they'll continue in background[/]")
|
|
1927
|
-
|
|
1928
|
-
# Cleanup adapters
|
|
1929
|
-
for adapter_instance in adapters_cache.values():
|
|
1930
|
-
try:
|
|
1931
|
-
asyncio.run(adapter_instance.cleanup())
|
|
1932
|
-
except Exception:
|
|
1933
|
-
pass
|
|
1934
|
-
|
|
1935
|
-
console.print("\n[dim]Goodbye![/]\n")
|
|
1936
|
-
break
|
|
1937
|
-
|
|
1938
|
-
elif cmd in ("h", "help"):
|
|
1939
|
-
show_help()
|
|
1940
|
-
|
|
1941
|
-
elif cmd in ("ls", "list"):
|
|
1942
|
-
show_sessions()
|
|
1943
|
-
|
|
1944
|
-
elif cmd == "spawn":
|
|
1945
|
-
do_spawn(args)
|
|
1946
|
-
|
|
1947
|
-
elif cmd == "async":
|
|
1948
|
-
# Async spawn shorthand
|
|
1949
|
-
do_spawn(args + ["--async"])
|
|
1950
|
-
|
|
1951
|
-
elif cmd == "orchestrate":
|
|
1952
|
-
do_orchestrate(args)
|
|
1953
|
-
|
|
1954
|
-
elif cmd in ("?", "peek"):
|
|
1955
|
-
if not args:
|
|
1956
|
-
console.print(" [red]Usage:[/] ? SESSION_ID")
|
|
1957
|
-
else:
|
|
1958
|
-
do_peek(args[0])
|
|
1959
|
-
|
|
1960
|
-
elif cmd in ("show", "check"):
|
|
1961
|
-
if not args:
|
|
1962
|
-
console.print(" [red]Usage:[/] show SESSION_ID")
|
|
1963
|
-
else:
|
|
1964
|
-
do_show(args[0])
|
|
1965
|
-
|
|
1966
|
-
elif cmd in ("traj", "trajectory"):
|
|
1967
|
-
if not args:
|
|
1968
|
-
console.print(" [red]Usage:[/] traj SESSION_ID [--full]")
|
|
1969
|
-
else:
|
|
1970
|
-
full_mode = "--full" in args
|
|
1971
|
-
session_arg = [a for a in args if a != "--full"]
|
|
1972
|
-
if session_arg:
|
|
1973
|
-
do_trajectory(session_arg[0], full=full_mode)
|
|
1974
|
-
else:
|
|
1975
|
-
console.print(" [red]Usage:[/] traj SESSION_ID [--full]")
|
|
1976
|
-
|
|
1977
|
-
elif cmd in ("c", "continue"):
|
|
1978
|
-
# Sync continue - waits for response
|
|
1979
|
-
if len(args) < 2:
|
|
1980
|
-
console.print(" [red]Usage:[/] c SESSION_ID \"message\"")
|
|
1981
|
-
else:
|
|
1982
|
-
do_continue(args[0], " ".join(args[1:]), wait=True)
|
|
1429
|
+
# Default: list all sessions
|
|
1430
|
+
all_sessions = manager.list_sessions()
|
|
1983
1431
|
|
|
1984
|
-
|
|
1985
|
-
|
|
1986
|
-
|
|
1987
|
-
|
|
1988
|
-
console.print(" [dim]Sends message and returns immediately (check with '?')[/]")
|
|
1989
|
-
else:
|
|
1990
|
-
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
|
|
1991
1436
|
|
|
1992
|
-
|
|
1993
|
-
|
|
1994
|
-
|
|
1995
|
-
|
|
1996
|
-
|
|
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()
|
|
1997
1455
|
|
|
1998
|
-
|
|
1999
|
-
|
|
2000
|
-
|
|
2001
|
-
|
|
2002
|
-
|
|
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")
|
|
2003
1463
|
|
|
2004
|
-
|
|
2005
|
-
|
|
1464
|
+
status_icons = {
|
|
1465
|
+
SessionStatus.RUNNING: "[yellow]⟳[/]",
|
|
1466
|
+
SessionStatus.COMPLETED: "[green]✓[/]",
|
|
1467
|
+
SessionStatus.FAILED: "[red]✗[/]",
|
|
1468
|
+
SessionStatus.KILLED: "[dim]⊘[/]",
|
|
1469
|
+
SessionStatus.PENDING: "[dim]○[/]",
|
|
1470
|
+
}
|
|
2006
1471
|
|
|
2007
|
-
|
|
2008
|
-
|
|
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 "-"
|
|
2009
1477
|
|
|
2010
|
-
|
|
2011
|
-
|
|
2012
|
-
|
|
2013
|
-
else:
|
|
2014
|
-
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"))
|
|
2015
1481
|
|
|
2016
|
-
|
|
2017
|
-
console.print(f" [yellow]Unknown command:[/] {cmd}")
|
|
2018
|
-
console.print(" [dim]Type 'help' for available commands[/]")
|
|
1482
|
+
table.add_row(session.short_id, icon, task, tokens_str, cost_str)
|
|
2019
1483
|
|
|
2020
|
-
|
|
2021
|
-
|
|
2022
|
-
|
|
2023
|
-
console.print("\n[dim]Goodbye![/]\n")
|
|
2024
|
-
break
|
|
2025
|
-
except Exception as e:
|
|
2026
|
-
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[/]")
|
|
2027
1487
|
|
|
2028
1488
|
|
|
2029
1489
|
# =============================================================================
|