tenseleyflow/loader / c000559

Browse files

Add runtime-handle execution entrypoints

Authored by espadonne
SHA
c00055905796ba6c78ac75cd1c229cbfe681ce6d
Parents
28a4d01
Tree
fb4fbb1

2 changed files

StatusFile+-
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 @@
22
 
33
 from __future__ import annotations
44
 
5
+from collections.abc import AsyncIterator, Awaitable, Callable
56
 from pathlib import Path
67
 
78
 from loader.runtime.safeguards import RuntimeSafeguards
@@ -10,7 +11,7 @@ from ..context.project import ProjectContext, detect_project
1011
 from ..llm.base import LLMBackend, Message
1112
 from ..tools.base import ToolRegistry, create_default_registry
1213
 from .capabilities import resolve_backend_capability_profile
13
-from .events import TurnSummary
14
+from .events import AgentEvent, TurnSummary
1415
 from .permissions import (
1516
     PermissionMode,
1617
     build_permission_policy,
@@ -25,13 +26,16 @@ from .public_shell import (
2526
     refresh_runtime_shell_capability_profile,
2627
     resolve_runtime_shell_use_react,
2728
     resume_runtime_shell_session,
29
+    run_runtime_shell,
30
+    run_runtime_shell_explore,
2831
     set_runtime_shell_workflow_mode,
32
+    stream_runtime_shell,
2933
 )
3034
 from .workflow import WorkflowMode
3135
 
3236
 
3337
 class RuntimeHandle:
34
-    """Runtime-first internal owner for launcher and turn-runtime tests."""
38
+    """Runtime-first internal owner for launcher, shell, and integration paths."""
3539
 
3640
     def __init__(
3741
         self,
@@ -123,6 +127,58 @@ class RuntimeHandle:
123127
 
124128
         clear_runtime_shell_history(self)
125129
 
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
+
126182
     def set_workflow_mode(self, workflow_mode: str) -> None:
127183
         """Update the active workflow mode used by the system prompt."""
128184
 
tests/test_runtime_handle.pymodified
@@ -7,7 +7,7 @@ from pathlib import Path
77
 import pytest
88
 
99
 from loader.agent.loop import AgentConfig
10
-from loader.llm.base import CompletionResponse
10
+from loader.llm.base import CompletionResponse, StreamChunk
1111
 from loader.runtime.bootstrap import RuntimeBootstrapView, build_runtime_context
1212
 from loader.runtime.conversation import ConversationRuntime
1313
 from loader.runtime.launcher import RuntimeLauncher, build_runtime_launcher
@@ -65,3 +65,67 @@ async def test_runtime_handle_runs_conversation_runtime_without_agent(
6565
     assert summary.final_response == "Runtime handle reply."
6666
     assert runtime.source.metadata == {"owner_type": "RuntimeHandle"}
6767
     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"