steerable-agent-runtime 0.1.0__py3-none-any.whl
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- steerable_agent_runtime/__init__.py +34 -0
- steerable_agent_runtime/errors.py +34 -0
- steerable_agent_runtime/llm/__init__.py +104 -0
- steerable_agent_runtime/llm/anthropic_native.py +261 -0
- steerable_agent_runtime/llm/openai_compat.py +256 -0
- steerable_agent_runtime/storage/__init__.py +82 -0
- steerable_agent_runtime/storage/in_memory.py +151 -0
- steerable_agent_runtime/storage/sqlalchemy_store.py +340 -0
- steerable_agent_runtime/tools.py +251 -0
- steerable_agent_runtime/transport/__init__.py +39 -0
- steerable_agent_runtime/transport/fastapi_sse.py +116 -0
- steerable_agent_runtime/transport/stdio_jsonrpc.py +319 -0
- steerable_agent_runtime-0.1.0.dist-info/METADATA +61 -0
- steerable_agent_runtime-0.1.0.dist-info/RECORD +16 -0
- steerable_agent_runtime-0.1.0.dist-info/WHEEL +5 -0
- steerable_agent_runtime-0.1.0.dist-info/top_level.txt +1 -0
|
@@ -0,0 +1,319 @@
|
|
|
1
|
+
"""JSON-RPC 2.0 transport over stdio.
|
|
2
|
+
|
|
3
|
+
Powers the ``steerable-sidecar`` (a portable Python process spawned by an
|
|
4
|
+
Electron / desktop host). Each frame is a single-line UTF-8 JSON document
|
|
5
|
+
terminated by ``\n``, matching ``spec/sidecar/``.
|
|
6
|
+
|
|
7
|
+
The module ships two layers:
|
|
8
|
+
|
|
9
|
+
* ``StdioJsonRpcTransport`` — implements ``TransportAdapter.emit()`` so the
|
|
10
|
+
runtime can stream `SSEEvent` instances over JSON-RPC notifications.
|
|
11
|
+
* ``JsonRpcServer`` — minimal request/response/notification dispatcher used by
|
|
12
|
+
the sidecar entrypoint. Handlers are registered with
|
|
13
|
+
``server.register("method.name", handler)``.
|
|
14
|
+
"""
|
|
15
|
+
|
|
16
|
+
from __future__ import annotations
|
|
17
|
+
|
|
18
|
+
import asyncio
|
|
19
|
+
import json
|
|
20
|
+
import logging
|
|
21
|
+
import sys
|
|
22
|
+
from collections.abc import Awaitable, Callable
|
|
23
|
+
from dataclasses import dataclass
|
|
24
|
+
from typing import Any
|
|
25
|
+
|
|
26
|
+
from steerable_agent_protocol.generated import (
|
|
27
|
+
SidecarError,
|
|
28
|
+
SidecarNotification,
|
|
29
|
+
SidecarRequest,
|
|
30
|
+
SidecarResponse,
|
|
31
|
+
SSEEvent,
|
|
32
|
+
)
|
|
33
|
+
|
|
34
|
+
logger = logging.getLogger(__name__)
|
|
35
|
+
|
|
36
|
+
JsonRpcMethodHandler = Callable[[dict[str, Any] | None], Awaitable[Any]]
|
|
37
|
+
|
|
38
|
+
|
|
39
|
+
# ---------------------------------------------------------------------------
|
|
40
|
+
# Wire helpers
|
|
41
|
+
# ---------------------------------------------------------------------------
|
|
42
|
+
|
|
43
|
+
|
|
44
|
+
def encode_frame(payload: dict[str, Any]) -> bytes:
|
|
45
|
+
"""Encode one JSON-RPC frame to a single-line UTF-8 bytes buffer."""
|
|
46
|
+
|
|
47
|
+
return (json.dumps(payload, ensure_ascii=False, separators=(",", ":")) + "\n").encode("utf-8")
|
|
48
|
+
|
|
49
|
+
|
|
50
|
+
def decode_frame(line: str) -> dict[str, Any] | None:
|
|
51
|
+
"""Decode a single JSON-RPC frame line. Returns None for blank lines."""
|
|
52
|
+
|
|
53
|
+
line = line.strip()
|
|
54
|
+
if not line:
|
|
55
|
+
return None
|
|
56
|
+
return json.loads(line)
|
|
57
|
+
|
|
58
|
+
|
|
59
|
+
def build_request(
|
|
60
|
+
*,
|
|
61
|
+
request_id: str | int,
|
|
62
|
+
method: str,
|
|
63
|
+
params: dict[str, Any] | None = None,
|
|
64
|
+
) -> SidecarRequest:
|
|
65
|
+
return SidecarRequest(jsonrpc="2.0", id=request_id, method=method, params=params)
|
|
66
|
+
|
|
67
|
+
|
|
68
|
+
def build_notification(
|
|
69
|
+
*,
|
|
70
|
+
method: str,
|
|
71
|
+
params: dict[str, Any] | None = None,
|
|
72
|
+
) -> SidecarNotification:
|
|
73
|
+
return SidecarNotification(jsonrpc="2.0", method=method, params=params)
|
|
74
|
+
|
|
75
|
+
|
|
76
|
+
def build_response(
|
|
77
|
+
*,
|
|
78
|
+
request_id: str | int | None,
|
|
79
|
+
result: Any | None = None,
|
|
80
|
+
error: SidecarError | None = None,
|
|
81
|
+
) -> SidecarResponse:
|
|
82
|
+
return SidecarResponse(jsonrpc="2.0", id=request_id, result=result, error=error)
|
|
83
|
+
|
|
84
|
+
|
|
85
|
+
def make_error(
|
|
86
|
+
*,
|
|
87
|
+
code: int,
|
|
88
|
+
message: str,
|
|
89
|
+
kind: str | None = None,
|
|
90
|
+
data: Any | None = None,
|
|
91
|
+
) -> SidecarError:
|
|
92
|
+
return SidecarError(code=code, message=message, kind=kind, data=data)
|
|
93
|
+
|
|
94
|
+
|
|
95
|
+
# ---------------------------------------------------------------------------
|
|
96
|
+
# Transport
|
|
97
|
+
# ---------------------------------------------------------------------------
|
|
98
|
+
|
|
99
|
+
|
|
100
|
+
@dataclass(slots=True)
|
|
101
|
+
class _PendingRequest:
|
|
102
|
+
future: asyncio.Future
|
|
103
|
+
|
|
104
|
+
|
|
105
|
+
class StdioJsonRpcTransport:
|
|
106
|
+
"""Runtime-side adapter that emits `SSEEvent` as JSON-RPC notifications.
|
|
107
|
+
|
|
108
|
+
Concrete writer is injected so this class is testable without touching real
|
|
109
|
+
stdio: pass any object supporting ``write(bytes) -> Awaitable[None]`` (or a
|
|
110
|
+
sync ``write(bytes) -> int``).
|
|
111
|
+
"""
|
|
112
|
+
|
|
113
|
+
def __init__(
|
|
114
|
+
self,
|
|
115
|
+
writer: Any,
|
|
116
|
+
*,
|
|
117
|
+
notification_method: str = "stream.chunk",
|
|
118
|
+
) -> None:
|
|
119
|
+
self._writer = writer
|
|
120
|
+
self._notification_method = notification_method
|
|
121
|
+
self._closed = False
|
|
122
|
+
self._lock = asyncio.Lock()
|
|
123
|
+
|
|
124
|
+
async def emit(self, event: SSEEvent) -> None:
|
|
125
|
+
if self._closed:
|
|
126
|
+
raise RuntimeError("Transport is closed")
|
|
127
|
+
notification = build_notification(
|
|
128
|
+
method=self._notification_method,
|
|
129
|
+
params=event.model_dump(exclude_none=True),
|
|
130
|
+
)
|
|
131
|
+
await self._send(notification.model_dump(exclude_none=True))
|
|
132
|
+
|
|
133
|
+
async def emit_notification(self, method: str, params: dict[str, Any] | None = None) -> None:
|
|
134
|
+
if self._closed:
|
|
135
|
+
raise RuntimeError("Transport is closed")
|
|
136
|
+
notification = build_notification(method=method, params=params)
|
|
137
|
+
await self._send(notification.model_dump(exclude_none=True))
|
|
138
|
+
|
|
139
|
+
async def aclose(self) -> None:
|
|
140
|
+
self._closed = True
|
|
141
|
+
|
|
142
|
+
async def _send(self, payload: dict[str, Any]) -> None:
|
|
143
|
+
frame = encode_frame(payload)
|
|
144
|
+
async with self._lock:
|
|
145
|
+
result = self._writer.write(frame)
|
|
146
|
+
if asyncio.iscoroutine(result):
|
|
147
|
+
await result
|
|
148
|
+
drain = getattr(self._writer, "drain", None)
|
|
149
|
+
if drain is not None:
|
|
150
|
+
drained = drain()
|
|
151
|
+
if asyncio.iscoroutine(drained):
|
|
152
|
+
await drained
|
|
153
|
+
|
|
154
|
+
|
|
155
|
+
# ---------------------------------------------------------------------------
|
|
156
|
+
# Server
|
|
157
|
+
# ---------------------------------------------------------------------------
|
|
158
|
+
|
|
159
|
+
|
|
160
|
+
class JsonRpcServer:
|
|
161
|
+
"""Minimal stdio JSON-RPC dispatcher.
|
|
162
|
+
|
|
163
|
+
Handlers are async callables ``handler(params: dict | None) -> Any``.
|
|
164
|
+
|
|
165
|
+
Returning ``None`` from a handler still produces a JSON-RPC ``result: null``
|
|
166
|
+
response (so the client knows the request succeeded). Raise to surface an
|
|
167
|
+
error; ``JsonRpcError`` instances pass their ``code/kind/data`` through.
|
|
168
|
+
"""
|
|
169
|
+
|
|
170
|
+
def __init__(self) -> None:
|
|
171
|
+
self._handlers: dict[str, JsonRpcMethodHandler] = {}
|
|
172
|
+
self._notification_handlers: dict[str, JsonRpcMethodHandler] = {}
|
|
173
|
+
|
|
174
|
+
# ------------------------------------------------------------------
|
|
175
|
+
# Registration
|
|
176
|
+
# ------------------------------------------------------------------
|
|
177
|
+
|
|
178
|
+
def register(self, method: str, handler: JsonRpcMethodHandler) -> None:
|
|
179
|
+
if method in self._handlers:
|
|
180
|
+
raise ValueError(f"Method already registered: {method}")
|
|
181
|
+
self._handlers[method] = handler
|
|
182
|
+
|
|
183
|
+
def register_notification(self, method: str, handler: JsonRpcMethodHandler) -> None:
|
|
184
|
+
if method in self._notification_handlers:
|
|
185
|
+
raise ValueError(f"Notification handler already registered: {method}")
|
|
186
|
+
self._notification_handlers[method] = handler
|
|
187
|
+
|
|
188
|
+
# ------------------------------------------------------------------
|
|
189
|
+
# Dispatch
|
|
190
|
+
# ------------------------------------------------------------------
|
|
191
|
+
|
|
192
|
+
async def handle_frame(self, raw: str) -> dict[str, Any] | None:
|
|
193
|
+
try:
|
|
194
|
+
payload = decode_frame(raw)
|
|
195
|
+
except json.JSONDecodeError as exc:
|
|
196
|
+
return build_response(
|
|
197
|
+
request_id=None,
|
|
198
|
+
error=make_error(code=-32700, kind="parse", message=str(exc)),
|
|
199
|
+
).model_dump(exclude_none=True)
|
|
200
|
+
if payload is None:
|
|
201
|
+
return None
|
|
202
|
+
if "id" not in payload: # notification
|
|
203
|
+
await self._dispatch_notification(payload)
|
|
204
|
+
return None
|
|
205
|
+
return await self._dispatch_request(payload)
|
|
206
|
+
|
|
207
|
+
async def _dispatch_notification(self, payload: dict[str, Any]) -> None:
|
|
208
|
+
method = payload.get("method")
|
|
209
|
+
handler = self._notification_handlers.get(method or "")
|
|
210
|
+
if handler is None:
|
|
211
|
+
logger.debug("Unhandled notification: %s", method)
|
|
212
|
+
return
|
|
213
|
+
try:
|
|
214
|
+
await handler(payload.get("params"))
|
|
215
|
+
except Exception: # noqa: BLE001
|
|
216
|
+
logger.exception("Notification handler %s raised", method)
|
|
217
|
+
|
|
218
|
+
async def _dispatch_request(self, payload: dict[str, Any]) -> dict[str, Any]:
|
|
219
|
+
request_id = payload.get("id")
|
|
220
|
+
method = payload.get("method")
|
|
221
|
+
if not isinstance(method, str) or not method:
|
|
222
|
+
return build_response(
|
|
223
|
+
request_id=request_id,
|
|
224
|
+
error=make_error(code=-32600, kind="invalid_request", message="missing method"),
|
|
225
|
+
).model_dump(exclude_none=True)
|
|
226
|
+
handler = self._handlers.get(method)
|
|
227
|
+
if handler is None:
|
|
228
|
+
return build_response(
|
|
229
|
+
request_id=request_id,
|
|
230
|
+
error=make_error(
|
|
231
|
+
code=-32601, kind="method_not_found", message=f"unknown method '{method}'"
|
|
232
|
+
),
|
|
233
|
+
).model_dump(exclude_none=True)
|
|
234
|
+
params = payload.get("params")
|
|
235
|
+
try:
|
|
236
|
+
result = await handler(params)
|
|
237
|
+
except JsonRpcError as exc:
|
|
238
|
+
return build_response(
|
|
239
|
+
request_id=request_id,
|
|
240
|
+
error=make_error(code=exc.code, kind=exc.kind, message=exc.message, data=exc.data),
|
|
241
|
+
).model_dump(exclude_none=True)
|
|
242
|
+
except Exception as exc: # noqa: BLE001
|
|
243
|
+
logger.exception("Method %s raised", method)
|
|
244
|
+
return build_response(
|
|
245
|
+
request_id=request_id,
|
|
246
|
+
error=make_error(code=-32603, kind="internal", message=str(exc)),
|
|
247
|
+
).model_dump(exclude_none=True)
|
|
248
|
+
return build_response(request_id=request_id, result=result).model_dump(exclude_none=True)
|
|
249
|
+
|
|
250
|
+
# ------------------------------------------------------------------
|
|
251
|
+
# Pump
|
|
252
|
+
# ------------------------------------------------------------------
|
|
253
|
+
|
|
254
|
+
async def serve_stdio(
|
|
255
|
+
self,
|
|
256
|
+
*,
|
|
257
|
+
reader: Any | None = None,
|
|
258
|
+
writer: Any | None = None,
|
|
259
|
+
) -> None:
|
|
260
|
+
"""Drive the server using asyncio stdio streams.
|
|
261
|
+
|
|
262
|
+
When ``reader`` / ``writer`` are not provided, fall back to wrapping
|
|
263
|
+
``sys.stdin`` and ``sys.stdout`` so the sidecar entrypoint can simply
|
|
264
|
+
call ``await server.serve_stdio()``.
|
|
265
|
+
"""
|
|
266
|
+
|
|
267
|
+
if reader is None or writer is None:
|
|
268
|
+
reader, writer = await _connect_default_stdio()
|
|
269
|
+
try:
|
|
270
|
+
while True:
|
|
271
|
+
line = await reader.readline()
|
|
272
|
+
if not line:
|
|
273
|
+
return
|
|
274
|
+
response = await self.handle_frame(line.decode("utf-8"))
|
|
275
|
+
if response is None:
|
|
276
|
+
continue
|
|
277
|
+
writer.write(encode_frame(response))
|
|
278
|
+
drain = getattr(writer, "drain", None)
|
|
279
|
+
if drain is not None:
|
|
280
|
+
drained = drain()
|
|
281
|
+
if asyncio.iscoroutine(drained):
|
|
282
|
+
await drained
|
|
283
|
+
finally:
|
|
284
|
+
close = getattr(writer, "close", None)
|
|
285
|
+
if close is not None:
|
|
286
|
+
close()
|
|
287
|
+
|
|
288
|
+
|
|
289
|
+
class JsonRpcError(Exception):
|
|
290
|
+
"""Raise from a handler to return a structured JSON-RPC error."""
|
|
291
|
+
|
|
292
|
+
def __init__(
|
|
293
|
+
self,
|
|
294
|
+
message: str,
|
|
295
|
+
*,
|
|
296
|
+
code: int = -32000,
|
|
297
|
+
kind: str | None = None,
|
|
298
|
+
data: Any | None = None,
|
|
299
|
+
) -> None:
|
|
300
|
+
super().__init__(message)
|
|
301
|
+
self.code = code
|
|
302
|
+
self.kind = kind
|
|
303
|
+
self.data = data
|
|
304
|
+
self.message = message
|
|
305
|
+
|
|
306
|
+
|
|
307
|
+
# ---------------------------------------------------------------------------
|
|
308
|
+
# Default stdio adapter
|
|
309
|
+
# ---------------------------------------------------------------------------
|
|
310
|
+
|
|
311
|
+
|
|
312
|
+
async def _connect_default_stdio() -> tuple[asyncio.StreamReader, asyncio.StreamWriter]:
|
|
313
|
+
loop = asyncio.get_running_loop()
|
|
314
|
+
reader = asyncio.StreamReader()
|
|
315
|
+
protocol = asyncio.StreamReaderProtocol(reader)
|
|
316
|
+
await loop.connect_read_pipe(lambda: protocol, sys.stdin)
|
|
317
|
+
transport, _ = await loop.connect_write_pipe(asyncio.streams.FlowControlMixin, sys.stdout)
|
|
318
|
+
writer = asyncio.StreamWriter(transport, protocol, reader, loop)
|
|
319
|
+
return reader, writer
|
|
@@ -0,0 +1,61 @@
|
|
|
1
|
+
Metadata-Version: 2.4
|
|
2
|
+
Name: steerable-agent-runtime
|
|
3
|
+
Version: 0.1.0
|
|
4
|
+
Summary: Steerable agent runtime: LLM, tool, storage, and transport adapters.
|
|
5
|
+
Requires-Python: >=3.10
|
|
6
|
+
Description-Content-Type: text/markdown
|
|
7
|
+
Requires-Dist: pydantic>=2.10.0
|
|
8
|
+
Requires-Dist: steerable-agent-protocol<1.0.0,>=0.1.0
|
|
9
|
+
Requires-Dist: steerable-agent-harness<1.0.0,>=0.1.0
|
|
10
|
+
Provides-Extra: sqlalchemy
|
|
11
|
+
Requires-Dist: sqlalchemy>=2.0; extra == "sqlalchemy"
|
|
12
|
+
Provides-Extra: fastapi
|
|
13
|
+
Requires-Dist: fastapi>=0.110; extra == "fastapi"
|
|
14
|
+
Requires-Dist: starlette>=0.37; extra == "fastapi"
|
|
15
|
+
Provides-Extra: openai
|
|
16
|
+
Requires-Dist: httpx>=0.27; extra == "openai"
|
|
17
|
+
Provides-Extra: anthropic
|
|
18
|
+
Requires-Dist: anthropic>=0.40; extra == "anthropic"
|
|
19
|
+
Provides-Extra: all
|
|
20
|
+
Requires-Dist: sqlalchemy>=2.0; extra == "all"
|
|
21
|
+
Requires-Dist: fastapi>=0.110; extra == "all"
|
|
22
|
+
Requires-Dist: starlette>=0.37; extra == "all"
|
|
23
|
+
Requires-Dist: httpx>=0.27; extra == "all"
|
|
24
|
+
Requires-Dist: anthropic>=0.40; extra == "all"
|
|
25
|
+
|
|
26
|
+
# steerable-agent-runtime
|
|
27
|
+
|
|
28
|
+
Tier 3 runtime for the Steerable framework.
|
|
29
|
+
|
|
30
|
+
Provides four orthogonal pluggable adapters:
|
|
31
|
+
|
|
32
|
+
- `LLMProvider` — chat-completion / streaming / tool-call abstraction, with
|
|
33
|
+
reference implementations for OpenAI-compatible servers (covers OpenAI,
|
|
34
|
+
Ollama, vLLM, SiliconFlow, etc.) and Anthropic native.
|
|
35
|
+
- `ToolRouter` — in-process tool registry. Auto-classifies tools into
|
|
36
|
+
`ToolMode`s using `steerable_agent_harness.policy`, supports per-tool
|
|
37
|
+
permission overrides, and dispatches `ToolCall` → `ToolResult`.
|
|
38
|
+
- `StorageAdapter` — persistence interface for `AgentSession`, `ChatMessage`,
|
|
39
|
+
`ChatAgent`, and `HarnessTrace + spans + events`. Reference impls: in-memory
|
|
40
|
+
(default for sidecar/dev) and SQLAlchemy (for hosted backends).
|
|
41
|
+
- `TransportAdapter` — wire format. `FastAPISseTransport` exports SSE for
|
|
42
|
+
hosted setups; `StdioJsonRpcTransport` powers the steerable-sidecar.
|
|
43
|
+
|
|
44
|
+
The runtime is **Python only** by design — frontends never depend on it
|
|
45
|
+
directly. Browsers/Electron consume runtime output via either the SSE transport
|
|
46
|
+
(over HTTP) or the stdio JSON-RPC transport (sidecar pattern).
|
|
47
|
+
|
|
48
|
+
## Install
|
|
49
|
+
|
|
50
|
+
```bash
|
|
51
|
+
pip install steerable-agent-runtime[all]
|
|
52
|
+
```
|
|
53
|
+
|
|
54
|
+
Selectively install just the bits you need:
|
|
55
|
+
|
|
56
|
+
```bash
|
|
57
|
+
pip install "steerable-agent-runtime[openai]"
|
|
58
|
+
pip install "steerable-agent-runtime[anthropic]"
|
|
59
|
+
pip install "steerable-agent-runtime[sqlalchemy]"
|
|
60
|
+
pip install "steerable-agent-runtime[fastapi]"
|
|
61
|
+
```
|
|
@@ -0,0 +1,16 @@
|
|
|
1
|
+
steerable_agent_runtime/__init__.py,sha256=4PQ1QigZnrPwP__civ1euCGBG3gBEBBJjAhP2o6N5fI,791
|
|
2
|
+
steerable_agent_runtime/errors.py,sha256=jNdYyCOCKnNDUAPNBZ-TLuSfRciedTgMrHq_Ep_lE3g,913
|
|
3
|
+
steerable_agent_runtime/tools.py,sha256=3fRyh-8JbS7VCAzLsIqtVAYEMDN5MeKWPy7EmxLzBn8,8469
|
|
4
|
+
steerable_agent_runtime/llm/__init__.py,sha256=J-PfiNPT-97LALtyPVPFAXA6aKJ19Nx_DIugnX8Vdqg,2940
|
|
5
|
+
steerable_agent_runtime/llm/anthropic_native.py,sha256=GOfZlLVEgvNl8wY5cH7qt9nwsn8RRrTbjVlSzGW_lk0,9680
|
|
6
|
+
steerable_agent_runtime/llm/openai_compat.py,sha256=Q9tc_tkOzQc19Z0G_yjUjT71KPY38Zqp_J1lGh1NrN8,8517
|
|
7
|
+
steerable_agent_runtime/storage/__init__.py,sha256=VfA-iqrQGRXhsZdLX-6HL40lTKs9v1uwu0IBBeOOgpU,2516
|
|
8
|
+
steerable_agent_runtime/storage/in_memory.py,sha256=hSxpnWKdm6gDbuPdryqrCUFJtfmnGqKlcKYz-OiWhgc,5772
|
|
9
|
+
steerable_agent_runtime/storage/sqlalchemy_store.py,sha256=AJBRp_R8KXAZUBYh77RaisONfY_Oac8zTYpoq4QiJNc,12843
|
|
10
|
+
steerable_agent_runtime/transport/__init__.py,sha256=jy2kvEiZ7VQQeZAV3AUoBhYAZrtulyqmp7eL1q1hy_Q,996
|
|
11
|
+
steerable_agent_runtime/transport/fastapi_sse.py,sha256=u6yuKdSFxYeRajyArvHTW_wrtFbQAdcFo8Ib3WWyKMg,3555
|
|
12
|
+
steerable_agent_runtime/transport/stdio_jsonrpc.py,sha256=NuMXX2GlcM2Lc98J-4z5rwqKcmVuTJ9tECEK_0Tjwbs,11132
|
|
13
|
+
steerable_agent_runtime-0.1.0.dist-info/METADATA,sha256=KdyQbS8ES5pG_kMNVJvF_DOp2ZZXCSw44fL18YrJnGk,2398
|
|
14
|
+
steerable_agent_runtime-0.1.0.dist-info/WHEEL,sha256=aeYiig01lYGDzBgS8HxWXOg3uV61G9ijOsup-k9o1sk,91
|
|
15
|
+
steerable_agent_runtime-0.1.0.dist-info/top_level.txt,sha256=oH6AX0Sbye_DmFcu-JFEwuu-yXwyCkkdquPt8t0pFJE,24
|
|
16
|
+
steerable_agent_runtime-0.1.0.dist-info/RECORD,,
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
steerable_agent_runtime
|