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,173 @@
|
|
|
1
|
+
"""
|
|
2
|
+
OrchestratorEnv: A lean environment for the zwarm orchestrator.
|
|
3
|
+
|
|
4
|
+
Unlike ChatEnv, this environment:
|
|
5
|
+
- Has no notes/observations (we use StateManager instead)
|
|
6
|
+
- Has no chat() tool (orchestrator communicates via output_handler)
|
|
7
|
+
- Shows active sessions, step progress, and budget in observe()
|
|
8
|
+
"""
|
|
9
|
+
|
|
10
|
+
from __future__ import annotations
|
|
11
|
+
|
|
12
|
+
from pathlib import Path
|
|
13
|
+
from typing import TYPE_CHECKING, Any, Callable
|
|
14
|
+
|
|
15
|
+
from pydantic import PrivateAttr
|
|
16
|
+
from wbal.environment import Environment
|
|
17
|
+
|
|
18
|
+
if TYPE_CHECKING:
|
|
19
|
+
from zwarm.core.models import ConversationSession
|
|
20
|
+
from zwarm.sessions import CodexSessionManager
|
|
21
|
+
|
|
22
|
+
|
|
23
|
+
class OrchestratorEnv(Environment):
|
|
24
|
+
"""
|
|
25
|
+
Lean environment for the orchestrator agent.
|
|
26
|
+
|
|
27
|
+
Provides:
|
|
28
|
+
- Task context
|
|
29
|
+
- Working directory info
|
|
30
|
+
- Active session visibility
|
|
31
|
+
- Step progress tracking
|
|
32
|
+
- Budget/resource monitoring
|
|
33
|
+
- Output handler for messages
|
|
34
|
+
"""
|
|
35
|
+
|
|
36
|
+
task: str = ""
|
|
37
|
+
working_dir: Path = Path(".")
|
|
38
|
+
output_handler: Callable[[str], None] = lambda x: print(x)
|
|
39
|
+
|
|
40
|
+
# Session manager (set by orchestrator) - pulls live data each observe()
|
|
41
|
+
_session_manager: "CodexSessionManager | None" = PrivateAttr(default=None)
|
|
42
|
+
|
|
43
|
+
# Legacy: old sessions dict (deprecated, for backwards compat)
|
|
44
|
+
_sessions: dict[str, "ConversationSession"] | None = PrivateAttr(default=None)
|
|
45
|
+
|
|
46
|
+
# Progress tracking (updated by orchestrator each step)
|
|
47
|
+
_step_count: int = PrivateAttr(default=0)
|
|
48
|
+
_max_steps: int = PrivateAttr(default=50)
|
|
49
|
+
_total_tokens: int = PrivateAttr(default=0)
|
|
50
|
+
_executor_tokens: int = PrivateAttr(default=0) # Executor token usage
|
|
51
|
+
|
|
52
|
+
# Budget config (set from config)
|
|
53
|
+
_budget_max_sessions: int | None = PrivateAttr(default=None)
|
|
54
|
+
|
|
55
|
+
def set_session_manager(self, manager: "CodexSessionManager") -> None:
|
|
56
|
+
"""Set the session manager for live session visibility in observe()."""
|
|
57
|
+
self._session_manager = manager
|
|
58
|
+
|
|
59
|
+
def set_sessions(self, sessions: dict[str, "ConversationSession"]) -> None:
|
|
60
|
+
"""Legacy: Set the sessions dict for observe() visibility."""
|
|
61
|
+
self._sessions = sessions
|
|
62
|
+
|
|
63
|
+
def update_progress(
|
|
64
|
+
self,
|
|
65
|
+
step_count: int,
|
|
66
|
+
max_steps: int,
|
|
67
|
+
total_tokens: int = 0,
|
|
68
|
+
executor_tokens: int = 0,
|
|
69
|
+
) -> None:
|
|
70
|
+
"""Update progress tracking (called by orchestrator each step)."""
|
|
71
|
+
self._step_count = step_count
|
|
72
|
+
self._max_steps = max_steps
|
|
73
|
+
self._total_tokens = total_tokens
|
|
74
|
+
self._executor_tokens = executor_tokens
|
|
75
|
+
|
|
76
|
+
def set_budget(self, max_sessions: int | None = None) -> None:
|
|
77
|
+
"""Set budget limits from config."""
|
|
78
|
+
self._budget_max_sessions = max_sessions
|
|
79
|
+
|
|
80
|
+
def observe(self) -> str:
|
|
81
|
+
"""
|
|
82
|
+
Return observable state for the orchestrator.
|
|
83
|
+
|
|
84
|
+
Shows:
|
|
85
|
+
- Progress (steps, tokens)
|
|
86
|
+
- Session summary (pulled LIVE from CodexSessionManager)
|
|
87
|
+
- Active sessions with their status
|
|
88
|
+
- Working directory
|
|
89
|
+
|
|
90
|
+
Note: Task is NOT included here as it's already in the user message.
|
|
91
|
+
"""
|
|
92
|
+
parts = []
|
|
93
|
+
|
|
94
|
+
# Progress bar and stats
|
|
95
|
+
progress_pct = (
|
|
96
|
+
(self._step_count / self._max_steps * 100) if self._max_steps > 0 else 0
|
|
97
|
+
)
|
|
98
|
+
bar_len = 20
|
|
99
|
+
filled = (
|
|
100
|
+
int(bar_len * self._step_count / self._max_steps)
|
|
101
|
+
if self._max_steps > 0
|
|
102
|
+
else 0
|
|
103
|
+
)
|
|
104
|
+
bar = "█" * filled + "░" * (bar_len - filled)
|
|
105
|
+
|
|
106
|
+
progress_lines = [
|
|
107
|
+
f"Steps: [{bar}] {self._step_count}/{self._max_steps} ({progress_pct:.0f}%)",
|
|
108
|
+
]
|
|
109
|
+
if self._total_tokens > 0 or self._executor_tokens > 0:
|
|
110
|
+
token_parts = []
|
|
111
|
+
if self._total_tokens > 0:
|
|
112
|
+
token_parts.append(f"orchestrator: ~{self._total_tokens:,}")
|
|
113
|
+
if self._executor_tokens > 0:
|
|
114
|
+
token_parts.append(f"executors: ~{self._executor_tokens:,}")
|
|
115
|
+
progress_lines.append(f"Tokens: {', '.join(token_parts)}")
|
|
116
|
+
|
|
117
|
+
parts.append("## Progress\n" + "\n".join(progress_lines))
|
|
118
|
+
|
|
119
|
+
# Session summary - pull LIVE from CodexSessionManager
|
|
120
|
+
if self._session_manager is not None:
|
|
121
|
+
sessions = self._session_manager.list_sessions()
|
|
122
|
+
|
|
123
|
+
running = sum(1 for s in sessions if s.status.value == "running")
|
|
124
|
+
completed = sum(1 for s in sessions if s.status.value == "completed")
|
|
125
|
+
failed = sum(1 for s in sessions if s.status.value == "failed")
|
|
126
|
+
total = len(sessions)
|
|
127
|
+
|
|
128
|
+
summary = f"Sessions: {running} running, {completed} done, {failed} failed ({total} total)"
|
|
129
|
+
if self._budget_max_sessions:
|
|
130
|
+
summary += f" [limit: {self._budget_max_sessions}]"
|
|
131
|
+
|
|
132
|
+
parts.append(f"## Resources\n{summary}")
|
|
133
|
+
|
|
134
|
+
# Running sessions detail
|
|
135
|
+
running_sessions = [s for s in sessions if s.status.value == "running"]
|
|
136
|
+
if running_sessions:
|
|
137
|
+
session_lines = []
|
|
138
|
+
for session in running_sessions:
|
|
139
|
+
task_preview = (
|
|
140
|
+
session.task[:50] + "..."
|
|
141
|
+
if len(session.task) > 50
|
|
142
|
+
else session.task
|
|
143
|
+
)
|
|
144
|
+
tokens = session.token_usage.get("total_tokens", 0)
|
|
145
|
+
token_info = f", {tokens:,} tok" if tokens else ""
|
|
146
|
+
session_lines.append(
|
|
147
|
+
f" • {session.short_id} (turn {session.turn}{token_info}): {task_preview}"
|
|
148
|
+
)
|
|
149
|
+
parts.append("## Running Sessions\n" + "\n".join(session_lines))
|
|
150
|
+
|
|
151
|
+
# Recently completed (for visibility)
|
|
152
|
+
recent_completed = [
|
|
153
|
+
s for s in sessions
|
|
154
|
+
if s.status.value == "completed"
|
|
155
|
+
][:3] # Last 3 completed
|
|
156
|
+
if recent_completed:
|
|
157
|
+
session_lines = []
|
|
158
|
+
for session in recent_completed:
|
|
159
|
+
task_preview = (
|
|
160
|
+
session.task[:40] + "..."
|
|
161
|
+
if len(session.task) > 40
|
|
162
|
+
else session.task
|
|
163
|
+
)
|
|
164
|
+
tokens = session.token_usage.get("total_tokens", 0)
|
|
165
|
+
session_lines.append(
|
|
166
|
+
f" • {session.short_id} ✓ ({tokens:,} tok): {task_preview}"
|
|
167
|
+
)
|
|
168
|
+
parts.append("## Recently Completed\n" + "\n".join(session_lines))
|
|
169
|
+
|
|
170
|
+
# Working directory (less prominent)
|
|
171
|
+
parts.append(f"## Context\nWorking dir: {self.working_dir.absolute()}")
|
|
172
|
+
|
|
173
|
+
return "\n\n".join(parts)
|
zwarm/core/models.py
ADDED
|
@@ -0,0 +1,315 @@
|
|
|
1
|
+
"""
|
|
2
|
+
Core models for zwarm.
|
|
3
|
+
|
|
4
|
+
These are the fundamental data structures:
|
|
5
|
+
- ConversationSession: A session with an executor agent (sync or async)
|
|
6
|
+
- Task: A unit of work that may be delegated
|
|
7
|
+
- Event: An append-only log entry for audit/debugging
|
|
8
|
+
"""
|
|
9
|
+
|
|
10
|
+
from __future__ import annotations
|
|
11
|
+
|
|
12
|
+
import subprocess
|
|
13
|
+
from dataclasses import dataclass, field
|
|
14
|
+
from datetime import datetime
|
|
15
|
+
from enum import Enum
|
|
16
|
+
from pathlib import Path
|
|
17
|
+
from typing import Any, Literal
|
|
18
|
+
from uuid import uuid4
|
|
19
|
+
|
|
20
|
+
|
|
21
|
+
class SessionMode(str, Enum):
|
|
22
|
+
"""Execution mode for a session."""
|
|
23
|
+
|
|
24
|
+
SYNC = "sync"
|
|
25
|
+
ASYNC = "async"
|
|
26
|
+
|
|
27
|
+
|
|
28
|
+
class SessionStatus(str, Enum):
|
|
29
|
+
"""Status of a conversation session."""
|
|
30
|
+
|
|
31
|
+
ACTIVE = "active"
|
|
32
|
+
COMPLETED = "completed"
|
|
33
|
+
FAILED = "failed"
|
|
34
|
+
|
|
35
|
+
|
|
36
|
+
class TaskStatus(str, Enum):
|
|
37
|
+
"""Status of a task."""
|
|
38
|
+
|
|
39
|
+
PENDING = "pending"
|
|
40
|
+
IN_PROGRESS = "in_progress"
|
|
41
|
+
COMPLETED = "completed"
|
|
42
|
+
FAILED = "failed"
|
|
43
|
+
|
|
44
|
+
|
|
45
|
+
@dataclass
|
|
46
|
+
class Message:
|
|
47
|
+
"""A single message in a conversation."""
|
|
48
|
+
|
|
49
|
+
role: Literal["user", "assistant", "system"]
|
|
50
|
+
content: str
|
|
51
|
+
timestamp: datetime = field(default_factory=datetime.now)
|
|
52
|
+
|
|
53
|
+
def to_dict(self) -> dict[str, Any]:
|
|
54
|
+
return {
|
|
55
|
+
"role": self.role,
|
|
56
|
+
"content": self.content,
|
|
57
|
+
"timestamp": self.timestamp.isoformat(),
|
|
58
|
+
}
|
|
59
|
+
|
|
60
|
+
@classmethod
|
|
61
|
+
def from_dict(cls, data: dict[str, Any]) -> Message:
|
|
62
|
+
return cls(
|
|
63
|
+
role=data["role"],
|
|
64
|
+
content=data["content"],
|
|
65
|
+
timestamp=datetime.fromisoformat(data["timestamp"]) if "timestamp" in data else datetime.now(),
|
|
66
|
+
)
|
|
67
|
+
|
|
68
|
+
|
|
69
|
+
@dataclass
|
|
70
|
+
class ConversationSession:
|
|
71
|
+
"""
|
|
72
|
+
A conversational session with an executor agent.
|
|
73
|
+
|
|
74
|
+
Supports both sync (iterative conversation) and async (fire-and-forget) modes.
|
|
75
|
+
"""
|
|
76
|
+
|
|
77
|
+
id: str = field(default_factory=lambda: str(uuid4()))
|
|
78
|
+
adapter: str = "codex_mcp" # codex_mcp | codex_exec | claude_code
|
|
79
|
+
mode: SessionMode = SessionMode.SYNC
|
|
80
|
+
status: SessionStatus = SessionStatus.ACTIVE
|
|
81
|
+
working_dir: Path = field(default_factory=Path.cwd)
|
|
82
|
+
messages: list[Message] = field(default_factory=list)
|
|
83
|
+
started_at: datetime = field(default_factory=datetime.now)
|
|
84
|
+
completed_at: datetime | None = None
|
|
85
|
+
|
|
86
|
+
# Adapter-specific handles (not serialized)
|
|
87
|
+
conversation_id: str | None = None # MCP conversationId for codex
|
|
88
|
+
process: subprocess.Popen | None = field(default=None, repr=False)
|
|
89
|
+
|
|
90
|
+
# Metadata
|
|
91
|
+
task_description: str = ""
|
|
92
|
+
model: str | None = None
|
|
93
|
+
exit_message: str | None = None
|
|
94
|
+
|
|
95
|
+
# Token usage tracking for cost calculation
|
|
96
|
+
token_usage: dict[str, int] = field(default_factory=lambda: {
|
|
97
|
+
"input_tokens": 0,
|
|
98
|
+
"output_tokens": 0,
|
|
99
|
+
"total_tokens": 0,
|
|
100
|
+
})
|
|
101
|
+
|
|
102
|
+
def add_usage(self, usage: dict[str, int]) -> None:
|
|
103
|
+
"""Add token usage from an interaction."""
|
|
104
|
+
if not usage:
|
|
105
|
+
return
|
|
106
|
+
for key in self.token_usage:
|
|
107
|
+
self.token_usage[key] += usage.get(key, 0)
|
|
108
|
+
|
|
109
|
+
def add_message(self, role: Literal["user", "assistant", "system"], content: str) -> Message:
|
|
110
|
+
"""Add a message to the conversation."""
|
|
111
|
+
msg = Message(role=role, content=content)
|
|
112
|
+
self.messages.append(msg)
|
|
113
|
+
return msg
|
|
114
|
+
|
|
115
|
+
def complete(self, exit_message: str | None = None) -> None:
|
|
116
|
+
"""Mark session as completed."""
|
|
117
|
+
self.status = SessionStatus.COMPLETED
|
|
118
|
+
self.completed_at = datetime.now()
|
|
119
|
+
self.exit_message = exit_message
|
|
120
|
+
|
|
121
|
+
def fail(self, error: str | None = None) -> None:
|
|
122
|
+
"""Mark session as failed."""
|
|
123
|
+
self.status = SessionStatus.FAILED
|
|
124
|
+
self.completed_at = datetime.now()
|
|
125
|
+
self.exit_message = error
|
|
126
|
+
|
|
127
|
+
def to_dict(self) -> dict[str, Any]:
|
|
128
|
+
"""Serialize to dictionary (for persistence)."""
|
|
129
|
+
return {
|
|
130
|
+
"id": self.id,
|
|
131
|
+
"adapter": self.adapter,
|
|
132
|
+
"mode": self.mode.value,
|
|
133
|
+
"status": self.status.value,
|
|
134
|
+
"working_dir": str(self.working_dir),
|
|
135
|
+
"messages": [m.to_dict() for m in self.messages],
|
|
136
|
+
"started_at": self.started_at.isoformat(),
|
|
137
|
+
"completed_at": self.completed_at.isoformat() if self.completed_at else None,
|
|
138
|
+
"conversation_id": self.conversation_id,
|
|
139
|
+
"task_description": self.task_description,
|
|
140
|
+
"model": self.model,
|
|
141
|
+
"exit_message": self.exit_message,
|
|
142
|
+
"token_usage": self.token_usage,
|
|
143
|
+
}
|
|
144
|
+
|
|
145
|
+
@classmethod
|
|
146
|
+
def from_dict(cls, data: dict[str, Any]) -> ConversationSession:
|
|
147
|
+
"""Deserialize from dictionary."""
|
|
148
|
+
return cls(
|
|
149
|
+
id=data["id"],
|
|
150
|
+
adapter=data.get("adapter", "codex_mcp"),
|
|
151
|
+
mode=SessionMode(data["mode"]),
|
|
152
|
+
status=SessionStatus(data["status"]),
|
|
153
|
+
working_dir=Path(data["working_dir"]),
|
|
154
|
+
messages=[Message.from_dict(m) for m in data.get("messages", [])],
|
|
155
|
+
started_at=datetime.fromisoformat(data["started_at"]),
|
|
156
|
+
completed_at=datetime.fromisoformat(data["completed_at"]) if data.get("completed_at") else None,
|
|
157
|
+
conversation_id=data.get("conversation_id"),
|
|
158
|
+
task_description=data.get("task_description", ""),
|
|
159
|
+
model=data.get("model"),
|
|
160
|
+
exit_message=data.get("exit_message"),
|
|
161
|
+
token_usage=data.get("token_usage", {"input_tokens": 0, "output_tokens": 0, "total_tokens": 0}),
|
|
162
|
+
)
|
|
163
|
+
|
|
164
|
+
|
|
165
|
+
@dataclass
|
|
166
|
+
class Task:
|
|
167
|
+
"""
|
|
168
|
+
A unit of work that may be delegated to an executor.
|
|
169
|
+
|
|
170
|
+
Tasks track what needs to be done and link to the session doing the work.
|
|
171
|
+
"""
|
|
172
|
+
|
|
173
|
+
id: str = field(default_factory=lambda: str(uuid4()))
|
|
174
|
+
description: str = ""
|
|
175
|
+
status: TaskStatus = TaskStatus.PENDING
|
|
176
|
+
session_id: str | None = None
|
|
177
|
+
created_at: datetime = field(default_factory=datetime.now)
|
|
178
|
+
completed_at: datetime | None = None
|
|
179
|
+
result: str | None = None
|
|
180
|
+
parent_task_id: str | None = None # For subtasks
|
|
181
|
+
|
|
182
|
+
def start(self, session_id: str) -> None:
|
|
183
|
+
"""Mark task as started with a session."""
|
|
184
|
+
self.status = TaskStatus.IN_PROGRESS
|
|
185
|
+
self.session_id = session_id
|
|
186
|
+
|
|
187
|
+
def complete(self, result: str | None = None) -> None:
|
|
188
|
+
"""Mark task as completed."""
|
|
189
|
+
self.status = TaskStatus.COMPLETED
|
|
190
|
+
self.completed_at = datetime.now()
|
|
191
|
+
self.result = result
|
|
192
|
+
|
|
193
|
+
def fail(self, error: str | None = None) -> None:
|
|
194
|
+
"""Mark task as failed."""
|
|
195
|
+
self.status = TaskStatus.FAILED
|
|
196
|
+
self.completed_at = datetime.now()
|
|
197
|
+
self.result = error
|
|
198
|
+
|
|
199
|
+
def to_dict(self) -> dict[str, Any]:
|
|
200
|
+
return {
|
|
201
|
+
"id": self.id,
|
|
202
|
+
"description": self.description,
|
|
203
|
+
"status": self.status.value,
|
|
204
|
+
"session_id": self.session_id,
|
|
205
|
+
"created_at": self.created_at.isoformat(),
|
|
206
|
+
"completed_at": self.completed_at.isoformat() if self.completed_at else None,
|
|
207
|
+
"result": self.result,
|
|
208
|
+
"parent_task_id": self.parent_task_id,
|
|
209
|
+
}
|
|
210
|
+
|
|
211
|
+
@classmethod
|
|
212
|
+
def from_dict(cls, data: dict[str, Any]) -> Task:
|
|
213
|
+
return cls(
|
|
214
|
+
id=data["id"],
|
|
215
|
+
description=data["description"],
|
|
216
|
+
status=TaskStatus(data["status"]),
|
|
217
|
+
session_id=data.get("session_id"),
|
|
218
|
+
created_at=datetime.fromisoformat(data["created_at"]),
|
|
219
|
+
completed_at=datetime.fromisoformat(data["completed_at"]) if data.get("completed_at") else None,
|
|
220
|
+
result=data.get("result"),
|
|
221
|
+
parent_task_id=data.get("parent_task_id"),
|
|
222
|
+
)
|
|
223
|
+
|
|
224
|
+
|
|
225
|
+
@dataclass
|
|
226
|
+
class Event:
|
|
227
|
+
"""
|
|
228
|
+
An append-only log entry for audit and debugging.
|
|
229
|
+
|
|
230
|
+
Events capture everything that happens in the system.
|
|
231
|
+
"""
|
|
232
|
+
|
|
233
|
+
id: str = field(default_factory=lambda: str(uuid4()))
|
|
234
|
+
timestamp: datetime = field(default_factory=datetime.now)
|
|
235
|
+
kind: str = "" # session_started, message_sent, task_completed, etc.
|
|
236
|
+
session_id: str | None = None
|
|
237
|
+
task_id: str | None = None
|
|
238
|
+
payload: dict[str, Any] = field(default_factory=dict)
|
|
239
|
+
|
|
240
|
+
def to_dict(self) -> dict[str, Any]:
|
|
241
|
+
return {
|
|
242
|
+
"id": self.id,
|
|
243
|
+
"timestamp": self.timestamp.isoformat(),
|
|
244
|
+
"kind": self.kind,
|
|
245
|
+
"session_id": self.session_id,
|
|
246
|
+
"task_id": self.task_id,
|
|
247
|
+
"payload": self.payload,
|
|
248
|
+
}
|
|
249
|
+
|
|
250
|
+
@classmethod
|
|
251
|
+
def from_dict(cls, data: dict[str, Any]) -> Event:
|
|
252
|
+
return cls(
|
|
253
|
+
id=data["id"],
|
|
254
|
+
timestamp=datetime.fromisoformat(data["timestamp"]),
|
|
255
|
+
kind=data["kind"],
|
|
256
|
+
session_id=data.get("session_id"),
|
|
257
|
+
task_id=data.get("task_id"),
|
|
258
|
+
payload=data.get("payload", {}),
|
|
259
|
+
)
|
|
260
|
+
|
|
261
|
+
|
|
262
|
+
# Event factory functions for common event types
|
|
263
|
+
def event_session_started(session: ConversationSession) -> Event:
|
|
264
|
+
return Event(
|
|
265
|
+
kind="session_started",
|
|
266
|
+
session_id=session.id,
|
|
267
|
+
payload={
|
|
268
|
+
"adapter": session.adapter,
|
|
269
|
+
"mode": session.mode.value,
|
|
270
|
+
"task": session.task_description,
|
|
271
|
+
},
|
|
272
|
+
)
|
|
273
|
+
|
|
274
|
+
|
|
275
|
+
def event_message_sent(session: ConversationSession, message: Message) -> Event:
|
|
276
|
+
return Event(
|
|
277
|
+
kind="message_sent",
|
|
278
|
+
session_id=session.id,
|
|
279
|
+
payload={
|
|
280
|
+
"role": message.role,
|
|
281
|
+
"content": message.content[:500], # Truncate for log
|
|
282
|
+
},
|
|
283
|
+
)
|
|
284
|
+
|
|
285
|
+
|
|
286
|
+
def event_session_completed(session: ConversationSession) -> Event:
|
|
287
|
+
return Event(
|
|
288
|
+
kind="session_completed",
|
|
289
|
+
session_id=session.id,
|
|
290
|
+
payload={
|
|
291
|
+
"status": session.status.value,
|
|
292
|
+
"exit_message": session.exit_message,
|
|
293
|
+
"message_count": len(session.messages),
|
|
294
|
+
},
|
|
295
|
+
)
|
|
296
|
+
|
|
297
|
+
|
|
298
|
+
def event_task_created(task: Task) -> Event:
|
|
299
|
+
return Event(
|
|
300
|
+
kind="task_created",
|
|
301
|
+
task_id=task.id,
|
|
302
|
+
payload={"description": task.description},
|
|
303
|
+
)
|
|
304
|
+
|
|
305
|
+
|
|
306
|
+
def event_task_completed(task: Task) -> Event:
|
|
307
|
+
return Event(
|
|
308
|
+
kind="task_completed",
|
|
309
|
+
task_id=task.id,
|
|
310
|
+
session_id=task.session_id,
|
|
311
|
+
payload={
|
|
312
|
+
"status": task.status.value,
|
|
313
|
+
"result": task.result[:500] if task.result else None,
|
|
314
|
+
},
|
|
315
|
+
)
|