zwarm 3.2.1__py3-none-any.whl → 3.4.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 CHANGED
@@ -157,7 +157,7 @@ def cmd_help():
157
157
  table.add_column("Description")
158
158
 
159
159
  table.add_row("[bold]Session Lifecycle[/]", "")
160
- table.add_row('spawn "task" [--dir PATH]', "Start new session")
160
+ table.add_row('spawn "task" [--dir PATH] [--model M]', "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)")
@@ -307,7 +307,7 @@ def cmd_show(manager, session_id: str):
307
307
  icon = STATUS_ICONS.get(session.status.value, "?")
308
308
  console.print(f"\n{icon} [bold cyan]{session.short_id}[/] - {session.status.value}")
309
309
  console.print(f" [dim]Task:[/] {session.task}")
310
- console.print(f" [dim]Turn:[/] {session.turn} | [dim]Runtime:[/] {session.runtime:.1f}s")
310
+ console.print(f" [dim]Turn:[/] {session.turn} | [dim]Runtime:[/] {session.runtime}")
311
311
 
312
312
  # Token usage with cost estimate
313
313
  usage = session.token_usage
@@ -694,7 +694,7 @@ def run_interactive(
694
694
 
695
695
  elif cmd == "spawn":
696
696
  if not args:
697
- console.print(" [red]Usage:[/] spawn \"task\" [--dir PATH]")
697
+ console.print(" [red]Usage:[/] spawn \"task\" [--dir PATH] [--search]")
698
698
  else:
699
699
  # Parse spawn args
700
700
  task_parts = []
zwarm/cli/main.py CHANGED
@@ -122,16 +122,6 @@ Manage zwarm configurations.
122
122
  app.add_typer(configs_app, name="configs")
123
123
 
124
124
 
125
- class AdapterType(str, Enum):
126
- codex_mcp = "codex_mcp"
127
- claude_code = "claude_code"
128
-
129
-
130
- class ModeType(str, Enum):
131
- sync = "sync"
132
- async_ = "async"
133
-
134
-
135
125
  @app.command()
136
126
  def orchestrate(
137
127
  task: Annotated[Optional[str], typer.Option("--task", "-t", help="The task to accomplish")] = None,
@@ -228,6 +218,26 @@ def orchestrate(
228
218
  if orchestrator.instance_id and not instance:
229
219
  console.print(f" [dim]Instance: {orchestrator.instance_id[:8]}[/]")
230
220
 
221
+ # Set up step callback for live progress display
222
+ def step_callback(step_num: int, tool_results: list) -> None:
223
+ """Print tool calls and results as they happen."""
224
+ if not tool_results:
225
+ return
226
+ for tool_info, result in tool_results:
227
+ name = tool_info.get("name", "?")
228
+ # Truncate args for display
229
+ args_str = str(tool_info.get("args", {}))
230
+ if len(args_str) > 80:
231
+ args_str = args_str[:77] + "..."
232
+ # Truncate result for display
233
+ result_str = str(result)
234
+ if len(result_str) > 100:
235
+ result_str = result_str[:97] + "..."
236
+ console.print(f"[dim]step {step_num}[/] → [cyan]{name}[/]({args_str})")
237
+ console.print(f" └ {result_str}")
238
+
239
+ orchestrator._step_callback = step_callback
240
+
231
241
  # Run the orchestrator loop
232
242
  console.print("[bold]--- Orchestrator running ---[/]\n")
233
243
  result = orchestrator.run(task=task)
@@ -384,78 +394,68 @@ def pilot(
384
394
  @app.command()
385
395
  def exec(
386
396
  task: Annotated[str, typer.Option("--task", "-t", help="Task to execute")],
387
- adapter: Annotated[AdapterType, typer.Option("--adapter", "-a", help="Executor adapter")] = AdapterType.codex_mcp,
388
- mode: Annotated[ModeType, typer.Option("--mode", "-m", help="Execution mode")] = ModeType.sync,
389
397
  working_dir: Annotated[Path, typer.Option("--working-dir", "-w", help="Working directory")] = Path("."),
390
398
  model: Annotated[Optional[str], typer.Option("--model", help="Model override")] = None,
399
+ wait: Annotated[bool, typer.Option("--wait", help="Wait for completion and show result")] = False,
391
400
  ):
392
401
  """
393
- Run a single executor directly (for testing).
402
+ Run a single Codex session directly (for testing).
394
403
 
395
- Useful for testing adapters without the full orchestrator loop.
404
+ Spawns a session using CodexSessionManager - same as interactive/pilot.
405
+ Web search is always enabled via .codex/config.toml (set up by `zwarm init`).
396
406
 
397
407
  [bold]Examples:[/]
398
- [dim]# Test Codex[/]
399
- $ zwarm exec --task "What is 2+2?"
408
+ [dim]# Quick test[/]
409
+ $ zwarm exec --task "What is 2+2?" --wait
400
410
 
401
- [dim]# Test Claude Code[/]
402
- $ zwarm exec -a claude_code --task "List files in current dir"
411
+ [dim]# Run in background[/]
412
+ $ zwarm exec --task "Build feature"
403
413
 
404
- [dim]# Async mode[/]
405
- $ zwarm exec --task "Build feature" --mode async
414
+ [dim]# Web search is always available[/]
415
+ $ zwarm exec --task "Find latest FastAPI docs" --wait
406
416
  """
407
- from zwarm.adapters import get_adapter
408
-
409
- console.print(f"[bold]Running executor directly...[/]")
410
- console.print(f" Adapter: [cyan]{adapter.value}[/]")
411
- console.print(f" Mode: {mode.value}")
412
- console.print(f" Task: {task}")
413
-
414
- # Use isolated codex config if available
415
- config_path = working_dir / ".zwarm" / "codex.toml"
416
- if not config_path.exists():
417
- config_path = None
418
-
419
- try:
420
- executor = get_adapter(adapter.value, model=model, config_path=config_path)
421
- except ValueError as e:
422
- console.print(f"[red]Error:[/] {e}")
423
- sys.exit(1)
424
-
425
- async def run():
426
- try:
427
- session = await executor.start_session(
428
- task=task,
429
- working_dir=working_dir.absolute(),
430
- mode=mode.value,
431
- model=model,
432
- )
433
-
434
- console.print(f"\n[green]Session started:[/] {session.id[:8]}")
435
-
436
- if mode == ModeType.sync:
437
- response = session.messages[-1].content if session.messages else "(no response)"
438
- console.print(f"\n[bold]Response:[/]\n{response}")
417
+ import time
418
+ from zwarm.sessions import CodexSessionManager, SessionStatus
439
419
 
440
- # Interactive loop for sync mode
441
- while True:
442
- try:
443
- user_input = console.input("\n[dim]> (type message or 'exit')[/] ")
444
- if user_input.lower() == "exit" or not user_input:
445
- break
420
+ console.print(f"[bold]Running Codex session...[/]")
421
+ console.print(f" Task: {task[:60]}{'...' if len(task) > 60 else ''}")
422
+ if model:
423
+ console.print(f" Model: {model}")
446
424
 
447
- response = await executor.send_message(session, user_input)
448
- console.print(f"\n[bold]Response:[/]\n{response}")
449
- except KeyboardInterrupt:
450
- break
451
- else:
452
- console.print("[dim]Async mode - session running in background.[/]")
453
- console.print("Use 'zwarm status' to check progress.")
425
+ manager = CodexSessionManager(working_dir / ".zwarm")
426
+ effective_model = model or "gpt-5.1-codex-mini"
454
427
 
455
- finally:
456
- await executor.cleanup()
428
+ session = manager.start_session(
429
+ task=task,
430
+ working_dir=working_dir.absolute(),
431
+ model=effective_model,
432
+ )
457
433
 
458
- asyncio.run(run())
434
+ console.print(f"\n[green]Session started:[/] {session.short_id}")
435
+
436
+ if wait:
437
+ console.print("[dim]Waiting for completion...[/]")
438
+ while True:
439
+ time.sleep(2)
440
+ session = manager.get_session(session.id)
441
+ if session.status != SessionStatus.RUNNING:
442
+ break
443
+
444
+ if session.status == SessionStatus.COMPLETED:
445
+ console.print(f"\n[green]✓ Completed[/]")
446
+ # Show last assistant message
447
+ for msg in reversed(session.messages):
448
+ if msg.role == "assistant":
449
+ console.print(f"\n[bold]Response:[/]\n{msg.content}")
450
+ break
451
+ else:
452
+ console.print(f"\n[red]Status:[/] {session.status.value}")
453
+ if session.error:
454
+ console.print(f"[red]Error:[/] {session.error}")
455
+ else:
456
+ console.print("[dim]Running in background. Check with:[/]")
457
+ console.print(f" zwarm sessions")
458
+ console.print(f" zwarm session show {session.short_id}")
459
459
 
460
460
 
461
461
  @app.command()
@@ -740,12 +740,12 @@ def init(
740
740
  [bold]Creates:[/]
741
741
  [cyan].zwarm/[/] State directory for sessions and events
742
742
  [cyan].zwarm/config.toml[/] Runtime settings (weave, adapter, watchers)
743
- [cyan].zwarm/codex.toml[/] Codex CLI settings (model, reasoning effort)
743
+ [cyan].zwarm/codex.toml[/] Codex CLI settings (model, web search, etc.)
744
744
  [cyan]zwarm.yaml[/] Project config (optional, with --with-project)
745
745
 
746
746
  [bold]Configuration relationship:[/]
747
747
  config.toml → Controls zwarm itself (tracing, which watchers run)
748
- codex.toml → Controls the Codex CLI that runs executor sessions
748
+ codex.toml → Codex settings, parsed by zwarm and passed via -c overrides
749
749
  zwarm.yaml → Project-specific context injected into orchestrator
750
750
 
751
751
  [bold]Examples:[/]
@@ -939,7 +939,7 @@ def init(
939
939
  # Explain config files
940
940
  console.print("[bold]Configuration files:[/]")
941
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)")
942
+ console.print(" [cyan].zwarm/codex.toml[/] - Codex CLI settings (model, web search, sandbox)")
943
943
  if create_project_config:
944
944
  console.print(" [cyan]zwarm.yaml[/] - Project context and constraints")
945
945
  console.print()
@@ -982,6 +982,7 @@ def _generate_config_toml(
982
982
  "[executor]",
983
983
  f'adapter = "{adapter}"',
984
984
  "# model = \"\" # Optional model override",
985
+ "# web_search = false # Enable web search for delegated sessions",
985
986
  "",
986
987
  "[watchers]",
987
988
  f"enabled = {watchers}",
@@ -1006,22 +1007,35 @@ def _generate_codex_toml(
1006
1007
  """
1007
1008
  Generate codex.toml for isolated codex configuration.
1008
1009
 
1009
- This file is used by zwarm instead of ~/.codex/config.toml to ensure
1010
- consistent behavior across different environments.
1010
+ This file is parsed by zwarm and settings are passed to codex via -c overrides.
1011
+ Each .zwarm directory has its own codex config, independent of ~/.codex/config.toml.
1011
1012
  """
1012
1013
  lines = [
1013
1014
  "# Codex configuration for zwarm",
1014
- "# This file isolates zwarm's codex settings from your global ~/.codex/config.toml",
1015
+ "# zwarm parses this file and passes settings to codex via -c overrides",
1016
+ "# Each .zwarm dir has its own config, independent of ~/.codex/config.toml",
1015
1017
  "# Generated by 'zwarm init'",
1016
1018
  "",
1017
1019
  "# Model settings",
1018
1020
  f'model = "{model}"',
1019
1021
  f'model_reasoning_effort = "{reasoning_effort}" # low | medium | high',
1020
1022
  "",
1021
- "# Approval settings - zwarm manages these automatically",
1022
- "# disable_response_storage = false",
1023
+ "# DANGER MODE - bypasses all safety controls",
1024
+ "# Set to true to use --dangerously-bypass-approvals-and-sandbox",
1025
+ "full_danger = true",
1026
+ "",
1027
+ "# Web search - enables web_search tool for agents",
1028
+ "[features]",
1029
+ "web_search_request = true",
1023
1030
  "",
1024
- "# You can override any codex setting here",
1031
+ "# Sandbox settings - network access required for web search",
1032
+ "[sandbox_workspace_write]",
1033
+ "network_access = true",
1034
+ "",
1035
+ "# Approval policy - 'never' means no human approval needed",
1036
+ "# approval_policy = \"never\"",
1037
+ "",
1038
+ "# You can add any codex config key here",
1025
1039
  "# See: https://github.com/openai/codex#configuration",
1026
1040
  "",
1027
1041
  ]
@@ -1534,6 +1548,7 @@ def session_start(
1534
1548
  Start a new Codex session in the background.
1535
1549
 
1536
1550
  The session runs independently and you can check on it later.
1551
+ Web search is always enabled via .codex/config.toml (set up by `zwarm init`).
1537
1552
 
1538
1553
  [bold]Examples:[/]
1539
1554
  [dim]# Simple task[/]
@@ -1541,6 +1556,9 @@ def session_start(
1541
1556
 
1542
1557
  [dim]# With specific model[/]
1543
1558
  $ zwarm session start "Refactor the API" --model gpt-5.1-codex-max
1559
+
1560
+ [dim]# Web search is always available[/]
1561
+ $ zwarm session start "Research latest OAuth2 best practices"
1544
1562
  """
1545
1563
  from zwarm.sessions import CodexSessionManager
1546
1564
 
zwarm/cli/pilot.py CHANGED
@@ -186,10 +186,12 @@ def build_pilot_orchestrator(
186
186
  lm_class = lm_map.get(lm_choice, GPT5LargeVerbose)
187
187
  lm = lm_class()
188
188
 
189
- # Load configuration
189
+ # Load configuration from working_dir (not cwd!)
190
+ # This ensures config.toml and .env are loaded from the project being worked on
190
191
  config = load_config(
191
192
  config_path=config_path,
192
193
  overrides=overrides,
194
+ working_dir=working_dir,
193
195
  )
194
196
 
195
197
  # Resolve working directory
@@ -592,6 +594,18 @@ def execute_step_with_events(
592
594
  """
593
595
  had_message = False
594
596
 
597
+ # Update environment with current progress before perceive
598
+ # This ensures the observation has fresh step/token counts
599
+ if hasattr(orchestrator, "env") and hasattr(orchestrator.env, "update_progress"):
600
+ total_tokens = getattr(orchestrator, "_total_tokens", 0)
601
+ executor_usage = orchestrator.get_executor_usage() if hasattr(orchestrator, "get_executor_usage") else {}
602
+ orchestrator.env.update_progress(
603
+ step_count=getattr(orchestrator, "_step_count", 0),
604
+ max_steps=getattr(orchestrator, "maxSteps", 50),
605
+ total_tokens=total_tokens,
606
+ executor_tokens=executor_usage.get("total_tokens", 0),
607
+ )
608
+
595
609
  # Execute perceive (updates environment observation)
596
610
  orchestrator.perceive()
597
611
 
@@ -647,7 +661,7 @@ def execute_step_with_events(
647
661
  def run_until_response(
648
662
  orchestrator: Any,
649
663
  renderer: EventRenderer,
650
- max_steps: int = 20,
664
+ max_steps: int = 60,
651
665
  ) -> List[tuple]:
652
666
  """
653
667
  Run the orchestrator until it produces a message response.
@@ -655,7 +669,7 @@ def run_until_response(
655
669
  Keeps stepping while the agent only produces tool calls.
656
670
  Stops when:
657
671
  - Agent produces a text message (returns to user)
658
- - Max steps reached
672
+ - Max steps reached (configurable via orchestrator.max_steps_per_turn)
659
673
  - Stop condition triggered
660
674
 
661
675
  This is wrapped as a weave.op to group all child calls per turn.
@@ -663,7 +677,7 @@ def run_until_response(
663
677
  Args:
664
678
  orchestrator: The orchestrator instance
665
679
  renderer: Event renderer for output
666
- max_steps: Safety limit on steps per turn
680
+ max_steps: Safety limit on steps per turn (default: 60)
667
681
 
668
682
  Returns:
669
683
  All tool results from the turn
@@ -701,6 +715,9 @@ def run_until_response(
701
715
  if not results:
702
716
  break
703
717
 
718
+ # Show session status at end of turn (if there are any sessions)
719
+ render_session_status(orchestrator, renderer)
720
+
704
721
  return all_results
705
722
 
706
723
  return _run_turn()
@@ -756,6 +773,38 @@ def get_sessions_snapshot(orchestrator: Any) -> Dict[str, Any]:
756
773
  return {"sessions": []}
757
774
 
758
775
 
776
+ def render_session_status(orchestrator: Any, renderer: EventRenderer) -> None:
777
+ """
778
+ Render a compact session status line if there are active sessions.
779
+
780
+ Shows: "Sessions: 2 running, 1 done, 0 failed"
781
+ Only displays if there are any sessions.
782
+ """
783
+ if not hasattr(orchestrator, "_session_manager"):
784
+ return
785
+
786
+ sessions = orchestrator._session_manager.list_sessions()
787
+ if not sessions:
788
+ return
789
+
790
+ running = sum(1 for s in sessions if s.status.value == "running")
791
+ completed = sum(1 for s in sessions if s.status.value == "completed")
792
+ failed = sum(1 for s in sessions if s.status.value == "failed")
793
+
794
+ # Build status line with colors
795
+ parts = []
796
+ if running > 0:
797
+ parts.append(f"[cyan]{running} running[/]")
798
+ if completed > 0:
799
+ parts.append(f"[green]{completed} done[/]")
800
+ if failed > 0:
801
+ parts.append(f"[red]{failed} failed[/]")
802
+
803
+ if parts:
804
+ status_line = ", ".join(parts)
805
+ console.print(f"[dim]Sessions:[/] {status_line}")
806
+
807
+
759
808
  def run_pilot(
760
809
  orchestrator: Any,
761
810
  *,
@@ -812,7 +861,8 @@ def _run_pilot_repl(
812
861
  })
813
862
 
814
863
  renderer.reset_turn()
815
- results = run_until_response(orchestrator, renderer)
864
+ max_steps = getattr(orchestrator.config.orchestrator, "max_steps_per_turn", 60)
865
+ results = run_until_response(orchestrator, renderer, max_steps=max_steps)
816
866
 
817
867
  # Record checkpoint
818
868
  state.record(
@@ -1101,8 +1151,9 @@ def _run_pilot_repl(
1101
1151
 
1102
1152
  # Execute steps until agent responds with a message
1103
1153
  renderer.reset_turn()
1154
+ max_steps = getattr(orchestrator.config.orchestrator, "max_steps_per_turn", 60)
1104
1155
  try:
1105
- results = run_until_response(orchestrator, renderer)
1156
+ results = run_until_response(orchestrator, renderer, max_steps=max_steps)
1106
1157
  except Exception as e:
1107
1158
  renderer.error(f"Step failed: {e}")
1108
1159
  # Remove the user message on failure
zwarm/core/config.py CHANGED
@@ -37,6 +37,7 @@ class ExecutorConfig:
37
37
  sandbox: str = "workspace-write" # read-only | workspace-write | danger-full-access
38
38
  timeout: int = 3600
39
39
  reasoning_effort: str | None = "high" # low | medium | high (default to high for compatibility)
40
+ # Note: web_search is always enabled via .codex/config.toml (set up by `zwarm init`)
40
41
 
41
42
 
42
43
  @dataclass
@@ -59,8 +60,8 @@ class OrchestratorConfig:
59
60
  prompt: str | None = None # path to prompt yaml
60
61
  tools: list[str] = field(default_factory=lambda: ["delegate", "converse", "check_session", "end_session", "bash"])
61
62
  max_steps: int = 50
63
+ max_steps_per_turn: int = 60 # Max tool-call steps before returning to user (pilot mode)
62
64
  parallel_delegations: int = 4
63
- sync_first: bool = True # prefer sync mode by default
64
65
  compaction: CompactionConfig = field(default_factory=CompactionConfig)
65
66
 
66
67
  # Directory restrictions for agent delegations
@@ -172,8 +173,8 @@ class ZwarmConfig:
172
173
  "prompt": self.orchestrator.prompt,
173
174
  "tools": self.orchestrator.tools,
174
175
  "max_steps": self.orchestrator.max_steps,
176
+ "max_steps_per_turn": self.orchestrator.max_steps_per_turn,
175
177
  "parallel_delegations": self.orchestrator.parallel_delegations,
176
- "sync_first": self.orchestrator.sync_first,
177
178
  "compaction": {
178
179
  "enabled": self.orchestrator.compaction.enabled,
179
180
  "max_tokens": self.orchestrator.compaction.max_tokens,
@@ -195,15 +196,16 @@ class ZwarmConfig:
195
196
  }
196
197
 
197
198
 
198
- def load_env(path: Path | None = None) -> None:
199
+ def load_env(path: Path | None = None, base_dir: Path | None = None) -> None:
199
200
  """Load .env file if it exists."""
200
201
  if path is None:
201
- path = Path.cwd() / ".env"
202
+ base = base_dir or Path.cwd()
203
+ path = base / ".env"
202
204
  if path.exists():
203
205
  load_dotenv(path)
204
206
 
205
207
 
206
- def load_toml_config(path: Path | None = None) -> dict[str, Any]:
208
+ def load_toml_config(path: Path | None = None, base_dir: Path | None = None) -> dict[str, Any]:
207
209
  """
208
210
  Load config.toml file.
209
211
 
@@ -211,11 +213,16 @@ def load_toml_config(path: Path | None = None) -> dict[str, Any]:
211
213
  1. Explicit path (if provided)
212
214
  2. .zwarm/config.toml (new standard location)
213
215
  3. config.toml (legacy location for backwards compat)
216
+
217
+ Args:
218
+ path: Explicit path to config.toml
219
+ base_dir: Base directory to search in (defaults to cwd)
214
220
  """
215
221
  if path is None:
222
+ base = base_dir or Path.cwd()
216
223
  # Try new location first
217
- new_path = Path.cwd() / ".zwarm" / "config.toml"
218
- legacy_path = Path.cwd() / "config.toml"
224
+ new_path = base / ".zwarm" / "config.toml"
225
+ legacy_path = base / "config.toml"
219
226
  if new_path.exists():
220
227
  path = new_path
221
228
  elif legacy_path.exists():
@@ -306,6 +313,7 @@ def load_config(
306
313
  toml_path: Path | None = None,
307
314
  env_path: Path | None = None,
308
315
  overrides: list[str] | None = None,
316
+ working_dir: Path | None = None,
309
317
  ) -> ZwarmConfig:
310
318
  """
311
319
  Load configuration with full precedence chain:
@@ -314,15 +322,24 @@ def load_config(
314
322
  3. YAML config file (if provided)
315
323
  4. CLI overrides (--set key=value)
316
324
  5. Environment variables (for secrets)
325
+
326
+ Args:
327
+ config_path: Path to YAML config file
328
+ toml_path: Explicit path to config.toml
329
+ env_path: Explicit path to .env file
330
+ overrides: CLI overrides (--set key=value)
331
+ working_dir: Working directory to search for config files (defaults to cwd).
332
+ This is important when using --working-dir flag to ensure
333
+ config is loaded from the project directory, not invoke directory.
317
334
  """
318
335
  # Load .env first (for secrets)
319
- load_env(env_path)
336
+ load_env(env_path, base_dir=working_dir)
320
337
 
321
338
  # Start with defaults
322
339
  config_dict: dict[str, Any] = {}
323
340
 
324
341
  # Layer in config.toml
325
- toml_config = load_toml_config(toml_path)
342
+ toml_config = load_toml_config(toml_path, base_dir=working_dir)
326
343
  if toml_config:
327
344
  config_dict = deep_merge(config_dict, toml_config)
328
345
 
zwarm/core/test_config.py CHANGED
@@ -20,7 +20,6 @@ def test_default_config():
20
20
  assert config.executor.adapter == "codex_mcp"
21
21
  assert config.executor.sandbox == "workspace-write"
22
22
  assert config.orchestrator.lm == "gpt-5-mini"
23
- assert config.orchestrator.sync_first is True
24
23
  assert config.state_dir == ".zwarm"
25
24
 
26
25
 
@@ -68,8 +67,8 @@ def test_apply_overrides():
68
67
  assert result["executor"]["adapter"] == "claude_code"
69
68
 
70
69
  # Override with boolean
71
- result = apply_overrides(config, ["orchestrator.sync_first=false"])
72
- assert result["orchestrator"]["sync_first"] is False
70
+ result = apply_overrides(config, ["executor.web_search=true"])
71
+ assert result["executor"]["web_search"] is True
73
72
 
74
73
  # Create new nested path
75
74
  result = apply_overrides(config, ["weave.project=my-project"])
zwarm/orchestrator.py CHANGED
@@ -23,7 +23,6 @@ from wbal.helper import TOOL_CALL_TYPE, format_openai_tool_response
23
23
  from wbal.lm import LM as wbalLMGeneric
24
24
  from wbal.lm import GPT5LargeVerbose
25
25
 
26
- from zwarm.adapters import ExecutorAdapter, get_adapter
27
26
  from zwarm.core.compact import compact_messages, should_compact
28
27
  from zwarm.core.config import ZwarmConfig, load_config
29
28
  from zwarm.core.environment import OrchestratorEnv
@@ -72,7 +71,6 @@ class Orchestrator(YamlAgent):
72
71
  # State management
73
72
  _state: StateManager = PrivateAttr()
74
73
  _sessions: dict[str, ConversationSession] = PrivateAttr(default_factory=dict)
75
- _adapters: dict[str, ExecutorAdapter] = PrivateAttr(default_factory=dict)
76
74
  _watcher_manager: WatcherManager | None = PrivateAttr(default=None)
77
75
  _resumed: bool = PrivateAttr(default=False)
78
76
  _total_tokens: int = PrivateAttr(default=0) # Cumulative orchestrator tokens
@@ -83,9 +81,11 @@ class Orchestrator(YamlAgent):
83
81
  "total_tokens": 0,
84
82
  }
85
83
  )
84
+ # Callback for step progress (used by CLI to print tool calls)
85
+ _step_callback: Callable[[int, list[tuple[dict[str, Any], Any]]], None] | None = PrivateAttr(default=None)
86
86
 
87
87
  def model_post_init(self, __context: Any) -> None:
88
- """Initialize state and adapters after model creation."""
88
+ """Initialize state after model creation."""
89
89
  super().model_post_init(__context)
90
90
 
91
91
  # Initialize state manager with instance isolation
@@ -151,40 +151,9 @@ class Orchestrator(YamlAgent):
151
151
  """Access state manager."""
152
152
  return self._state
153
153
 
154
- def _get_adapter(self, name: str) -> ExecutorAdapter:
155
- """Get or create an adapter by name using the adapter registry."""
156
- if name not in self._adapters:
157
- # Get model from config (adapters have their own defaults if None)
158
- model = self.config.executor.model
159
-
160
- # Use isolated codex config if available
161
- config_path = self.working_dir / self.config.state_dir / "codex.toml"
162
- if not config_path.exists():
163
- config_path = None # Fallback to adapter defaults
164
-
165
- self._adapters[name] = get_adapter(
166
- name, model=model, config_path=config_path
167
- )
168
- return self._adapters[name]
169
-
170
154
  def get_executor_usage(self) -> dict[str, int]:
171
- """Get aggregated token usage across all executors."""
172
- total = {
173
- "input_tokens": 0,
174
- "output_tokens": 0,
175
- "total_tokens": 0,
176
- }
177
- for adapter in self._adapters.values():
178
- if hasattr(adapter, "total_usage"):
179
- usage = adapter.total_usage
180
- for key in total:
181
- total[key] += usage.get(key, 0)
182
- return total
183
-
184
- @property
185
- def executor_usage(self) -> dict[str, int]:
186
- """Aggregated executor token usage (for Weave tracking)."""
187
- return self.get_executor_usage()
155
+ """Get aggregated token usage from executor sessions."""
156
+ return self._executor_usage
188
157
 
189
158
  def save_state(self) -> None:
190
159
  """Save orchestrator state for resume."""
@@ -587,7 +556,11 @@ Review what was accomplished in the previous session and delegate new tasks as n
587
556
  }
588
557
  # NUDGE and CONTINUE just continue
589
558
 
590
- self.step()
559
+ tool_results = self.step()
560
+
561
+ # Call step callback if registered (for CLI progress display)
562
+ if self._step_callback:
563
+ self._step_callback(self._step_count, tool_results)
591
564
 
592
565
  if self.stopCondition:
593
566
  break
@@ -599,8 +572,7 @@ Review what was accomplished in the previous session and delegate new tasks as n
599
572
 
600
573
  async def cleanup(self) -> None:
601
574
  """Clean up resources."""
602
- for adapter in self._adapters.values():
603
- await adapter.cleanup()
575
+ pass # Session cleanup handled by CodexSessionManager
604
576
 
605
577
 
606
578
  def build_orchestrator(
@@ -631,15 +603,17 @@ def build_orchestrator(
631
603
  """
632
604
  from uuid import uuid4
633
605
 
634
- # Load configuration
606
+ # Resolve working directory first (needed for config loading)
607
+ working_dir = working_dir or Path.cwd()
608
+
609
+ # Load configuration from working_dir (not cwd!)
610
+ # This ensures config.toml and .env are loaded from the project being worked on
635
611
  config = load_config(
636
612
  config_path=config_path,
637
613
  overrides=overrides,
614
+ working_dir=working_dir,
638
615
  )
639
616
 
640
- # Resolve working directory
641
- working_dir = working_dir or Path.cwd()
642
-
643
617
  # Generate instance ID if not provided (enables isolation by default for new runs)
644
618
  # For resume, instance_id should be provided explicitly
645
619
  if instance_id is None and not resume: