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,160 @@
|
|
|
1
|
+
"""Tests for the config system."""
|
|
2
|
+
|
|
3
|
+
import tempfile
|
|
4
|
+
from pathlib import Path
|
|
5
|
+
|
|
6
|
+
import pytest
|
|
7
|
+
|
|
8
|
+
from zwarm.core.config import (
|
|
9
|
+
ZwarmConfig,
|
|
10
|
+
apply_overrides,
|
|
11
|
+
deep_merge,
|
|
12
|
+
load_config,
|
|
13
|
+
load_yaml_config,
|
|
14
|
+
)
|
|
15
|
+
|
|
16
|
+
|
|
17
|
+
def test_default_config():
|
|
18
|
+
"""Test default configuration values."""
|
|
19
|
+
config = ZwarmConfig()
|
|
20
|
+
assert config.executor.adapter == "codex_mcp"
|
|
21
|
+
assert config.executor.sandbox == "workspace-write"
|
|
22
|
+
assert config.orchestrator.lm == "gpt-5-mini"
|
|
23
|
+
assert config.orchestrator.sync_first is True
|
|
24
|
+
assert config.state_dir == ".zwarm"
|
|
25
|
+
|
|
26
|
+
|
|
27
|
+
def test_config_from_dict():
|
|
28
|
+
"""Test creating config from dictionary."""
|
|
29
|
+
data = {
|
|
30
|
+
"executor": {"adapter": "claude_code", "model": "claude-sonnet"},
|
|
31
|
+
"orchestrator": {"lm": "gpt-5", "max_steps": 100},
|
|
32
|
+
"state_dir": ".my_state",
|
|
33
|
+
}
|
|
34
|
+
config = ZwarmConfig.from_dict(data)
|
|
35
|
+
assert config.executor.adapter == "claude_code"
|
|
36
|
+
assert config.executor.model == "claude-sonnet"
|
|
37
|
+
assert config.orchestrator.lm == "gpt-5"
|
|
38
|
+
assert config.orchestrator.max_steps == 100
|
|
39
|
+
assert config.state_dir == ".my_state"
|
|
40
|
+
|
|
41
|
+
|
|
42
|
+
def test_config_to_dict():
|
|
43
|
+
"""Test converting config to dictionary."""
|
|
44
|
+
config = ZwarmConfig()
|
|
45
|
+
data = config.to_dict()
|
|
46
|
+
assert data["executor"]["adapter"] == "codex_mcp"
|
|
47
|
+
assert data["orchestrator"]["lm"] == "gpt-5-mini"
|
|
48
|
+
|
|
49
|
+
|
|
50
|
+
def test_deep_merge():
|
|
51
|
+
"""Test deep merging of dictionaries."""
|
|
52
|
+
base = {"a": 1, "b": {"c": 2, "d": 3}}
|
|
53
|
+
override = {"b": {"c": 99}, "e": 4}
|
|
54
|
+
result = deep_merge(base, override)
|
|
55
|
+
assert result == {"a": 1, "b": {"c": 99, "d": 3}, "e": 4}
|
|
56
|
+
|
|
57
|
+
|
|
58
|
+
def test_apply_overrides():
|
|
59
|
+
"""Test CLI override application."""
|
|
60
|
+
config = {"executor": {"adapter": "codex"}, "orchestrator": {"max_steps": 10}}
|
|
61
|
+
|
|
62
|
+
# Override nested value
|
|
63
|
+
result = apply_overrides(config, ["orchestrator.max_steps=50"])
|
|
64
|
+
assert result["orchestrator"]["max_steps"] == 50
|
|
65
|
+
|
|
66
|
+
# Override with string
|
|
67
|
+
result = apply_overrides(config, ["executor.adapter=claude_code"])
|
|
68
|
+
assert result["executor"]["adapter"] == "claude_code"
|
|
69
|
+
|
|
70
|
+
# Override with boolean
|
|
71
|
+
result = apply_overrides(config, ["orchestrator.sync_first=false"])
|
|
72
|
+
assert result["orchestrator"]["sync_first"] is False
|
|
73
|
+
|
|
74
|
+
# Create new nested path
|
|
75
|
+
result = apply_overrides(config, ["weave.project=my-project"])
|
|
76
|
+
assert result["weave"]["project"] == "my-project"
|
|
77
|
+
|
|
78
|
+
|
|
79
|
+
def test_yaml_inheritance():
|
|
80
|
+
"""Test YAML config inheritance via extends."""
|
|
81
|
+
with tempfile.TemporaryDirectory() as tmpdir:
|
|
82
|
+
base_path = Path(tmpdir) / "base.yaml"
|
|
83
|
+
child_path = Path(tmpdir) / "child.yaml"
|
|
84
|
+
|
|
85
|
+
# Write base config
|
|
86
|
+
base_path.write_text("""
|
|
87
|
+
executor:
|
|
88
|
+
adapter: codex_mcp
|
|
89
|
+
timeout: 3600
|
|
90
|
+
orchestrator:
|
|
91
|
+
lm: gpt-5-mini
|
|
92
|
+
max_steps: 30
|
|
93
|
+
""")
|
|
94
|
+
|
|
95
|
+
# Write child config that extends base
|
|
96
|
+
child_path.write_text("""
|
|
97
|
+
extends: base.yaml
|
|
98
|
+
orchestrator:
|
|
99
|
+
lm: gpt-5
|
|
100
|
+
prompt: prompts/aggressive.yaml
|
|
101
|
+
""")
|
|
102
|
+
|
|
103
|
+
config = load_yaml_config(child_path)
|
|
104
|
+
assert config["executor"]["adapter"] == "codex_mcp"
|
|
105
|
+
assert config["executor"]["timeout"] == 3600
|
|
106
|
+
assert config["orchestrator"]["lm"] == "gpt-5" # overridden
|
|
107
|
+
assert config["orchestrator"]["max_steps"] == 30 # inherited
|
|
108
|
+
assert config["orchestrator"]["prompt"] == "prompts/aggressive.yaml" # new
|
|
109
|
+
|
|
110
|
+
|
|
111
|
+
def test_load_config_full_chain():
|
|
112
|
+
"""Test full config loading with precedence."""
|
|
113
|
+
import os
|
|
114
|
+
|
|
115
|
+
# Save and clear WEAVE_PROJECT to test config precedence
|
|
116
|
+
orig_weave = os.environ.pop("WEAVE_PROJECT", None)
|
|
117
|
+
|
|
118
|
+
try:
|
|
119
|
+
with tempfile.TemporaryDirectory() as tmpdir:
|
|
120
|
+
tmpdir = Path(tmpdir)
|
|
121
|
+
|
|
122
|
+
# Write config.toml
|
|
123
|
+
toml_path = tmpdir / "config.toml"
|
|
124
|
+
toml_path.write_text("""
|
|
125
|
+
[weave]
|
|
126
|
+
project = "my-weave-project"
|
|
127
|
+
|
|
128
|
+
[executor]
|
|
129
|
+
adapter = "codex_mcp"
|
|
130
|
+
""")
|
|
131
|
+
|
|
132
|
+
# Write yaml config
|
|
133
|
+
yaml_path = tmpdir / "experiment.yaml"
|
|
134
|
+
yaml_path.write_text("""
|
|
135
|
+
orchestrator:
|
|
136
|
+
lm: claude-sonnet
|
|
137
|
+
max_steps: 100
|
|
138
|
+
""")
|
|
139
|
+
|
|
140
|
+
# Load with override (use non-existent env_path to prevent loading cwd's .env)
|
|
141
|
+
config = load_config(
|
|
142
|
+
config_path=yaml_path,
|
|
143
|
+
toml_path=toml_path,
|
|
144
|
+
env_path=tmpdir / ".env.nonexistent",
|
|
145
|
+
overrides=["orchestrator.max_steps=200"],
|
|
146
|
+
)
|
|
147
|
+
|
|
148
|
+
# Check precedence: override > yaml > toml > default
|
|
149
|
+
assert config.weave.project == "my-weave-project" # from toml
|
|
150
|
+
assert config.executor.adapter == "codex_mcp" # from toml
|
|
151
|
+
assert config.orchestrator.lm == "claude-sonnet" # from yaml
|
|
152
|
+
assert config.orchestrator.max_steps == 200 # from override
|
|
153
|
+
finally:
|
|
154
|
+
# Restore WEAVE_PROJECT
|
|
155
|
+
if orig_weave is not None:
|
|
156
|
+
os.environ["WEAVE_PROJECT"] = orig_weave
|
|
157
|
+
|
|
158
|
+
|
|
159
|
+
if __name__ == "__main__":
|
|
160
|
+
pytest.main([__file__, "-v"])
|
|
@@ -0,0 +1,265 @@
|
|
|
1
|
+
"""Tests for core models and state management."""
|
|
2
|
+
|
|
3
|
+
import tempfile
|
|
4
|
+
from pathlib import Path
|
|
5
|
+
|
|
6
|
+
import pytest
|
|
7
|
+
|
|
8
|
+
from zwarm.core.models import (
|
|
9
|
+
ConversationSession,
|
|
10
|
+
Event,
|
|
11
|
+
Message,
|
|
12
|
+
SessionMode,
|
|
13
|
+
SessionStatus,
|
|
14
|
+
Task,
|
|
15
|
+
TaskStatus,
|
|
16
|
+
event_session_completed,
|
|
17
|
+
event_session_started,
|
|
18
|
+
event_task_created,
|
|
19
|
+
)
|
|
20
|
+
from zwarm.core.state import StateManager
|
|
21
|
+
|
|
22
|
+
|
|
23
|
+
class TestMessage:
|
|
24
|
+
def test_create_message(self):
|
|
25
|
+
msg = Message(role="user", content="Hello")
|
|
26
|
+
assert msg.role == "user"
|
|
27
|
+
assert msg.content == "Hello"
|
|
28
|
+
assert msg.timestamp is not None
|
|
29
|
+
|
|
30
|
+
def test_message_serialization(self):
|
|
31
|
+
msg = Message(role="assistant", content="Hi there")
|
|
32
|
+
data = msg.to_dict()
|
|
33
|
+
restored = Message.from_dict(data)
|
|
34
|
+
assert restored.role == msg.role
|
|
35
|
+
assert restored.content == msg.content
|
|
36
|
+
|
|
37
|
+
|
|
38
|
+
class TestConversationSession:
|
|
39
|
+
def test_create_session(self):
|
|
40
|
+
session = ConversationSession(
|
|
41
|
+
adapter="codex_mcp",
|
|
42
|
+
mode=SessionMode.SYNC,
|
|
43
|
+
task_description="Test task",
|
|
44
|
+
)
|
|
45
|
+
assert session.id is not None
|
|
46
|
+
assert session.adapter == "codex_mcp"
|
|
47
|
+
assert session.mode == SessionMode.SYNC
|
|
48
|
+
assert session.status == SessionStatus.ACTIVE
|
|
49
|
+
|
|
50
|
+
def test_add_message(self):
|
|
51
|
+
session = ConversationSession()
|
|
52
|
+
session.add_message("user", "Hello")
|
|
53
|
+
session.add_message("assistant", "Hi there")
|
|
54
|
+
assert len(session.messages) == 2
|
|
55
|
+
assert session.messages[0].role == "user"
|
|
56
|
+
assert session.messages[1].role == "assistant"
|
|
57
|
+
|
|
58
|
+
def test_complete_session(self):
|
|
59
|
+
session = ConversationSession()
|
|
60
|
+
session.complete("Done!")
|
|
61
|
+
assert session.status == SessionStatus.COMPLETED
|
|
62
|
+
assert session.completed_at is not None
|
|
63
|
+
assert session.exit_message == "Done!"
|
|
64
|
+
|
|
65
|
+
def test_fail_session(self):
|
|
66
|
+
session = ConversationSession()
|
|
67
|
+
session.fail("Error occurred")
|
|
68
|
+
assert session.status == SessionStatus.FAILED
|
|
69
|
+
assert session.exit_message == "Error occurred"
|
|
70
|
+
|
|
71
|
+
def test_session_serialization(self):
|
|
72
|
+
session = ConversationSession(
|
|
73
|
+
adapter="claude_code",
|
|
74
|
+
mode=SessionMode.ASYNC,
|
|
75
|
+
task_description="Build feature",
|
|
76
|
+
model="claude-sonnet",
|
|
77
|
+
)
|
|
78
|
+
session.add_message("user", "Start")
|
|
79
|
+
session.conversation_id = "conv-123"
|
|
80
|
+
|
|
81
|
+
data = session.to_dict()
|
|
82
|
+
restored = ConversationSession.from_dict(data)
|
|
83
|
+
|
|
84
|
+
assert restored.id == session.id
|
|
85
|
+
assert restored.adapter == "claude_code"
|
|
86
|
+
assert restored.mode == SessionMode.ASYNC
|
|
87
|
+
assert restored.conversation_id == "conv-123"
|
|
88
|
+
assert len(restored.messages) == 1
|
|
89
|
+
|
|
90
|
+
|
|
91
|
+
class TestTask:
|
|
92
|
+
def test_create_task(self):
|
|
93
|
+
task = Task(description="Fix the bug")
|
|
94
|
+
assert task.id is not None
|
|
95
|
+
assert task.status == TaskStatus.PENDING
|
|
96
|
+
|
|
97
|
+
def test_task_lifecycle(self):
|
|
98
|
+
task = Task(description="Implement feature")
|
|
99
|
+
|
|
100
|
+
# Start
|
|
101
|
+
task.start("session-123")
|
|
102
|
+
assert task.status == TaskStatus.IN_PROGRESS
|
|
103
|
+
assert task.session_id == "session-123"
|
|
104
|
+
|
|
105
|
+
# Complete
|
|
106
|
+
task.complete("Feature implemented")
|
|
107
|
+
assert task.status == TaskStatus.COMPLETED
|
|
108
|
+
assert task.result == "Feature implemented"
|
|
109
|
+
|
|
110
|
+
def test_task_serialization(self):
|
|
111
|
+
task = Task(description="Test task", parent_task_id="parent-123")
|
|
112
|
+
task.start("session-456")
|
|
113
|
+
|
|
114
|
+
data = task.to_dict()
|
|
115
|
+
restored = Task.from_dict(data)
|
|
116
|
+
|
|
117
|
+
assert restored.id == task.id
|
|
118
|
+
assert restored.description == "Test task"
|
|
119
|
+
assert restored.parent_task_id == "parent-123"
|
|
120
|
+
assert restored.session_id == "session-456"
|
|
121
|
+
|
|
122
|
+
|
|
123
|
+
class TestEvent:
|
|
124
|
+
def test_create_event(self):
|
|
125
|
+
event = Event(
|
|
126
|
+
kind="test_event",
|
|
127
|
+
session_id="session-123",
|
|
128
|
+
payload={"key": "value"},
|
|
129
|
+
)
|
|
130
|
+
assert event.id is not None
|
|
131
|
+
assert event.kind == "test_event"
|
|
132
|
+
|
|
133
|
+
def test_event_factories(self):
|
|
134
|
+
session = ConversationSession(task_description="Test")
|
|
135
|
+
event = event_session_started(session)
|
|
136
|
+
assert event.kind == "session_started"
|
|
137
|
+
assert event.session_id == session.id
|
|
138
|
+
|
|
139
|
+
task = Task(description="Do something")
|
|
140
|
+
event = event_task_created(task)
|
|
141
|
+
assert event.kind == "task_created"
|
|
142
|
+
assert event.task_id == task.id
|
|
143
|
+
|
|
144
|
+
|
|
145
|
+
class TestStateManager:
|
|
146
|
+
def test_init_creates_directories(self):
|
|
147
|
+
with tempfile.TemporaryDirectory() as tmpdir:
|
|
148
|
+
state_dir = Path(tmpdir) / ".zwarm"
|
|
149
|
+
manager = StateManager(state_dir)
|
|
150
|
+
manager.init()
|
|
151
|
+
|
|
152
|
+
assert state_dir.exists()
|
|
153
|
+
assert (state_dir / "sessions").exists()
|
|
154
|
+
assert (state_dir / "orchestrator").exists()
|
|
155
|
+
assert (state_dir / "events.jsonl").exists()
|
|
156
|
+
|
|
157
|
+
def test_session_crud(self):
|
|
158
|
+
with tempfile.TemporaryDirectory() as tmpdir:
|
|
159
|
+
manager = StateManager(Path(tmpdir) / ".zwarm")
|
|
160
|
+
manager.init()
|
|
161
|
+
|
|
162
|
+
# Add
|
|
163
|
+
session = ConversationSession(task_description="Test")
|
|
164
|
+
manager.add_session(session)
|
|
165
|
+
|
|
166
|
+
# Get
|
|
167
|
+
retrieved = manager.get_session(session.id)
|
|
168
|
+
assert retrieved is not None
|
|
169
|
+
assert retrieved.task_description == "Test"
|
|
170
|
+
|
|
171
|
+
# Update
|
|
172
|
+
session.add_message("user", "Hello")
|
|
173
|
+
manager.update_session(session)
|
|
174
|
+
|
|
175
|
+
# List
|
|
176
|
+
sessions = manager.list_sessions()
|
|
177
|
+
assert len(sessions) == 1
|
|
178
|
+
|
|
179
|
+
# Filter by status
|
|
180
|
+
active = manager.list_sessions(status="active")
|
|
181
|
+
assert len(active) == 1
|
|
182
|
+
|
|
183
|
+
def test_task_crud(self):
|
|
184
|
+
with tempfile.TemporaryDirectory() as tmpdir:
|
|
185
|
+
manager = StateManager(Path(tmpdir) / ".zwarm")
|
|
186
|
+
manager.init()
|
|
187
|
+
|
|
188
|
+
# Add
|
|
189
|
+
task = Task(description="Build feature")
|
|
190
|
+
manager.add_task(task)
|
|
191
|
+
|
|
192
|
+
# Get
|
|
193
|
+
retrieved = manager.get_task(task.id)
|
|
194
|
+
assert retrieved is not None
|
|
195
|
+
|
|
196
|
+
# Update
|
|
197
|
+
task.start("session-123")
|
|
198
|
+
manager.update_task(task)
|
|
199
|
+
|
|
200
|
+
# List
|
|
201
|
+
tasks = manager.list_tasks(status="in_progress")
|
|
202
|
+
assert len(tasks) == 1
|
|
203
|
+
|
|
204
|
+
def test_event_logging(self):
|
|
205
|
+
with tempfile.TemporaryDirectory() as tmpdir:
|
|
206
|
+
manager = StateManager(Path(tmpdir) / ".zwarm")
|
|
207
|
+
manager.init()
|
|
208
|
+
|
|
209
|
+
# Log events
|
|
210
|
+
session = ConversationSession()
|
|
211
|
+
manager.log_event(event_session_started(session))
|
|
212
|
+
manager.log_event(event_session_completed(session))
|
|
213
|
+
|
|
214
|
+
# Read events
|
|
215
|
+
events = manager.get_events()
|
|
216
|
+
assert len(events) == 2
|
|
217
|
+
assert events[0].kind == "session_completed" # Most recent first
|
|
218
|
+
|
|
219
|
+
# Filter by session
|
|
220
|
+
events = manager.get_events(session_id=session.id)
|
|
221
|
+
assert len(events) == 2
|
|
222
|
+
|
|
223
|
+
# Filter by kind
|
|
224
|
+
events = manager.get_events(kind="session_started")
|
|
225
|
+
assert len(events) == 1
|
|
226
|
+
|
|
227
|
+
def test_orchestrator_messages(self):
|
|
228
|
+
with tempfile.TemporaryDirectory() as tmpdir:
|
|
229
|
+
manager = StateManager(Path(tmpdir) / ".zwarm")
|
|
230
|
+
manager.init()
|
|
231
|
+
|
|
232
|
+
messages = [
|
|
233
|
+
{"role": "system", "content": "You are an orchestrator"},
|
|
234
|
+
{"role": "user", "content": "Build a feature"},
|
|
235
|
+
]
|
|
236
|
+
manager.save_orchestrator_messages(messages)
|
|
237
|
+
|
|
238
|
+
loaded = manager.load_orchestrator_messages()
|
|
239
|
+
assert len(loaded) == 2
|
|
240
|
+
assert loaded[0]["role"] == "system"
|
|
241
|
+
|
|
242
|
+
def test_state_persistence(self):
|
|
243
|
+
with tempfile.TemporaryDirectory() as tmpdir:
|
|
244
|
+
state_dir = Path(tmpdir) / ".zwarm"
|
|
245
|
+
|
|
246
|
+
# Create and save state
|
|
247
|
+
manager1 = StateManager(state_dir)
|
|
248
|
+
manager1.init()
|
|
249
|
+
|
|
250
|
+
session = ConversationSession(task_description="Persistent session")
|
|
251
|
+
manager1.add_session(session)
|
|
252
|
+
|
|
253
|
+
task = Task(description="Persistent task")
|
|
254
|
+
manager1.add_task(task)
|
|
255
|
+
|
|
256
|
+
# Load in new manager
|
|
257
|
+
manager2 = StateManager(state_dir)
|
|
258
|
+
manager2.load()
|
|
259
|
+
|
|
260
|
+
assert manager2.get_session(session.id) is not None
|
|
261
|
+
assert manager2.get_task(task.id) is not None
|
|
262
|
+
|
|
263
|
+
|
|
264
|
+
if __name__ == "__main__":
|
|
265
|
+
pytest.main([__file__, "-v"])
|