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,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
|
+
]
|