| 1 | """Tests for the public runtime launcher seam.""" |
| 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, ReasoningConfig |
| 10 | from loader.llm.base import CompletionResponse, StreamChunk |
| 11 | from loader.runtime.bootstrap import RuntimeBootstrapView |
| 12 | from loader.runtime.launcher import RuntimeLauncher, build_runtime_launcher |
| 13 | from loader.runtime.public_shell import get_runtime_shell_system_message |
| 14 | from loader.runtime.runtime_handle import RuntimeHandle |
| 15 | from loader.utils.todos import active_todo_store_path |
| 16 | from tests.helpers.runtime_harness import ScriptedBackend |
| 17 | |
| 18 | |
| 19 | def test_build_runtime_launcher_returns_launcher_for_agent_source( |
| 20 | temp_dir: Path, |
| 21 | ) -> None: |
| 22 | agent = Agent( |
| 23 | backend=ScriptedBackend(), |
| 24 | config=AgentConfig(auto_context=False, stream=False), |
| 25 | project_root=temp_dir, |
| 26 | ) |
| 27 | |
| 28 | launcher = build_runtime_launcher(agent) |
| 29 | |
| 30 | assert isinstance(launcher, RuntimeLauncher) |
| 31 | assert isinstance(launcher.source, RuntimeBootstrapView) |
| 32 | assert launcher.source is not agent |
| 33 | assert launcher.source.metadata == { |
| 34 | "owner_type": "Agent", |
| 35 | "owner_path": "public-agent", |
| 36 | } |
| 37 | |
| 38 | |
| 39 | @pytest.mark.asyncio |
| 40 | async def test_runtime_launcher_runs_conversation_turn( |
| 41 | temp_dir: Path, |
| 42 | ) -> None: |
| 43 | handle = RuntimeHandle( |
| 44 | backend=ScriptedBackend( |
| 45 | completions=[CompletionResponse(content="Hello back.")] |
| 46 | ), |
| 47 | config=AgentConfig(auto_context=False, stream=False), |
| 48 | project_root=temp_dir, |
| 49 | ) |
| 50 | launcher = build_runtime_launcher(handle) |
| 51 | events = [] |
| 52 | |
| 53 | async def emit(event) -> None: |
| 54 | events.append(event) |
| 55 | |
| 56 | summary = await launcher.run_turn("Hello there", emit) |
| 57 | |
| 58 | assert summary.final_response == "Hello back." |
| 59 | assert any(event.type == "response" for event in events) |
| 60 | |
| 61 | |
| 62 | @pytest.mark.asyncio |
| 63 | async def test_runtime_launcher_runs_explore_query( |
| 64 | temp_dir: Path, |
| 65 | ) -> None: |
| 66 | backend = ScriptedBackend( |
| 67 | completions=[ |
| 68 | CompletionResponse(content="Quick repo summary.") |
| 69 | ] |
| 70 | ) |
| 71 | handle = RuntimeHandle( |
| 72 | backend=backend, |
| 73 | config=AgentConfig(auto_context=False, stream=False), |
| 74 | project_root=temp_dir, |
| 75 | ) |
| 76 | launcher = build_runtime_launcher(handle) |
| 77 | events = [] |
| 78 | |
| 79 | async def emit(event) -> None: |
| 80 | events.append(event) |
| 81 | |
| 82 | summary = await launcher.run_explore("Give me a quick repo summary.", emit) |
| 83 | |
| 84 | assert summary.workflow_mode == "explore" |
| 85 | assert summary.final_response == "Quick repo summary." |
| 86 | assert any(event.type == "response" for event in events) |
| 87 | |
| 88 | |
| 89 | @pytest.mark.asyncio |
| 90 | async def test_runtime_launcher_runs_decomposition_fallback_turn( |
| 91 | temp_dir: Path, |
| 92 | ) -> None: |
| 93 | backend = ScriptedBackend( |
| 94 | completions=[ |
| 95 | CompletionResponse( |
| 96 | content=( |
| 97 | '{"subtasks": [{"id": "1", "description": "Ship the feature", ' |
| 98 | '"verification": "Done"}]}' |
| 99 | ) |
| 100 | ), |
| 101 | CompletionResponse(content="Feature shipped directly."), |
| 102 | ] |
| 103 | ) |
| 104 | handle = RuntimeHandle( |
| 105 | backend=backend, |
| 106 | config=AgentConfig( |
| 107 | auto_context=False, |
| 108 | stream=False, |
| 109 | reasoning=ReasoningConfig(decomposition=True), |
| 110 | ), |
| 111 | project_root=temp_dir, |
| 112 | ) |
| 113 | launcher = build_runtime_launcher(handle) |
| 114 | events = [] |
| 115 | |
| 116 | async def emit(event) -> None: |
| 117 | events.append(event) |
| 118 | |
| 119 | response = await launcher.run_decomposed( |
| 120 | "Ship the feature", |
| 121 | emit, |
| 122 | requested_mode="execute", |
| 123 | original_task="Ship the feature", |
| 124 | ) |
| 125 | |
| 126 | assert response == "Feature shipped directly." |
| 127 | assert events[0].type == "thinking" |
| 128 | assert any(event.type == "response" for event in events) |
| 129 | assert not any(event.type == "decomposition" for event in events) |
| 130 | assert handle.session.messages[0].content == "Ship the feature" |
| 131 | |
| 132 | |
| 133 | @pytest.mark.asyncio |
| 134 | async def test_runtime_launcher_routes_user_message_to_conversational_fast_path( |
| 135 | temp_dir: Path, |
| 136 | ) -> None: |
| 137 | handle = RuntimeHandle( |
| 138 | backend=ScriptedBackend( |
| 139 | streams=[ |
| 140 | [ |
| 141 | StreamChunk(content="Quick ", is_done=False), |
| 142 | StreamChunk( |
| 143 | content="reply.", |
| 144 | full_content="Quick reply.", |
| 145 | is_done=True, |
| 146 | ), |
| 147 | ] |
| 148 | ] |
| 149 | ), |
| 150 | config=AgentConfig(auto_context=False), |
| 151 | project_root=temp_dir, |
| 152 | ) |
| 153 | launcher = build_runtime_launcher(handle) |
| 154 | events = [] |
| 155 | |
| 156 | async def emit(event) -> None: |
| 157 | events.append(event) |
| 158 | |
| 159 | response = await launcher.run_user_message("thanks", emit) |
| 160 | |
| 161 | assert response == "Quick reply." |
| 162 | assert handle.current_task is None |
| 163 | assert any(event.type == "response" and event.content == "Quick reply." for event in events) |
| 164 | |
| 165 | |
| 166 | @pytest.mark.asyncio |
| 167 | async def test_runtime_launcher_routes_user_message_to_direct_runtime_turn( |
| 168 | temp_dir: Path, |
| 169 | ) -> None: |
| 170 | handle = RuntimeHandle( |
| 171 | backend=ScriptedBackend( |
| 172 | completions=[CompletionResponse(content="Feature shipped directly.")] |
| 173 | ), |
| 174 | config=AgentConfig( |
| 175 | auto_context=False, |
| 176 | stream=False, |
| 177 | reasoning=ReasoningConfig(completion_check=False), |
| 178 | ), |
| 179 | project_root=temp_dir, |
| 180 | ) |
| 181 | launcher = build_runtime_launcher(handle) |
| 182 | events = [] |
| 183 | |
| 184 | async def emit(event) -> None: |
| 185 | events.append(event) |
| 186 | |
| 187 | response = await launcher.run_user_message( |
| 188 | "Write a short release-note style summary of what Loader does well.", |
| 189 | emit, |
| 190 | use_plan=False, |
| 191 | ) |
| 192 | |
| 193 | assert response == "Feature shipped directly." |
| 194 | assert handle.current_task == "Write a short release-note style summary of what Loader does well." |
| 195 | assert handle.last_turn_summary is not None |
| 196 | assert handle.last_turn_summary.final_response == "Feature shipped directly." |
| 197 | assert ( |
| 198 | handle.session.messages[0].content |
| 199 | == "Write a short release-note style summary of what Loader does well." |
| 200 | ) |
| 201 | assert any(event.type == "response" for event in events) |
| 202 | |
| 203 | |
| 204 | @pytest.mark.asyncio |
| 205 | async def test_runtime_launcher_routes_user_message_through_decomposition_lane( |
| 206 | temp_dir: Path, |
| 207 | monkeypatch, |
| 208 | ) -> None: |
| 209 | handle = RuntimeHandle( |
| 210 | backend=ScriptedBackend(), |
| 211 | config=AgentConfig( |
| 212 | auto_context=False, |
| 213 | stream=False, |
| 214 | reasoning=ReasoningConfig(decomposition=True, completion_check=False), |
| 215 | ), |
| 216 | project_root=temp_dir, |
| 217 | ) |
| 218 | launcher = build_runtime_launcher(handle) |
| 219 | events = [] |
| 220 | calls = [] |
| 221 | |
| 222 | async def emit(event) -> None: |
| 223 | events.append(event) |
| 224 | |
| 225 | async def fake_run_decomposed( |
| 226 | task: str, |
| 227 | emit, |
| 228 | *, |
| 229 | on_confirmation=None, |
| 230 | on_user_question=None, |
| 231 | requested_mode: str | None = None, |
| 232 | original_task: str | None = None, |
| 233 | ) -> str: |
| 234 | calls.append( |
| 235 | { |
| 236 | "task": task, |
| 237 | "requested_mode": requested_mode, |
| 238 | "original_task": original_task, |
| 239 | } |
| 240 | ) |
| 241 | return "All done." |
| 242 | |
| 243 | monkeypatch.setattr(launcher, "run_decomposed", fake_run_decomposed) |
| 244 | |
| 245 | response = await launcher.run_user_message( |
| 246 | "Read the spec and implement the feature", |
| 247 | emit, |
| 248 | use_plan=False, |
| 249 | ) |
| 250 | |
| 251 | assert response == "All done." |
| 252 | assert handle.current_task == "Read the spec and implement the feature" |
| 253 | assert calls == [ |
| 254 | { |
| 255 | "task": "Read the spec and implement the feature", |
| 256 | "requested_mode": "execute", |
| 257 | "original_task": "Read the spec and implement the feature", |
| 258 | } |
| 259 | ] |
| 260 | assert events == [] |
| 261 | |
| 262 | |
| 263 | @pytest.mark.asyncio |
| 264 | async def test_runtime_launcher_resets_task_scoped_state_for_new_top_level_prompt( |
| 265 | temp_dir: Path, |
| 266 | ) -> None: |
| 267 | backend = ScriptedBackend( |
| 268 | completions=[CompletionResponse(content="Penguins page shipped.")] |
| 269 | ) |
| 270 | handle = RuntimeHandle( |
| 271 | backend=backend, |
| 272 | config=AgentConfig( |
| 273 | auto_context=False, |
| 274 | stream=False, |
| 275 | reasoning=ReasoningConfig(completion_check=False), |
| 276 | ), |
| 277 | project_root=temp_dir, |
| 278 | ) |
| 279 | handle.current_task = "Create a collection of animal pages." |
| 280 | cached_prompt = get_runtime_shell_system_message(handle) |
| 281 | assert "Create a collection of animal pages." in cached_prompt.content |
| 282 | |
| 283 | todo_store = active_todo_store_path(temp_dir) |
| 284 | todo_store.parent.mkdir(parents=True, exist_ok=True) |
| 285 | todo_store.write_text( |
| 286 | '[{"content": "Build cat page", "active_form": "Building cat page", "status": "pending"}]' |
| 287 | ) |
| 288 | |
| 289 | launcher = build_runtime_launcher(handle) |
| 290 | events = [] |
| 291 | |
| 292 | async def emit(event) -> None: |
| 293 | events.append(event) |
| 294 | |
| 295 | response = await launcher.run_user_message( |
| 296 | "Generate penguins.html and penguins.css for the new page.", |
| 297 | emit, |
| 298 | use_plan=False, |
| 299 | ) |
| 300 | |
| 301 | assert response == "Penguins page shipped." |
| 302 | assert handle.current_task == "Generate penguins.html and penguins.css for the new page." |
| 303 | assert not todo_store.exists() |
| 304 | assert any( |
| 305 | event.type == "todo_update" and event.todo_items == [] |
| 306 | for event in events |
| 307 | ) |
| 308 | invocation = backend.invocations[-1] |
| 309 | assert ( |
| 310 | "Current task: Generate penguins.html and penguins.css for the new page." |
| 311 | in invocation.messages[0].content |
| 312 | ) |
| 313 | assert "Current task: Create a collection of animal pages." not in invocation.messages[0].content |