tenseleyflow/loader / cdb33c6

Browse files

Extract assistant-cycle controller from conversation runtime

Authored by espadonne
SHA
cdb33c64288b9c178d4674498d2362b020ea77f0
Parents
7cee985
Tree
71be361

2 changed files

StatusFile+-
M src/loader/runtime/conversation.py 32 149
A src/loader/runtime/turn_iteration.py 366 0
src/loader/runtime/conversation.pymodified
@@ -13,12 +13,13 @@ from .completion_policy import CompletionPolicy
1313
 from .dod import DefinitionOfDone, DefinitionOfDoneStore
1414
 from .events import AgentEvent, TurnSummary
1515
 from .executor import ToolExecutor
16
-from .finalization import TurnFinalizer, merge_usage
16
+from .finalization import TurnFinalizer
1717
 from .phases import TurnPhase, TurnPhaseTracker, TurnTransitionKind
1818
 from .repair import ResponseRepairer
1919
 from .tool_batches import ToolBatchRunner
2020
 from .tracing import RuntimeTracer
21
-from .turn_completion import TurnCompletionAction, TurnCompletionController
21
+from .turn_completion import TurnCompletionController
22
+from .turn_iteration import TurnIterationAction, TurnIterationController
2223
 from .turn_preparation import TurnPreparationController
