zwarm 3.2.1__py3-none-any.whl → 3.3.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 +2 -2
- zwarm/cli/main.py +75 -77
- zwarm/cli/pilot.py +3 -1
- zwarm/core/config.py +24 -9
- zwarm/core/test_config.py +2 -3
- zwarm/orchestrator.py +8 -44
- zwarm/sessions/manager.py +210 -90
- zwarm/tools/delegation.py +6 -1
- {zwarm-3.2.1.dist-info → zwarm-3.3.0.dist-info}/METADATA +6 -3
- {zwarm-3.2.1.dist-info → zwarm-3.3.0.dist-info}/RECORD +12 -19
- zwarm/adapters/__init__.py +0 -21
- zwarm/adapters/base.py +0 -109
- zwarm/adapters/claude_code.py +0 -357
- zwarm/adapters/codex_mcp.py +0 -1262
- zwarm/adapters/registry.py +0 -69
- zwarm/adapters/test_codex_mcp.py +0 -274
- zwarm/adapters/test_registry.py +0 -68
- {zwarm-3.2.1.dist-info → zwarm-3.3.0.dist-info}/WHEEL +0 -0
- {zwarm-3.2.1.dist-info → zwarm-3.3.0.dist-info}/entry_points.txt +0 -0
zwarm/sessions/manager.py
CHANGED
|
@@ -6,6 +6,7 @@ Architecture:
|
|
|
6
6
|
- Output is streamed to .zwarm/sessions/<session_id>/output.jsonl
|
|
7
7
|
- Session metadata stored in meta.json
|
|
8
8
|
- Can inject follow-up messages by starting new turns with context
|
|
9
|
+
- Config is read from .zwarm/codex.toml and passed via -c overrides
|
|
9
10
|
"""
|
|
10
11
|
|
|
11
12
|
from __future__ import annotations
|
|
@@ -15,6 +16,7 @@ import os
|
|
|
15
16
|
import signal
|
|
16
17
|
import subprocess
|
|
17
18
|
import time
|
|
19
|
+
import tomllib
|
|
18
20
|
from dataclasses import dataclass, field
|
|
19
21
|
from datetime import datetime
|
|
20
22
|
from enum import Enum
|
|
@@ -25,16 +27,18 @@ from uuid import uuid4
|
|
|
25
27
|
|
|
26
28
|
class SessionStatus(str, Enum):
|
|
27
29
|
"""Status of a codex session."""
|
|
28
|
-
|
|
29
|
-
|
|
30
|
+
|
|
31
|
+
PENDING = "pending" # Created but not started
|
|
32
|
+
RUNNING = "running" # Process is running
|
|
30
33
|
COMPLETED = "completed" # Process exited successfully
|
|
31
|
-
FAILED = "failed"
|
|
32
|
-
KILLED = "killed"
|
|
34
|
+
FAILED = "failed" # Process exited with error
|
|
35
|
+
KILLED = "killed" # Manually killed
|
|
33
36
|
|
|
34
37
|
|
|
35
38
|
@dataclass
|
|
36
39
|
class SessionMessage:
|
|
37
40
|
"""A message in a session's history."""
|
|
41
|
+
|
|
38
42
|
role: str # "user", "assistant", "system", "tool"
|
|
39
43
|
content: str
|
|
40
44
|
timestamp: str = ""
|
|
@@ -61,6 +65,7 @@ class SessionMessage:
|
|
|
61
65
|
@dataclass
|
|
62
66
|
class CodexSession:
|
|
63
67
|
"""A managed Codex session."""
|
|
68
|
+
|
|
64
69
|
id: str
|
|
65
70
|
task: str
|
|
66
71
|
status: SessionStatus
|
|
@@ -180,6 +185,48 @@ class CodexSessionManager:
|
|
|
180
185
|
self.sessions_dir = self.state_dir / "sessions"
|
|
181
186
|
self.sessions_dir.mkdir(parents=True, exist_ok=True)
|
|
182
187
|
|
|
188
|
+
def _load_codex_config(self) -> dict[str, Any]:
|
|
189
|
+
"""
|
|
190
|
+
Load codex.toml from state_dir.
|
|
191
|
+
|
|
192
|
+
Returns parsed TOML as dict, or empty dict if not found.
|
|
193
|
+
"""
|
|
194
|
+
codex_toml = self.state_dir / "codex.toml"
|
|
195
|
+
if not codex_toml.exists():
|
|
196
|
+
return {}
|
|
197
|
+
try:
|
|
198
|
+
with open(codex_toml, "rb") as f:
|
|
199
|
+
return tomllib.load(f)
|
|
200
|
+
except Exception:
|
|
201
|
+
return {}
|
|
202
|
+
|
|
203
|
+
def _build_codex_overrides(self, config: dict[str, Any]) -> list[str]:
|
|
204
|
+
"""
|
|
205
|
+
Convert codex.toml config to -c override flags.
|
|
206
|
+
|
|
207
|
+
Handles nested sections like [features] and [sandbox_workspace_write].
|
|
208
|
+
|
|
209
|
+
Returns list of ["-c", "key=value", "-c", "key2=value2", ...]
|
|
210
|
+
"""
|
|
211
|
+
overrides = []
|
|
212
|
+
|
|
213
|
+
def add_override(key: str, value: Any):
|
|
214
|
+
"""Add a -c override for a key=value pair."""
|
|
215
|
+
if isinstance(value, bool):
|
|
216
|
+
value = "true" if value else "false"
|
|
217
|
+
overrides.extend(["-c", f"{key}={value}"])
|
|
218
|
+
|
|
219
|
+
for key, value in config.items():
|
|
220
|
+
if isinstance(value, dict):
|
|
221
|
+
# Nested section like [features] or [sandbox_workspace_write]
|
|
222
|
+
for subkey, subvalue in value.items():
|
|
223
|
+
add_override(f"{key}.{subkey}", subvalue)
|
|
224
|
+
else:
|
|
225
|
+
# Top-level key
|
|
226
|
+
add_override(key, value)
|
|
227
|
+
|
|
228
|
+
return overrides
|
|
229
|
+
|
|
183
230
|
def _session_dir(self, session_id: str) -> Path:
|
|
184
231
|
"""Get the directory for a session."""
|
|
185
232
|
return self.sessions_dir / session_id
|
|
@@ -228,7 +275,10 @@ class CodexSessionManager:
|
|
|
228
275
|
# Update status if process died OR output indicates completion
|
|
229
276
|
# (output check is more reliable than PID check due to PID reuse)
|
|
230
277
|
if session.status == SessionStatus.RUNNING:
|
|
231
|
-
if
|
|
278
|
+
if (
|
|
279
|
+
self._is_output_complete(session.id, session.turn)
|
|
280
|
+
or not session.is_running
|
|
281
|
+
):
|
|
232
282
|
self._update_session_status(session)
|
|
233
283
|
|
|
234
284
|
if status is None or session.status == status:
|
|
@@ -244,7 +294,10 @@ class CodexSessionManager:
|
|
|
244
294
|
session = self._load_session(session_id)
|
|
245
295
|
if session:
|
|
246
296
|
if session.status == SessionStatus.RUNNING:
|
|
247
|
-
if
|
|
297
|
+
if (
|
|
298
|
+
self._is_output_complete(session.id, session.turn)
|
|
299
|
+
or not session.is_running
|
|
300
|
+
):
|
|
248
301
|
self._update_session_status(session)
|
|
249
302
|
return session
|
|
250
303
|
|
|
@@ -254,7 +307,10 @@ class CodexSessionManager:
|
|
|
254
307
|
session = self._load_session(session_dir.name)
|
|
255
308
|
if session:
|
|
256
309
|
if session.status == SessionStatus.RUNNING:
|
|
257
|
-
if
|
|
310
|
+
if (
|
|
311
|
+
self._is_output_complete(session.id, session.turn)
|
|
312
|
+
or not session.is_running
|
|
313
|
+
):
|
|
258
314
|
self._update_session_status(session)
|
|
259
315
|
return session
|
|
260
316
|
|
|
@@ -280,7 +336,12 @@ class CodexSessionManager:
|
|
|
280
336
|
event = json.loads(line)
|
|
281
337
|
event_type = event.get("type", "")
|
|
282
338
|
# Check for any completion marker
|
|
283
|
-
if event_type in (
|
|
339
|
+
if event_type in (
|
|
340
|
+
"turn.completed",
|
|
341
|
+
"task.completed",
|
|
342
|
+
"completed",
|
|
343
|
+
"done",
|
|
344
|
+
):
|
|
284
345
|
return True
|
|
285
346
|
# Also check for error as a form of completion
|
|
286
347
|
if event_type == "error":
|
|
@@ -325,7 +386,7 @@ class CodexSessionManager:
|
|
|
325
386
|
self,
|
|
326
387
|
task: str,
|
|
327
388
|
working_dir: Path | None = None,
|
|
328
|
-
model: str =
|
|
389
|
+
model: str | None = None,
|
|
329
390
|
sandbox: str = "workspace-write",
|
|
330
391
|
source: str = "user",
|
|
331
392
|
adapter: str = "codex",
|
|
@@ -336,18 +397,31 @@ class CodexSessionManager:
|
|
|
336
397
|
Args:
|
|
337
398
|
task: The task description
|
|
338
399
|
working_dir: Working directory for codex (default: cwd)
|
|
339
|
-
model: Model
|
|
340
|
-
sandbox: Sandbox mode
|
|
400
|
+
model: Model override (default: from codex.toml or gpt-5.1-codex-mini)
|
|
401
|
+
sandbox: Sandbox mode (ignored if full_danger=true in codex.toml)
|
|
341
402
|
source: Who spawned this session ("user" or "orchestrator:<id>")
|
|
342
403
|
adapter: Which adapter to use ("codex", "claude_code")
|
|
343
404
|
|
|
344
405
|
Returns:
|
|
345
406
|
The created session
|
|
407
|
+
|
|
408
|
+
Note:
|
|
409
|
+
Settings are read from .zwarm/codex.toml and passed via -c overrides.
|
|
410
|
+
Run `zwarm init` to set up the config.
|
|
346
411
|
"""
|
|
347
412
|
session_id = str(uuid4())
|
|
348
413
|
working_dir = working_dir or Path.cwd()
|
|
349
414
|
now = datetime.now().isoformat()
|
|
350
415
|
|
|
416
|
+
# Load codex config from .zwarm/codex.toml
|
|
417
|
+
codex_config = self._load_codex_config()
|
|
418
|
+
|
|
419
|
+
# Get model from config or use default
|
|
420
|
+
effective_model = model or codex_config.get("model", "gpt-5.1-codex-mini")
|
|
421
|
+
|
|
422
|
+
# Check if full_danger mode is enabled
|
|
423
|
+
full_danger = codex_config.get("full_danger", False)
|
|
424
|
+
|
|
351
425
|
session = CodexSession(
|
|
352
426
|
id=session_id,
|
|
353
427
|
task=task,
|
|
@@ -355,7 +429,7 @@ class CodexSessionManager:
|
|
|
355
429
|
working_dir=working_dir,
|
|
356
430
|
created_at=now,
|
|
357
431
|
updated_at=now,
|
|
358
|
-
model=
|
|
432
|
+
model=effective_model,
|
|
359
433
|
turn=1,
|
|
360
434
|
messages=[SessionMessage(role="user", content=task, timestamp=now)],
|
|
361
435
|
source=source,
|
|
@@ -366,22 +440,28 @@ class CodexSessionManager:
|
|
|
366
440
|
session_dir = self._session_dir(session_id)
|
|
367
441
|
session_dir.mkdir(parents=True, exist_ok=True)
|
|
368
442
|
|
|
369
|
-
# Build command
|
|
370
|
-
|
|
371
|
-
|
|
443
|
+
# Build command with -c overrides from codex.toml
|
|
444
|
+
# This ensures each .zwarm dir has its own codex config
|
|
445
|
+
cmd = ["codex"]
|
|
446
|
+
|
|
447
|
+
# Add -c overrides from codex.toml (excluding special keys we handle separately)
|
|
448
|
+
config_for_overrides = {k: v for k, v in codex_config.items() if k not in ("model", "full_danger")}
|
|
449
|
+
cmd.extend(self._build_codex_overrides(config_for_overrides))
|
|
450
|
+
|
|
451
|
+
# Add exec command and flags
|
|
452
|
+
cmd.extend([
|
|
453
|
+
"exec",
|
|
372
454
|
"--json",
|
|
373
|
-
"--full-auto",
|
|
374
455
|
"--skip-git-repo-check",
|
|
375
|
-
"--model",
|
|
376
|
-
|
|
377
|
-
|
|
378
|
-
|
|
379
|
-
|
|
380
|
-
|
|
456
|
+
"--model",
|
|
457
|
+
effective_model,
|
|
458
|
+
"-C",
|
|
459
|
+
str(working_dir.absolute()),
|
|
460
|
+
])
|
|
461
|
+
|
|
462
|
+
# Full danger mode bypasses all safety controls
|
|
463
|
+
if full_danger:
|
|
381
464
|
cmd.append("--dangerously-bypass-approvals-and-sandbox")
|
|
382
|
-
elif sandbox == "workspace-write":
|
|
383
|
-
# Default codex behavior
|
|
384
|
-
pass
|
|
385
465
|
|
|
386
466
|
cmd.extend(["--", task])
|
|
387
467
|
|
|
@@ -449,18 +529,29 @@ Continue from where you left off, addressing the user's new message."""
|
|
|
449
529
|
# Start new turn
|
|
450
530
|
session.turn += 1
|
|
451
531
|
now = datetime.now().isoformat()
|
|
452
|
-
session.messages.append(
|
|
532
|
+
session.messages.append(
|
|
533
|
+
SessionMessage(role="user", content=message, timestamp=now)
|
|
534
|
+
)
|
|
453
535
|
|
|
454
536
|
# Build command
|
|
455
|
-
|
|
456
|
-
|
|
457
|
-
|
|
458
|
-
"--
|
|
459
|
-
|
|
460
|
-
|
|
461
|
-
|
|
462
|
-
|
|
463
|
-
|
|
537
|
+
# Note: --search is a global flag that must come before 'exec'
|
|
538
|
+
cmd = ["codex"]
|
|
539
|
+
if session.web_search:
|
|
540
|
+
cmd.append("--search")
|
|
541
|
+
cmd.extend(
|
|
542
|
+
[
|
|
543
|
+
"exec",
|
|
544
|
+
"--json",
|
|
545
|
+
"--full-auto",
|
|
546
|
+
"--skip-git-repo-check",
|
|
547
|
+
"--model",
|
|
548
|
+
session.model,
|
|
549
|
+
"-C",
|
|
550
|
+
str(session.working_dir.absolute()),
|
|
551
|
+
"--",
|
|
552
|
+
augmented_task,
|
|
553
|
+
]
|
|
554
|
+
)
|
|
464
555
|
|
|
465
556
|
# Start process
|
|
466
557
|
output_path = self._output_path(session.id, session.turn)
|
|
@@ -578,7 +669,9 @@ Continue from where you left off, addressing the user's new message."""
|
|
|
578
669
|
|
|
579
670
|
return all_messages
|
|
580
671
|
|
|
581
|
-
def _parse_output(
|
|
672
|
+
def _parse_output(
|
|
673
|
+
self, output_path: Path
|
|
674
|
+
) -> tuple[list[SessionMessage], dict[str, int], str | None]:
|
|
582
675
|
"""
|
|
583
676
|
Parse JSONL output from codex exec.
|
|
584
677
|
|
|
@@ -612,11 +705,13 @@ Continue from where you left off, addressing the user's new message."""
|
|
|
612
705
|
if item_type == "agent_message":
|
|
613
706
|
text = item.get("text", "")
|
|
614
707
|
if text:
|
|
615
|
-
messages.append(
|
|
616
|
-
|
|
617
|
-
|
|
618
|
-
|
|
619
|
-
|
|
708
|
+
messages.append(
|
|
709
|
+
SessionMessage(
|
|
710
|
+
role="assistant",
|
|
711
|
+
content=text,
|
|
712
|
+
timestamp=datetime.now().isoformat(),
|
|
713
|
+
)
|
|
714
|
+
)
|
|
620
715
|
|
|
621
716
|
elif item_type == "reasoning":
|
|
622
717
|
# Could optionally capture reasoning
|
|
@@ -625,19 +720,23 @@ Continue from where you left off, addressing the user's new message."""
|
|
|
625
720
|
elif item_type == "function_call":
|
|
626
721
|
# Track tool calls
|
|
627
722
|
func_name = item.get("name", "unknown")
|
|
628
|
-
messages.append(
|
|
629
|
-
|
|
630
|
-
|
|
631
|
-
|
|
632
|
-
|
|
723
|
+
messages.append(
|
|
724
|
+
SessionMessage(
|
|
725
|
+
role="tool",
|
|
726
|
+
content=f"[Calling: {func_name}]",
|
|
727
|
+
metadata={"function": func_name},
|
|
728
|
+
)
|
|
729
|
+
)
|
|
633
730
|
|
|
634
731
|
elif item_type == "function_call_output":
|
|
635
732
|
output = item.get("output", "")
|
|
636
733
|
if output and len(output) < 500:
|
|
637
|
-
messages.append(
|
|
638
|
-
|
|
639
|
-
|
|
640
|
-
|
|
734
|
+
messages.append(
|
|
735
|
+
SessionMessage(
|
|
736
|
+
role="tool",
|
|
737
|
+
content=f"[Output]: {output[:500]}",
|
|
738
|
+
)
|
|
739
|
+
)
|
|
641
740
|
|
|
642
741
|
elif event_type == "turn.completed":
|
|
643
742
|
turn_usage = event.get("usage", {})
|
|
@@ -645,14 +744,18 @@ Continue from where you left off, addressing the user's new message."""
|
|
|
645
744
|
usage[key] = usage.get(key, 0) + value
|
|
646
745
|
# Compute total_tokens if not present
|
|
647
746
|
if "total_tokens" not in usage:
|
|
648
|
-
usage["total_tokens"] = usage.get("input_tokens", 0) + usage.get(
|
|
747
|
+
usage["total_tokens"] = usage.get("input_tokens", 0) + usage.get(
|
|
748
|
+
"output_tokens", 0
|
|
749
|
+
)
|
|
649
750
|
|
|
650
751
|
elif event_type == "error":
|
|
651
752
|
error = event.get("message", str(event))
|
|
652
753
|
|
|
653
754
|
return messages, usage, error
|
|
654
755
|
|
|
655
|
-
def get_trajectory(
|
|
756
|
+
def get_trajectory(
|
|
757
|
+
self, session_id: str, full: bool = False, max_output_len: int = 200
|
|
758
|
+
) -> list[dict]:
|
|
656
759
|
"""
|
|
657
760
|
Get the full trajectory of a session - all steps in order.
|
|
658
761
|
|
|
@@ -699,13 +802,16 @@ Continue from where you left off, addressing the user's new message."""
|
|
|
699
802
|
if item_type == "reasoning":
|
|
700
803
|
text = item.get("text", "")
|
|
701
804
|
summary_len = max_output_len if full else 100
|
|
702
|
-
trajectory.append(
|
|
703
|
-
|
|
704
|
-
|
|
705
|
-
|
|
706
|
-
|
|
707
|
-
|
|
708
|
-
|
|
805
|
+
trajectory.append(
|
|
806
|
+
{
|
|
807
|
+
"turn": turn,
|
|
808
|
+
"step": step_num,
|
|
809
|
+
"type": "reasoning",
|
|
810
|
+
"summary": text[:summary_len]
|
|
811
|
+
+ ("..." if len(text) > summary_len else ""),
|
|
812
|
+
"full_text": text if full else None,
|
|
813
|
+
}
|
|
814
|
+
)
|
|
709
815
|
|
|
710
816
|
elif item_type == "command_execution":
|
|
711
817
|
cmd = item.get("command", "")
|
|
@@ -715,52 +821,62 @@ Continue from where you left off, addressing the user's new message."""
|
|
|
715
821
|
output_preview = output[:max_output_len]
|
|
716
822
|
if len(output) > max_output_len:
|
|
717
823
|
output_preview += "..."
|
|
718
|
-
trajectory.append(
|
|
719
|
-
|
|
720
|
-
|
|
721
|
-
|
|
722
|
-
|
|
723
|
-
|
|
724
|
-
|
|
725
|
-
|
|
824
|
+
trajectory.append(
|
|
825
|
+
{
|
|
826
|
+
"turn": turn,
|
|
827
|
+
"step": step_num,
|
|
828
|
+
"type": "command",
|
|
829
|
+
"command": cmd,
|
|
830
|
+
"output": output_preview.strip(),
|
|
831
|
+
"exit_code": exit_code,
|
|
832
|
+
}
|
|
833
|
+
)
|
|
726
834
|
|
|
727
835
|
elif item_type == "function_call":
|
|
728
836
|
func_name = item.get("name", "unknown")
|
|
729
837
|
args = item.get("arguments", {})
|
|
730
838
|
args_str = str(args)
|
|
731
839
|
args_len = max_output_len if full else 100
|
|
732
|
-
trajectory.append(
|
|
733
|
-
|
|
734
|
-
|
|
735
|
-
|
|
736
|
-
|
|
737
|
-
|
|
738
|
-
|
|
739
|
-
|
|
840
|
+
trajectory.append(
|
|
841
|
+
{
|
|
842
|
+
"turn": turn,
|
|
843
|
+
"step": step_num,
|
|
844
|
+
"type": "tool_call",
|
|
845
|
+
"tool": func_name,
|
|
846
|
+
"args_preview": args_str[:args_len]
|
|
847
|
+
+ ("..." if len(args_str) > args_len else ""),
|
|
848
|
+
"full_args": args if full else None,
|
|
849
|
+
}
|
|
850
|
+
)
|
|
740
851
|
|
|
741
852
|
elif item_type == "function_call_output":
|
|
742
853
|
output = item.get("output", "")
|
|
743
854
|
output_preview = output[:max_output_len]
|
|
744
855
|
if len(output) > max_output_len:
|
|
745
856
|
output_preview += "..."
|
|
746
|
-
trajectory.append(
|
|
747
|
-
|
|
748
|
-
|
|
749
|
-
|
|
750
|
-
|
|
751
|
-
|
|
857
|
+
trajectory.append(
|
|
858
|
+
{
|
|
859
|
+
"turn": turn,
|
|
860
|
+
"step": step_num,
|
|
861
|
+
"type": "tool_output",
|
|
862
|
+
"output": output_preview,
|
|
863
|
+
}
|
|
864
|
+
)
|
|
752
865
|
|
|
753
866
|
elif item_type == "agent_message":
|
|
754
867
|
text = item.get("text", "")
|
|
755
868
|
summary_len = max_output_len if full else 200
|
|
756
|
-
trajectory.append(
|
|
757
|
-
|
|
758
|
-
|
|
759
|
-
|
|
760
|
-
|
|
761
|
-
|
|
762
|
-
|
|
763
|
-
|
|
869
|
+
trajectory.append(
|
|
870
|
+
{
|
|
871
|
+
"turn": turn,
|
|
872
|
+
"step": step_num,
|
|
873
|
+
"type": "message",
|
|
874
|
+
"summary": text[:summary_len]
|
|
875
|
+
+ ("..." if len(text) > summary_len else ""),
|
|
876
|
+
"full_text": text if full else None,
|
|
877
|
+
"full_length": len(text),
|
|
878
|
+
}
|
|
879
|
+
)
|
|
764
880
|
|
|
765
881
|
return trajectory
|
|
766
882
|
|
|
@@ -781,7 +897,11 @@ Continue from where you left off, addressing the user's new message."""
|
|
|
781
897
|
cleaned = 0
|
|
782
898
|
|
|
783
899
|
for session in self.list_sessions():
|
|
784
|
-
if session.status in (
|
|
900
|
+
if session.status in (
|
|
901
|
+
SessionStatus.COMPLETED,
|
|
902
|
+
SessionStatus.FAILED,
|
|
903
|
+
SessionStatus.KILLED,
|
|
904
|
+
):
|
|
785
905
|
created = datetime.fromisoformat(session.created_at)
|
|
786
906
|
if created < cutoff:
|
|
787
907
|
session_dir = self._session_dir(session.id)
|
zwarm/tools/delegation.py
CHANGED
|
@@ -136,6 +136,8 @@ def delegate(
|
|
|
136
136
|
This spawns a codex session - the exact same way `zwarm interactive` does.
|
|
137
137
|
All sessions run async - you get a session_id immediately and poll for results.
|
|
138
138
|
|
|
139
|
+
Web search is always enabled via .codex/config.toml (set up by `zwarm init`).
|
|
140
|
+
|
|
139
141
|
Workflow pattern:
|
|
140
142
|
1. delegate(task="Add logout button") -> session_id
|
|
141
143
|
2. sleep(30) -> give it time
|
|
@@ -155,6 +157,9 @@ def delegate(
|
|
|
155
157
|
delegate(task="Add a logout button to the navbar")
|
|
156
158
|
sleep(30)
|
|
157
159
|
peek_session(session_id) # Check progress
|
|
160
|
+
|
|
161
|
+
Example with web search (always available):
|
|
162
|
+
delegate(task="Find the latest FastAPI docs and add OAuth2 auth")
|
|
158
163
|
"""
|
|
159
164
|
# Validate working directory
|
|
160
165
|
effective_dir, dir_error = _validate_working_dir(
|
|
@@ -180,7 +185,7 @@ def delegate(
|
|
|
180
185
|
sandbox = self.config.executor.sandbox or "workspace-write"
|
|
181
186
|
|
|
182
187
|
# Start the session using CodexSessionManager
|
|
183
|
-
#
|
|
188
|
+
# Web search is enabled via .codex/config.toml (symlink to .zwarm/config.toml)
|
|
184
189
|
session = manager.start_session(
|
|
185
190
|
task=task,
|
|
186
191
|
working_dir=effective_dir,
|
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
Metadata-Version: 2.4
|
|
2
2
|
Name: zwarm
|
|
3
|
-
Version: 3.
|
|
3
|
+
Version: 3.3.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"]
|
|
@@ -1,35 +1,28 @@
|
|
|
1
1
|
zwarm/__init__.py,sha256=3i3LMjHwIzE-LFIS2aUrwv3EZmpkvVMe-xj1h97rcSM,837
|
|
2
|
-
zwarm/orchestrator.py,sha256=
|
|
2
|
+
zwarm/orchestrator.py,sha256=sUSYJ1_Zr1pDV-tMyS-jovBCnSmA_JVmgv9_PrIqzps,22277
|
|
3
3
|
zwarm/test_orchestrator_watchers.py,sha256=QpoaehPU7ekT4XshbTOWnJ2H0wRveV3QOZjxbgyJJLY,807
|
|
4
|
-
zwarm/adapters/__init__.py,sha256=O0b-SfZpb6txeNqFkXZ2aaf34yLFYreznyrAV25jF_Q,656
|
|
5
|
-
zwarm/adapters/base.py,sha256=fZlQviTgVvOcwnxduTla6WuM6FzQJ_yoHMW5SxwVgQg,2527
|
|
6
|
-
zwarm/adapters/claude_code.py,sha256=vAjsjD-_JjARmC4_FBSILQZmQCBrk_oNHo18a9ubuqk,11481
|
|
7
|
-
zwarm/adapters/codex_mcp.py,sha256=EhdkM3gj5hc01AcM1ERhtfZbydK390yN4Pg3dawKIGU,48791
|
|
8
|
-
zwarm/adapters/registry.py,sha256=EdyHECaNA5Kv1od64pYFBJyA_r_6I1r_eJTNP1XYLr4,1781
|
|
9
|
-
zwarm/adapters/test_codex_mcp.py,sha256=0qhVzxn_KF-XUS30gXSJKwMdR3kWGsDY9iPk1Ihqn3w,10698
|
|
10
|
-
zwarm/adapters/test_registry.py,sha256=otxcVDONwFCMisyANToF3iy7Y8dSbCL8bTmZNhxNuF4,2383
|
|
11
4
|
zwarm/cli/__init__.py,sha256=47DEQpj8HBSa-_TImW-5JCeuQeRkm5NMpJWZG3hSuFU,0
|
|
12
|
-
zwarm/cli/interactive.py,sha256=
|
|
13
|
-
zwarm/cli/main.py,sha256=
|
|
14
|
-
zwarm/cli/pilot.py,sha256=
|
|
5
|
+
zwarm/cli/interactive.py,sha256=fCjX71tlN5fE2GvTgIbPRnRtmSiGmWiHsUYrQdVnwJE,25916
|
|
6
|
+
zwarm/cli/main.py,sha256=JfYS_PXNqSVU0RknKJZ3legfvr4lG-UZrK4X_TVnuOo,70559
|
|
7
|
+
zwarm/cli/pilot.py,sha256=SIjP5skBYTz0-VEJ94W0XjS07SAWdJ4zNnnHmuwBzio,38156
|
|
15
8
|
zwarm/core/__init__.py,sha256=nEdpEHMFo0gEEKgX-eKHabyOdrOI6UXfWqLu3FfZDao,376
|
|
16
9
|
zwarm/core/checkpoints.py,sha256=D6sXCMB7Sa1kchQ9_lQx_rabwc5-_7jbuynWgA1nkNY,6560
|
|
17
10
|
zwarm/core/compact.py,sha256=Y8C7Gs-5-WOU43WRvQ863Qzd5xtuEqR6Aw3r2p8_-i8,10907
|
|
18
|
-
zwarm/core/config.py,sha256=
|
|
11
|
+
zwarm/core/config.py,sha256=QtxvbdVg_mU33x9ItfHu-Rov7ewUHfSi0K_4O1iBJJQ,12486
|
|
19
12
|
zwarm/core/costs.py,sha256=pLyixKn9hbDvtzQofOqajeK3XxOpoHPpA8nw8QM7j3g,5403
|
|
20
13
|
zwarm/core/environment.py,sha256=zrgh0N3Ng4HI2F1gCYkcQVGzjQPKiIFWuRe1OPRuRn0,6558
|
|
21
14
|
zwarm/core/models.py,sha256=PrC3okRBVJxISUa1Fax4KkagqLT6Xub-kTxC9drN0sY,10083
|
|
22
15
|
zwarm/core/state.py,sha256=MzrvODKEiJovI7YI1jajW4uukineZ3ezmW5oQinMgjg,11563
|
|
23
16
|
zwarm/core/test_compact.py,sha256=WSdjCB5t4YMcknsrkmJIUsVOPY28s4y9GnDmu3Z4BFw,11878
|
|
24
|
-
zwarm/core/test_config.py,sha256=
|
|
17
|
+
zwarm/core/test_config.py,sha256=bXXd3OHhK-ndC7wAxePWIdpu73s4O1eScxi3xDzrZwA,4828
|
|
25
18
|
zwarm/core/test_models.py,sha256=sWTIhMZvuLP5AooGR6y8OR2EyWydqVfhmGrE7NPBBnk,8450
|
|
26
19
|
zwarm/prompts/__init__.py,sha256=DI307o712F8qQyDt5vwnFgpVBrxpKwjhr0MaBHLzr9E,334
|
|
27
20
|
zwarm/prompts/orchestrator.py,sha256=AkVbEpT91QbYFjUYOzm0d37wXrpm0esLBD1MG_W-3FI,15367
|
|
28
21
|
zwarm/prompts/pilot.py,sha256=BcaV04-43FZyrtmoqCbA7DqnTlQ330TcDp9wNGhRojo,5586
|
|
29
22
|
zwarm/sessions/__init__.py,sha256=jRibY8IfmNcnkgNmrgK2T81oa1w71wP_KQp9A1hPL7Q,568
|
|
30
|
-
zwarm/sessions/manager.py,sha256=
|
|
23
|
+
zwarm/sessions/manager.py,sha256=bLxwFA9D4EBW3wCC6OqolKcBiZtv8igz0Z7ZGA7EcVc,31559
|
|
31
24
|
zwarm/tools/__init__.py,sha256=FpqxwXJA6-fQ7C-oLj30jjK_0qqcE7MbI0dQuaB56kU,290
|
|
32
|
-
zwarm/tools/delegation.py,sha256=
|
|
25
|
+
zwarm/tools/delegation.py,sha256=wXKIb7eCL23c5NUwWi1G6JNg1PyXnwW-TNn7NrqchaE,21697
|
|
33
26
|
zwarm/watchers/__init__.py,sha256=a96s7X6ruYkF2ItWWOZ3Q5QUOMOoeCW4Vz8XXcYLXPM,956
|
|
34
27
|
zwarm/watchers/base.py,sha256=r1GoPlj06nOT2xp4fghfSjxbRyFFFQUB6HpZbEyO2OY,3834
|
|
35
28
|
zwarm/watchers/builtin.py,sha256=IL5QwwKOIqWEfJ_uQWb321Px4i5OLtI_vnWQMudqKoA,19064
|
|
@@ -37,7 +30,7 @@ zwarm/watchers/llm_watcher.py,sha256=yJGpE3BGKNZX3qgPsiNtJ5d3UJpiTT1V-A-Rh4AiMYM
|
|
|
37
30
|
zwarm/watchers/manager.py,sha256=XZjBVeHjgCUlkTUeHqdvBvHoBC862U1ik0fG6nlRGog,5587
|
|
38
31
|
zwarm/watchers/registry.py,sha256=A9iBIVIFNtO7KPX0kLpUaP8dAK7ozqWLA44ocJGnOw4,1219
|
|
39
32
|
zwarm/watchers/test_watchers.py,sha256=zOsxumBqKfR5ZVGxrNlxz6KcWjkcdp0QhW9WB0_20zM,7855
|
|
40
|
-
zwarm-3.
|
|
41
|
-
zwarm-3.
|
|
42
|
-
zwarm-3.
|
|
43
|
-
zwarm-3.
|
|
33
|
+
zwarm-3.3.0.dist-info/METADATA,sha256=XuIf4S9hSl8xtmpqbTdHJiH15_jkLJagQ1Y8p5y5XHI,9670
|
|
34
|
+
zwarm-3.3.0.dist-info/WHEEL,sha256=WLgqFyCfm_KASv4WHyYy0P3pM_m7J5L9k2skdKLirC8,87
|
|
35
|
+
zwarm-3.3.0.dist-info/entry_points.txt,sha256=u0OXq4q8d3yJ3EkUXwZfkS-Y8Lcy0F8cWrcQfoRxM6Q,46
|
|
36
|
+
zwarm-3.3.0.dist-info/RECORD,,
|
zwarm/adapters/__init__.py
DELETED
|
@@ -1,21 +0,0 @@
|
|
|
1
|
-
"""
|
|
2
|
-
Adapters: Executor wrappers for CLI coding agents.
|
|
3
|
-
|
|
4
|
-
Adapters provide a unified interface to different coding CLIs (Codex, Claude Code).
|
|
5
|
-
Use the registry to discover and instantiate adapters by name.
|
|
6
|
-
"""
|
|
7
|
-
|
|
8
|
-
from zwarm.adapters.base import ExecutorAdapter
|
|
9
|
-
from zwarm.adapters.registry import register_adapter, get_adapter, list_adapters, adapter_exists
|
|
10
|
-
|
|
11
|
-
# Import built-in adapters to register them
|
|
12
|
-
from zwarm.adapters import codex_mcp as _codex_mcp # noqa: F401
|
|
13
|
-
from zwarm.adapters import claude_code as _claude_code # noqa: F401
|
|
14
|
-
|
|
15
|
-
__all__ = [
|
|
16
|
-
"ExecutorAdapter",
|
|
17
|
-
"register_adapter",
|
|
18
|
-
"get_adapter",
|
|
19
|
-
"list_adapters",
|
|
20
|
-
"adapter_exists",
|
|
21
|
-
]
|