Add runtime-handle execution entrypoints
- SHA
c00055905796ba6c78ac75cd1c229cbfe681ce6d- Parents
-
28a4d01 - Tree
fb4fbb1
c000559
c00055905796ba6c78ac75cd1c229cbfe681ce6d28a4d01
fb4fbb1| Status | File | + | - |
|---|---|---|---|
| M |
src/loader/runtime/runtime_handle.py
|
58 | 2 |
| M |
tests/test_runtime_handle.py
|
65 | 1 |
src/loader/runtime/runtime_handle.pymodified@@ -2,6 +2,7 @@ | ||
| 2 | 2 | |
| 3 | 3 | from __future__ import annotations |
| 4 | 4 | |
| 5 | +from collections.abc import AsyncIterator, Awaitable, Callable | |
| 5 | 6 | from pathlib import Path |
| 6 | 7 | |
| 7 | 8 | from loader.runtime.safeguards import RuntimeSafeguards |
@@ -10,7 +11,7 @@ from ..context.project import ProjectContext, detect_project | ||
| 10 | 11 | from ..llm.base import LLMBackend, Message |
| 11 | 12 | from ..tools.base import ToolRegistry, create_default_registry |
| 12 | 13 | from .capabilities import resolve_backend_capability_profile |
| 13 | -from .events import TurnSummary | |
| 14 | +from .events import AgentEvent, TurnSummary | |
| 14 | 15 | from .permissions import ( |
| 15 | 16 | PermissionMode, |
| 16 | 17 | build_permission_policy, |
@@ -25,13 +26,16 @@ from .public_shell import ( | ||
| 25 | 26 | refresh_runtime_shell_capability_profile, |
| 26 | 27 | resolve_runtime_shell_use_react, |
| 27 | 28 | resume_runtime_shell_session, |
| 29 | + run_runtime_shell, | |
| 30 | + run_runtime_shell_explore, | |
| 28 | 31 | set_runtime_shell_workflow_mode, |
| 32 | + stream_runtime_shell, | |
| 29 | 33 | ) |
| 30 | 34 | from .workflow import WorkflowMode |
| 31 | 35 | |
| 32 | 36 | |
| 33 | 37 | class RuntimeHandle: |
| 34 | - """Runtime-first internal owner for launcher and turn-runtime tests.""" | |
| 38 | + """Runtime-first internal owner for launcher, shell, and integration paths.""" | |
| 35 | 39 | |
| 36 | 40 | def __init__( |
| 37 | 41 | self, |
@@ -123,6 +127,58 @@ class RuntimeHandle: | ||
| 123 | 127 | |
| 124 | 128 | clear_runtime_shell_history(self) |
| 125 | 129 | |
| 130 | + async def run( | |
| 131 | + self, | |
| 132 | + user_message: str, | |
| 133 | + on_event: ( | |
| 134 | + Callable[[AgentEvent], None] | |
| 135 | + | Callable[[AgentEvent], Awaitable[None]] | |
| 136 | + | None | |
| 137 | + ) = None, | |
| 138 | + on_confirmation: Callable[[str, str, str], Awaitable[bool]] | None = None, | |
| 139 | + on_user_question: Callable[[str, list[str] | None], Awaitable[str]] | None = None, | |
| 140 | + use_plan: bool | None = None, | |
| 141 | + ) -> str: | |
| 142 | + """Run one user message through the runtime-owned shell entrypoint.""" | |
| 143 | + | |
| 144 | + return await run_runtime_shell( | |
| 145 | + self, | |
| 146 | + user_message, | |
| 147 | + on_event=on_event, | |
| 148 | + on_confirmation=on_confirmation, | |
| 149 | + on_user_question=on_user_question, | |
| 150 | + use_plan=use_plan, | |
| 151 | + ) | |
| 152 | + | |
| 153 | + async def run_streaming( | |
| 154 | + self, | |
| 155 | + user_message: str, | |
| 156 | + ) -> AsyncIterator[AgentEvent]: | |
| 157 | + """Yield the streamed event sequence from the runtime-owned shell.""" | |
| 158 | + | |
| 159 | + async for event in stream_runtime_shell(self, user_message): | |
| 160 | + yield event | |
| 161 | + | |
| 162 | + async def run_explore( | |
| 163 | + self, | |
| 164 | + user_message: str, | |
| 165 | + on_event: ( | |
| 166 | + Callable[[AgentEvent], None] | |
| 167 | + | Callable[[AgentEvent], Awaitable[None]] | |
| 168 | + | None | |
| 169 | + ) = None, | |
| 170 | + *, | |
| 171 | + fresh: bool = False, | |
| 172 | + ) -> str: | |
| 173 | + """Run one read-only explore query through the runtime-owned shell.""" | |
| 174 | + | |
| 175 | + return await run_runtime_shell_explore( | |
| 176 | + self, | |
| 177 | + user_message, | |
| 178 | + on_event=on_event, | |
| 179 | + fresh=fresh, | |
| 180 | + ) | |
| 181 | + | |
| 126 | 182 | def set_workflow_mode(self, workflow_mode: str) -> None: |
| 127 | 183 | """Update the active workflow mode used by the system prompt.""" |
| 128 | 184 | |
tests/test_runtime_handle.pymodified@@ -7,7 +7,7 @@ from pathlib import Path | ||
| 7 | 7 | import pytest |
| 8 | 8 | |
| 9 | 9 | from loader.agent.loop import AgentConfig |
| 10 | -from loader.llm.base import CompletionResponse | |
| 10 | +from loader.llm.base import CompletionResponse, StreamChunk | |
| 11 | 11 | from loader.runtime.bootstrap import RuntimeBootstrapView, build_runtime_context |
| 12 | 12 | from loader.runtime.conversation import ConversationRuntime |
| 13 | 13 | from loader.runtime.launcher import RuntimeLauncher, build_runtime_launcher |
@@ -65,3 +65,67 @@ async def test_runtime_handle_runs_conversation_runtime_without_agent( | ||
| 65 | 65 | assert summary.final_response == "Runtime handle reply." |
| 66 | 66 | assert runtime.source.metadata == {"owner_type": "RuntimeHandle"} |
| 67 | 67 | assert any(event.type == "response" for event in events) |
| 68 | + | |
| 69 | + | |
| 70 | +@pytest.mark.asyncio | |
| 71 | +async def test_runtime_handle_runs_public_shell_entrypoint_without_agent( | |
| 72 | + temp_dir: Path, | |
| 73 | +) -> None: | |
| 74 | + handle = RuntimeHandle( | |
| 75 | + backend=ScriptedBackend( | |
| 76 | + completions=[CompletionResponse(content="Runtime handle shell reply.")] | |
| 77 | + ), | |
| 78 | + config=AgentConfig(auto_context=False, stream=False), | |
| 79 | + project_root=temp_dir, | |
| 80 | + ) | |
| 81 | + events = [] | |
| 82 | + | |
| 83 | + async def emit(event) -> None: | |
| 84 | + events.append(event) | |
| 85 | + | |
| 86 | + response = await handle.run( | |
| 87 | + "Summarize the runtime-first shell path.", | |
| 88 | + on_event=emit, | |
| 89 | + use_plan=False, | |
| 90 | + ) | |
| 91 | + | |
| 92 | + assert response == "Runtime handle shell reply." | |
| 93 | + assert handle.last_turn_summary is not None | |
| 94 | + assert handle.last_turn_summary.final_response == "Runtime handle shell reply." | |
| 95 | + assert any(event.type == "response" for event in events) | |
| 96 | + | |
| 97 | + | |
| 98 | +@pytest.mark.asyncio | |
| 99 | +async def test_runtime_handle_runs_explore_and_streaming_entrypoints_without_agent( | |
| 100 | + temp_dir: Path, | |
| 101 | +) -> None: | |
| 102 | + handle = RuntimeHandle( | |
| 103 | + backend=ScriptedBackend( | |
| 104 | + completions=[CompletionResponse(content="Explore with runtime handle.")], | |
| 105 | + streams=[ | |
| 106 | + [ | |
| 107 | + StreamChunk(content="Streamed ", is_done=False), | |
| 108 | + StreamChunk( | |
| 109 | + content="runtime reply.", | |
| 110 | + full_content="Streamed runtime reply.", | |
| 111 | + is_done=True, | |
| 112 | + ), | |
| 113 | + ] | |
| 114 | + ], | |
| 115 | + ), | |
| 116 | + config=AgentConfig(auto_context=False), | |
| 117 | + project_root=temp_dir, | |
| 118 | + ) | |
| 119 | + | |
| 120 | + stream_events = [event async for event in handle.run_streaming("thanks")] | |
| 121 | + explore_response = await handle.run_explore( | |
| 122 | + "Where should I start in this repo?", | |
| 123 | + ) | |
| 124 | + | |
| 125 | + assert any( | |
| 126 | + event.type == "response" and event.content == "Streamed runtime reply." | |
| 127 | + for event in stream_events | |
| 128 | + ) | |
| 129 | + assert explore_response == "Explore with runtime handle." | |
| 130 | + assert handle.last_turn_summary is not None | |
| 131 | + assert handle.last_turn_summary.workflow_mode == "explore" | |