2324
 from .workflow import (
2425
     ModeDecision,
@@ -50,8 +51,6 @@ class ConversationRuntime:
5051
         self.workflow_policy = WorkflowPolicy(self.workflow_signals)
5152
         self.artifact_invalidation = ArtifactInvalidationAssessor()
5253
         self.artifact_store = WorkflowArtifactStore(agent.project_root)
53
-        self.turn_requester = AssistantTurnRequester(agent, self.tracer)
54
-        self.tool_batches = ToolBatchRunner(agent, self.dod_store)
5554
         self.workflow_lanes = WorkflowLaneRunner(
5655
             agent,
5756
             artifact_store=self.artifact_store,
@@ -84,6 +83,15 @@ class ConversationRuntime:
8483
             finalizer=self.finalizer,
8584
             phase_tracker=self.phase_tracker,
8685
         )
86
+        self.turn_iteration = TurnIterationController(
87
+            agent,
88
+            tracer=self.tracer,
89
+            phase_tracker=self.phase_tracker,
90
+            turn_requester=AssistantTurnRequester(agent, self.tracer),
91
+            repairer=self.repairer,
92
+            tool_batches=ToolBatchRunner(agent, self.dod_store),
93
+            turn_completion=self.turn_completion,
94
+        )
8795
         self.turn_preparation = TurnPreparationController(
8896
             agent,
8997
             tracer=self.tracer,
@@ -110,7 +118,6 @@ class ConversationRuntime:
110118
         """Run one task turn and return a structured summary."""
111119
 
112120
         iterations = 0
113
-        final_response = ""
114121
         actions_taken: list[str] = []
115122
         continuation_count = 0
116123
         empty_retry_count = 0
@@ -180,168 +187,44 @@ class ConversationRuntime:
180187
             ):
181188
                 continue
182189
 
183
-            await self.phase_tracker.enter(
184
-                TurnPhase.ASSISTANT,
185
-                emit,
186
-                detail="Requesting assistant response",
187
-                reason_code="request_assistant_response",
188
-            )
189
-            await emit(AgentEvent(type="thinking"))
190
-            assistant_turn = await self.turn_requester.request_turn(
191
-                emit=emit,
192
-                max_tokens=effective_max_tokens,
193
-            )
194
-            merge_usage(summary.usage, assistant_turn.usage)
195
-
196
-            content = assistant_turn.content
197
-            response_content = assistant_turn.response_content
198
-            tool_calls = list(assistant_turn.tool_calls)
199
-            pending_tool_calls_seen = set(assistant_turn.pending_tool_calls_seen)
200
-
201
-            if not content.strip():
202
-                await self.phase_tracker.enter(
203
-                    TurnPhase.REPAIR,
204
-                    emit,
205
-                    detail="Repairing empty assistant response",
206
-                    reason_code="repair_empty_response",
207
-                    kind=TurnTransitionKind.RETRY,
208
-                )
209
-                empty_retry_count += 1
210
-                empty_decision = self.repairer.handle_empty_response(
211
-                    task=task,
212
-                    original_task=original_task,
213
-                    empty_retry_count=empty_retry_count,
214
-                    max_empty_retries=max_empty_retries,
215
-                )
216
-                if empty_decision.should_continue and empty_decision.retry_prompt:
217
-                    self.agent.session.append(
218
-                        Message(
219
-                            role=Role.ASSISTANT,
220
-                            content=empty_decision.retry_prompt,
221
-                        )
222
-                    )
223
-                    continue
224
-
225
-                final_response = empty_decision.final_response or ""
226
-                summary.final_response = final_response
227
-                if empty_decision.failure:
228
-                    summary.failures.append(empty_decision.failure)
229
-                await emit(AgentEvent(type="response", content=final_response))
230
-                break
231
-
232
-            analysis = self.repairer.analyze_response(
233
-                content=content,
234
-                response_content=response_content,
235
-                tool_calls=tool_calls,
236
-                extracted_iterations=extracted_iterations,
237
-                max_extracted_iterations=max_extracted_iterations,
238
-            )
239
-            content = analysis.content
240
-            tool_calls = list(analysis.tool_calls)
241
-            tool_source = analysis.tool_source
242
-            extracted_iterations = analysis.extracted_iterations
243
-            if analysis.clear_stream:
244
-                await self.phase_tracker.enter(
245
-                    TurnPhase.REPAIR,
246
-                    emit,
247
-                    detail="Repairing raw-text tool fallback",
248
-                    reason_code="repair_raw_text_tool_fallback",
249
-                    kind=TurnTransitionKind.REROUTE,
250
-                )
251
-                await emit(AgentEvent(type="clear_stream"))
252
-
253
-            if analysis.is_final_answer:
254
-                assistant_message = Message(role=Role.ASSISTANT, content=response_content)
255
-                self.agent.session.append(assistant_message)
256
-                summary.assistant_messages.append(assistant_message)
257
-                final_response = analysis.final_response or content
258
-                summary.final_response = final_response
259
-                self.tracer.record("turn.completed", reason="final_answer")
260
-                await emit(AgentEvent(type="response", content=final_response))
261
-                break
262
-
263
-            if tool_calls:
264
-                if analysis.should_stop:
265
-                    assistant_message = Message(role=Role.ASSISTANT, content=response_content)
266
-                    self.agent.session.append(assistant_message)
267
-                    summary.assistant_messages.append(assistant_message)
268
-                    final_response = analysis.final_response or content
269
-                    summary.final_response = final_response
270
-                    if analysis.failure:
271
-                        summary.failures.append(analysis.failure)
272
-                    await emit(AgentEvent(type="response", content=final_response))
273
-                    break
274
-
275
-                await self.phase_tracker.enter(
276
-                    TurnPhase.TOOLS,
277
-                    emit,
278
-                    detail="Executing tool batch",
279
-                    reason_code="execute_tool_batch",
280
-                )
281
-                assistant_message = Message(
282
-                    role=Role.ASSISTANT,
283
-                    content=response_content,
284
-                    tool_calls=tool_calls,
285
-                )
286
-                self.agent.session.append(assistant_message)
287
-                summary.assistant_messages.append(assistant_message)
288
-                self.tracer.record(
289
-                    "assistant.tool_batch",
290
-                    tool_count=len(tool_calls),
291
-                    source=tool_source,
292
-                )
293
-
294
-                batch_result = await self.tool_batches.execute_batch(
295
-                    tool_calls=tool_calls,
296
-                    tool_source=tool_source,
297
-                    pending_tool_calls_seen=pending_tool_calls_seen,
298
-                    emit=emit,
299
-                    summary=summary,
300
-                    dod=dod,
301
-                    executor=self.executor,
302
-                    on_confirmation=on_confirmation,
303
-                    on_user_question=on_user_question,
304
-                    emit_confirmation=self._emit_confirmation(emit),
305
-                    consecutive_errors=consecutive_errors,
306
-                )
307
-                actions_taken.extend(batch_result.actions_taken)
308
-                consecutive_errors = batch_result.consecutive_errors
309
-                if batch_result.halted:
310
-                    return await self._finalize_turn(
311
-                        summary,
312
-                        emit,
313
-                        reason_code="tool_batch_halted",
314
-                        reason_summary="Finalizing after halted tool batch",
315
-                    )
316
-
317
-                continue
318
-
319190
             assert self.executor is not None
320
-            completion_decision = await self.turn_completion.handle_text_response(
321
-                content=content,
322
-                response_content=response_content,
191
+            iteration_decision = await self.turn_iteration.run_iteration(
323192
                 task=task,
324193
                 effective_task=effective_task,
194
+                original_task=original_task,
195
+                effective_max_tokens=effective_max_tokens,
325196
                 iterations=iterations,
326197
                 max_iterations=self.agent.config.max_iterations,
327198
                 actions_taken=actions_taken,
328199
                 continuation_count=continuation_count,
200
+                empty_retry_count=empty_retry_count,
201
+                max_empty_retries=max_empty_retries,
202
+                extracted_iterations=extracted_iterations,
203
+                max_extracted_iterations=max_extracted_iterations,
204
+                consecutive_errors=consecutive_errors,
329205
                 dod=dod,
330206
                 emit=emit,
331207
                 summary=summary,
332208
                 executor=self.executor,
333209
                 rollback_plan=rollback_plan,
210
+                on_confirmation=on_confirmation,
211
+                on_user_question=on_user_question,
212
+                emit_confirmation=self._emit_confirmation(emit),
334213
             )
335
-            continuation_count = completion_decision.continuation_count
336
-            if completion_decision.action == TurnCompletionAction.CONTINUE:
214
+            continuation_count = iteration_decision.continuation_count
215
+            empty_retry_count = iteration_decision.empty_retry_count
216
+            extracted_iterations = iteration_decision.extracted_iterations
217
+            consecutive_errors = iteration_decision.consecutive_errors
218
+            actions_taken.extend(iteration_decision.new_actions_taken)
219
+            if iteration_decision.action == TurnIterationAction.CONTINUE:
337220
                 continue
338
-            if completion_decision.action == TurnCompletionAction.FINALIZE:
221
+            if iteration_decision.action == TurnIterationAction.FINALIZE:
339222
                 return await self._finalize_turn(
340223
                     summary,
341224
                     emit,
342
-                    reason_code=completion_decision.finalize_reason_code
225
+                    reason_code=iteration_decision.finalize_reason_code
343226
                     or "turn_complete",
344
-                    reason_summary=completion_decision.finalize_reason_summary
227
+                    reason_summary=iteration_decision.finalize_reason_summary
345228
                     or "Finalizing completed turn",
346229
                 )
347230
             break
src/loader/runtime/turn_iteration.pyadded
@@ -0,0 +1,366 @@
1
+"""Assistant-cycle orchestration for one conversation-loop iteration."""
2
+
3
+from __future__ import annotations
4
+
5
+from collections.abc import Awaitable, Callable
6
+from dataclasses import dataclass, field
7
+from enum import StrEnum
8
+
9
+from ..agent.reasoning import RollbackPlan
10
+from ..llm.base import Message, Role, ToolCall
11
+from .assistant_turns import AssistantTurnRequester
12
+from .dod import DefinitionOfDone
13
+from .events import AgentEvent, TurnSummary
14
+from .executor import ToolExecutor
15
+from .finalization import merge_usage
16
+from .phases import TurnPhase, TurnPhaseTracker, TurnTransitionKind
17
+from .repair import ResponseRepairer
18
+from .tool_batches import ToolBatchRunner
19
+from .tracing import RuntimeTracer
20
+from .turn_completion import TurnCompletionAction, TurnCompletionController
21
+
22
+EventSink = Callable[[AgentEvent], Awaitable[None]]
23
+ConfirmationHandler = Callable[[str, str, str], Awaitable[bool]] | None
24
+UserQuestionHandler = Callable[[str, list[str] | None], Awaitable[str]] | None
25
+
26
+
27
+class TurnIterationAction(StrEnum):
28
+    """What the conversation loop should do after one assistant-cycle."""
29
+
30
+    CONTINUE = "continue"
31
+    COMPLETE = "complete"
32
+    FINALIZE = "finalize"
33
+
34
+
35
+@dataclass(slots=True)
36
+class TurnIterationDecision:
37
+    """Structured outcome of one assistant-cycle iteration."""
38
+
39
+    action: TurnIterationAction
40
+    continuation_count: int
41
+    empty_retry_count: int
42
+    extracted_iterations: int
43
+    consecutive_errors: int
44
+    new_actions_taken: list[str] = field(default_factory=list)
45
+    finalize_reason_code: str | None = None
46
+    finalize_reason_summary: str | None = None
47
+
48
+
49
+class TurnIterationController:
50
+    """Own assistant response handling, routing, and tool-batch continuation."""
51
+
52
+    def __init__(
53
+        self,
54
+        agent,
55
+        *,
56
+        tracer: RuntimeTracer,
57
+        phase_tracker: TurnPhaseTracker,
58
+        turn_requester: AssistantTurnRequester,
59
+        repairer: ResponseRepairer,
60
+        tool_batches: ToolBatchRunner,
61
+        turn_completion: TurnCompletionController,
62
+    ) -> None:
63
+        self.agent = agent
64
+        self.tracer = tracer
65
+        self.phase_tracker = phase_tracker
66
+        self.turn_requester = turn_requester
67
+        self.repairer = repairer
68
+        self.tool_batches = tool_batches
69
+        self.turn_completion = turn_completion
70
+
71
+    async def run_iteration(
72
+        self,
73
+        *,
74
+        task: str,
75
+        effective_task: str,
76
+        original_task: str | None,
77
+        effective_max_tokens: int,
78
+        iterations: int,
79
+        max_iterations: int,
80
+        actions_taken: list[str],
81
+        continuation_count: int,
82
+        empty_retry_count: int,
83
+        max_empty_retries: int,
84
+        extracted_iterations: int,
85
+        max_extracted_iterations: int,
86
+        consecutive_errors: int,
87
+        dod: DefinitionOfDone,
88
+        emit: EventSink,
89
+        summary: TurnSummary,
90
+        executor: ToolExecutor,
91
+        rollback_plan: RollbackPlan | None,
92
+        on_confirmation: ConfirmationHandler,
93
+        on_user_question: UserQuestionHandler,
94
+        emit_confirmation,
95
+    ) -> TurnIterationDecision:
96
+        """Run one assistant-cycle and return the next loop decision."""
97
+
98
+        await self.phase_tracker.enter(
99
+            TurnPhase.ASSISTANT,
100
+            emit,
101
+            detail="Requesting assistant response",
102
+            reason_code="request_assistant_response",
103
+        )
104
+        await emit(AgentEvent(type="thinking"))
105
+        assistant_turn = await self.turn_requester.request_turn(
106
+            emit=emit,
107
+            max_tokens=effective_max_tokens,
108
+        )
109
+        merge_usage(summary.usage, assistant_turn.usage)
110
+
111
+        content = assistant_turn.content
112
+        response_content = assistant_turn.response_content
113
+        tool_calls = list(assistant_turn.tool_calls)
114
+        pending_tool_calls_seen = set(assistant_turn.pending_tool_calls_seen)
115
+
116
+        if not content.strip():
117
+            return await self._handle_empty_response(
118
+                task=task,
119
+                original_task=original_task,
120
+                empty_retry_count=empty_retry_count,
121
+                max_empty_retries=max_empty_retries,
122
+                extracted_iterations=extracted_iterations,
123
+                continuation_count=continuation_count,
124
+                consecutive_errors=consecutive_errors,
125
+                emit=emit,
126
+                summary=summary,
127
+            )
128
+
129
+        analysis = self.repairer.analyze_response(
130
+            content=content,
131
+            response_content=response_content,
132
+            tool_calls=tool_calls,
133
+            extracted_iterations=extracted_iterations,
134
+            max_extracted_iterations=max_extracted_iterations,
135
+        )
136
+        content = analysis.content
137
+        tool_calls = list(analysis.tool_calls)
138
+        tool_source = analysis.tool_source
139
+        extracted_iterations = analysis.extracted_iterations
140
+        if analysis.clear_stream:
141
+            await self.phase_tracker.enter(
142
+                TurnPhase.REPAIR,
143
+                emit,
144
+                detail="Repairing raw-text tool fallback",
145
+                reason_code="repair_raw_text_tool_fallback",
146
+                kind=TurnTransitionKind.REROUTE,
147
+            )
148
+            await emit(AgentEvent(type="clear_stream"))
149
+
150
+        if analysis.is_final_answer:
151
+            assistant_message = Message(role=Role.ASSISTANT, content=response_content)
152
+            self.agent.session.append(assistant_message)
153
+            summary.assistant_messages.append(assistant_message)
154
+            final_response = analysis.final_response or content
155
+            summary.final_response = final_response
156
+            self.tracer.record("turn.completed", reason="final_answer")
157
+            await emit(AgentEvent(type="response", content=final_response))
158
+            return TurnIterationDecision(
159
+                action=TurnIterationAction.COMPLETE,
160
+                continuation_count=continuation_count,
161
+                empty_retry_count=empty_retry_count,
162
+                extracted_iterations=extracted_iterations,
163
+                consecutive_errors=consecutive_errors,
164
+            )
165
+
166
+        if tool_calls:
167
+            if analysis.should_stop:
168
+                assistant_message = Message(role=Role.ASSISTANT, content=response_content)
169
+                self.agent.session.append(assistant_message)
170
+                summary.assistant_messages.append(assistant_message)
171
+                final_response = analysis.final_response or content
172
+                summary.final_response = final_response
173
+                if analysis.failure:
174
+                    summary.failures.append(analysis.failure)
175
+                await emit(AgentEvent(type="response", content=final_response))
176
+                return TurnIterationDecision(
177
+                    action=TurnIterationAction.COMPLETE,
178
+                    continuation_count=continuation_count,
179
+                    empty_retry_count=empty_retry_count,
180
+                    extracted_iterations=extracted_iterations,
181
+                    consecutive_errors=consecutive_errors,
182
+                )
183
+            return await self._handle_tool_batch(
184
+                response_content=response_content,
185
+                tool_calls=tool_calls,
186
+                tool_source=tool_source,
187
+                pending_tool_calls_seen=pending_tool_calls_seen,
188
+                extracted_iterations=extracted_iterations,
189
+                continuation_count=continuation_count,
190
+                empty_retry_count=empty_retry_count,
191
+                consecutive_errors=consecutive_errors,
192
+                dod=dod,
193
+                emit=emit,
194
+                summary=summary,
195
+                executor=executor,
196
+                on_confirmation=on_confirmation,
197
+                on_user_question=on_user_question,
198
+                emit_confirmation=emit_confirmation,
199
+            )
200
+
201
+        completion_decision = await self.turn_completion.handle_text_response(
202
+            content=content,
203
+            response_content=response_content,
204
+            task=task,
205
+            effective_task=effective_task,
206
+            iterations=iterations,
207
+            max_iterations=max_iterations,
208
+            actions_taken=actions_taken,
209
+            continuation_count=continuation_count,
210
+            dod=dod,
211
+            emit=emit,
212
+            summary=summary,
213
+            executor=executor,
214
+            rollback_plan=rollback_plan,
215
+        )
216
+        if completion_decision.action == TurnCompletionAction.CONTINUE:
217
+            return TurnIterationDecision(
218
+                action=TurnIterationAction.CONTINUE,
219
+                continuation_count=completion_decision.continuation_count,
220
+                empty_retry_count=empty_retry_count,
221
+                extracted_iterations=extracted_iterations,
222
+                consecutive_errors=consecutive_errors,
223
+            )
224
+        if completion_decision.action == TurnCompletionAction.FINALIZE:
225
+            return TurnIterationDecision(
226
+                action=TurnIterationAction.FINALIZE,
227
+                continuation_count=completion_decision.continuation_count,
228
+                empty_retry_count=empty_retry_count,
229
+                extracted_iterations=extracted_iterations,
230
+                consecutive_errors=consecutive_errors,
231
+                finalize_reason_code=completion_decision.finalize_reason_code,
232
+                finalize_reason_summary=completion_decision.finalize_reason_summary,
233
+            )
234
+        return TurnIterationDecision(
235
+            action=TurnIterationAction.COMPLETE,
236
+            continuation_count=completion_decision.continuation_count,
237
+            empty_retry_count=empty_retry_count,
238
+            extracted_iterations=extracted_iterations,
239
+            consecutive_errors=consecutive_errors,
240
+        )
241
+
242
+    async def _handle_empty_response(
243
+        self,
244
+        *,
245
+        task: str,
246
+        original_task: str | None,
247
+        empty_retry_count: int,
248
+        max_empty_retries: int,
249
+        extracted_iterations: int,
250
+        continuation_count: int,
251
+        consecutive_errors: int,
252
+        emit: EventSink,
253
+        summary: TurnSummary,
254
+    ) -> TurnIterationDecision:
255
+        await self.phase_tracker.enter(
256
+            TurnPhase.REPAIR,
257
+            emit,
258
+            detail="Repairing empty assistant response",
259
+            reason_code="repair_empty_response",
260
+            kind=TurnTransitionKind.RETRY,
261
+        )
262
+        next_empty_retry_count = empty_retry_count + 1
263
+        empty_decision = self.repairer.handle_empty_response(
264
+            task=task,
265
+            original_task=original_task,
266
+            empty_retry_count=next_empty_retry_count,
267
+            max_empty_retries=max_empty_retries,
268
+        )
269
+        if empty_decision.should_continue and empty_decision.retry_prompt:
270
+            self.agent.session.append(
271
+                Message(
272
+                    role=Role.ASSISTANT,
273
+                    content=empty_decision.retry_prompt,
274
+                )
275
+            )
276
+            return TurnIterationDecision(
277
+                action=TurnIterationAction.CONTINUE,
278
+                continuation_count=continuation_count,
279
+                empty_retry_count=next_empty_retry_count,
280
+                extracted_iterations=extracted_iterations,
281
+                consecutive_errors=consecutive_errors,
282
+            )
283
+
284
+        final_response = empty_decision.final_response or ""
285
+        summary.final_response = final_response
286
+        if empty_decision.failure:
287
+            summary.failures.append(empty_decision.failure)
288
+        await emit(AgentEvent(type="response", content=final_response))
289
+        return TurnIterationDecision(
290
+            action=TurnIterationAction.COMPLETE,
291
+            continuation_count=continuation_count,
292
+            empty_retry_count=next_empty_retry_count,
293
+            extracted_iterations=extracted_iterations,
294
+            consecutive_errors=consecutive_errors,
295
+        )
296
+
297
+    async def _handle_tool_batch(
298
+        self,
299
+        *,
300
+        response_content: str,
301
+        tool_calls: list[ToolCall],
302
+        tool_source: str,
303
+        pending_tool_calls_seen: set[str],
304
+        extracted_iterations: int,
305
+        continuation_count: int,
306
+        empty_retry_count: int,
307
+        consecutive_errors: int,
308
+        dod: DefinitionOfDone,
309
+        emit: EventSink,
310
+        summary: TurnSummary,
311
+        executor: ToolExecutor,
312
+        on_confirmation: ConfirmationHandler,
313
+        on_user_question: UserQuestionHandler,
314
+        emit_confirmation,
315
+    ) -> TurnIterationDecision:
316
+        await self.phase_tracker.enter(
317
+            TurnPhase.TOOLS,
318
+            emit,
319
+            detail="Executing tool batch",
320
+            reason_code="execute_tool_batch",
321
+        )
322
+        assistant_message = Message(
323
+            role=Role.ASSISTANT,
324
+            content=response_content,
325
+            tool_calls=tool_calls,
326
+        )
327
+        self.agent.session.append(assistant_message)
328
+        summary.assistant_messages.append(assistant_message)
329
+        self.tracer.record(
330
+            "assistant.tool_batch",
331
+            tool_count=len(tool_calls),
332
+            source=tool_source,
333
+        )
334
+
335
+        batch_result = await self.tool_batches.execute_batch(
336
+            tool_calls=tool_calls,
337
+            tool_source=tool_source,
338
+            pending_tool_calls_seen=pending_tool_calls_seen,
339
+            emit=emit,
340
+            summary=summary,
341
+            dod=dod,
342
+            executor=executor,
343
+            on_confirmation=on_confirmation,
344
+            on_user_question=on_user_question,
345
+            emit_confirmation=emit_confirmation,
346
+            consecutive_errors=consecutive_errors,
347
+        )
348
+        if batch_result.halted:
349
+            return TurnIterationDecision(
350
+                action=TurnIterationAction.FINALIZE,
351
+                continuation_count=continuation_count,
352
+                empty_retry_count=empty_retry_count,
353
+                extracted_iterations=extracted_iterations,
354
+                consecutive_errors=batch_result.consecutive_errors,
355
+                new_actions_taken=batch_result.actions_taken,
356
+                finalize_reason_code="tool_batch_halted",
357
+                finalize_reason_summary="Finalizing after halted tool batch",
358
+            )
359
+        return TurnIterationDecision(
360
+            action=TurnIterationAction.CONTINUE,
361
+            continuation_count=continuation_count,
362
+            empty_retry_count=empty_retry_count,
363
+            extracted_iterations=extracted_iterations,
364
+            consecutive_errors=batch_result.consecutive_errors,
365
+            new_actions_taken=batch_result.actions_taken,
366
+        )