zwarm 3.2.1__tar.gz → 3.4.0__tar.gz

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.
Files changed (43) hide show
  1. {zwarm-3.2.1 → zwarm-3.4.0}/PKG-INFO +6 -3
  2. {zwarm-3.2.1 → zwarm-3.4.0}/README.md +5 -2
  3. {zwarm-3.2.1 → zwarm-3.4.0}/pyproject.toml +1 -1
  4. {zwarm-3.2.1 → zwarm-3.4.0}/src/zwarm/cli/interactive.py +3 -3
  5. {zwarm-3.2.1 → zwarm-3.4.0}/src/zwarm/cli/main.py +95 -77
  6. {zwarm-3.2.1 → zwarm-3.4.0}/src/zwarm/cli/pilot.py +57 -6
  7. {zwarm-3.2.1 → zwarm-3.4.0}/src/zwarm/core/config.py +26 -9
  8. {zwarm-3.2.1 → zwarm-3.4.0}/src/zwarm/core/test_config.py +2 -3
  9. {zwarm-3.2.1 → zwarm-3.4.0}/src/zwarm/orchestrator.py +17 -43
  10. {zwarm-3.2.1 → zwarm-3.4.0}/src/zwarm/sessions/manager.py +210 -90
  11. {zwarm-3.2.1 → zwarm-3.4.0}/src/zwarm/tools/delegation.py +6 -1
  12. zwarm-3.2.1/src/zwarm/adapters/__init__.py +0 -21
  13. zwarm-3.2.1/src/zwarm/adapters/base.py +0 -109
  14. zwarm-3.2.1/src/zwarm/adapters/claude_code.py +0 -357
  15. zwarm-3.2.1/src/zwarm/adapters/codex_mcp.py +0 -1262
  16. zwarm-3.2.1/src/zwarm/adapters/registry.py +0 -69
  17. zwarm-3.2.1/src/zwarm/adapters/test_codex_mcp.py +0 -274
  18. zwarm-3.2.1/src/zwarm/adapters/test_registry.py +0 -68
  19. {zwarm-3.2.1 → zwarm-3.4.0}/.gitignore +0 -0
  20. {zwarm-3.2.1 → zwarm-3.4.0}/src/zwarm/__init__.py +0 -0
  21. {zwarm-3.2.1 → zwarm-3.4.0}/src/zwarm/cli/__init__.py +0 -0
  22. {zwarm-3.2.1 → zwarm-3.4.0}/src/zwarm/core/__init__.py +0 -0
  23. {zwarm-3.2.1 → zwarm-3.4.0}/src/zwarm/core/checkpoints.py +0 -0
  24. {zwarm-3.2.1 → zwarm-3.4.0}/src/zwarm/core/compact.py +0 -0
  25. {zwarm-3.2.1 → zwarm-3.4.0}/src/zwarm/core/costs.py +0 -0
  26. {zwarm-3.2.1 → zwarm-3.4.0}/src/zwarm/core/environment.py +0 -0
  27. {zwarm-3.2.1 → zwarm-3.4.0}/src/zwarm/core/models.py +0 -0
  28. {zwarm-3.2.1 → zwarm-3.4.0}/src/zwarm/core/state.py +0 -0
  29. {zwarm-3.2.1 → zwarm-3.4.0}/src/zwarm/core/test_compact.py +0 -0
  30. {zwarm-3.2.1 → zwarm-3.4.0}/src/zwarm/core/test_models.py +0 -0
  31. {zwarm-3.2.1 → zwarm-3.4.0}/src/zwarm/prompts/__init__.py +0 -0
  32. {zwarm-3.2.1 → zwarm-3.4.0}/src/zwarm/prompts/orchestrator.py +0 -0
  33. {zwarm-3.2.1 → zwarm-3.4.0}/src/zwarm/prompts/pilot.py +0 -0
  34. {zwarm-3.2.1 → zwarm-3.4.0}/src/zwarm/sessions/__init__.py +0 -0
  35. {zwarm-3.2.1 → zwarm-3.4.0}/src/zwarm/test_orchestrator_watchers.py +0 -0
  36. {zwarm-3.2.1 → zwarm-3.4.0}/src/zwarm/tools/__init__.py +0 -0
  37. {zwarm-3.2.1 → zwarm-3.4.0}/src/zwarm/watchers/__init__.py +0 -0
  38. {zwarm-3.2.1 → zwarm-3.4.0}/src/zwarm/watchers/base.py +0 -0
  39. {zwarm-3.2.1 → zwarm-3.4.0}/src/zwarm/watchers/builtin.py +0 -0
  40. {zwarm-3.2.1 → zwarm-3.4.0}/src/zwarm/watchers/llm_watcher.py +0 -0
  41. {zwarm-3.2.1 → zwarm-3.4.0}/src/zwarm/watchers/manager.py +0 -0
  42. {zwarm-3.2.1 → zwarm-3.4.0}/src/zwarm/watchers/registry.py +0 -0
  43. {zwarm-3.2.1 → zwarm-3.4.0}/src/zwarm/watchers/test_watchers.py +0 -0
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: zwarm
3
- Version: 3.2.1
3
+ Version: 3.4.0
4
4
  Summary: Multi-Agent CLI Orchestration Research Platform
