tenseleyflow/loader / 8c70869

Browse files

Extract workflow state control from conversation runtime

Authored by espadonne
SHA
8c70869d5023f05ff197c128e7fb708712948ab0
Parents
5904a64
Tree
44d7253

2 changed files

StatusFile+-
M src/loader/runtime/conversation.py 13 114
A src/loader/runtime/workflow_state.py 141 0
src/loader/runtime/conversation.pymodified
@@ -3,14 +3,12 @@
33
 from __future__ import annotations
44
 
55
 from collections.abc import Awaitable, Callable
6
-from pathlib import Path
76
 from typing import Any
87
 
9
-from ..llm.base import Message, Role
108
 from .artifact_invalidation import ArtifactInvalidationAssessor
119
 from .assistant_turns import AssistantTurnRequester
1210
 from .completion_policy import CompletionPolicy
13
-from .dod import DefinitionOfDone, DefinitionOfDoneStore
11
+from .dod import DefinitionOfDoneStore
1412
 from .events import AgentEvent, TurnSummary
1513
 from .executor import ToolExecutor
1614
 from .finalization import TurnFinalizer
@@ -23,17 +21,13 @@ from .turn_iteration import TurnIterationAction, TurnIterationController
2321
 from .turn_preamble import TurnPreludeController
2422
 from .turn_preparation import TurnPreparationController
2523
 from .workflow import (
26
-    ModeDecision,
2724
     WorkflowArtifactStore,
28
-    WorkflowDecisionKind,
2925
     WorkflowPolicy,
3026
     WorkflowSignalExtractor,
31
-    WorkflowTimelineEntry,
32
-    WorkflowTimelineEntryKind,
33
-    build_execute_bridge,
3427
 )
3528
 from .workflow_lanes import WorkflowLaneRunner
3629
 from .workflow_recovery import WorkflowRecoveryController
30
+from .workflow_state import WorkflowStateController
3731
 
3832
 EventSink = Callable[[AgentEvent], Awaitable[None]]
3933
 ConfirmationHandler = Callable[[str, str, str], Awaitable[bool]] | None
@@ -52,6 +46,10 @@ class ConversationRuntime:
5246
         self.workflow_policy = WorkflowPolicy(self.workflow_signals)
5347
         self.artifact_invalidation = ArtifactInvalidationAssessor()
5448
         self.artifact_store = WorkflowArtifactStore(agent.project_root)
49
+        self.workflow_state = WorkflowStateController(
50
+            agent,
51
+            dod_store=self.dod_store,
52
+        )
5553
         self.workflow_lanes = WorkflowLaneRunner(
5654
             agent,
5755
             artifact_store=self.artifact_store,
@@ -64,9 +62,9 @@ class ConversationRuntime:
6462
             workflow_policy=self.workflow_policy,
6563
             workflow_signals=self.workflow_signals,
6664
             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,
7068
         )
7169
         self.repairer = ResponseRepairer(agent)
7270
         self.completion_policy = CompletionPolicy(agent)
@@ -75,7 +73,7 @@ class ConversationRuntime:
7573
             agent,
7674
             self.tracer,
7775
             self.dod_store,
78
-            self._set_workflow_mode,
76
+            self.workflow_state.set_workflow_mode,
7977
         )
8078
         self.turn_completion = TurnCompletionController(
8179
             agent,
@@ -102,9 +100,9 @@ class ConversationRuntime:
102100
             workflow_signals=self.workflow_signals,
103101
             workflow_lanes=self.workflow_lanes,
104102
             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,
108106
         )
