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.
- {zwarm-3.2.1 → zwarm-3.4.0}/PKG-INFO +6 -3
- {zwarm-3.2.1 → zwarm-3.4.0}/README.md +5 -2
- {zwarm-3.2.1 → zwarm-3.4.0}/pyproject.toml +1 -1
- {zwarm-3.2.1 → zwarm-3.4.0}/src/zwarm/cli/interactive.py +3 -3
- {zwarm-3.2.1 → zwarm-3.4.0}/src/zwarm/cli/main.py +95 -77
- {zwarm-3.2.1 → zwarm-3.4.0}/src/zwarm/cli/pilot.py +57 -6
- {zwarm-3.2.1 → zwarm-3.4.0}/src/zwarm/core/config.py +26 -9
- {zwarm-3.2.1 → zwarm-3.4.0}/src/zwarm/core/test_config.py +2 -3
- {zwarm-3.2.1 → zwarm-3.4.0}/src/zwarm/orchestrator.py +17 -43
- {zwarm-3.2.1 → zwarm-3.4.0}/src/zwarm/sessions/manager.py +210 -90
- {zwarm-3.2.1 → zwarm-3.4.0}/src/zwarm/tools/delegation.py +6 -1
- zwarm-3.2.1/src/zwarm/adapters/__init__.py +0 -21
- zwarm-3.2.1/src/zwarm/adapters/base.py +0 -109
- zwarm-3.2.1/src/zwarm/adapters/claude_code.py +0 -357
- zwarm-3.2.1/src/zwarm/adapters/codex_mcp.py +0 -1262
- zwarm-3.2.1/src/zwarm/adapters/registry.py +0 -69
- zwarm-3.2.1/src/zwarm/adapters/test_codex_mcp.py +0 -274
- zwarm-3.2.1/src/zwarm/adapters/test_registry.py +0 -68
- {zwarm-3.2.1 → zwarm-3.4.0}/.gitignore +0 -0
- {zwarm-3.2.1 → zwarm-3.4.0}/src/zwarm/__init__.py +0 -0
- {zwarm-3.2.1 → zwarm-3.4.0}/src/zwarm/cli/__init__.py +0 -0
- {zwarm-3.2.1 → zwarm-3.4.0}/src/zwarm/core/__init__.py +0 -0
- {zwarm-3.2.1 → zwarm-3.4.0}/src/zwarm/core/checkpoints.py +0 -0
- {zwarm-3.2.1 → zwarm-3.4.0}/src/zwarm/core/compact.py +0 -0
- {zwarm-3.2.1 → zwarm-3.4.0}/src/zwarm/core/costs.py +0 -0
- {zwarm-3.2.1 → zwarm-3.4.0}/src/zwarm/core/environment.py +0 -0
- {zwarm-3.2.1 → zwarm-3.4.0}/src/zwarm/core/models.py +0 -0
- {zwarm-3.2.1 → zwarm-3.4.0}/src/zwarm/core/state.py +0 -0
- {zwarm-3.2.1 → zwarm-3.4.0}/src/zwarm/core/test_compact.py +0 -0
- {zwarm-3.2.1 → zwarm-3.4.0}/src/zwarm/core/test_models.py +0 -0
- {zwarm-3.2.1 → zwarm-3.4.0}/src/zwarm/prompts/__init__.py +0 -0
- {zwarm-3.2.1 → zwarm-3.4.0}/src/zwarm/prompts/orchestrator.py +0 -0
- {zwarm-3.2.1 → zwarm-3.4.0}/src/zwarm/prompts/pilot.py +0 -0
- {zwarm-3.2.1 → zwarm-3.4.0}/src/zwarm/sessions/__init__.py +0 -0
- {zwarm-3.2.1 → zwarm-3.4.0}/src/zwarm/test_orchestrator_watchers.py +0 -0
- {zwarm-3.2.1 → zwarm-3.4.0}/src/zwarm/tools/__init__.py +0 -0
- {zwarm-3.2.1 → zwarm-3.4.0}/src/zwarm/watchers/__init__.py +0 -0
- {zwarm-3.2.1 → zwarm-3.4.0}/src/zwarm/watchers/base.py +0 -0
- {zwarm-3.2.1 → zwarm-3.4.0}/src/zwarm/watchers/builtin.py +0 -0
- {zwarm-3.2.1 → zwarm-3.4.0}/src/zwarm/watchers/llm_watcher.py +0 -0
- {zwarm-3.2.1 → zwarm-3.4.0}/src/zwarm/watchers/manager.py +0 -0
- {zwarm-3.2.1 → zwarm-3.4.0}/src/zwarm/watchers/registry.py +0 -0
- {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.
|
|
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"]
|
|
@@ -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
|
|
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
|
|
402
|
+
Run a single Codex session directly (for testing).
|
|
394
403
|
|
|
395
|
-
|
|
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]#
|
|
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]#
|
|
402
|
-
$ zwarm exec
|
|
411
|
+
[dim]# Run in background[/]
|
|
412
|
+
$ zwarm exec --task "Build feature"
|
|
403
413
|
|
|
404
|
-
[dim]#
|
|
405
|
-
$ zwarm exec --task "
|
|
414
|
+
[dim]# Web search is always available[/]
|
|
415
|
+
$ zwarm exec --task "Find latest FastAPI docs" --wait
|
|
406
416
|
"""
|
|
407
|
-
|
|
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
|
-
|
|
441
|
-
|
|
442
|
-
|
|
443
|
-
|
|
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
|
-
|
|
448
|
-
|
|
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
|
-
|
|
456
|
-
|
|
428
|
+
session = manager.start_session(
|
|
429
|
+
task=task,
|
|
430
|
+
working_dir=working_dir.absolute(),
|
|
431
|
+
model=effective_model,
|
|
432
|
+
)
|
|
457
433
|
|
|
458
|
-
|
|
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,
|
|
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 →
|
|
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,
|
|
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
|
|
1010
|
-
|
|
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
|
-
"#
|
|
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
|
-
"#
|
|
1022
|
-
"#
|
|
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
|
-
"#
|
|
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 =
|
|
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
|
-
|
|
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
|
-
|
|
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 =
|
|
218
|
-
legacy_path =
|
|
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, ["
|
|
72
|
-
assert result["
|
|
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"])
|