@@ -3,14 +3,12 @@ |
| 3 | 3 | from __future__ import annotations |
| 4 | 4 | |
| 5 | 5 | from collections.abc import Awaitable, Callable |
| 6 | | -from pathlib import Path |
| 7 | 6 | from typing import Any |
| 8 | 7 | |
| 9 | | -from ..llm.base import Message, Role |
| 10 | 8 | from .artifact_invalidation import ArtifactInvalidationAssessor |
| 11 | 9 | from .assistant_turns import AssistantTurnRequester |
| 12 | 10 | from .completion_policy import CompletionPolicy |
| 13 | | -from .dod import DefinitionOfDone, DefinitionOfDoneStore |
| 11 | +from .dod import DefinitionOfDoneStore |
| 14 | 12 | from .events import AgentEvent, TurnSummary |
| 15 | 13 | from .executor import ToolExecutor |
| 16 | 14 | from .finalization import TurnFinalizer |
@@ -23,17 +21,13 @@ from .turn_iteration import TurnIterationAction, TurnIterationController |
| 23 | 21 | from .turn_preamble import TurnPreludeController |
| 24 | 22 | from .turn_preparation import TurnPreparationController |
| 25 | 23 | from .workflow import ( |
| 26 | | - ModeDecision, |
| 27 | 24 | WorkflowArtifactStore, |
| 28 | | - WorkflowDecisionKind, |
| 29 | 25 | WorkflowPolicy, |
| 30 | 26 | WorkflowSignalExtractor, |
| 31 | | - WorkflowTimelineEntry, |
| 32 | | - WorkflowTimelineEntryKind, |
| 33 | | - build_execute_bridge, |
| 34 | 27 | ) |
| 35 | 28 | from .workflow_lanes import WorkflowLaneRunner |
| 36 | 29 | from .workflow_recovery import WorkflowRecoveryController |
| 30 | +from .workflow_state import WorkflowStateController |
| 37 | 31 | |
| 38 | 32 | EventSink = Callable[[AgentEvent], Awaitable[None]] |
| 39 | 33 | ConfirmationHandler = Callable[[str, str, str], Awaitable[bool]] | None |
@@ -52,6 +46,10 @@ class ConversationRuntime: |
| 52 | 46 | self.workflow_policy = WorkflowPolicy(self.workflow_signals) |
| 53 | 47 | self.artifact_invalidation = ArtifactInvalidationAssessor() |
| 54 | 48 | self.artifact_store = WorkflowArtifactStore(agent.project_root) |
| 49 | + self.workflow_state = WorkflowStateController( |
| 50 | + agent, |
| 51 | + dod_store=self.dod_store, |
| 52 | + ) |
| 55 | 53 | self.workflow_lanes = WorkflowLaneRunner( |
| 56 | 54 | agent, |
| 57 | 55 | artifact_store=self.artifact_store, |
@@ -64,9 +62,9 @@ class ConversationRuntime: |
| 64 | 62 | workflow_policy=self.workflow_policy, |
| 65 | 63 | workflow_signals=self.workflow_signals, |
| 66 | 64 | workflow_lanes=self.workflow_lanes, |
| 67 | | - set_workflow_mode=self._set_workflow_mode, |
| 68 | | - append_timeline=self._append_workflow_timeline_from_decision, |
| 69 | | - append_execute_bridge=self._maybe_append_execute_bridge, |
| 65 | + set_workflow_mode=self.workflow_state.set_workflow_mode, |
| 66 | + append_timeline=self.workflow_state.append_timeline_from_decision, |
| 67 | + append_execute_bridge=self.workflow_state.maybe_append_execute_bridge, |
| 70 | 68 | ) |
| 71 | 69 | self.repairer = ResponseRepairer(agent) |
| 72 | 70 | self.completion_policy = CompletionPolicy(agent) |
@@ -75,7 +73,7 @@ class ConversationRuntime: |
| 75 | 73 | agent, |
| 76 | 74 | self.tracer, |
| 77 | 75 | self.dod_store, |
| 78 | | - self._set_workflow_mode, |
| 76 | + self.workflow_state.set_workflow_mode, |
| 79 | 77 | ) |
| 80 | 78 | self.turn_completion = TurnCompletionController( |
| 81 | 79 | agent, |
@@ -102,9 +100,9 @@ class ConversationRuntime: |
| 102 | 100 | workflow_signals=self.workflow_signals, |
| 103 | 101 | workflow_lanes=self.workflow_lanes, |
| 104 | 102 | finalizer=self.finalizer, |
| 105 | | - set_workflow_mode=self._set_workflow_mode, |
| 106 | | - append_timeline=self._append_workflow_timeline_from_decision, |
| 107 | | - append_execute_bridge=self._maybe_append_execute_bridge, |
| 103 | + set_workflow_mode=self.workflow_state.set_workflow_mode, |
| 104 | + append_timeline=self.workflow_state.append_timeline_from_decision, |
| 105 | + append_execute_bridge=self.workflow_state.maybe_append_execute_bridge, |
| 108 | 106 | ) |
| 109 | 107 | self.turn_preamble = TurnPreludeController( |
| 110 | 108 | agent, |
@@ -230,105 +228,6 @@ class ConversationRuntime: |
| 230 | 228 | self.phase_tracker.clear() |
| 231 | 229 | return final_summary |
| 232 | 230 | |
| 233 | | - async def _set_workflow_mode( |
| 234 | | - self, |
| 235 | | - decision: ModeDecision, |
| 236 | | - *, |
| 237 | | - dod: DefinitionOfDone, |
| 238 | | - emit: EventSink, |
| 239 | | - summary: TurnSummary, |
| 240 | | - ) -> None: |
| 241 | | - mode = decision.mode |
| 242 | | - self.agent.set_workflow_mode(mode.value) |
| 243 | | - self.agent.session.update_runtime_state( |
| 244 | | - workflow_mode=mode.value, |
| 245 | | - workflow_reason_code=decision.reason_code, |
| 246 | | - workflow_reason_summary=decision.reason_summary, |
| 247 | | - workflow_decision_kind=decision.decision_kind.value, |
| 248 | | - workflow_ambiguity_score=decision.ambiguity_score, |
| 249 | | - workflow_complexity_score=decision.complexity_score, |
| 250 | | - workflow_scheduled_next_mode=( |
| 251 | | - decision.scheduled_next_mode.value |
| 252 | | - if decision.scheduled_next_mode is not None |
| 253 | | - else None |
| 254 | | - ), |
| 255 | | - ) |
| 256 | | - dod.current_mode = mode.value |
| 257 | | - if not dod.mode_history or dod.mode_history[-1] != mode.value: |
| 258 | | - dod.mode_history.append(mode.value) |
| 259 | | - summary.workflow_mode = mode.value |
| 260 | | - summary.workflow_reason_code = decision.reason_code |
| 261 | | - summary.workflow_reason_summary = decision.reason_summary |
| 262 | | - summary.workflow_decision_kind = decision.decision_kind.value |
| 263 | | - self._append_workflow_timeline_from_decision( |
| 264 | | - decision, |
| 265 | | - kind={ |
| 266 | | - WorkflowDecisionKind.HANDOFF: WorkflowTimelineEntryKind.HANDOFF, |
| 267 | | - WorkflowDecisionKind.REENTRY: WorkflowTimelineEntryKind.REENTRY, |
| 268 | | - }.get(decision.decision_kind, WorkflowTimelineEntryKind.ROUTE), |
| 269 | | - summary=summary, |
| 270 | | - artifact_paths=[ |
| 271 | | - path |
| 272 | | - for path in ( |
| 273 | | - dod.clarify_brief, |
| 274 | | - dod.implementation_plan, |
| 275 | | - dod.verification_plan, |
| 276 | | - ) |
| 277 | | - if path |
| 278 | | - ], |
| 279 | | - ) |
| 280 | | - summary.definition_of_done = dod |
| 281 | | - self.dod_store.save(dod) |
| 282 | | - await emit( |
| 283 | | - AgentEvent( |
| 284 | | - type="workflow_mode", |
| 285 | | - content=f"Workflow: {mode.value} ({decision.reason_summary})", |
| 286 | | - workflow_mode=mode.value, |
| 287 | | - definition_of_done=dod, |
| 288 | | - ) |
| 289 | | - ) |
| 290 | | - |
| 291 | | - def _append_workflow_timeline_from_decision( |
| 292 | | - self, |
| 293 | | - decision: ModeDecision, |
| 294 | | - *, |
| 295 | | - kind: WorkflowTimelineEntryKind, |
| 296 | | - summary: TurnSummary | None = None, |
| 297 | | - artifact_paths: list[str] | None = None, |
| 298 | | - ) -> None: |
| 299 | | - entry = WorkflowTimelineEntry.from_decision( |
| 300 | | - decision, |
| 301 | | - kind=kind, |
| 302 | | - prompt_format=self.agent.prompt_format, |
| 303 | | - prompt_sections=self.agent.prompt_sections, |
| 304 | | - artifact_paths=artifact_paths, |
| 305 | | - ) |
| 306 | | - self.agent.session.append_workflow_timeline_entry(entry) |
| 307 | | - if summary is not None: |
| 308 | | - summary.workflow_timeline = list(self.agent.session.workflow_timeline) |
| 309 | | - |
| 310 | | - def _maybe_append_execute_bridge(self, dod: DefinitionOfDone) -> None: |
| 311 | | - bridge = build_execute_bridge( |
| 312 | | - Path(dod.clarify_brief) if dod.clarify_brief else None, |
| 313 | | - Path(dod.implementation_plan) if dod.implementation_plan else None, |
| 314 | | - Path(dod.verification_plan) if dod.verification_plan else None, |
| 315 | | - ) |
| 316 | | - if bridge and not any( |
| 317 | | - message.role == Role.USER and "[WORKFLOW BRIDGE]" in message.content |
| 318 | | - for message in self.agent.messages[-4:] |
| 319 | | - ): |
| 320 | | - self.agent.session.append( |
| 321 | | - Message( |
| 322 | | - role=Role.USER, |
| 323 | | - content=( |
| 324 | | - "[WORKFLOW BRIDGE]\n" |
| 325 | | - f"{bridge}\n\n" |
| 326 | | - "Honor these artifacts while you execute the task. " |
| 327 | | - "Keep TodoWrite current when the work spans multiple steps." |
| 328 | | - ), |
| 329 | | - ) |
| 330 | | - ) |
| 331 | | - |
| 332 | 231 | @staticmethod |
| 333 | 232 | def _emit_confirmation(emit: EventSink): |
| 334 | 233 | async def _emit(tool_name: str, message: str, details: str) -> None: |