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.
@@ -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})>"