Python · 4430 bytes Raw Blame History
1 """Tests for assistant-cycle iteration orchestration."""
2
3 from __future__ import annotations
4
5 from pathlib import Path
6
7 import pytest
8
9 from loader.agent.loop import Agent, AgentConfig
10 from loader.llm.base import CompletionResponse, ToolCall
11 from loader.runtime.conversation import ConversationRuntime
12 from loader.runtime.turn_iteration import TurnIterationAction
13 from tests.helpers.runtime_harness import ScriptedBackend
14
15
16 def non_streaming_config() -> AgentConfig:
17 """Shared config for direct turn-iteration tests."""
18
19 return AgentConfig(auto_context=False, stream=False, max_iterations=8)
20
21
22 async def _run_iteration(
23 runtime: ConversationRuntime,
24 *,
25 task: str,
26 requested_mode: str = "execute",
27 original_task: str | None = None,
28 ) -> tuple:
29 events = []
30
31 async def capture(event) -> None:
32 events.append(event)
33
34 prepared = await runtime.turn_preparation.prepare(
35 task=task,
36 emit=capture,
37 requested_mode=requested_mode,
38 original_task=original_task,
39 on_user_question=None,
40 )
41 decision = await runtime.turn_iteration.run_iteration(
42 task=prepared.task,
43 effective_task=prepared.effective_task,
44 original_task=original_task,
45 effective_max_tokens=prepared.effective_max_tokens,
46 iterations=1,
47 max_iterations=runtime.context.config.max_iterations,
48 actions_taken=[],
49 continuation_count=0,
50 empty_retry_count=0,
51 max_empty_retries=5,
52 extracted_iterations=0,
53 max_extracted_iterations=3,
54 consecutive_errors=0,
55 dod=prepared.definition_of_done,
56 emit=capture,
57 summary=prepared.summary,
58 executor=prepared.executor,
59 rollback_plan=prepared.rollback_plan,
60 on_confirmation=None,
61 on_user_question=None,
62 emit_confirmation=runtime._emit_confirmation(capture),
63 )
64 return prepared, decision, events
65
66
67 @pytest.mark.asyncio
68 async def test_turn_iteration_completes_react_final_answer(
69 temp_dir: Path,
70 ) -> None:
71 backend = ScriptedBackend(
72 completions=[
73 CompletionResponse(
74 content="Thought: I have enough context.\nFinal Answer: All set.",
75 )
76 ],
77 supports_native_tools=False,
78 )
79 agent = Agent(
80 backend=backend,
81 config=non_streaming_config(),
82 project_root=temp_dir,
83 )
84 runtime = ConversationRuntime(agent)
85
86 prepared, decision, events = await _run_iteration(
87 runtime,
88 task="Explain whether the turn iteration controller works.",
89 )
90
91 assert agent.use_react
92 assert decision.action == TurnIterationAction.COMPLETE
93 assert prepared.summary.final_response == "All set."
94 assert prepared.summary.assistant_messages[-1].content.endswith("Final Answer: All set.")
95 assert any(event.type == "response" and event.content == "All set." for event in events)
96
97
98 @pytest.mark.asyncio
99 async def test_turn_iteration_executes_native_tool_batch_and_continues(
100 temp_dir: Path,
101 ) -> None:
102 readme = temp_dir / "README.md"
103 readme.write_text("Loader runtime notes\n")
104
105 backend = ScriptedBackend(
106 completions=[
107 CompletionResponse(
108 content="I'll inspect the README first.",
109 tool_calls=[
110 ToolCall(
111 id="read-1",
112 name="read",
113 arguments={"file_path": str(readme)},
114 )
115 ],
116 )
117 ]
118 )
119 agent = Agent(
120 backend=backend,
121 config=non_streaming_config(),
122 project_root=temp_dir,
123 )
124 runtime = ConversationRuntime(agent)
125
126 prepared, decision, events = await _run_iteration(
127 runtime,
128 task="Inspect the README before continuing.",
129 )
130
131 assert decision.action == TurnIterationAction.CONTINUE
132 assert decision.new_actions_taken == [f"read: {{'file_path': '{readme}'}}"]
133 assert prepared.summary.assistant_messages[-1].tool_calls[0].name == "read"
134 assert len(prepared.summary.tool_result_messages) == 1
135 assert "Loader runtime notes" in prepared.summary.tool_result_messages[0].content
136 assert any(event.type == "tool_call" and event.tool_name == "read" for event in events)
137 assert any(
138 event.type == "tool_result" and "Loader runtime notes" in event.content
139 for event in events
140 )