5
5
  Requires-Python: <3.14,>=3.13
6
6
  Requires-Dist: prompt-toolkit>=3.0.52
@@ -161,7 +161,7 @@ zwarm interactive
161
161
 
162
162
  | Command | Description |
163
163
  |---------|-------------|
164
- | `spawn "task"` | Start a new session |
164
+ | `spawn "task" [--search]` | Start a new session (--search enables web) |
165
165
  | `ls` | Dashboard of all sessions (with costs) |
166
166
  | `? ID` / `peek ID` | Quick status check |
167
167
  | `show ID` | Full session details |
@@ -225,7 +225,7 @@ The orchestrator LLM has access to:
225
225
 
226
226
  | Tool | Description |
227
227
  |------|-------------|
228
- | `delegate(task)` | Start a new coding session |
228
+ | `delegate(task, web_search=False)` | Start a new coding session |
229
229
  | `converse(id, msg)` | Continue a session |
230
230
  | `check_session(id)` | Get full session details |
231
231
  | `peek_session(id)` | Quick status check |
@@ -235,6 +235,8 @@ The orchestrator LLM has access to:
235
235
 
236
236
  **Async-first**: All sessions run in the background. The orchestrator uses `sleep()` to wait, then checks on progress.
237
237
 
238
+ **Web Search**: Pass `web_search=True` to `delegate()` for tasks needing current info (API docs, latest releases, etc.).
239
+
238
240
  ### Watchers
239
241
 
240
242
  Watchers monitor the orchestrator and intervene when needed:
@@ -317,6 +319,7 @@ max_steps = 50
317
319
 
318
320
  [executor]
319
321
  adapter = "codex_mcp"
322
+ web_search = false # Enable web search for all delegated sessions
320
323
 
321
324
  [watchers]
322
325
  enabled = ["progress", "budget", "delegation", "delegation_reminder"]
@@ -148,7 +148,7 @@ zwarm interactive
148
148
 
149
149
  | Command | Description |
150
150
  |---------|-------------|
151
- | `spawn "task"` | Start a new session |
151
+ | `spawn "task" [--search]` | Start a new session (--search enables web) |
152
152
  | `ls` | Dashboard of all sessions (with costs) |
153
153
  | `? ID` / `peek ID` | Quick status check |
154
154
  | `show ID` | Full session details |
@@ -212,7 +212,7 @@ The orchestrator LLM has access to:
212
212
 
213
213
  | Tool | Description |
214
214
  |------|-------------|
215
- | `delegate(task)` | Start a new coding session |
215
+ | `delegate(task, web_search=False)` | Start a new coding session |
216
216
  | `converse(id, msg)` | Continue a session |
217
217
  | `check_session(id)` | Get full session details |
218
218
  | `peek_session(id)` | Quick status check |
@@ -222,6 +222,8 @@ The orchestrator LLM has access to:
222
222
 
223
223
  **Async-first**: All sessions run in the background. The orchestrator uses `sleep()` to wait, then checks on progress.
224
224
 
225
+ **Web Search**: Pass `web_search=True` to `delegate()` for tasks needing current info (API docs, latest releases, etc.).
226
+
225
227
  ### Watchers
226
228
 
227
229
  Watchers monitor the orchestrator and intervene when needed:
@@ -304,6 +306,7 @@ max_steps = 50
304
306
 
305
307
  [executor]
306
308
  adapter = "codex_mcp"
309
+ web_search = false # Enable web search for all delegated sessions
307
310
 
308
311
  [watchers]
309
312
  enabled = ["progress", "budget", "delegation", "delegation_reminder"]
@@ -1,6 +1,6 @@
1
1
  [project]
2
2
  name = "zwarm"
3
- version = "3.2.1"
3
+ version = "3.4.0"
4
4
  description = "Multi-Agent CLI Orchestration Research Platform"
5
5
  readme = "README.md"
6
6
  requires-python = ">=3.13,<3.14"
@@ -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 = []
@@ -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
 
@@ -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
@@ -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
 
@@ -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"])