zwarm 1.3.10__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 +968 -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 +2052 -0
- zwarm/core/__init__.py +0 -0
- zwarm/core/compact.py +329 -0
- zwarm/core/config.py +342 -0
- zwarm/core/environment.py +154 -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 +623 -0
- zwarm/prompts/__init__.py +10 -0
- zwarm/prompts/orchestrator.py +214 -0
- zwarm/sessions/__init__.py +24 -0
- zwarm/sessions/manager.py +589 -0
- zwarm/test_orchestrator_watchers.py +23 -0
- zwarm/tools/__init__.py +17 -0
- zwarm/tools/delegation.py +630 -0
- zwarm/watchers/__init__.py +26 -0
- zwarm/watchers/base.py +131 -0
- zwarm/watchers/builtin.py +424 -0
- zwarm/watchers/manager.py +181 -0
- zwarm/watchers/registry.py +57 -0
- zwarm/watchers/test_watchers.py +237 -0
- zwarm-1.3.10.dist-info/METADATA +525 -0
- zwarm-1.3.10.dist-info/RECORD +37 -0
- zwarm-1.3.10.dist-info/WHEEL +4 -0
- zwarm-1.3.10.dist-info/entry_points.txt +2 -0
|
@@ -0,0 +1,181 @@
|
|
|
1
|
+
"""
|
|
2
|
+
Watcher manager for running multiple watchers.
|
|
3
|
+
|
|
4
|
+
Handles:
|
|
5
|
+
- Running watchers in parallel
|
|
6
|
+
- Combining results by priority
|
|
7
|
+
- Injecting guidance into orchestrator
|
|
8
|
+
"""
|
|
9
|
+
|
|
10
|
+
from __future__ import annotations
|
|
11
|
+
|
|
12
|
+
import asyncio
|
|
13
|
+
from dataclasses import dataclass, field
|
|
14
|
+
from typing import Any
|
|
15
|
+
|
|
16
|
+
import weave
|
|
17
|
+
|
|
18
|
+
from zwarm.watchers.base import Watcher, WatcherContext, WatcherResult, WatcherAction
|
|
19
|
+
from zwarm.watchers.registry import get_watcher
|
|
20
|
+
|
|
21
|
+
|
|
22
|
+
@dataclass
|
|
23
|
+
class WatcherConfig:
|
|
24
|
+
"""Configuration for a watcher instance."""
|
|
25
|
+
|
|
26
|
+
name: str
|
|
27
|
+
enabled: bool = True
|
|
28
|
+
config: dict[str, Any] = field(default_factory=dict)
|
|
29
|
+
|
|
30
|
+
|
|
31
|
+
class WatcherManager:
|
|
32
|
+
"""
|
|
33
|
+
Manages and runs multiple watchers.
|
|
34
|
+
|
|
35
|
+
Watchers run in parallel and results are combined by priority.
|
|
36
|
+
"""
|
|
37
|
+
|
|
38
|
+
def __init__(self, watcher_configs: list[WatcherConfig | dict] | None = None):
|
|
39
|
+
"""
|
|
40
|
+
Initialize manager with watcher configurations.
|
|
41
|
+
|
|
42
|
+
Args:
|
|
43
|
+
watcher_configs: List of WatcherConfig or dicts with watcher configs
|
|
44
|
+
"""
|
|
45
|
+
self._watchers: list[Watcher] = []
|
|
46
|
+
self._results_history: list[tuple[str, WatcherResult]] = []
|
|
47
|
+
|
|
48
|
+
# Load watchers from configs
|
|
49
|
+
for cfg in watcher_configs or []:
|
|
50
|
+
if isinstance(cfg, dict):
|
|
51
|
+
cfg = WatcherConfig(**cfg)
|
|
52
|
+
|
|
53
|
+
if cfg.enabled:
|
|
54
|
+
try:
|
|
55
|
+
watcher = get_watcher(cfg.name, cfg.config)
|
|
56
|
+
self._watchers.append(watcher)
|
|
57
|
+
except ValueError:
|
|
58
|
+
# Unknown watcher, skip
|
|
59
|
+
pass
|
|
60
|
+
|
|
61
|
+
def add_watcher(self, watcher: Watcher) -> None:
|
|
62
|
+
"""Add a watcher instance."""
|
|
63
|
+
self._watchers.append(watcher)
|
|
64
|
+
|
|
65
|
+
@weave.op()
|
|
66
|
+
async def _run_single_watcher(
|
|
67
|
+
self,
|
|
68
|
+
watcher_name: str,
|
|
69
|
+
watcher: Watcher,
|
|
70
|
+
ctx: WatcherContext,
|
|
71
|
+
) -> dict[str, Any]:
|
|
72
|
+
"""Run a single watcher - traced by Weave."""
|
|
73
|
+
try:
|
|
74
|
+
result = await watcher.observe(ctx)
|
|
75
|
+
return {
|
|
76
|
+
"watcher": watcher_name,
|
|
77
|
+
"action": result.action.value,
|
|
78
|
+
"priority": result.priority,
|
|
79
|
+
"reason": result.reason,
|
|
80
|
+
"guidance": result.guidance,
|
|
81
|
+
"metadata": result.metadata,
|
|
82
|
+
"success": True,
|
|
83
|
+
}
|
|
84
|
+
except Exception as e:
|
|
85
|
+
return {
|
|
86
|
+
"watcher": watcher_name,
|
|
87
|
+
"success": False,
|
|
88
|
+
"error": str(e),
|
|
89
|
+
}
|
|
90
|
+
|
|
91
|
+
@weave.op()
|
|
92
|
+
async def observe(self, ctx: WatcherContext) -> WatcherResult:
|
|
93
|
+
"""
|
|
94
|
+
Run all watchers and return combined result.
|
|
95
|
+
|
|
96
|
+
Results are combined by priority:
|
|
97
|
+
- ABORT takes precedence over everything
|
|
98
|
+
- PAUSE takes precedence over NUDGE
|
|
99
|
+
- NUDGE takes precedence over CONTINUE
|
|
100
|
+
- Within same action, higher priority wins
|
|
101
|
+
|
|
102
|
+
Args:
|
|
103
|
+
ctx: Context for watchers
|
|
104
|
+
|
|
105
|
+
Returns:
|
|
106
|
+
Combined WatcherResult
|
|
107
|
+
"""
|
|
108
|
+
if not self._watchers:
|
|
109
|
+
return WatcherResult.ok()
|
|
110
|
+
|
|
111
|
+
# Run all watchers in parallel - each traced individually
|
|
112
|
+
tasks = [
|
|
113
|
+
self._run_single_watcher(watcher.name, watcher, ctx)
|
|
114
|
+
for watcher in self._watchers
|
|
115
|
+
]
|
|
116
|
+
watcher_outputs = await asyncio.gather(*tasks)
|
|
117
|
+
|
|
118
|
+
# Collect valid results with their watcher names
|
|
119
|
+
valid_results: list[tuple[str, WatcherResult]] = []
|
|
120
|
+
for watcher, output in zip(self._watchers, watcher_outputs):
|
|
121
|
+
if not output.get("success"):
|
|
122
|
+
# Log and skip failed watchers
|
|
123
|
+
continue
|
|
124
|
+
result = WatcherResult(
|
|
125
|
+
action=WatcherAction(output["action"]),
|
|
126
|
+
priority=output["priority"],
|
|
127
|
+
reason=output.get("reason"),
|
|
128
|
+
guidance=output.get("guidance"),
|
|
129
|
+
metadata=output.get("metadata", {}),
|
|
130
|
+
)
|
|
131
|
+
valid_results.append((watcher.name, result))
|
|
132
|
+
self._results_history.append((watcher.name, result))
|
|
133
|
+
|
|
134
|
+
if not valid_results:
|
|
135
|
+
return WatcherResult.ok()
|
|
136
|
+
|
|
137
|
+
# Sort by action severity (abort > pause > nudge > continue) then priority
|
|
138
|
+
def sort_key(item: tuple[str, WatcherResult]) -> tuple[int, int]:
|
|
139
|
+
_, result = item
|
|
140
|
+
action_order = {
|
|
141
|
+
WatcherAction.ABORT: 0,
|
|
142
|
+
WatcherAction.PAUSE: 1,
|
|
143
|
+
WatcherAction.NUDGE: 2,
|
|
144
|
+
WatcherAction.CONTINUE: 3,
|
|
145
|
+
}
|
|
146
|
+
return (action_order[result.action], -result.priority)
|
|
147
|
+
|
|
148
|
+
valid_results.sort(key=sort_key)
|
|
149
|
+
|
|
150
|
+
# Return highest priority non-continue result
|
|
151
|
+
for name, result in valid_results:
|
|
152
|
+
if result.action != WatcherAction.CONTINUE:
|
|
153
|
+
# Add which watcher triggered this
|
|
154
|
+
result.metadata["triggered_by"] = name
|
|
155
|
+
return result
|
|
156
|
+
|
|
157
|
+
return WatcherResult.ok()
|
|
158
|
+
|
|
159
|
+
def get_history(self) -> list[tuple[str, WatcherResult]]:
|
|
160
|
+
"""Get history of all watcher results."""
|
|
161
|
+
return list(self._results_history)
|
|
162
|
+
|
|
163
|
+
def clear_history(self) -> None:
|
|
164
|
+
"""Clear results history."""
|
|
165
|
+
self._results_history.clear()
|
|
166
|
+
|
|
167
|
+
|
|
168
|
+
def build_watcher_manager(
|
|
169
|
+
config: dict[str, Any] | None = None
|
|
170
|
+
) -> WatcherManager:
|
|
171
|
+
"""
|
|
172
|
+
Build a WatcherManager from configuration.
|
|
173
|
+
|
|
174
|
+
Args:
|
|
175
|
+
config: Dict with "watchers" key containing list of watcher configs
|
|
176
|
+
|
|
177
|
+
Returns:
|
|
178
|
+
Configured WatcherManager
|
|
179
|
+
"""
|
|
180
|
+
watcher_configs = (config or {}).get("watchers", [])
|
|
181
|
+
return WatcherManager(watcher_configs)
|
|
@@ -0,0 +1,57 @@
|
|
|
1
|
+
"""
|
|
2
|
+
Watcher registry for discovering and instantiating watchers.
|
|
3
|
+
"""
|
|
4
|
+
|
|
5
|
+
from __future__ import annotations
|
|
6
|
+
|
|
7
|
+
from typing import Any, Type
|
|
8
|
+
|
|
9
|
+
from zwarm.watchers.base import Watcher
|
|
10
|
+
|
|
11
|
+
|
|
12
|
+
# Global watcher registry
|
|
13
|
+
_WATCHERS: dict[str, Type[Watcher]] = {}
|
|
14
|
+
|
|
15
|
+
|
|
16
|
+
def register_watcher(name: str):
|
|
17
|
+
"""
|
|
18
|
+
Decorator to register a watcher class.
|
|
19
|
+
|
|
20
|
+
Example:
|
|
21
|
+
@register_watcher("progress")
|
|
22
|
+
class ProgressWatcher(Watcher):
|
|
23
|
+
...
|
|
24
|
+
"""
|
|
25
|
+
|
|
26
|
+
def decorator(cls: Type[Watcher]) -> Type[Watcher]:
|
|
27
|
+
cls.name = name
|
|
28
|
+
_WATCHERS[name] = cls
|
|
29
|
+
return cls
|
|
30
|
+
|
|
31
|
+
return decorator
|
|
32
|
+
|
|
33
|
+
|
|
34
|
+
def get_watcher(name: str, config: dict[str, Any] | None = None) -> Watcher:
|
|
35
|
+
"""
|
|
36
|
+
Get a watcher instance by name.
|
|
37
|
+
|
|
38
|
+
Args:
|
|
39
|
+
name: Registered watcher name
|
|
40
|
+
config: Optional config to pass to watcher
|
|
41
|
+
|
|
42
|
+
Returns:
|
|
43
|
+
Instantiated watcher
|
|
44
|
+
|
|
45
|
+
Raises:
|
|
46
|
+
ValueError: If watcher not found
|
|
47
|
+
"""
|
|
48
|
+
if name not in _WATCHERS:
|
|
49
|
+
raise ValueError(
|
|
50
|
+
f"Unknown watcher: {name}. Available: {list(_WATCHERS.keys())}"
|
|
51
|
+
)
|
|
52
|
+
return _WATCHERS[name](config)
|
|
53
|
+
|
|
54
|
+
|
|
55
|
+
def list_watchers() -> list[str]:
|
|
56
|
+
"""List all registered watcher names."""
|
|
57
|
+
return list(_WATCHERS.keys())
|
|
@@ -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
|