109107
         self.turn_preamble = TurnPreludeController(
110108
             agent,
@@ -230,105 +228,6 @@ class ConversationRuntime:
230228
         self.phase_tracker.clear()
231229
         return final_summary
232230
 
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
-
332231
     @staticmethod
333232
     def _emit_confirmation(emit: EventSink):
334233
         async def _emit(tool_name: str, message: str, details: str) -> None:
src/loader/runtime/workflow_state.pyadded
@@ -0,0 +1,141 @@
1
+"""Workflow-state coordination for conversation runtime turns."""
2
+
3
+from __future__ import annotations
4
+
5
+from collections.abc import Awaitable, Callable
6
+from pathlib import Path
7
+
8
+from ..llm.base import Message, Role
9
+from .dod import DefinitionOfDone, DefinitionOfDoneStore
10
+from .events import AgentEvent, TurnSummary
11
+from .workflow import (
12
+    ModeDecision,
13
+    WorkflowDecisionKind,
14
+    WorkflowTimelineEntry,
15
+    WorkflowTimelineEntryKind,
16
+    build_execute_bridge,
17
+)
18
+
19
+EventSink = Callable[[AgentEvent], Awaitable[None]]
20
+
21
+
22
+class WorkflowStateController:
23
+    """Own workflow-mode state, timeline persistence, and execute-bridge prompts."""
24
+
25
+    def __init__(
26
+        self,
27
+        agent,
28
+        *,
29
+        dod_store: DefinitionOfDoneStore,
30
+    ) -> None:
31
+        self.agent = agent
32
+        self.dod_store = dod_store
33
+
34
+    async def set_workflow_mode(
35
+        self,
36
+        decision: ModeDecision,
37
+        *,
38
+        dod: DefinitionOfDone,
39
+        emit: EventSink,
40
+        summary: TurnSummary,
41
+    ) -> None:
42
+        """Apply one workflow-mode decision across session, DoD, and summary state."""
43
+
44
+        mode = decision.mode
45
+        self.agent.set_workflow_mode(mode.value)
46
+        self.agent.session.update_runtime_state(
47
+            workflow_mode=mode.value,
48
+            workflow_reason_code=decision.reason_code,
49
+            workflow_reason_summary=decision.reason_summary,
50
+            workflow_decision_kind=decision.decision_kind.value,
51
+            workflow_ambiguity_score=decision.ambiguity_score,
52
+            workflow_complexity_score=decision.complexity_score,
53
+            workflow_scheduled_next_mode=(
54
+                decision.scheduled_next_mode.value
55
+                if decision.scheduled_next_mode is not None
56
+                else None
57
+            ),
58
+        )
59
+        dod.current_mode = mode.value
60
+        if not dod.mode_history or dod.mode_history[-1] != mode.value:
61
+            dod.mode_history.append(mode.value)
62
+        summary.workflow_mode = mode.value
63
+        summary.workflow_reason_code = decision.reason_code
64
+        summary.workflow_reason_summary = decision.reason_summary
65
+        summary.workflow_decision_kind = decision.decision_kind.value
66
+        self.append_timeline_from_decision(
67
+            decision,
68
+            kind={
69
+                WorkflowDecisionKind.HANDOFF: WorkflowTimelineEntryKind.HANDOFF,
70
+                WorkflowDecisionKind.REENTRY: WorkflowTimelineEntryKind.REENTRY,
71
+            }.get(decision.decision_kind, WorkflowTimelineEntryKind.ROUTE),
72
+            summary=summary,
73
+            artifact_paths=self._artifact_paths_for(dod),
74
+        )
75
+        summary.definition_of_done = dod
76
+        self.dod_store.save(dod)
77
+        await emit(
78
+            AgentEvent(
79
+                type="workflow_mode",
80
+                content=f"Workflow: {mode.value} ({decision.reason_summary})",
81
+                workflow_mode=mode.value,
82
+                definition_of_done=dod,
83
+            )
84
+        )
85
+
86
+    def append_timeline_from_decision(
87
+        self,
88
+        decision: ModeDecision,
89
+        *,
90
+        kind: WorkflowTimelineEntryKind,
91
+        summary: TurnSummary | None = None,
92
+        artifact_paths: list[str] | None = None,
93
+    ) -> None:
94
+        """Persist one workflow timeline entry derived from a routing decision."""
95
+
96
+        entry = WorkflowTimelineEntry.from_decision(
97
+            decision,
98
+            kind=kind,
99
+            prompt_format=self.agent.prompt_format,
100
+            prompt_sections=self.agent.prompt_sections,
101
+            artifact_paths=artifact_paths,
102
+        )
103
+        self.agent.session.append_workflow_timeline_entry(entry)
104
+        if summary is not None:
105
+            summary.workflow_timeline = list(self.agent.session.workflow_timeline)
106
+
107
+    def maybe_append_execute_bridge(self, dod: DefinitionOfDone) -> None:
108
+        """Append one workflow bridge prompt before execute mode, if needed."""
109
+
110
+        bridge = build_execute_bridge(
111
+            Path(dod.clarify_brief) if dod.clarify_brief else None,
112
+            Path(dod.implementation_plan) if dod.implementation_plan else None,
113
+            Path(dod.verification_plan) if dod.verification_plan else None,
114
+        )
115
+        if bridge and not any(
116
+            message.role == Role.USER and "[WORKFLOW BRIDGE]" in message.content
117
+            for message in self.agent.session.messages[-4:]
118
+        ):
119
+            self.agent.session.append(
120
+                Message(
121
+                    role=Role.USER,
122
+                    content=(
123
+                        "[WORKFLOW BRIDGE]\n"
124
+                        f"{bridge}\n\n"
125
+                        "Honor these artifacts while you execute the task. "
126
+                        "Keep TodoWrite current when the work spans multiple steps."
127
+                    ),
128
+                )
129
+            )
130
+
131
+    @staticmethod
132
+    def _artifact_paths_for(dod: DefinitionOfDone) -> list[str]:
133
+        return [
134
+            path
135
+            for path in (
136
+                dod.clarify_brief,
137
+                dod.implementation_plan,
138
+                dod.verification_plan,
139
+            )
140
+            if path
141
+        ]