tenseleyflow/loader / feb234f

Browse files

Extract turn prelude control from conversation runtime

Authored by espadonne
SHA
feb234f4adefbfa4bd5b866dea4193c7c8e51f14
Parents
eda0bc4
Tree
fcdf89f

2 changed files

StatusFile+-
M src/loader/runtime/conversation.py 13 38
A src/loader/runtime/turn_preamble.py 104 0
src/loader/runtime/conversation.pymodified
@@ -20,6 +20,7 @@ from .tool_batches import ToolBatchRunner
2020
 from .tracing import RuntimeTracer
2121
 from .turn_completion import TurnCompletionController
2222
 from .turn_iteration import TurnIterationAction, TurnIterationController
23
+from .turn_preamble import TurnPreludeController
2324
 from .turn_preparation import TurnPreparationController
2425
 from .workflow import (
2526
     ModeDecision,
@@ -105,6 +106,11 @@ class ConversationRuntime:
105106
             append_timeline=self._append_workflow_timeline_from_decision,
106107
             append_execute_bridge=self._maybe_append_execute_bridge,
107108
         )
109
+        self.turn_preamble = TurnPreludeController(
110
+            agent,
111
+            tracer=self.tracer,
112
+            workflow_recovery=self.workflow_recovery,
113
+        )
108114
 
109115
     async def run_turn(
110116
         self,
@@ -143,51 +149,20 @@ class ConversationRuntime:
143149
 
144150
         while iterations < self.agent.config.max_iterations:
145151
             iterations += 1
146
-            summary.iterations = iterations
147
-            self.tracer.record("turn.iteration_started", iteration=iterations)
148
-
149
-            if iterations == 1 and len(self.agent.messages) == 1:
150
-                task_lower = task.lower()
151
-                action_keywords = [
152
-                    "create",
153
-                    "write",
154
-                    "make",
155
-                    "run",
156
-                    "execute",
157
-                    "build",
158
-                    "install",
159
-                    "delete",
160
-                    "remove",
161
-                    "add",
162
-                    "edit",
163
-                    "modify",
164
-                    "update",
165
-                    "fix",
166
-                ]
167
-                if any(keyword in task_lower for keyword in action_keywords):
168
-                    self.agent.session.append(Message(role=Role.ASSISTANT, content="["))
169
-
170
-            steering_messages = self.agent._drain_steering_queue()
171
-            for steering_message in steering_messages:
172
-                await emit(AgentEvent(type="steering", content=steering_message))
173
-                self.agent.session.append(
174
-                    Message(
175
-                        role=Role.USER,
176
-                        content=f"[USER INTERRUPTION]: {steering_message}",
177
-                    )
178
-                )
179
-
180
-            if await self.workflow_recovery.maybe_refresh_plan_for_drift(
181
-                task=original_task or task,
152
+            assert self.executor is not None
153
+            prelude_decision = await self.turn_preamble.prepare_iteration(
154
+                task=task,
155
+                original_task=original_task,
156
+                iterations=iterations,
182157
                 dod=dod,
183158
                 emit=emit,
184159
                 summary=summary,
185160
                 on_user_question=on_user_question,
186161
                 executor=self.executor,
187
-            ):
162
+            )
163
+            if prelude_decision.should_continue:
188164
                 continue
189165
 
190
-            assert self.executor is not None
191166
             iteration_decision = await self.turn_iteration.run_iteration(
192167
                 task=task,
193168
                 effective_task=effective_task,
src/loader/runtime/turn_preamble.pyadded
@@ -0,0 +1,104 @@
1
+"""Per-iteration bookkeeping and prelude control for conversation turns."""
2
+
3
+from __future__ import annotations
4
+
5
+from collections.abc import Awaitable, Callable
6
+from dataclasses import dataclass
7
+
8
+from ..llm.base import Message, Role
9
+from .dod import DefinitionOfDone
10
+from .events import AgentEvent, TurnSummary
11
+from .tracing import RuntimeTracer
12
+from .workflow_recovery import WorkflowRecoveryController
13
+
14
+EventSink = Callable[[AgentEvent], Awaitable[None]]
15
+UserQuestionHandler = Callable[[str, list[str] | None], Awaitable[str]] | None
16
+
17
+_ACTION_KEYWORDS = (
18
+    "create",
19
+    "write",
20
+    "make",
21
+    "run",
22
+    "execute",
23
+    "build",
24
+    "install",
25
+    "delete",
26
+    "remove",
27
+    "add",
28
+    "edit",
29
+    "modify",
30
+    "update",
31
+    "fix",
32
+)
33
+
34
+
35
+@dataclass(slots=True)
36
+class TurnPreludeDecision:
37
+    """Outcome of per-iteration prelude handling."""
38
+
39
+    should_continue: bool = False
40
+
41
+
42
+class TurnPreludeController:
43
+    """Own iteration bookkeeping, steering drainage, and drift gating."""
44
+
45
+    def __init__(
46
+        self,
47
+        agent,
48
+        *,
49
+        tracer: RuntimeTracer,
50
+        workflow_recovery: WorkflowRecoveryController,
51
+    ) -> None:
52
+        self.agent = agent
53
+        self.tracer = tracer
54
+        self.workflow_recovery = workflow_recovery
55
+
56
+    async def prepare_iteration(
57
+        self,
58
+        *,
59
+        task: str,
60
+        original_task: str | None,
61
+        iterations: int,
62
+        dod: DefinitionOfDone,
63
+        emit: EventSink,
64
+        summary: TurnSummary,
65
+        on_user_question: UserQuestionHandler,
66
+        executor,
67
+    ) -> TurnPreludeDecision:
68
+        """Handle iteration bookkeeping before requesting the assistant turn."""
69
+
70
+        summary.iterations = iterations
71
+        self.tracer.record("turn.iteration_started", iteration=iterations)
72
+
73
+        if self._should_seed_action_bracket(task=task, iterations=iterations):
74
+            self.agent.session.append(Message(role=Role.ASSISTANT, content="["))
75
+
76
+        steering_messages = self.agent._drain_steering_queue()
77
+        for steering_message in steering_messages:
78
+            await emit(AgentEvent(type="steering", content=steering_message))
79
+            self.agent.session.append(
80
+                Message(
81
+                    role=Role.USER,
82
+                    content=f"[USER INTERRUPTION]: {steering_message}",
83
+                )
84
+            )
85
+
86
+        if await self.workflow_recovery.maybe_refresh_plan_for_drift(
87
+            task=original_task or task,
88
+            dod=dod,
89
+            emit=emit,
90
+            summary=summary,
91
+            on_user_question=on_user_question,
92
+            executor=executor,
93
+        ):
94
+            return TurnPreludeDecision(should_continue=True)
95
+
96
+        return TurnPreludeDecision()
97
+
98
+    def _should_seed_action_bracket(self, *, task: str, iterations: int) -> bool:
99
+        """Return whether to preserve the legacy action-seed hint."""
100
+
101
+        if iterations != 1 or len(self.agent.messages) != 1:
102
+            return False
103
+        task_lower = task.lower()
104
+        return any(keyword in task_lower for keyword in _ACTION_KEYWORDS)