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,597 @@
1
+ """Orchestrator runner for executing seeds via Claude Agent SDK.
2
+
3
+ This module provides the main orchestration logic:
4
+ - OrchestratorRunner: Converts Seed → prompt, executes via adapter, tracks progress
5
+ - OrchestratorResult: Frozen dataclass with execution results
6
+
7
+ The runner integrates:
8
+ - ClaudeAgentAdapter for task execution
9
+ - SessionRepository for event-based session tracking
10
+ - Rich console for progress display
11
+ - Event emission for observability
12
+
13
+ Usage:
14
+ runner = OrchestratorRunner(adapter, event_store)
15
+ result = await runner.execute_seed(seed, execution_id)
16
+ if result.is_ok:
17
+ print(f"Success: {result.value.summary}")
18
+ """
19
+
20
+ from __future__ import annotations
21
+
22
+ from dataclasses import dataclass, field
23
+ from datetime import UTC, datetime
24
+ from typing import TYPE_CHECKING, Any
25
+ from uuid import uuid4
26
+
27
+ from rich.console import Console
28
+ from rich.panel import Panel
29
+ from rich.progress import Progress, SpinnerColumn, TextColumn, TimeElapsedColumn
30
+ from rich.text import Text
31
+
32
+ from ouroboros.core.errors import OuroborosError
33
+ from ouroboros.core.types import Result
34
+ from ouroboros.observability.logging import get_logger
35
+ from ouroboros.orchestrator.adapter import DEFAULT_TOOLS, AgentMessage, ClaudeAgentAdapter
36
+ from ouroboros.orchestrator.events import (
37
+ create_progress_event,
38
+ create_session_completed_event,
39
+ create_session_failed_event,
40
+ create_session_started_event,
41
+ create_tool_called_event,
42
+ )
43
+ from ouroboros.orchestrator.session import SessionRepository, SessionStatus
44
+
45
+ if TYPE_CHECKING:
46
+ from ouroboros.core.seed import Seed
47
+ from ouroboros.persistence.event_store import EventStore
48
+
49
+ log = get_logger(__name__)
50
+
51
+
52
+ # =============================================================================
53
+ # Result Types
54
+ # =============================================================================
55
+
56
+
57
+ @dataclass(frozen=True, slots=True)
58
+ class OrchestratorResult:
59
+ """Result of orchestrator execution.
60
+
61
+ Attributes:
62
+ success: Whether execution completed successfully.
63
+ session_id: Session identifier for resumption.
64
+ execution_id: Workflow execution ID.
65
+ summary: Execution summary dict.
66
+ messages_processed: Total messages from agent.
67
+ final_message: Final result message from agent.
68
+ duration_seconds: Execution duration.
69
+ """
70
+
71
+ success: bool
72
+ session_id: str
73
+ execution_id: str
74
+ summary: dict[str, Any] = field(default_factory=dict)
75
+ messages_processed: int = 0
76
+ final_message: str = ""
77
+ duration_seconds: float = 0.0
78
+
79
+
80
+ # =============================================================================
81
+ # Errors
82
+ # =============================================================================
83
+
84
+
85
+ class OrchestratorError(OuroborosError):
86
+ """Error during orchestrator execution."""
87
+
88
+ pass
89
+
90
+
91
+ # =============================================================================
92
+ # Prompt Building
93
+ # =============================================================================
94
+
95
+
96
+ def build_system_prompt(seed: Seed) -> str:
97
+ """Build system prompt from seed specification.
98
+
99
+ Args:
100
+ seed: Seed to extract system prompt from.
101
+
102
+ Returns:
103
+ System prompt string.
104
+ """
105
+ constraints_text = "\n".join(f"- {c}" for c in seed.constraints) if seed.constraints else "None"
106
+
107
+ principles_text = (
108
+ "\n".join(f"- {p.name}: {p.description}" for p in seed.evaluation_principles)
109
+ if seed.evaluation_principles
110
+ else "None"
111
+ )
112
+
113
+ return f"""You are an autonomous coding agent executing a task for the Ouroboros workflow system.
114
+
115
+ ## Goal
116
+ {seed.goal}
117
+
118
+ ## Constraints
119
+ {constraints_text}
120
+
121
+ ## Evaluation Principles
122
+ {principles_text}
123
+
124
+ ## Guidelines
125
+ - Execute each acceptance criterion thoroughly
126
+ - Use the available tools (Read, Edit, Bash, Glob, Grep) to accomplish tasks
127
+ - Write clean, well-tested code following project conventions
128
+ - Report progress clearly as you work
129
+ - If you encounter blockers, explain them clearly
130
+ """
131
+
132
+
133
+ def build_task_prompt(seed: Seed) -> str:
134
+ """Build task prompt from seed acceptance criteria.
135
+
136
+ Args:
137
+ seed: Seed containing acceptance criteria.
138
+
139
+ Returns:
140
+ Task prompt string.
141
+ """
142
+ ac_list = "\n".join(f"{i + 1}. {ac}" for i, ac in enumerate(seed.acceptance_criteria))
143
+
144
+ return f"""Execute the following task according to the acceptance criteria:
145
+
146
+ ## Goal
147
+ {seed.goal}
148
+
149
+ ## Acceptance Criteria
150
+ {ac_list}
151
+
152
+ Please execute each criterion in order, using the available tools to read, write, and modify code as needed.
153
+ Report your progress and results for each criterion.
154
+ """
155
+
156
+
157
+ # =============================================================================
158
+ # Runner
159
+ # =============================================================================
160
+
161
+
162
+ # Progress event emission interval (every N messages)
163
+ PROGRESS_EMIT_INTERVAL = 10
164
+
165
+
166
+ class OrchestratorRunner:
167
+ """Main orchestration runner for executing seeds via Claude Agent.
168
+
169
+ Converts Seed specifications to agent prompts, executes via adapter,
170
+ tracks progress through event emission, and displays status via Rich.
171
+ """
172
+
173
+ def __init__(
174
+ self,
175
+ adapter: ClaudeAgentAdapter,
176
+ event_store: EventStore,
177
+ console: Console | None = None,
178
+ ) -> None:
179
+ """Initialize orchestrator runner.
180
+
181
+ Args:
182
+ adapter: Claude Agent adapter for task execution.
183
+ event_store: Event store for persistence.
184
+ console: Rich console for output. Uses default if not provided.
185
+ """
186
+ self._adapter = adapter
187
+ self._event_store = event_store
188
+ self._console = console or Console()
189
+ self._session_repo = SessionRepository(event_store)
190
+
191
+ async def execute_seed(
192
+ self,
193
+ seed: Seed,
194
+ execution_id: str | None = None,
195
+ ) -> Result[OrchestratorResult, OrchestratorError]:
196
+ """Execute seed via Claude Agent.
197
+
198
+ This is the main entry point for orchestrator execution.
199
+ It converts the seed to prompts, executes via the adapter,
200
+ and tracks progress through events.
201
+
202
+ Args:
203
+ seed: Seed specification to execute.
204
+ execution_id: Optional execution ID. Generated if not provided.
205
+
206
+ Returns:
207
+ Result containing OrchestratorResult on success.
208
+ """
209
+ exec_id = execution_id or f"exec_{uuid4().hex[:12]}"
210
+ start_time = datetime.now(UTC)
211
+
212
+ log.info(
213
+ "orchestrator.runner.execute_started",
214
+ execution_id=exec_id,
215
+ seed_id=seed.metadata.seed_id,
216
+ goal=seed.goal[:100],
217
+ )
218
+
219
+ # Create session
220
+ session_result = await self._session_repo.create_session(
221
+ execution_id=exec_id,
222
+ seed_id=seed.metadata.seed_id,
223
+ )
224
+
225
+ if session_result.is_err:
226
+ return Result.err(
227
+ OrchestratorError(
228
+ message=f"Failed to create session: {session_result.error}",
229
+ details={"execution_id": exec_id},
230
+ )
231
+ )
232
+
233
+ tracker = session_result.value
234
+
235
+ # Emit session started event
236
+ start_event = create_session_started_event(
237
+ session_id=tracker.session_id,
238
+ execution_id=exec_id,
239
+ seed_id=seed.metadata.seed_id,
240
+ seed_goal=seed.goal,
241
+ )
242
+ await self._event_store.append(start_event)
243
+
244
+ # Build prompts
245
+ system_prompt = build_system_prompt(seed)
246
+ task_prompt = build_task_prompt(seed)
247
+
248
+ # Execute with progress display
249
+ messages_processed = 0
250
+ final_message = ""
251
+ success = False
252
+
253
+ try:
254
+ with Progress(
255
+ SpinnerColumn(),
256
+ TextColumn("[progress.description]{task.description}"),
257
+ TimeElapsedColumn(),
258
+ console=self._console,
259
+ transient=True,
260
+ ) as progress:
261
+ task_id = progress.add_task(
262
+ "[cyan]Executing via Claude Agent...",
263
+ total=None,
264
+ )
265
+
266
+ async for message in self._adapter.execute_task(
267
+ prompt=task_prompt,
268
+ tools=DEFAULT_TOOLS,
269
+ system_prompt=system_prompt,
270
+ ):
271
+ messages_processed += 1
272
+ tracker = tracker.with_progress(
273
+ {
274
+ "last_message_type": message.type,
275
+ "messages_processed": messages_processed,
276
+ }
277
+ )
278
+
279
+ # Update progress display
280
+ display_text = self._format_progress_text(message, messages_processed)
281
+ progress.update(task_id, description=display_text)
282
+
283
+ # Emit tool called event
284
+ if message.tool_name:
285
+ tool_event = create_tool_called_event(
286
+ session_id=tracker.session_id,
287
+ tool_name=message.tool_name,
288
+ )
289
+ await self._event_store.append(tool_event)
290
+
291
+ # Emit progress event periodically
292
+ if messages_processed % PROGRESS_EMIT_INTERVAL == 0:
293
+ progress_event = create_progress_event(
294
+ session_id=tracker.session_id,
295
+ message_type=message.type,
296
+ content_preview=message.content,
297
+ step=messages_processed,
298
+ tool_name=message.tool_name,
299
+ )
300
+ await self._event_store.append(progress_event)
301
+
302
+ # Handle final message
303
+ if message.is_final:
304
+ final_message = message.content
305
+ success = not message.is_error
306
+
307
+ # Calculate duration
308
+ duration = (datetime.now(UTC) - start_time).total_seconds()
309
+
310
+ # Emit completion event
311
+ if success:
312
+ completed_event = create_session_completed_event(
313
+ session_id=tracker.session_id,
314
+ summary={"final_message": final_message[:500]},
315
+ messages_processed=messages_processed,
316
+ )
317
+ await self._event_store.append(completed_event)
318
+ await self._session_repo.mark_completed(
319
+ tracker.session_id,
320
+ {"messages_processed": messages_processed},
321
+ )
322
+
323
+ # Display success
324
+ self._console.print(
325
+ Panel(
326
+ Text(final_message[:1000], style="green"),
327
+ title="[green]Execution Completed[/green]",
328
+ border_style="green",
329
+ )
330
+ )
331
+ else:
332
+ failed_event = create_session_failed_event(
333
+ session_id=tracker.session_id,
334
+ error_message=final_message,
335
+ messages_processed=messages_processed,
336
+ )
337
+ await self._event_store.append(failed_event)
338
+ await self._session_repo.mark_failed(
339
+ tracker.session_id,
340
+ final_message,
341
+ )
342
+
343
+ # Display failure
344
+ self._console.print(
345
+ Panel(
346
+ Text(final_message[:1000], style="red"),
347
+ title="[red]Execution Failed[/red]",
348
+ border_style="red",
349
+ )
350
+ )
351
+
352
+ log.info(
353
+ "orchestrator.runner.execute_completed",
354
+ execution_id=exec_id,
355
+ session_id=tracker.session_id,
356
+ success=success,
357
+ messages_processed=messages_processed,
358
+ duration_seconds=duration,
359
+ )
360
+
361
+ return Result.ok(
362
+ OrchestratorResult(
363
+ success=success,
364
+ session_id=tracker.session_id,
365
+ execution_id=exec_id,
366
+ summary={
367
+ "goal": seed.goal,
368
+ "acceptance_criteria_count": len(seed.acceptance_criteria),
369
+ },
370
+ messages_processed=messages_processed,
371
+ final_message=final_message,
372
+ duration_seconds=duration,
373
+ )
374
+ )
375
+
376
+ except Exception as e:
377
+ log.exception(
378
+ "orchestrator.runner.execute_failed",
379
+ execution_id=exec_id,
380
+ error=str(e),
381
+ )
382
+
383
+ # Emit failure event
384
+ failed_event = create_session_failed_event(
385
+ session_id=tracker.session_id,
386
+ error_message=str(e),
387
+ error_type=type(e).__name__,
388
+ messages_processed=messages_processed,
389
+ )
390
+ await self._event_store.append(failed_event)
391
+
392
+ return Result.err(
393
+ OrchestratorError(
394
+ message=f"Orchestrator execution failed: {e}",
395
+ details={
396
+ "execution_id": exec_id,
397
+ "session_id": tracker.session_id,
398
+ "messages_processed": messages_processed,
399
+ },
400
+ )
401
+ )
402
+
403
+ async def resume_session(
404
+ self,
405
+ session_id: str,
406
+ seed: Seed,
407
+ ) -> Result[OrchestratorResult, OrchestratorError]:
408
+ """Resume a paused or failed session.
409
+
410
+ Reconstructs session state from events and continues execution.
411
+
412
+ Args:
413
+ session_id: Session to resume.
414
+ seed: Original seed (needed for prompt building).
415
+
416
+ Returns:
417
+ Result containing OrchestratorResult on success.
418
+ """
419
+ log.info(
420
+ "orchestrator.runner.resume_started",
421
+ session_id=session_id,
422
+ )
423
+
424
+ # Reconstruct session
425
+ session_result = await self._session_repo.reconstruct_session(session_id)
426
+
427
+ if session_result.is_err:
428
+ return Result.err(
429
+ OrchestratorError(
430
+ message=f"Failed to reconstruct session: {session_result.error}",
431
+ details={"session_id": session_id},
432
+ )
433
+ )
434
+
435
+ tracker = session_result.value
436
+
437
+ # Check if session can be resumed
438
+ if tracker.status == SessionStatus.COMPLETED:
439
+ return Result.err(
440
+ OrchestratorError(
441
+ message="Session already completed, cannot resume",
442
+ details={"session_id": session_id, "status": tracker.status.value},
443
+ )
444
+ )
445
+
446
+ self._console.print(
447
+ f"[cyan]Resuming session {session_id}[/cyan]\n"
448
+ f"[dim]Previously processed: {tracker.messages_processed} messages[/dim]"
449
+ )
450
+
451
+ # Build resume prompt
452
+ system_prompt = build_system_prompt(seed)
453
+ resume_prompt = f"""Continue executing the task from where you left off.
454
+
455
+ {build_task_prompt(seed)}
456
+
457
+ Note: This is a resumed session. Please continue from where execution was interrupted.
458
+ """
459
+
460
+ # Get Claude Agent session ID if stored
461
+ agent_session_id = tracker.progress.get("agent_session_id")
462
+
463
+ start_time = datetime.now(UTC)
464
+ messages_processed = tracker.messages_processed
465
+ final_message = ""
466
+ success = False
467
+
468
+ try:
469
+ with Progress(
470
+ SpinnerColumn(),
471
+ TextColumn("[progress.description]{task.description}"),
472
+ TimeElapsedColumn(),
473
+ console=self._console,
474
+ transient=True,
475
+ ) as progress:
476
+ task_id = progress.add_task(
477
+ "[cyan]Resuming execution...",
478
+ total=None,
479
+ )
480
+
481
+ async for message in self._adapter.execute_task(
482
+ prompt=resume_prompt,
483
+ tools=DEFAULT_TOOLS,
484
+ system_prompt=system_prompt,
485
+ resume_session_id=agent_session_id,
486
+ ):
487
+ messages_processed += 1
488
+
489
+ display_text = self._format_progress_text(message, messages_processed)
490
+ progress.update(task_id, description=display_text)
491
+
492
+ if message.tool_name:
493
+ tool_event = create_tool_called_event(
494
+ session_id=session_id,
495
+ tool_name=message.tool_name,
496
+ )
497
+ await self._event_store.append(tool_event)
498
+
499
+ if messages_processed % PROGRESS_EMIT_INTERVAL == 0:
500
+ progress_event = create_progress_event(
501
+ session_id=session_id,
502
+ message_type=message.type,
503
+ content_preview=message.content,
504
+ step=messages_processed,
505
+ tool_name=message.tool_name,
506
+ )
507
+ await self._event_store.append(progress_event)
508
+
509
+ if message.is_final:
510
+ final_message = message.content
511
+ success = not message.is_error
512
+
513
+ duration = (datetime.now(UTC) - start_time).total_seconds()
514
+
515
+ if success:
516
+ await self._session_repo.mark_completed(
517
+ session_id,
518
+ {"messages_processed": messages_processed},
519
+ )
520
+ self._console.print(
521
+ Panel(
522
+ Text(final_message[:1000], style="green"),
523
+ title="[green]Resumed Execution Completed[/green]",
524
+ border_style="green",
525
+ )
526
+ )
527
+ else:
528
+ await self._session_repo.mark_failed(session_id, final_message)
529
+ self._console.print(
530
+ Panel(
531
+ Text(final_message[:1000], style="red"),
532
+ title="[red]Resumed Execution Failed[/red]",
533
+ border_style="red",
534
+ )
535
+ )
536
+
537
+ log.info(
538
+ "orchestrator.runner.resume_completed",
539
+ session_id=session_id,
540
+ success=success,
541
+ messages_processed=messages_processed,
542
+ duration_seconds=duration,
543
+ )
544
+
545
+ return Result.ok(
546
+ OrchestratorResult(
547
+ success=success,
548
+ session_id=session_id,
549
+ execution_id=tracker.execution_id,
550
+ summary={"resumed": True},
551
+ messages_processed=messages_processed,
552
+ final_message=final_message,
553
+ duration_seconds=duration,
554
+ )
555
+ )
556
+
557
+ except Exception as e:
558
+ log.exception(
559
+ "orchestrator.runner.resume_failed",
560
+ session_id=session_id,
561
+ error=str(e),
562
+ )
563
+ return Result.err(
564
+ OrchestratorError(
565
+ message=f"Session resume failed: {e}",
566
+ details={"session_id": session_id},
567
+ )
568
+ )
569
+
570
+ def _format_progress_text(self, message: AgentMessage, count: int) -> str:
571
+ """Format progress text for display.
572
+
573
+ Args:
574
+ message: Current agent message.
575
+ count: Message count.
576
+
577
+ Returns:
578
+ Formatted progress text.
579
+ """
580
+ if message.tool_name:
581
+ return f"[cyan]({count}) Using {message.tool_name}...[/cyan]"
582
+ elif message.type == "assistant":
583
+ preview = message.content[:50].replace("\n", " ")
584
+ return f"[cyan]({count}) {preview}...[/cyan]"
585
+ elif message.type == "result":
586
+ return f"[green]({count}) Finalizing...[/green]"
587
+ else:
588
+ return f"[dim]({count}) Processing...[/dim]"
589
+
590
+
591
+ __all__ = [
592
+ "OrchestratorError",
593
+ "OrchestratorResult",
594
+ "OrchestratorRunner",
595
+ "build_system_prompt",
596
+ "build_task_prompt",
597
+ ]