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.

Files changed (81) hide show
  1. ouroboros/__init__.py +15 -0
  2. ouroboros/__main__.py +9 -0
  3. ouroboros/bigbang/__init__.py +39 -0
  4. ouroboros/bigbang/ambiguity.py +464 -0
  5. ouroboros/bigbang/interview.py +530 -0
  6. ouroboros/bigbang/seed_generator.py +610 -0
  7. ouroboros/cli/__init__.py +9 -0
  8. ouroboros/cli/commands/__init__.py +7 -0
  9. ouroboros/cli/commands/config.py +79 -0
  10. ouroboros/cli/commands/init.py +425 -0
  11. ouroboros/cli/commands/run.py +201 -0
  12. ouroboros/cli/commands/status.py +85 -0
  13. ouroboros/cli/formatters/__init__.py +31 -0
  14. ouroboros/cli/formatters/panels.py +157 -0
  15. ouroboros/cli/formatters/progress.py +112 -0
  16. ouroboros/cli/formatters/tables.py +166 -0
  17. ouroboros/cli/main.py +60 -0
  18. ouroboros/config/__init__.py +81 -0
  19. ouroboros/config/loader.py +292 -0
  20. ouroboros/config/models.py +332 -0
  21. ouroboros/core/__init__.py +62 -0
  22. ouroboros/core/ac_tree.py +401 -0
  23. ouroboros/core/context.py +472 -0
  24. ouroboros/core/errors.py +246 -0
  25. ouroboros/core/seed.py +212 -0
  26. ouroboros/core/types.py +205 -0
  27. ouroboros/evaluation/__init__.py +110 -0
  28. ouroboros/evaluation/consensus.py +350 -0
  29. ouroboros/evaluation/mechanical.py +351 -0
  30. ouroboros/evaluation/models.py +235 -0
  31. ouroboros/evaluation/pipeline.py +286 -0
  32. ouroboros/evaluation/semantic.py +302 -0
  33. ouroboros/evaluation/trigger.py +278 -0
  34. ouroboros/events/__init__.py +5 -0
  35. ouroboros/events/base.py +80 -0
  36. ouroboros/events/decomposition.py +153 -0
  37. ouroboros/events/evaluation.py +248 -0
  38. ouroboros/execution/__init__.py +44 -0
  39. ouroboros/execution/atomicity.py +451 -0
  40. ouroboros/execution/decomposition.py +481 -0
  41. ouroboros/execution/double_diamond.py +1386 -0
  42. ouroboros/execution/subagent.py +275 -0
  43. ouroboros/observability/__init__.py +63 -0
  44. ouroboros/observability/drift.py +383 -0
  45. ouroboros/observability/logging.py +504 -0
  46. ouroboros/observability/retrospective.py +338 -0
  47. ouroboros/orchestrator/__init__.py +78 -0
  48. ouroboros/orchestrator/adapter.py +391 -0
  49. ouroboros/orchestrator/events.py +278 -0
  50. ouroboros/orchestrator/runner.py +597 -0
  51. ouroboros/orchestrator/session.py +486 -0
  52. ouroboros/persistence/__init__.py +23 -0
  53. ouroboros/persistence/checkpoint.py +511 -0
  54. ouroboros/persistence/event_store.py +183 -0
  55. ouroboros/persistence/migrations/__init__.py +1 -0
  56. ouroboros/persistence/migrations/runner.py +100 -0
  57. ouroboros/persistence/migrations/scripts/001_initial.sql +20 -0
  58. ouroboros/persistence/schema.py +56 -0
  59. ouroboros/persistence/uow.py +230 -0
  60. ouroboros/providers/__init__.py +28 -0
  61. ouroboros/providers/base.py +133 -0
  62. ouroboros/providers/claude_code_adapter.py +212 -0
  63. ouroboros/providers/litellm_adapter.py +316 -0
  64. ouroboros/py.typed +0 -0
  65. ouroboros/resilience/__init__.py +67 -0
  66. ouroboros/resilience/lateral.py +595 -0
  67. ouroboros/resilience/stagnation.py +727 -0
  68. ouroboros/routing/__init__.py +60 -0
  69. ouroboros/routing/complexity.py +272 -0
  70. ouroboros/routing/downgrade.py +664 -0
  71. ouroboros/routing/escalation.py +340 -0
  72. ouroboros/routing/router.py +204 -0
  73. ouroboros/routing/tiers.py +247 -0
  74. ouroboros/secondary/__init__.py +40 -0
  75. ouroboros/secondary/scheduler.py +467 -0
  76. ouroboros/secondary/todo_registry.py +483 -0
  77. ouroboros_ai-0.1.0.dist-info/METADATA +607 -0
  78. ouroboros_ai-0.1.0.dist-info/RECORD +81 -0
  79. ouroboros_ai-0.1.0.dist-info/WHEEL +4 -0
  80. ouroboros_ai-0.1.0.dist-info/entry_points.txt +2 -0
  81. 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
+ ]