zwarm 0.1.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/__init__.py +38 -0
- zwarm/adapters/__init__.py +0 -0
- zwarm/adapters/base.py +109 -0
- zwarm/adapters/claude_code.py +303 -0
- zwarm/adapters/codex_mcp.py +428 -0
- zwarm/adapters/test_codex_mcp.py +224 -0
- zwarm/cli/__init__.py +0 -0
- zwarm/cli/main.py +534 -0
- zwarm/core/__init__.py +0 -0
- zwarm/core/config.py +271 -0
- zwarm/core/environment.py +83 -0
- zwarm/core/models.py +299 -0
- zwarm/core/state.py +224 -0
- zwarm/core/test_config.py +160 -0
- zwarm/core/test_models.py +265 -0
- zwarm/orchestrator.py +405 -0
- zwarm/prompts/__init__.py +10 -0
- zwarm/prompts/orchestrator.py +214 -0
- zwarm/tools/__init__.py +17 -0
- zwarm/tools/delegation.py +357 -0
- zwarm/watchers/__init__.py +26 -0
- zwarm/watchers/base.py +131 -0
- zwarm/watchers/builtin.py +256 -0
- zwarm/watchers/manager.py +143 -0
- zwarm/watchers/registry.py +57 -0
- zwarm/watchers/test_watchers.py +195 -0
- zwarm-0.1.0.dist-info/METADATA +382 -0
- zwarm-0.1.0.dist-info/RECORD +30 -0
- zwarm-0.1.0.dist-info/WHEEL +4 -0
- zwarm-0.1.0.dist-info/entry_points.txt +2 -0
|
@@ -0,0 +1,357 @@
|
|
|
1
|
+
"""
|
|
2
|
+
Delegation tools for the orchestrator.
|
|
3
|
+
|
|
4
|
+
These are the core tools that orchestrators use to delegate work to executors:
|
|
5
|
+
- delegate: Start a new session with an executor
|
|
6
|
+
- converse: Continue a sync conversation
|
|
7
|
+
- check_session: Check status of an async session
|
|
8
|
+
- end_session: End a session
|
|
9
|
+
"""
|
|
10
|
+
|
|
11
|
+
from __future__ import annotations
|
|
12
|
+
|
|
13
|
+
import asyncio
|
|
14
|
+
from typing import TYPE_CHECKING, Any, Literal
|
|
15
|
+
|
|
16
|
+
from wbal.helper import weaveTool
|
|
17
|
+
|
|
18
|
+
if TYPE_CHECKING:
|
|
19
|
+
from zwarm.orchestrator import Orchestrator
|
|
20
|
+
|
|
21
|
+
|
|
22
|
+
def _truncate(text: str, max_len: int = 200) -> str:
|
|
23
|
+
"""Truncate text with ellipsis."""
|
|
24
|
+
if len(text) <= max_len:
|
|
25
|
+
return text
|
|
26
|
+
return text[:max_len - 3] + "..."
|
|
27
|
+
|
|
28
|
+
|
|
29
|
+
def _format_session_header(session_id: str, adapter: str, mode: str) -> str:
|
|
30
|
+
"""Format a nice session header."""
|
|
31
|
+
return f"[{session_id[:8]}] {adapter} ({mode})"
|
|
32
|
+
|
|
33
|
+
|
|
34
|
+
@weaveTool
|
|
35
|
+
def delegate(
|
|
36
|
+
self: "Orchestrator",
|
|
37
|
+
task: str,
|
|
38
|
+
mode: Literal["sync", "async"] = "sync",
|
|
39
|
+
adapter: str | None = None,
|
|
40
|
+
model: str | None = None,
|
|
41
|
+
) -> dict[str, Any]:
|
|
42
|
+
"""
|
|
43
|
+
Delegate work to an executor agent.
|
|
44
|
+
|
|
45
|
+
Use this to assign coding tasks to an executor. Two modes available:
|
|
46
|
+
|
|
47
|
+
**sync** (default): Start a conversation with the executor.
|
|
48
|
+
You can iteratively refine requirements using converse().
|
|
49
|
+
Best for: ambiguous tasks, complex requirements, tasks needing guidance.
|
|
50
|
+
|
|
51
|
+
**async**: Fire-and-forget execution.
|
|
52
|
+
Check progress later with check_session().
|
|
53
|
+
Best for: clear self-contained tasks, parallel work.
|
|
54
|
+
|
|
55
|
+
Args:
|
|
56
|
+
task: Clear description of what to do. Be specific about requirements.
|
|
57
|
+
mode: "sync" for conversational, "async" for fire-and-forget.
|
|
58
|
+
adapter: Which executor adapter to use (default: config setting).
|
|
59
|
+
model: Model override for the executor.
|
|
60
|
+
|
|
61
|
+
Returns:
|
|
62
|
+
{session_id, status, response (if sync)}
|
|
63
|
+
|
|
64
|
+
Example:
|
|
65
|
+
delegate(task="Add a logout button to the navbar", mode="sync")
|
|
66
|
+
# Then use converse() to refine: "Also add a confirmation dialog"
|
|
67
|
+
"""
|
|
68
|
+
# Get adapter (use default from config if not specified)
|
|
69
|
+
adapter_name = adapter or self.config.executor.adapter
|
|
70
|
+
executor = self._get_adapter(adapter_name)
|
|
71
|
+
|
|
72
|
+
# Run async start_session
|
|
73
|
+
session = asyncio.get_event_loop().run_until_complete(
|
|
74
|
+
executor.start_session(
|
|
75
|
+
task=task,
|
|
76
|
+
working_dir=self.working_dir,
|
|
77
|
+
mode=mode,
|
|
78
|
+
model=model or self.config.executor.model,
|
|
79
|
+
sandbox=self.config.executor.sandbox,
|
|
80
|
+
)
|
|
81
|
+
)
|
|
82
|
+
|
|
83
|
+
# Track session
|
|
84
|
+
self._sessions[session.id] = session
|
|
85
|
+
self.state.add_session(session)
|
|
86
|
+
|
|
87
|
+
# Log events
|
|
88
|
+
from zwarm.core.models import event_session_started, event_message_sent, Message
|
|
89
|
+
self.state.log_event(event_session_started(session))
|
|
90
|
+
self.state.log_event(event_message_sent(session, Message(role="user", content=task)))
|
|
91
|
+
|
|
92
|
+
# Get response for sync mode
|
|
93
|
+
response_text = ""
|
|
94
|
+
if mode == "sync" and session.messages:
|
|
95
|
+
response_text = session.messages[-1].content
|
|
96
|
+
# Log the assistant response too
|
|
97
|
+
self.state.log_event(event_message_sent(
|
|
98
|
+
session,
|
|
99
|
+
Message(role="assistant", content=response_text)
|
|
100
|
+
))
|
|
101
|
+
|
|
102
|
+
# Build nice result
|
|
103
|
+
header = _format_session_header(session.id, adapter_name, mode)
|
|
104
|
+
|
|
105
|
+
if mode == "sync":
|
|
106
|
+
return {
|
|
107
|
+
"success": True,
|
|
108
|
+
"session": header,
|
|
109
|
+
"session_id": session.id,
|
|
110
|
+
"status": "active",
|
|
111
|
+
"task": _truncate(task, 100),
|
|
112
|
+
"response": response_text,
|
|
113
|
+
"hint": "Use converse(session_id, message) to continue this conversation",
|
|
114
|
+
}
|
|
115
|
+
else:
|
|
116
|
+
return {
|
|
117
|
+
"success": True,
|
|
118
|
+
"session": header,
|
|
119
|
+
"session_id": session.id,
|
|
120
|
+
"status": "running",
|
|
121
|
+
"task": _truncate(task, 100),
|
|
122
|
+
"hint": "Use check_session(session_id) to monitor progress",
|
|
123
|
+
}
|
|
124
|
+
|
|
125
|
+
|
|
126
|
+
@weaveTool
|
|
127
|
+
def converse(
|
|
128
|
+
self: "Orchestrator",
|
|
129
|
+
session_id: str,
|
|
130
|
+
message: str,
|
|
131
|
+
) -> dict[str, Any]:
|
|
132
|
+
"""
|
|
133
|
+
Continue a sync conversation with an executor.
|
|
134
|
+
|
|
135
|
+
Use this to iteratively refine requirements, ask for changes,
|
|
136
|
+
or guide the executor step-by-step. Like chatting with a developer.
|
|
137
|
+
|
|
138
|
+
Args:
|
|
139
|
+
session_id: The session to continue (from delegate() result).
|
|
140
|
+
message: Your next message to the executor.
|
|
141
|
+
|
|
142
|
+
Returns:
|
|
143
|
+
{session_id, response, turn}
|
|
144
|
+
|
|
145
|
+
Example:
|
|
146
|
+
result = delegate(task="Add user authentication")
|
|
147
|
+
# Executor responds with initial plan
|
|
148
|
+
converse(session_id=result["session_id"], message="Use JWT, not sessions")
|
|
149
|
+
# Executor adjusts approach
|
|
150
|
+
converse(session_id=result["session_id"], message="Now add tests")
|
|
151
|
+
"""
|
|
152
|
+
session = self._sessions.get(session_id)
|
|
153
|
+
if not session:
|
|
154
|
+
return {
|
|
155
|
+
"success": False,
|
|
156
|
+
"error": f"Unknown session: {session_id}",
|
|
157
|
+
"hint": "Use list_sessions() to see available sessions",
|
|
158
|
+
}
|
|
159
|
+
|
|
160
|
+
if session.mode.value != "sync":
|
|
161
|
+
return {
|
|
162
|
+
"success": False,
|
|
163
|
+
"error": "Cannot converse with async session",
|
|
164
|
+
"hint": "Use check_session() for async sessions instead",
|
|
165
|
+
}
|
|
166
|
+
|
|
167
|
+
if session.status.value != "active":
|
|
168
|
+
return {
|
|
169
|
+
"success": False,
|
|
170
|
+
"error": f"Session is {session.status.value}, not active",
|
|
171
|
+
"hint": "Start a new session with delegate()",
|
|
172
|
+
}
|
|
173
|
+
|
|
174
|
+
# Get adapter and send message
|
|
175
|
+
executor = self._get_adapter(session.adapter)
|
|
176
|
+
try:
|
|
177
|
+
response = asyncio.get_event_loop().run_until_complete(
|
|
178
|
+
executor.send_message(session, message)
|
|
179
|
+
)
|
|
180
|
+
except Exception as e:
|
|
181
|
+
return {
|
|
182
|
+
"success": False,
|
|
183
|
+
"error": str(e),
|
|
184
|
+
"session_id": session_id,
|
|
185
|
+
}
|
|
186
|
+
|
|
187
|
+
# Update state
|
|
188
|
+
self.state.update_session(session)
|
|
189
|
+
|
|
190
|
+
# Log both messages
|
|
191
|
+
from zwarm.core.models import event_message_sent, Message
|
|
192
|
+
self.state.log_event(event_message_sent(session, Message(role="user", content=message)))
|
|
193
|
+
self.state.log_event(event_message_sent(session, Message(role="assistant", content=response)))
|
|
194
|
+
|
|
195
|
+
# Calculate turn number
|
|
196
|
+
turn = len([m for m in session.messages if m.role == "user"])
|
|
197
|
+
header = _format_session_header(session.id, session.adapter, session.mode.value)
|
|
198
|
+
|
|
199
|
+
return {
|
|
200
|
+
"success": True,
|
|
201
|
+
"session": header,
|
|
202
|
+
"session_id": session_id,
|
|
203
|
+
"turn": turn,
|
|
204
|
+
"you_said": _truncate(message, 100),
|
|
205
|
+
"response": response,
|
|
206
|
+
}
|
|
207
|
+
|
|
208
|
+
|
|
209
|
+
@weaveTool
|
|
210
|
+
def check_session(
|
|
211
|
+
self: "Orchestrator",
|
|
212
|
+
session_id: str,
|
|
213
|
+
) -> dict[str, Any]:
|
|
214
|
+
"""
|
|
215
|
+
Check the status of a session.
|
|
216
|
+
|
|
217
|
+
For async sessions: Check if the executor has finished.
|
|
218
|
+
For sync sessions: Get current status and message count.
|
|
219
|
+
|
|
220
|
+
Args:
|
|
221
|
+
session_id: The session to check.
|
|
222
|
+
|
|
223
|
+
Returns:
|
|
224
|
+
{session_id, status, ...}
|
|
225
|
+
"""
|
|
226
|
+
session = self._sessions.get(session_id)
|
|
227
|
+
if not session:
|
|
228
|
+
return {
|
|
229
|
+
"success": False,
|
|
230
|
+
"error": f"Unknown session: {session_id}",
|
|
231
|
+
"hint": "Use list_sessions() to see available sessions",
|
|
232
|
+
}
|
|
233
|
+
|
|
234
|
+
executor = self._get_adapter(session.adapter)
|
|
235
|
+
status = asyncio.get_event_loop().run_until_complete(
|
|
236
|
+
executor.check_status(session)
|
|
237
|
+
)
|
|
238
|
+
|
|
239
|
+
# Update state if status changed
|
|
240
|
+
self.state.update_session(session)
|
|
241
|
+
|
|
242
|
+
header = _format_session_header(session.id, session.adapter, session.mode.value)
|
|
243
|
+
|
|
244
|
+
return {
|
|
245
|
+
"success": True,
|
|
246
|
+
"session": header,
|
|
247
|
+
"session_id": session_id,
|
|
248
|
+
"mode": session.mode.value,
|
|
249
|
+
"status": session.status.value,
|
|
250
|
+
"messages": len(session.messages),
|
|
251
|
+
"task": _truncate(session.task_description, 80),
|
|
252
|
+
**status,
|
|
253
|
+
}
|
|
254
|
+
|
|
255
|
+
|
|
256
|
+
@weaveTool
|
|
257
|
+
def end_session(
|
|
258
|
+
self: "Orchestrator",
|
|
259
|
+
session_id: str,
|
|
260
|
+
verdict: Literal["completed", "failed", "cancelled"] = "completed",
|
|
261
|
+
summary: str | None = None,
|
|
262
|
+
) -> dict[str, Any]:
|
|
263
|
+
"""
|
|
264
|
+
End a session with a verdict.
|
|
265
|
+
|
|
266
|
+
Call this when:
|
|
267
|
+
- Task is done (verdict="completed")
|
|
268
|
+
- Task failed and you're giving up (verdict="failed")
|
|
269
|
+
- You want to stop early (verdict="cancelled")
|
|
270
|
+
|
|
271
|
+
Args:
|
|
272
|
+
session_id: The session to end.
|
|
273
|
+
verdict: How the session ended.
|
|
274
|
+
summary: Optional summary of what was accomplished.
|
|
275
|
+
|
|
276
|
+
Returns:
|
|
277
|
+
{session_id, status, summary}
|
|
278
|
+
"""
|
|
279
|
+
session = self._sessions.get(session_id)
|
|
280
|
+
if not session:
|
|
281
|
+
return {
|
|
282
|
+
"success": False,
|
|
283
|
+
"error": f"Unknown session: {session_id}",
|
|
284
|
+
}
|
|
285
|
+
|
|
286
|
+
# Stop the session if still running
|
|
287
|
+
if session.status.value == "active":
|
|
288
|
+
executor = self._get_adapter(session.adapter)
|
|
289
|
+
if verdict == "completed":
|
|
290
|
+
session.complete(summary)
|
|
291
|
+
else:
|
|
292
|
+
asyncio.get_event_loop().run_until_complete(executor.stop(session))
|
|
293
|
+
if verdict == "failed":
|
|
294
|
+
session.fail(summary)
|
|
295
|
+
else:
|
|
296
|
+
session.fail(f"Cancelled: {summary}" if summary else "Cancelled")
|
|
297
|
+
|
|
298
|
+
# Update state
|
|
299
|
+
self.state.update_session(session)
|
|
300
|
+
|
|
301
|
+
# Log event
|
|
302
|
+
from zwarm.core.models import event_session_completed
|
|
303
|
+
self.state.log_event(event_session_completed(session))
|
|
304
|
+
|
|
305
|
+
header = _format_session_header(session.id, session.adapter, session.mode.value)
|
|
306
|
+
verdict_icon = {"completed": "✓", "failed": "✗", "cancelled": "○"}.get(verdict, "?")
|
|
307
|
+
|
|
308
|
+
return {
|
|
309
|
+
"success": True,
|
|
310
|
+
"session": header,
|
|
311
|
+
"session_id": session_id,
|
|
312
|
+
"verdict": f"{verdict_icon} {verdict}",
|
|
313
|
+
"summary": session.exit_message or "(no summary)",
|
|
314
|
+
"total_turns": len([m for m in session.messages if m.role == "user"]),
|
|
315
|
+
}
|
|
316
|
+
|
|
317
|
+
|
|
318
|
+
@weaveTool
|
|
319
|
+
def list_sessions(
|
|
320
|
+
self: "Orchestrator",
|
|
321
|
+
status: str | None = None,
|
|
322
|
+
) -> dict[str, Any]:
|
|
323
|
+
"""
|
|
324
|
+
List all sessions, optionally filtered by status.
|
|
325
|
+
|
|
326
|
+
Args:
|
|
327
|
+
status: Filter by status ("active", "completed", "failed").
|
|
328
|
+
|
|
329
|
+
Returns:
|
|
330
|
+
{sessions: [...], count}
|
|
331
|
+
"""
|
|
332
|
+
sessions = self.state.list_sessions(status=status)
|
|
333
|
+
|
|
334
|
+
session_list = []
|
|
335
|
+
for s in sessions:
|
|
336
|
+
status_icon = {
|
|
337
|
+
"active": "●",
|
|
338
|
+
"completed": "✓",
|
|
339
|
+
"failed": "✗",
|
|
340
|
+
}.get(s.status.value, "?")
|
|
341
|
+
|
|
342
|
+
session_list.append({
|
|
343
|
+
"id": s.id[:8] + "...",
|
|
344
|
+
"full_id": s.id,
|
|
345
|
+
"status": f"{status_icon} {s.status.value}",
|
|
346
|
+
"adapter": s.adapter,
|
|
347
|
+
"mode": s.mode.value,
|
|
348
|
+
"task": _truncate(s.task_description, 60),
|
|
349
|
+
"turns": len([m for m in s.messages if m.role == "user"]),
|
|
350
|
+
})
|
|
351
|
+
|
|
352
|
+
return {
|
|
353
|
+
"success": True,
|
|
354
|
+
"sessions": session_list,
|
|
355
|
+
"count": len(sessions),
|
|
356
|
+
"filter": status or "all",
|
|
357
|
+
}
|
|
@@ -0,0 +1,26 @@
|
|
|
1
|
+
"""
|
|
2
|
+
Watchers: Trajectory aligners for agent behavior.
|
|
3
|
+
|
|
4
|
+
Watchers observe agent activity and can intervene to correct course.
|
|
5
|
+
They are composable and can be layered.
|
|
6
|
+
"""
|
|
7
|
+
|
|
8
|
+
from zwarm.watchers.base import Watcher, WatcherContext, WatcherResult, WatcherAction
|
|
9
|
+
from zwarm.watchers.registry import register_watcher, get_watcher, list_watchers
|
|
10
|
+
from zwarm.watchers.manager import WatcherManager, WatcherConfig, build_watcher_manager
|
|
11
|
+
|
|
12
|
+
# Import built-in watchers to register them
|
|
13
|
+
from zwarm.watchers import builtin as _builtin # noqa: F401
|
|
14
|
+
|
|
15
|
+
__all__ = [
|
|
16
|
+
"Watcher",
|
|
17
|
+
"WatcherContext",
|
|
18
|
+
"WatcherResult",
|
|
19
|
+
"WatcherAction",
|
|
20
|
+
"WatcherConfig",
|
|
21
|
+
"WatcherManager",
|
|
22
|
+
"register_watcher",
|
|
23
|
+
"get_watcher",
|
|
24
|
+
"list_watchers",
|
|
25
|
+
"build_watcher_manager",
|
|
26
|
+
]
|
zwarm/watchers/base.py
ADDED
|
@@ -0,0 +1,131 @@
|
|
|
1
|
+
"""
|
|
2
|
+
Base watcher interface and types.
|
|
3
|
+
|
|
4
|
+
Watchers observe agent trajectories and can intervene to correct course.
|
|
5
|
+
They're designed to be:
|
|
6
|
+
- Composable: Layer multiple watchers for different concerns
|
|
7
|
+
- Non-blocking: Check asynchronously, don't slow down the agent
|
|
8
|
+
- Actionable: Return clear guidance when correction is needed
|
|
9
|
+
"""
|
|
10
|
+
|
|
11
|
+
from __future__ import annotations
|
|
12
|
+
|
|
13
|
+
from abc import ABC, abstractmethod
|
|
14
|
+
from dataclasses import dataclass, field
|
|
15
|
+
from enum import Enum
|
|
16
|
+
from typing import Any
|
|
17
|
+
|
|
18
|
+
|
|
19
|
+
class WatcherAction(str, Enum):
|
|
20
|
+
"""What action to take based on watcher observation."""
|
|
21
|
+
|
|
22
|
+
CONTINUE = "continue" # Keep going, trajectory looks good
|
|
23
|
+
NUDGE = "nudge" # Insert guidance into next prompt
|
|
24
|
+
PAUSE = "pause" # Pause for human review
|
|
25
|
+
ABORT = "abort" # Stop execution immediately
|
|
26
|
+
|
|
27
|
+
|
|
28
|
+
@dataclass
|
|
29
|
+
class WatcherContext:
|
|
30
|
+
"""
|
|
31
|
+
Context provided to watchers for observation.
|
|
32
|
+
|
|
33
|
+
Contains everything a watcher might need to evaluate trajectory.
|
|
34
|
+
"""
|
|
35
|
+
|
|
36
|
+
# Current orchestrator state
|
|
37
|
+
task: str # Original task
|
|
38
|
+
step: int # Current step number
|
|
39
|
+
max_steps: int # Maximum steps allowed
|
|
40
|
+
messages: list[dict[str, Any]] # Conversation history
|
|
41
|
+
|
|
42
|
+
# Session activity
|
|
43
|
+
sessions: list[dict[str, Any]] = field(default_factory=list)
|
|
44
|
+
events: list[dict[str, Any]] = field(default_factory=list)
|
|
45
|
+
|
|
46
|
+
# Working directory context
|
|
47
|
+
working_dir: str | None = None
|
|
48
|
+
files_changed: list[str] = field(default_factory=list)
|
|
49
|
+
|
|
50
|
+
# Custom metadata
|
|
51
|
+
metadata: dict[str, Any] = field(default_factory=dict)
|
|
52
|
+
|
|
53
|
+
|
|
54
|
+
@dataclass
|
|
55
|
+
class WatcherResult:
|
|
56
|
+
"""
|
|
57
|
+
Result from a watcher observation.
|
|
58
|
+
|
|
59
|
+
Contains the recommended action and any guidance to inject.
|
|
60
|
+
"""
|
|
61
|
+
|
|
62
|
+
action: WatcherAction = WatcherAction.CONTINUE
|
|
63
|
+
reason: str = "" # Why this action was recommended
|
|
64
|
+
guidance: str = "" # Message to inject if action is NUDGE
|
|
65
|
+
priority: int = 0 # Higher priority watchers take precedence
|
|
66
|
+
metadata: dict[str, Any] = field(default_factory=dict)
|
|
67
|
+
|
|
68
|
+
@staticmethod
|
|
69
|
+
def ok() -> "WatcherResult":
|
|
70
|
+
"""Trajectory looks good, continue."""
|
|
71
|
+
return WatcherResult(action=WatcherAction.CONTINUE)
|
|
72
|
+
|
|
73
|
+
@staticmethod
|
|
74
|
+
def nudge(guidance: str, reason: str = "", priority: int = 0) -> "WatcherResult":
|
|
75
|
+
"""Insert guidance to correct trajectory."""
|
|
76
|
+
return WatcherResult(
|
|
77
|
+
action=WatcherAction.NUDGE,
|
|
78
|
+
guidance=guidance,
|
|
79
|
+
reason=reason,
|
|
80
|
+
priority=priority,
|
|
81
|
+
)
|
|
82
|
+
|
|
83
|
+
@staticmethod
|
|
84
|
+
def pause(reason: str, priority: int = 0) -> "WatcherResult":
|
|
85
|
+
"""Pause for human review."""
|
|
86
|
+
return WatcherResult(
|
|
87
|
+
action=WatcherAction.PAUSE,
|
|
88
|
+
reason=reason,
|
|
89
|
+
priority=priority,
|
|
90
|
+
)
|
|
91
|
+
|
|
92
|
+
@staticmethod
|
|
93
|
+
def abort(reason: str, priority: int = 100) -> "WatcherResult":
|
|
94
|
+
"""Stop execution immediately."""
|
|
95
|
+
return WatcherResult(
|
|
96
|
+
action=WatcherAction.ABORT,
|
|
97
|
+
reason=reason,
|
|
98
|
+
priority=priority,
|
|
99
|
+
)
|
|
100
|
+
|
|
101
|
+
|
|
102
|
+
class Watcher(ABC):
|
|
103
|
+
"""
|
|
104
|
+
Base class for watchers.
|
|
105
|
+
|
|
106
|
+
Watchers observe agent trajectories and provide guidance when needed.
|
|
107
|
+
They're designed to be stateless - all context comes from WatcherContext.
|
|
108
|
+
"""
|
|
109
|
+
|
|
110
|
+
name: str = "base"
|
|
111
|
+
description: str = ""
|
|
112
|
+
|
|
113
|
+
def __init__(self, config: dict[str, Any] | None = None):
|
|
114
|
+
"""Initialize watcher with optional config."""
|
|
115
|
+
self.config = config or {}
|
|
116
|
+
|
|
117
|
+
@abstractmethod
|
|
118
|
+
async def observe(self, ctx: WatcherContext) -> WatcherResult:
|
|
119
|
+
"""
|
|
120
|
+
Observe the current trajectory and decide action.
|
|
121
|
+
|
|
122
|
+
Args:
|
|
123
|
+
ctx: Current context with all trajectory info
|
|
124
|
+
|
|
125
|
+
Returns:
|
|
126
|
+
WatcherResult with recommended action
|
|
127
|
+
"""
|
|
128
|
+
...
|
|
129
|
+
|
|
130
|
+
def __repr__(self) -> str:
|
|
131
|
+
return f"<{self.__class__.__name__}({self.name})>"
|