zwarm 2.3.5__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/__init__.py +38 -0
- zwarm/adapters/__init__.py +21 -0
- zwarm/adapters/base.py +109 -0
- zwarm/adapters/claude_code.py +357 -0
- zwarm/adapters/codex_mcp.py +1262 -0
- zwarm/adapters/registry.py +69 -0
- zwarm/adapters/test_codex_mcp.py +274 -0
- zwarm/adapters/test_registry.py +68 -0
- zwarm/cli/__init__.py +0 -0
- zwarm/cli/main.py +2503 -0
- zwarm/core/__init__.py +0 -0
- zwarm/core/compact.py +329 -0
- zwarm/core/config.py +344 -0
- zwarm/core/environment.py +173 -0
- zwarm/core/models.py +315 -0
- zwarm/core/state.py +355 -0
- zwarm/core/test_compact.py +312 -0
- zwarm/core/test_config.py +160 -0
- zwarm/core/test_models.py +265 -0
- zwarm/orchestrator.py +683 -0
- zwarm/prompts/__init__.py +10 -0
- zwarm/prompts/orchestrator.py +230 -0
- zwarm/sessions/__init__.py +26 -0
- zwarm/sessions/manager.py +792 -0
- zwarm/test_orchestrator_watchers.py +23 -0
- zwarm/tools/__init__.py +17 -0
- zwarm/tools/delegation.py +784 -0
- zwarm/watchers/__init__.py +31 -0
- zwarm/watchers/base.py +131 -0
- zwarm/watchers/builtin.py +518 -0
- zwarm/watchers/llm_watcher.py +319 -0
- zwarm/watchers/manager.py +181 -0
- zwarm/watchers/registry.py +57 -0
- zwarm/watchers/test_watchers.py +237 -0
- zwarm-2.3.5.dist-info/METADATA +309 -0
- zwarm-2.3.5.dist-info/RECORD +38 -0
- zwarm-2.3.5.dist-info/WHEEL +4 -0
- zwarm-2.3.5.dist-info/entry_points.txt +2 -0
|
@@ -0,0 +1,237 @@
|
|
|
1
|
+
"""Tests for the watcher system."""
|
|
2
|
+
|
|
3
|
+
import pytest
|
|
4
|
+
|
|
5
|
+
from zwarm.watchers import (
|
|
6
|
+
Watcher,
|
|
7
|
+
WatcherContext,
|
|
8
|
+
WatcherResult,
|
|
9
|
+
WatcherAction,
|
|
10
|
+
WatcherManager,
|
|
11
|
+
WatcherConfig,
|
|
12
|
+
get_watcher,
|
|
13
|
+
list_watchers,
|
|
14
|
+
)
|
|
15
|
+
|
|
16
|
+
|
|
17
|
+
class TestWatcherRegistry:
|
|
18
|
+
def test_list_watchers(self):
|
|
19
|
+
"""Built-in watchers should be registered."""
|
|
20
|
+
watchers = list_watchers()
|
|
21
|
+
assert "progress" in watchers
|
|
22
|
+
assert "budget" in watchers
|
|
23
|
+
assert "scope" in watchers
|
|
24
|
+
assert "pattern" in watchers
|
|
25
|
+
assert "quality" in watchers
|
|
26
|
+
|
|
27
|
+
def test_get_watcher(self):
|
|
28
|
+
"""Can get watcher by name."""
|
|
29
|
+
watcher = get_watcher("progress")
|
|
30
|
+
assert watcher.name == "progress"
|
|
31
|
+
|
|
32
|
+
def test_get_unknown_watcher(self):
|
|
33
|
+
"""Unknown watcher raises error."""
|
|
34
|
+
with pytest.raises(ValueError, match="Unknown watcher"):
|
|
35
|
+
get_watcher("nonexistent")
|
|
36
|
+
|
|
37
|
+
|
|
38
|
+
class TestProgressWatcher:
|
|
39
|
+
@pytest.mark.asyncio
|
|
40
|
+
async def test_continues_on_normal_progress(self):
|
|
41
|
+
"""Normal progress should continue."""
|
|
42
|
+
watcher = get_watcher("progress")
|
|
43
|
+
ctx = WatcherContext(
|
|
44
|
+
task="Test task",
|
|
45
|
+
step=2,
|
|
46
|
+
max_steps=10,
|
|
47
|
+
messages=[
|
|
48
|
+
{"role": "user", "content": "Start"},
|
|
49
|
+
{"role": "assistant", "content": "Working on it"},
|
|
50
|
+
],
|
|
51
|
+
)
|
|
52
|
+
result = await watcher.observe(ctx)
|
|
53
|
+
assert result.action == WatcherAction.CONTINUE
|
|
54
|
+
|
|
55
|
+
|
|
56
|
+
class TestBudgetWatcher:
|
|
57
|
+
@pytest.mark.asyncio
|
|
58
|
+
async def test_warns_at_budget_threshold(self):
|
|
59
|
+
"""Should warn when approaching step limit."""
|
|
60
|
+
watcher = get_watcher("budget", {"warn_at_percent": 80})
|
|
61
|
+
ctx = WatcherContext(
|
|
62
|
+
task="Test task",
|
|
63
|
+
step=9, # 90% of max
|
|
64
|
+
max_steps=10,
|
|
65
|
+
messages=[],
|
|
66
|
+
)
|
|
67
|
+
result = await watcher.observe(ctx)
|
|
68
|
+
assert result.action == WatcherAction.NUDGE
|
|
69
|
+
assert "remaining" in result.guidance.lower()
|
|
70
|
+
|
|
71
|
+
@pytest.mark.asyncio
|
|
72
|
+
async def test_continues_when_under_budget(self):
|
|
73
|
+
"""Should continue when well under budget."""
|
|
74
|
+
watcher = get_watcher("budget")
|
|
75
|
+
ctx = WatcherContext(
|
|
76
|
+
task="Test task",
|
|
77
|
+
step=2,
|
|
78
|
+
max_steps=10,
|
|
79
|
+
messages=[],
|
|
80
|
+
)
|
|
81
|
+
result = await watcher.observe(ctx)
|
|
82
|
+
assert result.action == WatcherAction.CONTINUE
|
|
83
|
+
|
|
84
|
+
@pytest.mark.asyncio
|
|
85
|
+
async def test_only_counts_active_sessions(self):
|
|
86
|
+
"""Should only count active sessions, not completed/failed ones."""
|
|
87
|
+
watcher = get_watcher("budget", {"max_sessions": 2})
|
|
88
|
+
# Create 5 sessions: 1 active, 2 completed, 2 failed
|
|
89
|
+
ctx = WatcherContext(
|
|
90
|
+
task="Test task",
|
|
91
|
+
step=2,
|
|
92
|
+
max_steps=10,
|
|
93
|
+
messages=[],
|
|
94
|
+
sessions=[
|
|
95
|
+
{"id": "s1", "status": "active"},
|
|
96
|
+
{"id": "s2", "status": "completed"},
|
|
97
|
+
{"id": "s3", "status": "completed"},
|
|
98
|
+
{"id": "s4", "status": "failed"},
|
|
99
|
+
{"id": "s5", "status": "failed"},
|
|
100
|
+
],
|
|
101
|
+
)
|
|
102
|
+
# Should continue because only 1 active session (limit is 2)
|
|
103
|
+
result = await watcher.observe(ctx)
|
|
104
|
+
assert result.action == WatcherAction.CONTINUE
|
|
105
|
+
|
|
106
|
+
@pytest.mark.asyncio
|
|
107
|
+
async def test_warns_when_active_sessions_at_limit(self):
|
|
108
|
+
"""Should warn when active sessions reach the limit."""
|
|
109
|
+
watcher = get_watcher("budget", {"max_sessions": 2})
|
|
110
|
+
ctx = WatcherContext(
|
|
111
|
+
task="Test task",
|
|
112
|
+
step=2,
|
|
113
|
+
max_steps=10,
|
|
114
|
+
messages=[],
|
|
115
|
+
sessions=[
|
|
116
|
+
{"id": "s1", "status": "active"},
|
|
117
|
+
{"id": "s2", "status": "active"},
|
|
118
|
+
{"id": "s3", "status": "completed"},
|
|
119
|
+
],
|
|
120
|
+
)
|
|
121
|
+
# Should nudge because 2 active sessions (at limit)
|
|
122
|
+
result = await watcher.observe(ctx)
|
|
123
|
+
assert result.action == WatcherAction.NUDGE
|
|
124
|
+
assert "2 active sessions" in result.guidance
|
|
125
|
+
|
|
126
|
+
|
|
127
|
+
class TestPatternWatcher:
|
|
128
|
+
@pytest.mark.asyncio
|
|
129
|
+
async def test_detects_pattern(self):
|
|
130
|
+
"""Should detect configured patterns."""
|
|
131
|
+
watcher = get_watcher("pattern", {
|
|
132
|
+
"patterns": [
|
|
133
|
+
{"regex": r"ERROR", "action": "nudge", "message": "Error detected!"}
|
|
134
|
+
]
|
|
135
|
+
})
|
|
136
|
+
ctx = WatcherContext(
|
|
137
|
+
task="Test task",
|
|
138
|
+
step=1,
|
|
139
|
+
max_steps=10,
|
|
140
|
+
messages=[
|
|
141
|
+
{"role": "assistant", "content": "Got ERROR in the build"}
|
|
142
|
+
],
|
|
143
|
+
)
|
|
144
|
+
result = await watcher.observe(ctx)
|
|
145
|
+
assert result.action == WatcherAction.NUDGE
|
|
146
|
+
assert "Error detected" in result.guidance
|
|
147
|
+
|
|
148
|
+
@pytest.mark.asyncio
|
|
149
|
+
async def test_abort_pattern(self):
|
|
150
|
+
"""Should abort on critical patterns."""
|
|
151
|
+
watcher = get_watcher("pattern", {
|
|
152
|
+
"patterns": [
|
|
153
|
+
{"regex": r"rm -rf /", "action": "abort", "message": "Dangerous command!"}
|
|
154
|
+
]
|
|
155
|
+
})
|
|
156
|
+
ctx = WatcherContext(
|
|
157
|
+
task="Test task",
|
|
158
|
+
step=1,
|
|
159
|
+
max_steps=10,
|
|
160
|
+
messages=[
|
|
161
|
+
{"role": "assistant", "content": "Running rm -rf /"}
|
|
162
|
+
],
|
|
163
|
+
)
|
|
164
|
+
result = await watcher.observe(ctx)
|
|
165
|
+
assert result.action == WatcherAction.ABORT
|
|
166
|
+
|
|
167
|
+
|
|
168
|
+
class TestWatcherManager:
|
|
169
|
+
@pytest.mark.asyncio
|
|
170
|
+
async def test_runs_multiple_watchers(self):
|
|
171
|
+
"""Manager runs all watchers."""
|
|
172
|
+
manager = WatcherManager([
|
|
173
|
+
WatcherConfig(name="progress"),
|
|
174
|
+
WatcherConfig(name="budget"),
|
|
175
|
+
])
|
|
176
|
+
ctx = WatcherContext(
|
|
177
|
+
task="Test task",
|
|
178
|
+
step=2,
|
|
179
|
+
max_steps=10,
|
|
180
|
+
messages=[],
|
|
181
|
+
)
|
|
182
|
+
result = await manager.observe(ctx)
|
|
183
|
+
assert isinstance(result, WatcherResult)
|
|
184
|
+
|
|
185
|
+
@pytest.mark.asyncio
|
|
186
|
+
async def test_highest_priority_wins(self):
|
|
187
|
+
"""Most severe action should win."""
|
|
188
|
+
manager = WatcherManager([
|
|
189
|
+
WatcherConfig(name="budget", config={"warn_at_percent": 50}), # Will nudge
|
|
190
|
+
WatcherConfig(name="pattern", config={
|
|
191
|
+
"patterns": [{"regex": "ABORT", "action": "abort", "message": "Abort!"}]
|
|
192
|
+
}),
|
|
193
|
+
])
|
|
194
|
+
ctx = WatcherContext(
|
|
195
|
+
task="Test task",
|
|
196
|
+
step=6, # 60% - triggers budget nudge
|
|
197
|
+
max_steps=10,
|
|
198
|
+
messages=[
|
|
199
|
+
{"role": "assistant", "content": "Must ABORT now"}
|
|
200
|
+
],
|
|
201
|
+
)
|
|
202
|
+
result = await manager.observe(ctx)
|
|
203
|
+
# Abort should take precedence over nudge
|
|
204
|
+
assert result.action == WatcherAction.ABORT
|
|
205
|
+
|
|
206
|
+
@pytest.mark.asyncio
|
|
207
|
+
async def test_empty_manager_continues(self):
|
|
208
|
+
"""Manager with no watchers should continue."""
|
|
209
|
+
manager = WatcherManager([])
|
|
210
|
+
ctx = WatcherContext(
|
|
211
|
+
task="Test task",
|
|
212
|
+
step=1,
|
|
213
|
+
max_steps=10,
|
|
214
|
+
messages=[],
|
|
215
|
+
)
|
|
216
|
+
result = await manager.observe(ctx)
|
|
217
|
+
assert result.action == WatcherAction.CONTINUE
|
|
218
|
+
|
|
219
|
+
@pytest.mark.asyncio
|
|
220
|
+
async def test_disabled_watcher_skipped(self):
|
|
221
|
+
"""Disabled watchers should be skipped."""
|
|
222
|
+
manager = WatcherManager([
|
|
223
|
+
WatcherConfig(name="pattern", enabled=False, config={
|
|
224
|
+
"patterns": [{"regex": ".*", "action": "abort", "message": "Always abort"}]
|
|
225
|
+
}),
|
|
226
|
+
])
|
|
227
|
+
ctx = WatcherContext(
|
|
228
|
+
task="Test task",
|
|
229
|
+
step=1,
|
|
230
|
+
max_steps=10,
|
|
231
|
+
messages=[
|
|
232
|
+
{"role": "assistant", "content": "This would normally trigger abort"}
|
|
233
|
+
],
|
|
234
|
+
)
|
|
235
|
+
result = await manager.observe(ctx)
|
|
236
|
+
# Since the pattern watcher is disabled, should continue
|
|
237
|
+
assert result.action == WatcherAction.CONTINUE
|
|
@@ -0,0 +1,309 @@
|
|
|
1
|
+
Metadata-Version: 2.4
|
|
2
|
+
Name: zwarm
|
|
3
|
+
Version: 2.3.5
|
|
4
|
+
Summary: Multi-Agent CLI Orchestration Research Platform
|
|
5
|
+
Requires-Python: <3.14,>=3.13
|
|
6
|
+
Requires-Dist: python-dotenv>=1.0.0
|
|
7
|
+
Requires-Dist: pyyaml>=6.0
|
|
8
|
+
Requires-Dist: rich>=13.0.0
|
|
9
|
+
Requires-Dist: typer>=0.9.0
|
|
10
|
+
Requires-Dist: wbal>=0.5.8
|
|
11
|
+
Description-Content-Type: text/markdown
|
|
12
|
+
|
|
13
|
+
# zwarm
|
|
14
|
+
|
|
15
|
+
Multi-agent CLI for orchestrating coding agents. Spawn, manage, and converse with multiple Codex sessions in parallel.
|
|
16
|
+
|
|
17
|
+
## Installation
|
|
18
|
+
|
|
19
|
+
```bash
|
|
20
|
+
# From the workspace
|
|
21
|
+
cd /path/to/labs
|
|
22
|
+
uv sync
|
|
23
|
+
|
|
24
|
+
# Or install directly
|
|
25
|
+
uv pip install -e ./zwarm
|
|
26
|
+
```
|
|
27
|
+
|
|
28
|
+
**Requirements:**
|
|
29
|
+
- Python 3.13+
|
|
30
|
+
- `codex` CLI installed and authenticated
|
|
31
|
+
|
|
32
|
+
**Environment:**
|
|
33
|
+
```bash
|
|
34
|
+
export OPENAI_API_KEY="sk-..." # Required for Codex
|
|
35
|
+
export WEAVE_PROJECT="entity/zwarm" # Optional: Weave tracing
|
|
36
|
+
```
|
|
37
|
+
|
|
38
|
+
## Two Modes
|
|
39
|
+
|
|
40
|
+
zwarm has two ways to orchestrate coding agents:
|
|
41
|
+
|
|
42
|
+
| Mode | Who's in charge | Use case |
|
|
43
|
+
|------|-----------------|----------|
|
|
44
|
+
| `zwarm interactive` | **You** | Manual control, experimentation |
|
|
45
|
+
| `zwarm orchestrate` | **LLM** | Autonomous task execution |
|
|
46
|
+
|
|
47
|
+
Both use the **same underlying session manager** - the orchestrator LLM has access to the exact same tools you do.
|
|
48
|
+
|
|
49
|
+
---
|
|
50
|
+
|
|
51
|
+
## Interactive Mode
|
|
52
|
+
|
|
53
|
+
**You are the orchestrator.** Spawn sessions, check on them, continue conversations.
|
|
54
|
+
|
|
55
|
+
```bash
|
|
56
|
+
zwarm interactive
|
|
57
|
+
```
|
|
58
|
+
|
|
59
|
+
### Commands
|
|
60
|
+
|
|
61
|
+
| Command | Description |
|
|
62
|
+
|---------|-------------|
|
|
63
|
+
| `spawn "task"` | Start a session (waits for completion) |
|
|
64
|
+
| `spawn --async "task"` | Start async (returns immediately) |
|
|
65
|
+
| `spawn -d /path "task"` | Start in specific directory |
|
|
66
|
+
| `ls` | List all sessions |
|
|
67
|
+
| `? <id>` | Quick peek: status + latest message |
|
|
68
|
+
| `show <id>` | Full details: all messages, tokens, etc. |
|
|
69
|
+
| `c <id> "msg"` | Continue conversation (waits) |
|
|
70
|
+
| `ca <id> "msg"` | Continue async (returns immediately) |
|
|
71
|
+
| `kill <id>` | Stop a running session |
|
|
72
|
+
| `rm <id>` | Delete session entirely |
|
|
73
|
+
| `killall` | Stop all running sessions |
|
|
74
|
+
| `clean` | Remove sessions older than 7 days |
|
|
75
|
+
| `q` | Quit |
|
|
76
|
+
|
|
77
|
+
### Example Session
|
|
78
|
+
|
|
79
|
+
```
|
|
80
|
+
$ zwarm interactive
|
|
81
|
+
|
|
82
|
+
> spawn "Add a login function to auth.py"
|
|
83
|
+
✓ Started session a1b2c3d4, waiting...
|
|
84
|
+
[a1b2c3d4] codex (completed) - 32s
|
|
85
|
+
Response: I've added a login function with JWT support...
|
|
86
|
+
|
|
87
|
+
> spawn --async "Fix the type errors in utils.py"
|
|
88
|
+
✓ Session: b2c3d4e5 (running in background)
|
|
89
|
+
|
|
90
|
+
> spawn --async "Add unit tests for auth.py"
|
|
91
|
+
✓ Session: c3d4e5f6 (running in background)
|
|
92
|
+
|
|
93
|
+
> ls
|
|
94
|
+
1 running | 2 done
|
|
95
|
+
|
|
96
|
+
ID │ │ T │ Task │ Updated │ Last Message
|
|
97
|
+
a1b2c3d4 │ ✓ │ 1 │ Add a login function... │ 2m │ I've added a login function...
|
|
98
|
+
b2c3d4e5 │ ✓ │ 1 │ Fix the type errors... │ 30s ★ │ Fixed 3 type errors in...
|
|
99
|
+
c3d4e5f6 │ ● │ 1 │ Add unit tests... │ 5s │ (working...)
|
|
100
|
+
|
|
101
|
+
> ? b2c3d4e5
|
|
102
|
+
✓ b2c3d4e5 Fixed 3 type errors: Optional[str] -> str | None, added missing...
|
|
103
|
+
|
|
104
|
+
> c a1b2c3d4 "Now add password hashing with bcrypt"
|
|
105
|
+
Continuing session a1b2c3d4...
|
|
106
|
+
[a1b2c3d4] codex (completed) - 28s
|
|
107
|
+
Response: Done! I've updated the login function to use bcrypt...
|
|
108
|
+
|
|
109
|
+
> rm b2c3d4e5
|
|
110
|
+
✓ Deleted session b2c3d4e5
|
|
111
|
+
|
|
112
|
+
> q
|
|
113
|
+
```
|
|
114
|
+
|
|
115
|
+
### Session Status Icons
|
|
116
|
+
|
|
117
|
+
| Icon | Status |
|
|
118
|
+
|------|--------|
|
|
119
|
+
| `●` | Running |
|
|
120
|
+
| `✓` | Completed |
|
|
121
|
+
| `✗` | Failed |
|
|
122
|
+
| `○` | Killed |
|
|
123
|
+
| `★` | Recently completed (< 60s) |
|
|
124
|
+
|
|
125
|
+
---
|
|
126
|
+
|
|
127
|
+
## Orchestrate Mode
|
|
128
|
+
|
|
129
|
+
**An LLM is the orchestrator.** Give it a task and it delegates to coding agents.
|
|
130
|
+
|
|
131
|
+
```bash
|
|
132
|
+
zwarm orchestrate --task "Build a REST API with authentication"
|
|
133
|
+
```
|
|
134
|
+
|
|
135
|
+
The orchestrator LLM uses the same tools available in interactive mode:
|
|
136
|
+
|
|
137
|
+
| Tool | Description |
|
|
138
|
+
|------|-------------|
|
|
139
|
+
| `delegate(task, ...)` | Start a new session |
|
|
140
|
+
| `converse(id, msg)` | Continue a conversation |
|
|
141
|
+
| `peek_session(id)` | Quick status check |
|
|
142
|
+
| `check_session(id)` | Full session details |
|
|
143
|
+
| `list_sessions()` | List all sessions with `needs_attention` flags |
|
|
144
|
+
| `end_session(id, delete=False)` | Kill/delete a session |
|
|
145
|
+
|
|
146
|
+
### Task Input
|
|
147
|
+
|
|
148
|
+
```bash
|
|
149
|
+
# Direct
|
|
150
|
+
zwarm orchestrate --task "Build a REST API"
|
|
151
|
+
|
|
152
|
+
# From file
|
|
153
|
+
zwarm orchestrate --task-file task.md
|
|
154
|
+
|
|
155
|
+
# From stdin
|
|
156
|
+
echo "Fix the bug in auth.py" | zwarm orchestrate
|
|
157
|
+
```
|
|
158
|
+
|
|
159
|
+
---
|
|
160
|
+
|
|
161
|
+
## Configuration
|
|
162
|
+
|
|
163
|
+
zwarm looks for config in this order:
|
|
164
|
+
1. `--config` flag
|
|
165
|
+
2. `.zwarm/config.toml`
|
|
166
|
+
3. `config.toml` in working directory
|
|
167
|
+
|
|
168
|
+
### Minimal Config
|
|
169
|
+
|
|
170
|
+
```toml
|
|
171
|
+
[weave]
|
|
172
|
+
enabled = true
|
|
173
|
+
project = "your-entity/zwarm"
|
|
174
|
+
|
|
175
|
+
[executor]
|
|
176
|
+
adapter = "codex_mcp"
|
|
177
|
+
model = "gpt-5.1-codex-mini"
|
|
178
|
+
```
|
|
179
|
+
|
|
180
|
+
### Full Config Reference
|
|
181
|
+
|
|
182
|
+
```toml
|
|
183
|
+
[orchestrator]
|
|
184
|
+
lm = "gpt-5-mini"
|
|
185
|
+
max_steps = 100
|
|
186
|
+
|
|
187
|
+
[orchestrator.compaction]
|
|
188
|
+
enabled = true
|
|
189
|
+
max_tokens = 100000
|
|
190
|
+
threshold_pct = 0.85
|
|
191
|
+
target_pct = 0.7
|
|
192
|
+
|
|
193
|
+
[executor]
|
|
194
|
+
adapter = "codex_mcp"
|
|
195
|
+
model = "gpt-5.1-codex-mini"
|
|
196
|
+
sandbox = "workspace-write"
|
|
197
|
+
timeout = 300
|
|
198
|
+
|
|
199
|
+
[weave]
|
|
200
|
+
enabled = true
|
|
201
|
+
project = "your-entity/zwarm"
|
|
202
|
+
|
|
203
|
+
[watchers]
|
|
204
|
+
enabled = true
|
|
205
|
+
watchers = [
|
|
206
|
+
{ name = "progress" },
|
|
207
|
+
{ name = "budget", config = { max_steps = 50, max_sessions = 10 } },
|
|
208
|
+
{ name = "delegation_reminder", config = { threshold = 10 } },
|
|
209
|
+
]
|
|
210
|
+
```
|
|
211
|
+
|
|
212
|
+
---
|
|
213
|
+
|
|
214
|
+
## Session Management
|
|
215
|
+
|
|
216
|
+
Sessions are the core abstraction. Each session is a conversation with a Codex agent.
|
|
217
|
+
|
|
218
|
+
### Lifecycle
|
|
219
|
+
|
|
220
|
+
```
|
|
221
|
+
spawn → running → completed/failed
|
|
222
|
+
↓
|
|
223
|
+
continue → running → completed
|
|
224
|
+
↓
|
|
225
|
+
continue → ...
|
|
226
|
+
```
|
|
227
|
+
|
|
228
|
+
### Storage
|
|
229
|
+
|
|
230
|
+
```
|
|
231
|
+
.zwarm/sessions/<uuid>/
|
|
232
|
+
├── meta.json # Status, task, model, messages, tokens
|
|
233
|
+
└── turns/
|
|
234
|
+
├── turn_1.jsonl # Raw codex output for turn 1
|
|
235
|
+
├── turn_2.jsonl # Output after first continue
|
|
236
|
+
└── ...
|
|
237
|
+
```
|
|
238
|
+
|
|
239
|
+
### Sync vs Async
|
|
240
|
+
|
|
241
|
+
| Mode | Spawn | Continue | Use case |
|
|
242
|
+
|------|-------|----------|----------|
|
|
243
|
+
| **Sync** | `spawn "task"` | `c id "msg"` | Sequential work, immediate feedback |
|
|
244
|
+
| **Async** | `spawn --async "task"` | `ca id "msg"` | Parallel work, batch processing |
|
|
245
|
+
|
|
246
|
+
Async sessions return immediately. Poll with `ls` or `?` to check status.
|
|
247
|
+
|
|
248
|
+
---
|
|
249
|
+
|
|
250
|
+
## Watchers
|
|
251
|
+
|
|
252
|
+
Watchers monitor agent behavior and intervene when needed.
|
|
253
|
+
|
|
254
|
+
| Watcher | Purpose |
|
|
255
|
+
|---------|---------|
|
|
256
|
+
| `progress` | Detects stuck/spinning agents |
|
|
257
|
+
| `budget` | Enforces step/session limits |
|
|
258
|
+
| `scope` | Detects scope creep |
|
|
259
|
+
| `delegation_reminder` | Nudges orchestrator to delegate |
|
|
260
|
+
|
|
261
|
+
Configure in `config.toml`:
|
|
262
|
+
|
|
263
|
+
```toml
|
|
264
|
+
[watchers]
|
|
265
|
+
enabled = true
|
|
266
|
+
watchers = [
|
|
267
|
+
{ name = "progress" },
|
|
268
|
+
{ name = "budget", config = { max_steps = 50 } },
|
|
269
|
+
]
|
|
270
|
+
```
|
|
271
|
+
|
|
272
|
+
---
|
|
273
|
+
|
|
274
|
+
## CLI Reference
|
|
275
|
+
|
|
276
|
+
```bash
|
|
277
|
+
zwarm init # Initialize .zwarm/ in current directory
|
|
278
|
+
zwarm interactive # Start interactive REPL
|
|
279
|
+
zwarm orchestrate # Start LLM orchestrator
|
|
280
|
+
zwarm exec # Run single executor directly (testing)
|
|
281
|
+
zwarm status # Show current state
|
|
282
|
+
zwarm history # Show event history
|
|
283
|
+
zwarm clean # Remove old sessions
|
|
284
|
+
```
|
|
285
|
+
|
|
286
|
+
---
|
|
287
|
+
|
|
288
|
+
## Project Structure
|
|
289
|
+
|
|
290
|
+
```
|
|
291
|
+
zwarm/
|
|
292
|
+
├── src/zwarm/
|
|
293
|
+
│ ├── sessions/ # Session management (core)
|
|
294
|
+
│ │ ├── manager.py # CodexSessionManager
|
|
295
|
+
│ │ └── __init__.py
|
|
296
|
+
│ ├── tools/
|
|
297
|
+
│ │ └── delegation.py # Orchestrator tools (delegate, converse, etc.)
|
|
298
|
+
│ ├── cli/
|
|
299
|
+
│ │ └── main.py # CLI commands and interactive REPL
|
|
300
|
+
│ ├── core/
|
|
301
|
+
│ │ ├── config.py # Configuration loading
|
|
302
|
+
│ │ ├── compact.py # Context window management
|
|
303
|
+
│ │ └── state.py # State persistence
|
|
304
|
+
│ ├── watchers/ # Trajectory alignment
|
|
305
|
+
│ └── orchestrator.py # Orchestrator agent
|
|
306
|
+
├── docs/
|
|
307
|
+
│ └── INTERNALS.md # Technical architecture
|
|
308
|
+
└── README.md
|
|
309
|
+
```
|
|
@@ -0,0 +1,38 @@
|
|
|
1
|
+
zwarm/__init__.py,sha256=3i3LMjHwIzE-LFIS2aUrwv3EZmpkvVMe-xj1h97rcSM,837
|
|
2
|
+
zwarm/orchestrator.py,sha256=JGRGuJP05Nf5QibuWytjQAC_NuGGaGUR3G-tLq4SVxY,23624
|
|
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
|
+
zwarm/cli/__init__.py,sha256=47DEQpj8HBSa-_TImW-5JCeuQeRkm5NMpJWZG3hSuFU,0
|
|
12
|
+
zwarm/cli/main.py,sha256=cSJ--IHJQv5o3Stb4PMKGIsiMNJn8s-xXvvm6DCjdmA,93294
|
|
13
|
+
zwarm/core/__init__.py,sha256=47DEQpj8HBSa-_TImW-5JCeuQeRkm5NMpJWZG3hSuFU,0
|
|
14
|
+
zwarm/core/compact.py,sha256=Y8C7Gs-5-WOU43WRvQ863Qzd5xtuEqR6Aw3r2p8_-i8,10907
|
|
15
|
+
zwarm/core/config.py,sha256=331i4io9uEnloFwUMjTPJ5_lQFKJR1nhTpA4SPfSpiI,11748
|
|
16
|
+
zwarm/core/environment.py,sha256=zrgh0N3Ng4HI2F1gCYkcQVGzjQPKiIFWuRe1OPRuRn0,6558
|
|
17
|
+
zwarm/core/models.py,sha256=PrC3okRBVJxISUa1Fax4KkagqLT6Xub-kTxC9drN0sY,10083
|
|
18
|
+
zwarm/core/state.py,sha256=MzrvODKEiJovI7YI1jajW4uukineZ3ezmW5oQinMgjg,11563
|
|
19
|
+
zwarm/core/test_compact.py,sha256=WSdjCB5t4YMcknsrkmJIUsVOPY28s4y9GnDmu3Z4BFw,11878
|
|
20
|
+
zwarm/core/test_config.py,sha256=26ozyiFOdjFF2c9Q-HDfFM6GOLfgw_5FZ55nTDMNYA8,4888
|
|
21
|
+
zwarm/core/test_models.py,sha256=sWTIhMZvuLP5AooGR6y8OR2EyWydqVfhmGrE7NPBBnk,8450
|
|
22
|
+
zwarm/prompts/__init__.py,sha256=FiaIOniLrIyfD3_osxT6I7FfyKjtctbf8jNs5QTPs_s,213
|
|
23
|
+
zwarm/prompts/orchestrator.py,sha256=-VZ3B5t-2ALOTpdZyNZGSjjzaHiTufAuLzrTLgwg70M,15442
|
|
24
|
+
zwarm/sessions/__init__.py,sha256=jRibY8IfmNcnkgNmrgK2T81oa1w71wP_KQp9A1hPL7Q,568
|
|
25
|
+
zwarm/sessions/manager.py,sha256=Aq7Wh-WW7ZMP8LgGa3g70wfGg6E2GYjJOBucy6HUfGc,27700
|
|
26
|
+
zwarm/tools/__init__.py,sha256=FpqxwXJA6-fQ7C-oLj30jjK_0qqcE7MbI0dQuaB56kU,290
|
|
27
|
+
zwarm/tools/delegation.py,sha256=kNvc7YISAEUWhlGYCvacxfDVfGA0a4P2kuWgMN9rP0Y,25294
|
|
28
|
+
zwarm/watchers/__init__.py,sha256=a96s7X6ruYkF2ItWWOZ3Q5QUOMOoeCW4Vz8XXcYLXPM,956
|
|
29
|
+
zwarm/watchers/base.py,sha256=r1GoPlj06nOT2xp4fghfSjxbRyFFFQUB6HpZbEyO2OY,3834
|
|
30
|
+
zwarm/watchers/builtin.py,sha256=IL5QwwKOIqWEfJ_uQWb321Px4i5OLtI_vnWQMudqKoA,19064
|
|
31
|
+
zwarm/watchers/llm_watcher.py,sha256=yJGpE3BGKNZX3qgPsiNtJ5d3UJpiTT1V-A-Rh4AiMYM,11029
|
|
32
|
+
zwarm/watchers/manager.py,sha256=XZjBVeHjgCUlkTUeHqdvBvHoBC862U1ik0fG6nlRGog,5587
|
|
33
|
+
zwarm/watchers/registry.py,sha256=A9iBIVIFNtO7KPX0kLpUaP8dAK7ozqWLA44ocJGnOw4,1219
|
|
34
|
+
zwarm/watchers/test_watchers.py,sha256=zOsxumBqKfR5ZVGxrNlxz6KcWjkcdp0QhW9WB0_20zM,7855
|
|
35
|
+
zwarm-2.3.5.dist-info/METADATA,sha256=HAscgpL1b-0D0fBJxqTEJM0APjE-hijsy8G6Lozyr7M,7680
|
|
36
|
+
zwarm-2.3.5.dist-info/WHEEL,sha256=WLgqFyCfm_KASv4WHyYy0P3pM_m7J5L9k2skdKLirC8,87
|
|
37
|
+
zwarm-2.3.5.dist-info/entry_points.txt,sha256=u0OXq4q8d3yJ3EkUXwZfkS-Y8Lcy0F8cWrcQfoRxM6Q,46
|
|
38
|
+
zwarm-2.3.5.dist-info/RECORD,,
|