ouroboros-ai 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.
Potentially problematic release.
This version of ouroboros-ai might be problematic. Click here for more details.
- ouroboros/__init__.py +15 -0
- ouroboros/__main__.py +9 -0
- ouroboros/bigbang/__init__.py +39 -0
- ouroboros/bigbang/ambiguity.py +464 -0
- ouroboros/bigbang/interview.py +530 -0
- ouroboros/bigbang/seed_generator.py +610 -0
- ouroboros/cli/__init__.py +9 -0
- ouroboros/cli/commands/__init__.py +7 -0
- ouroboros/cli/commands/config.py +79 -0
- ouroboros/cli/commands/init.py +425 -0
- ouroboros/cli/commands/run.py +201 -0
- ouroboros/cli/commands/status.py +85 -0
- ouroboros/cli/formatters/__init__.py +31 -0
- ouroboros/cli/formatters/panels.py +157 -0
- ouroboros/cli/formatters/progress.py +112 -0
- ouroboros/cli/formatters/tables.py +166 -0
- ouroboros/cli/main.py +60 -0
- ouroboros/config/__init__.py +81 -0
- ouroboros/config/loader.py +292 -0
- ouroboros/config/models.py +332 -0
- ouroboros/core/__init__.py +62 -0
- ouroboros/core/ac_tree.py +401 -0
- ouroboros/core/context.py +472 -0
- ouroboros/core/errors.py +246 -0
- ouroboros/core/seed.py +212 -0
- ouroboros/core/types.py +205 -0
- ouroboros/evaluation/__init__.py +110 -0
- ouroboros/evaluation/consensus.py +350 -0
- ouroboros/evaluation/mechanical.py +351 -0
- ouroboros/evaluation/models.py +235 -0
- ouroboros/evaluation/pipeline.py +286 -0
- ouroboros/evaluation/semantic.py +302 -0
- ouroboros/evaluation/trigger.py +278 -0
- ouroboros/events/__init__.py +5 -0
- ouroboros/events/base.py +80 -0
- ouroboros/events/decomposition.py +153 -0
- ouroboros/events/evaluation.py +248 -0
- ouroboros/execution/__init__.py +44 -0
- ouroboros/execution/atomicity.py +451 -0
- ouroboros/execution/decomposition.py +481 -0
- ouroboros/execution/double_diamond.py +1386 -0
- ouroboros/execution/subagent.py +275 -0
- ouroboros/observability/__init__.py +63 -0
- ouroboros/observability/drift.py +383 -0
- ouroboros/observability/logging.py +504 -0
- ouroboros/observability/retrospective.py +338 -0
- ouroboros/orchestrator/__init__.py +78 -0
- ouroboros/orchestrator/adapter.py +391 -0
- ouroboros/orchestrator/events.py +278 -0
- ouroboros/orchestrator/runner.py +597 -0
- ouroboros/orchestrator/session.py +486 -0
- ouroboros/persistence/__init__.py +23 -0
- ouroboros/persistence/checkpoint.py +511 -0
- ouroboros/persistence/event_store.py +183 -0
- ouroboros/persistence/migrations/__init__.py +1 -0
- ouroboros/persistence/migrations/runner.py +100 -0
- ouroboros/persistence/migrations/scripts/001_initial.sql +20 -0
- ouroboros/persistence/schema.py +56 -0
- ouroboros/persistence/uow.py +230 -0
- ouroboros/providers/__init__.py +28 -0
- ouroboros/providers/base.py +133 -0
- ouroboros/providers/claude_code_adapter.py +212 -0
- ouroboros/providers/litellm_adapter.py +316 -0
- ouroboros/py.typed +0 -0
- ouroboros/resilience/__init__.py +67 -0
- ouroboros/resilience/lateral.py +595 -0
- ouroboros/resilience/stagnation.py +727 -0
- ouroboros/routing/__init__.py +60 -0
- ouroboros/routing/complexity.py +272 -0
- ouroboros/routing/downgrade.py +664 -0
- ouroboros/routing/escalation.py +340 -0
- ouroboros/routing/router.py +204 -0
- ouroboros/routing/tiers.py +247 -0
- ouroboros/secondary/__init__.py +40 -0
- ouroboros/secondary/scheduler.py +467 -0
- ouroboros/secondary/todo_registry.py +483 -0
- ouroboros_ai-0.1.0.dist-info/METADATA +607 -0
- ouroboros_ai-0.1.0.dist-info/RECORD +81 -0
- ouroboros_ai-0.1.0.dist-info/WHEEL +4 -0
- ouroboros_ai-0.1.0.dist-info/entry_points.txt +2 -0
- ouroboros_ai-0.1.0.dist-info/licenses/LICENSE +21 -0
|
@@ -0,0 +1,391 @@
|
|
|
1
|
+
"""Claude Agent SDK adapter for Ouroboros orchestrator.
|
|
2
|
+
|
|
3
|
+
This module provides a wrapper around the Claude Agent SDK that:
|
|
4
|
+
- Normalizes SDK messages to internal AgentMessage format
|
|
5
|
+
- Handles streaming with async generators
|
|
6
|
+
- Maps SDK exceptions to Ouroboros error types
|
|
7
|
+
- Supports configurable tools and permission modes
|
|
8
|
+
|
|
9
|
+
Usage:
|
|
10
|
+
adapter = ClaudeAgentAdapter(api_key="...")
|
|
11
|
+
async for message in adapter.execute_task(
|
|
12
|
+
prompt="Fix the bug in auth.py",
|
|
13
|
+
tools=["Read", "Edit", "Bash"],
|
|
14
|
+
):
|
|
15
|
+
print(message.content)
|
|
16
|
+
"""
|
|
17
|
+
|
|
18
|
+
from __future__ import annotations
|
|
19
|
+
|
|
20
|
+
from collections.abc import AsyncIterator
|
|
21
|
+
from dataclasses import dataclass, field
|
|
22
|
+
import os
|
|
23
|
+
from typing import TYPE_CHECKING, Any
|
|
24
|
+
|
|
25
|
+
from ouroboros.core.errors import ProviderError
|
|
26
|
+
from ouroboros.core.types import Result
|
|
27
|
+
from ouroboros.observability.logging import get_logger
|
|
28
|
+
|
|
29
|
+
if TYPE_CHECKING:
|
|
30
|
+
pass
|
|
31
|
+
|
|
32
|
+
log = get_logger(__name__)
|
|
33
|
+
|
|
34
|
+
|
|
35
|
+
# =============================================================================
|
|
36
|
+
# Data Models
|
|
37
|
+
# =============================================================================
|
|
38
|
+
|
|
39
|
+
|
|
40
|
+
@dataclass(frozen=True, slots=True)
|
|
41
|
+
class AgentMessage:
|
|
42
|
+
"""Normalized message from Claude Agent SDK.
|
|
43
|
+
|
|
44
|
+
Attributes:
|
|
45
|
+
type: Message type ("assistant", "tool", "result", "system").
|
|
46
|
+
content: Human-readable content.
|
|
47
|
+
tool_name: Name of tool being called (if type="tool").
|
|
48
|
+
data: Additional message data.
|
|
49
|
+
"""
|
|
50
|
+
|
|
51
|
+
type: str
|
|
52
|
+
content: str
|
|
53
|
+
tool_name: str | None = None
|
|
54
|
+
data: dict[str, Any] = field(default_factory=dict)
|
|
55
|
+
|
|
56
|
+
@property
|
|
57
|
+
def is_final(self) -> bool:
|
|
58
|
+
"""Return True if this is the final result message."""
|
|
59
|
+
return self.type == "result"
|
|
60
|
+
|
|
61
|
+
@property
|
|
62
|
+
def is_error(self) -> bool:
|
|
63
|
+
"""Return True if this message indicates an error."""
|
|
64
|
+
return self.data.get("subtype") == "error"
|
|
65
|
+
|
|
66
|
+
|
|
67
|
+
@dataclass(frozen=True, slots=True)
|
|
68
|
+
class TaskResult:
|
|
69
|
+
"""Result of executing a task via Claude Agent.
|
|
70
|
+
|
|
71
|
+
Attributes:
|
|
72
|
+
success: Whether the task completed successfully.
|
|
73
|
+
final_message: The final result message content.
|
|
74
|
+
messages: All messages from the execution.
|
|
75
|
+
session_id: Claude Agent session ID for resumption.
|
|
76
|
+
"""
|
|
77
|
+
|
|
78
|
+
success: bool
|
|
79
|
+
final_message: str
|
|
80
|
+
messages: tuple[AgentMessage, ...]
|
|
81
|
+
session_id: str | None = None
|
|
82
|
+
|
|
83
|
+
|
|
84
|
+
# =============================================================================
|
|
85
|
+
# Adapter
|
|
86
|
+
# =============================================================================
|
|
87
|
+
|
|
88
|
+
|
|
89
|
+
# Default tools for code execution tasks
|
|
90
|
+
DEFAULT_TOOLS: list[str] = ["Read", "Write", "Edit", "Bash", "Glob", "Grep"]
|
|
91
|
+
|
|
92
|
+
|
|
93
|
+
class ClaudeAgentAdapter:
|
|
94
|
+
"""Adapter for Claude Agent SDK with streaming support.
|
|
95
|
+
|
|
96
|
+
This adapter wraps the Claude Agent SDK's query() function to provide:
|
|
97
|
+
- Async generator interface for message streaming
|
|
98
|
+
- Normalized message format (AgentMessage)
|
|
99
|
+
- Error handling with Result type
|
|
100
|
+
- Configurable tools and permission modes
|
|
101
|
+
|
|
102
|
+
Example:
|
|
103
|
+
adapter = ClaudeAgentAdapter(permission_mode="acceptEdits")
|
|
104
|
+
|
|
105
|
+
async for message in adapter.execute_task(
|
|
106
|
+
prompt="Review and fix bugs in auth.py",
|
|
107
|
+
tools=["Read", "Edit", "Bash"],
|
|
108
|
+
):
|
|
109
|
+
if message.type == "assistant":
|
|
110
|
+
print(f"Claude: {message.content[:100]}")
|
|
111
|
+
elif message.type == "tool":
|
|
112
|
+
print(f"Using tool: {message.tool_name}")
|
|
113
|
+
"""
|
|
114
|
+
|
|
115
|
+
def __init__(
|
|
116
|
+
self,
|
|
117
|
+
api_key: str | None = None,
|
|
118
|
+
permission_mode: str = "acceptEdits",
|
|
119
|
+
) -> None:
|
|
120
|
+
"""Initialize Claude Agent adapter.
|
|
121
|
+
|
|
122
|
+
Args:
|
|
123
|
+
api_key: Anthropic API key. If not provided, uses ANTHROPIC_API_KEY
|
|
124
|
+
environment variable or Claude Code CLI authentication.
|
|
125
|
+
permission_mode: Permission mode for tool execution.
|
|
126
|
+
- "acceptEdits": Auto-approve file edits
|
|
127
|
+
- "bypassPermissions": Run without prompts (CI/CD)
|
|
128
|
+
- "default": Require canUseTool callback
|
|
129
|
+
"""
|
|
130
|
+
self._api_key = api_key or os.getenv("ANTHROPIC_API_KEY")
|
|
131
|
+
self._permission_mode = permission_mode
|
|
132
|
+
|
|
133
|
+
log.info(
|
|
134
|
+
"orchestrator.adapter.initialized",
|
|
135
|
+
permission_mode=permission_mode,
|
|
136
|
+
has_api_key=bool(self._api_key),
|
|
137
|
+
)
|
|
138
|
+
|
|
139
|
+
async def execute_task(
|
|
140
|
+
self,
|
|
141
|
+
prompt: str,
|
|
142
|
+
tools: list[str] | None = None,
|
|
143
|
+
system_prompt: str | None = None,
|
|
144
|
+
resume_session_id: str | None = None,
|
|
145
|
+
) -> AsyncIterator[AgentMessage]:
|
|
146
|
+
"""Execute a task and yield progress messages.
|
|
147
|
+
|
|
148
|
+
This is an async generator that streams messages as Claude works.
|
|
149
|
+
Use async for to consume messages in real-time.
|
|
150
|
+
|
|
151
|
+
Args:
|
|
152
|
+
prompt: The task for Claude to perform.
|
|
153
|
+
tools: List of tools Claude can use. Defaults to DEFAULT_TOOLS.
|
|
154
|
+
system_prompt: Optional custom system prompt.
|
|
155
|
+
resume_session_id: Session ID to resume from.
|
|
156
|
+
|
|
157
|
+
Yields:
|
|
158
|
+
AgentMessage for each SDK message (assistant reasoning, tool calls, results).
|
|
159
|
+
|
|
160
|
+
Raises:
|
|
161
|
+
ProviderError: If SDK initialization fails.
|
|
162
|
+
"""
|
|
163
|
+
try:
|
|
164
|
+
# Lazy import to avoid loading SDK at module import time
|
|
165
|
+
from claude_agent_sdk import ClaudeAgentOptions, query
|
|
166
|
+
except ImportError as e:
|
|
167
|
+
log.error(
|
|
168
|
+
"orchestrator.adapter.sdk_not_installed",
|
|
169
|
+
error=str(e),
|
|
170
|
+
)
|
|
171
|
+
yield AgentMessage(
|
|
172
|
+
type="result",
|
|
173
|
+
content="Claude Agent SDK is not installed. Run: pip install claude-agent-sdk",
|
|
174
|
+
data={"subtype": "error"},
|
|
175
|
+
)
|
|
176
|
+
return
|
|
177
|
+
|
|
178
|
+
effective_tools = tools or DEFAULT_TOOLS
|
|
179
|
+
|
|
180
|
+
log.info(
|
|
181
|
+
"orchestrator.adapter.task_started",
|
|
182
|
+
prompt_preview=prompt[:100],
|
|
183
|
+
tools=effective_tools,
|
|
184
|
+
has_system_prompt=bool(system_prompt),
|
|
185
|
+
resume_session_id=resume_session_id,
|
|
186
|
+
)
|
|
187
|
+
|
|
188
|
+
try:
|
|
189
|
+
# Build options
|
|
190
|
+
import os
|
|
191
|
+
options_kwargs: dict[str, Any] = {
|
|
192
|
+
"allowed_tools": effective_tools,
|
|
193
|
+
"permission_mode": self._permission_mode,
|
|
194
|
+
"cwd": os.getcwd(), # Use current working directory
|
|
195
|
+
}
|
|
196
|
+
|
|
197
|
+
if system_prompt:
|
|
198
|
+
options_kwargs["system_prompt"] = system_prompt
|
|
199
|
+
|
|
200
|
+
if resume_session_id:
|
|
201
|
+
options_kwargs["resume"] = resume_session_id
|
|
202
|
+
|
|
203
|
+
options = ClaudeAgentOptions(**options_kwargs)
|
|
204
|
+
|
|
205
|
+
# Stream messages from SDK
|
|
206
|
+
session_id: str | None = None
|
|
207
|
+
async for sdk_message in query(prompt=prompt, options=options):
|
|
208
|
+
agent_message = self._convert_message(sdk_message)
|
|
209
|
+
|
|
210
|
+
# Capture session ID from init message
|
|
211
|
+
if hasattr(sdk_message, "session_id"):
|
|
212
|
+
session_id = sdk_message.session_id
|
|
213
|
+
|
|
214
|
+
# Update data with session_id if available
|
|
215
|
+
if session_id and agent_message.is_final:
|
|
216
|
+
agent_message = AgentMessage(
|
|
217
|
+
type=agent_message.type,
|
|
218
|
+
content=agent_message.content,
|
|
219
|
+
tool_name=agent_message.tool_name,
|
|
220
|
+
data={**agent_message.data, "session_id": session_id},
|
|
221
|
+
)
|
|
222
|
+
|
|
223
|
+
yield agent_message
|
|
224
|
+
|
|
225
|
+
if agent_message.is_final:
|
|
226
|
+
log.info(
|
|
227
|
+
"orchestrator.adapter.task_completed",
|
|
228
|
+
success=not agent_message.is_error,
|
|
229
|
+
session_id=session_id,
|
|
230
|
+
)
|
|
231
|
+
|
|
232
|
+
except Exception as e:
|
|
233
|
+
log.exception(
|
|
234
|
+
"orchestrator.adapter.task_failed",
|
|
235
|
+
error=str(e),
|
|
236
|
+
)
|
|
237
|
+
yield AgentMessage(
|
|
238
|
+
type="result",
|
|
239
|
+
content=f"Task execution failed: {e!s}",
|
|
240
|
+
data={"subtype": "error", "error_type": type(e).__name__},
|
|
241
|
+
)
|
|
242
|
+
|
|
243
|
+
def _convert_message(self, sdk_message: Any) -> AgentMessage:
|
|
244
|
+
"""Convert SDK message to internal AgentMessage format.
|
|
245
|
+
|
|
246
|
+
Args:
|
|
247
|
+
sdk_message: Message from Claude Agent SDK.
|
|
248
|
+
|
|
249
|
+
Returns:
|
|
250
|
+
Normalized AgentMessage.
|
|
251
|
+
"""
|
|
252
|
+
# SDK uses class names, not 'type' attribute
|
|
253
|
+
class_name = type(sdk_message).__name__
|
|
254
|
+
|
|
255
|
+
log.debug(
|
|
256
|
+
"orchestrator.adapter.message_received",
|
|
257
|
+
class_name=class_name,
|
|
258
|
+
sdk_message=str(sdk_message)[:500],
|
|
259
|
+
)
|
|
260
|
+
|
|
261
|
+
# Extract content based on message class
|
|
262
|
+
content = ""
|
|
263
|
+
tool_name = None
|
|
264
|
+
data: dict[str, Any] = {}
|
|
265
|
+
msg_type = "unknown"
|
|
266
|
+
|
|
267
|
+
if class_name == "AssistantMessage":
|
|
268
|
+
msg_type = "assistant"
|
|
269
|
+
# Assistant message with content blocks
|
|
270
|
+
content_blocks = getattr(sdk_message, "content", [])
|
|
271
|
+
for block in content_blocks:
|
|
272
|
+
block_type = type(block).__name__
|
|
273
|
+
if block_type == "TextBlock" and hasattr(block, "text"):
|
|
274
|
+
content = block.text
|
|
275
|
+
break
|
|
276
|
+
elif block_type == "ToolUseBlock" and hasattr(block, "name"):
|
|
277
|
+
tool_name = block.name
|
|
278
|
+
content = f"Calling tool: {tool_name}"
|
|
279
|
+
break
|
|
280
|
+
|
|
281
|
+
elif class_name == "ResultMessage":
|
|
282
|
+
msg_type = "result"
|
|
283
|
+
# Final result message
|
|
284
|
+
content = getattr(sdk_message, "result", "") or ""
|
|
285
|
+
data["subtype"] = getattr(sdk_message, "subtype", "success")
|
|
286
|
+
data["is_error"] = getattr(sdk_message, "is_error", False)
|
|
287
|
+
data["session_id"] = getattr(sdk_message, "session_id", None)
|
|
288
|
+
log.info(
|
|
289
|
+
"orchestrator.adapter.result_message",
|
|
290
|
+
result_content=content[:200] if content else "empty",
|
|
291
|
+
subtype=data["subtype"],
|
|
292
|
+
is_error=data["is_error"],
|
|
293
|
+
)
|
|
294
|
+
|
|
295
|
+
elif class_name == "SystemMessage":
|
|
296
|
+
msg_type = "system"
|
|
297
|
+
subtype = getattr(sdk_message, "subtype", "")
|
|
298
|
+
msg_data = getattr(sdk_message, "data", {})
|
|
299
|
+
if subtype == "init":
|
|
300
|
+
session_id = msg_data.get("session_id")
|
|
301
|
+
content = f"Session initialized: {session_id}"
|
|
302
|
+
data["session_id"] = session_id
|
|
303
|
+
else:
|
|
304
|
+
content = f"System: {subtype}"
|
|
305
|
+
data["subtype"] = subtype
|
|
306
|
+
|
|
307
|
+
elif class_name == "UserMessage":
|
|
308
|
+
msg_type = "user"
|
|
309
|
+
# Tool result message
|
|
310
|
+
content_blocks = getattr(sdk_message, "content", [])
|
|
311
|
+
for block in content_blocks:
|
|
312
|
+
if hasattr(block, "content"):
|
|
313
|
+
content = str(block.content)[:500]
|
|
314
|
+
break
|
|
315
|
+
|
|
316
|
+
else:
|
|
317
|
+
# Unknown message type
|
|
318
|
+
content = str(sdk_message)
|
|
319
|
+
data["raw_class"] = class_name
|
|
320
|
+
|
|
321
|
+
return AgentMessage(
|
|
322
|
+
type=msg_type,
|
|
323
|
+
content=content,
|
|
324
|
+
tool_name=tool_name,
|
|
325
|
+
data=data,
|
|
326
|
+
)
|
|
327
|
+
|
|
328
|
+
async def execute_task_to_result(
|
|
329
|
+
self,
|
|
330
|
+
prompt: str,
|
|
331
|
+
tools: list[str] | None = None,
|
|
332
|
+
system_prompt: str | None = None,
|
|
333
|
+
resume_session_id: str | None = None,
|
|
334
|
+
) -> Result[TaskResult, ProviderError]:
|
|
335
|
+
"""Execute a task and collect all messages into a TaskResult.
|
|
336
|
+
|
|
337
|
+
This is a convenience method that collects all messages from
|
|
338
|
+
execute_task() into a single TaskResult. Use this when you don't
|
|
339
|
+
need streaming progress updates.
|
|
340
|
+
|
|
341
|
+
Args:
|
|
342
|
+
prompt: The task for Claude to perform.
|
|
343
|
+
tools: List of tools Claude can use. Defaults to DEFAULT_TOOLS.
|
|
344
|
+
system_prompt: Optional custom system prompt.
|
|
345
|
+
resume_session_id: Session ID to resume from.
|
|
346
|
+
|
|
347
|
+
Returns:
|
|
348
|
+
Result containing TaskResult on success, ProviderError on failure.
|
|
349
|
+
"""
|
|
350
|
+
messages: list[AgentMessage] = []
|
|
351
|
+
final_message = ""
|
|
352
|
+
success = True
|
|
353
|
+
session_id: str | None = None
|
|
354
|
+
|
|
355
|
+
async for message in self.execute_task(
|
|
356
|
+
prompt=prompt,
|
|
357
|
+
tools=tools,
|
|
358
|
+
system_prompt=system_prompt,
|
|
359
|
+
resume_session_id=resume_session_id,
|
|
360
|
+
):
|
|
361
|
+
messages.append(message)
|
|
362
|
+
|
|
363
|
+
if message.is_final:
|
|
364
|
+
final_message = message.content
|
|
365
|
+
success = not message.is_error
|
|
366
|
+
session_id = message.data.get("session_id")
|
|
367
|
+
|
|
368
|
+
if not success:
|
|
369
|
+
return Result.err(
|
|
370
|
+
ProviderError(
|
|
371
|
+
message=final_message,
|
|
372
|
+
details={"messages": [m.content for m in messages]},
|
|
373
|
+
)
|
|
374
|
+
)
|
|
375
|
+
|
|
376
|
+
return Result.ok(
|
|
377
|
+
TaskResult(
|
|
378
|
+
success=success,
|
|
379
|
+
final_message=final_message,
|
|
380
|
+
messages=tuple(messages),
|
|
381
|
+
session_id=session_id,
|
|
382
|
+
)
|
|
383
|
+
)
|
|
384
|
+
|
|
385
|
+
|
|
386
|
+
__all__ = [
|
|
387
|
+
"AgentMessage",
|
|
388
|
+
"ClaudeAgentAdapter",
|
|
389
|
+
"DEFAULT_TOOLS",
|
|
390
|
+
"TaskResult",
|
|
391
|
+
]
|
|
@@ -0,0 +1,278 @@
|
|
|
1
|
+
"""Event creation helpers for orchestrator.
|
|
2
|
+
|
|
3
|
+
This module provides factory functions for creating orchestrator-related events
|
|
4
|
+
following the project's event naming convention (dot.notation.past_tense).
|
|
5
|
+
|
|
6
|
+
Event Types:
|
|
7
|
+
- orchestrator.session.started: Session began execution
|
|
8
|
+
- orchestrator.session.completed: Session finished successfully
|
|
9
|
+
- orchestrator.session.failed: Session encountered fatal error
|
|
10
|
+
- orchestrator.session.paused: Session paused for resumption
|
|
11
|
+
- orchestrator.progress.updated: Progress checkpoint
|
|
12
|
+
- orchestrator.task.started: Individual task started
|
|
13
|
+
- orchestrator.task.completed: Individual task completed
|
|
14
|
+
- orchestrator.tool.called: Tool was invoked by agent
|
|
15
|
+
"""
|
|
16
|
+
|
|
17
|
+
from __future__ import annotations
|
|
18
|
+
|
|
19
|
+
from datetime import UTC, datetime
|
|
20
|
+
from typing import Any
|
|
21
|
+
|
|
22
|
+
from ouroboros.events.base import BaseEvent
|
|
23
|
+
|
|
24
|
+
|
|
25
|
+
def create_session_started_event(
|
|
26
|
+
session_id: str,
|
|
27
|
+
execution_id: str,
|
|
28
|
+
seed_id: str,
|
|
29
|
+
seed_goal: str,
|
|
30
|
+
) -> BaseEvent:
|
|
31
|
+
"""Create session started event.
|
|
32
|
+
|
|
33
|
+
Args:
|
|
34
|
+
session_id: Unique session identifier.
|
|
35
|
+
execution_id: Associated workflow execution ID.
|
|
36
|
+
seed_id: ID of the seed being executed.
|
|
37
|
+
seed_goal: Goal from the seed specification.
|
|
38
|
+
|
|
39
|
+
Returns:
|
|
40
|
+
BaseEvent for session start.
|
|
41
|
+
"""
|
|
42
|
+
return BaseEvent(
|
|
43
|
+
type="orchestrator.session.started",
|
|
44
|
+
aggregate_type="session",
|
|
45
|
+
aggregate_id=session_id,
|
|
46
|
+
data={
|
|
47
|
+
"execution_id": execution_id,
|
|
48
|
+
"seed_id": seed_id,
|
|
49
|
+
"seed_goal": seed_goal,
|
|
50
|
+
"start_time": datetime.now(UTC).isoformat(),
|
|
51
|
+
},
|
|
52
|
+
)
|
|
53
|
+
|
|
54
|
+
|
|
55
|
+
def create_session_completed_event(
|
|
56
|
+
session_id: str,
|
|
57
|
+
summary: dict[str, Any],
|
|
58
|
+
messages_processed: int,
|
|
59
|
+
) -> BaseEvent:
|
|
60
|
+
"""Create session completed event.
|
|
61
|
+
|
|
62
|
+
Args:
|
|
63
|
+
session_id: Session that completed.
|
|
64
|
+
summary: Execution summary data.
|
|
65
|
+
messages_processed: Total messages processed.
|
|
66
|
+
|
|
67
|
+
Returns:
|
|
68
|
+
BaseEvent for session completion.
|
|
69
|
+
"""
|
|
70
|
+
return BaseEvent(
|
|
71
|
+
type="orchestrator.session.completed",
|
|
72
|
+
aggregate_type="session",
|
|
73
|
+
aggregate_id=session_id,
|
|
74
|
+
data={
|
|
75
|
+
"summary": summary,
|
|
76
|
+
"messages_processed": messages_processed,
|
|
77
|
+
"completed_at": datetime.now(UTC).isoformat(),
|
|
78
|
+
},
|
|
79
|
+
)
|
|
80
|
+
|
|
81
|
+
|
|
82
|
+
def create_session_failed_event(
|
|
83
|
+
session_id: str,
|
|
84
|
+
error_message: str,
|
|
85
|
+
error_type: str | None = None,
|
|
86
|
+
messages_processed: int = 0,
|
|
87
|
+
) -> BaseEvent:
|
|
88
|
+
"""Create session failed event.
|
|
89
|
+
|
|
90
|
+
Args:
|
|
91
|
+
session_id: Session that failed.
|
|
92
|
+
error_message: Error description.
|
|
93
|
+
error_type: Type/category of error.
|
|
94
|
+
messages_processed: Messages processed before failure.
|
|
95
|
+
|
|
96
|
+
Returns:
|
|
97
|
+
BaseEvent for session failure.
|
|
98
|
+
"""
|
|
99
|
+
return BaseEvent(
|
|
100
|
+
type="orchestrator.session.failed",
|
|
101
|
+
aggregate_type="session",
|
|
102
|
+
aggregate_id=session_id,
|
|
103
|
+
data={
|
|
104
|
+
"error": error_message,
|
|
105
|
+
"error_type": error_type,
|
|
106
|
+
"messages_processed": messages_processed,
|
|
107
|
+
"failed_at": datetime.now(UTC).isoformat(),
|
|
108
|
+
},
|
|
109
|
+
)
|
|
110
|
+
|
|
111
|
+
|
|
112
|
+
def create_session_paused_event(
|
|
113
|
+
session_id: str,
|
|
114
|
+
reason: str,
|
|
115
|
+
resume_hint: str | None = None,
|
|
116
|
+
) -> BaseEvent:
|
|
117
|
+
"""Create session paused event.
|
|
118
|
+
|
|
119
|
+
Args:
|
|
120
|
+
session_id: Session being paused.
|
|
121
|
+
reason: Why the session was paused.
|
|
122
|
+
resume_hint: Hint for resumption (e.g., last AC processed).
|
|
123
|
+
|
|
124
|
+
Returns:
|
|
125
|
+
BaseEvent for session pause.
|
|
126
|
+
"""
|
|
127
|
+
return BaseEvent(
|
|
128
|
+
type="orchestrator.session.paused",
|
|
129
|
+
aggregate_type="session",
|
|
130
|
+
aggregate_id=session_id,
|
|
131
|
+
data={
|
|
132
|
+
"reason": reason,
|
|
133
|
+
"resume_hint": resume_hint,
|
|
134
|
+
"paused_at": datetime.now(UTC).isoformat(),
|
|
135
|
+
},
|
|
136
|
+
)
|
|
137
|
+
|
|
138
|
+
|
|
139
|
+
def create_progress_event(
|
|
140
|
+
session_id: str,
|
|
141
|
+
message_type: str,
|
|
142
|
+
content_preview: str,
|
|
143
|
+
step: int | None = None,
|
|
144
|
+
tool_name: str | None = None,
|
|
145
|
+
) -> BaseEvent:
|
|
146
|
+
"""Create progress update event.
|
|
147
|
+
|
|
148
|
+
Emitted periodically during execution to track progress.
|
|
149
|
+
Useful for reconstructing session state during resumption.
|
|
150
|
+
|
|
151
|
+
Args:
|
|
152
|
+
session_id: Session being updated.
|
|
153
|
+
message_type: Type of message ("assistant", "tool", etc.).
|
|
154
|
+
content_preview: Preview of message content (truncated).
|
|
155
|
+
step: Optional step number.
|
|
156
|
+
tool_name: Tool being called (if message_type="tool").
|
|
157
|
+
|
|
158
|
+
Returns:
|
|
159
|
+
BaseEvent for progress update.
|
|
160
|
+
"""
|
|
161
|
+
data: dict[str, Any] = {
|
|
162
|
+
"message_type": message_type,
|
|
163
|
+
"content_preview": content_preview[:200], # Truncate for storage
|
|
164
|
+
"timestamp": datetime.now(UTC).isoformat(),
|
|
165
|
+
}
|
|
166
|
+
|
|
167
|
+
if step is not None:
|
|
168
|
+
data["step"] = step
|
|
169
|
+
|
|
170
|
+
if tool_name:
|
|
171
|
+
data["tool_name"] = tool_name
|
|
172
|
+
|
|
173
|
+
return BaseEvent(
|
|
174
|
+
type="orchestrator.progress.updated",
|
|
175
|
+
aggregate_type="session",
|
|
176
|
+
aggregate_id=session_id,
|
|
177
|
+
data=data,
|
|
178
|
+
)
|
|
179
|
+
|
|
180
|
+
|
|
181
|
+
def create_task_started_event(
|
|
182
|
+
session_id: str,
|
|
183
|
+
task_description: str,
|
|
184
|
+
acceptance_criterion: str,
|
|
185
|
+
) -> BaseEvent:
|
|
186
|
+
"""Create task started event.
|
|
187
|
+
|
|
188
|
+
Args:
|
|
189
|
+
session_id: Session executing the task.
|
|
190
|
+
task_description: What the task aims to accomplish.
|
|
191
|
+
acceptance_criterion: AC from the seed being executed.
|
|
192
|
+
|
|
193
|
+
Returns:
|
|
194
|
+
BaseEvent for task start.
|
|
195
|
+
"""
|
|
196
|
+
return BaseEvent(
|
|
197
|
+
type="orchestrator.task.started",
|
|
198
|
+
aggregate_type="session",
|
|
199
|
+
aggregate_id=session_id,
|
|
200
|
+
data={
|
|
201
|
+
"task_description": task_description,
|
|
202
|
+
"acceptance_criterion": acceptance_criterion,
|
|
203
|
+
"started_at": datetime.now(UTC).isoformat(),
|
|
204
|
+
},
|
|
205
|
+
)
|
|
206
|
+
|
|
207
|
+
|
|
208
|
+
def create_task_completed_event(
|
|
209
|
+
session_id: str,
|
|
210
|
+
acceptance_criterion: str,
|
|
211
|
+
success: bool,
|
|
212
|
+
result_summary: str | None = None,
|
|
213
|
+
) -> BaseEvent:
|
|
214
|
+
"""Create task completed event.
|
|
215
|
+
|
|
216
|
+
Args:
|
|
217
|
+
session_id: Session that completed the task.
|
|
218
|
+
acceptance_criterion: AC that was executed.
|
|
219
|
+
success: Whether the task succeeded.
|
|
220
|
+
result_summary: Summary of what was accomplished.
|
|
221
|
+
|
|
222
|
+
Returns:
|
|
223
|
+
BaseEvent for task completion.
|
|
224
|
+
"""
|
|
225
|
+
return BaseEvent(
|
|
226
|
+
type="orchestrator.task.completed",
|
|
227
|
+
aggregate_type="session",
|
|
228
|
+
aggregate_id=session_id,
|
|
229
|
+
data={
|
|
230
|
+
"acceptance_criterion": acceptance_criterion,
|
|
231
|
+
"success": success,
|
|
232
|
+
"result_summary": result_summary,
|
|
233
|
+
"completed_at": datetime.now(UTC).isoformat(),
|
|
234
|
+
},
|
|
235
|
+
)
|
|
236
|
+
|
|
237
|
+
|
|
238
|
+
def create_tool_called_event(
|
|
239
|
+
session_id: str,
|
|
240
|
+
tool_name: str,
|
|
241
|
+
tool_input_preview: str | None = None,
|
|
242
|
+
) -> BaseEvent:
|
|
243
|
+
"""Create tool called event.
|
|
244
|
+
|
|
245
|
+
Args:
|
|
246
|
+
session_id: Session where tool was called.
|
|
247
|
+
tool_name: Name of the tool (Read, Edit, Bash, etc.).
|
|
248
|
+
tool_input_preview: Preview of tool input (truncated).
|
|
249
|
+
|
|
250
|
+
Returns:
|
|
251
|
+
BaseEvent for tool invocation.
|
|
252
|
+
"""
|
|
253
|
+
data: dict[str, Any] = {
|
|
254
|
+
"tool_name": tool_name,
|
|
255
|
+
"called_at": datetime.now(UTC).isoformat(),
|
|
256
|
+
}
|
|
257
|
+
|
|
258
|
+
if tool_input_preview:
|
|
259
|
+
data["tool_input_preview"] = tool_input_preview[:100]
|
|
260
|
+
|
|
261
|
+
return BaseEvent(
|
|
262
|
+
type="orchestrator.tool.called",
|
|
263
|
+
aggregate_type="session",
|
|
264
|
+
aggregate_id=session_id,
|
|
265
|
+
data=data,
|
|
266
|
+
)
|
|
267
|
+
|
|
268
|
+
|
|
269
|
+
__all__ = [
|
|
270
|
+
"create_progress_event",
|
|
271
|
+
"create_session_completed_event",
|
|
272
|
+
"create_session_failed_event",
|
|
273
|
+
"create_session_paused_event",
|
|
274
|
+
"create_session_started_event",
|
|
275
|
+
"create_task_completed_event",
|
|
276
|
+
"create_task_started_event",
|
|
277
|
+
"create_tool_called_event",
|
|
278
|
+
]
|