@elizaos/interop 2.0.0-alpha
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.
- package/LICENSE +21 -0
- package/README.md +436 -0
- package/dist/packages/interop/tsconfig.tsbuildinfo +1 -0
- package/dist/tsconfig.tsbuildinfo +1 -0
- package/dist/typescript/index.d.ts +33 -0
- package/dist/typescript/index.d.ts.map +1 -0
- package/dist/typescript/index.js +121 -0
- package/dist/typescript/python-bridge.d.ts +80 -0
- package/dist/typescript/python-bridge.d.ts.map +1 -0
- package/dist/typescript/python-bridge.js +334 -0
- package/dist/typescript/types.d.ts +301 -0
- package/dist/typescript/types.d.ts.map +1 -0
- package/dist/typescript/types.js +10 -0
- package/dist/typescript/wasm-loader.d.ts +32 -0
- package/dist/typescript/wasm-loader.d.ts.map +1 -0
- package/dist/typescript/wasm-loader.js +269 -0
- package/package.json +43 -0
- package/python/__init__.py +50 -0
- package/python/__pycache__/__init__.cpython-313.pyc +0 -0
- package/python/__pycache__/rust_ffi.cpython-313.pyc +0 -0
- package/python/__pycache__/ts_bridge.cpython-313.pyc +0 -0
- package/python/__pycache__/wasm_loader.cpython-313.pyc +0 -0
- package/python/bridge_server.py +505 -0
- package/python/rust_ffi.py +418 -0
- package/python/tests/__init__.py +1 -0
- package/python/tests/__pycache__/__init__.cpython-313.pyc +0 -0
- package/python/tests/__pycache__/test_bridge_server.cpython-313-pytest-9.0.2.pyc +0 -0
- package/python/tests/__pycache__/test_interop.cpython-313-pytest-9.0.2.pyc +0 -0
- package/python/tests/__pycache__/test_rust_ffi.cpython-313-pytest-9.0.2.pyc +0 -0
- package/python/tests/__pycache__/test_rust_ffi_loader.cpython-313-pytest-9.0.2.pyc +0 -0
- package/python/tests/test_bridge_server.py +525 -0
- package/python/tests/test_interop.py +319 -0
- package/python/tests/test_rust_ffi.py +352 -0
- package/python/tests/test_rust_ffi_loader.py +71 -0
- package/python/ts_bridge.py +452 -0
- package/python/ts_bridge_runner.mjs +284 -0
- package/python/wasm_loader.py +517 -0
- package/rust/ffi_exports.rs +362 -0
- package/rust/mod.rs +21 -0
- package/rust/py_loader.rs +412 -0
- package/rust/ts_loader.rs +467 -0
- package/rust/wasm_plugin.rs +320 -0
|
@@ -0,0 +1,71 @@
|
|
|
1
|
+
from __future__ import annotations
|
|
2
|
+
|
|
3
|
+
import pytest
|
|
4
|
+
|
|
5
|
+
from .. import rust_ffi
|
|
6
|
+
from elizaos.types.components import ActionResult, ProviderResult
|
|
7
|
+
|
|
8
|
+
|
|
9
|
+
class _FakeRustPluginFFI:
|
|
10
|
+
def __init__(self, lib_path: str) -> None:
|
|
11
|
+
self._lib_path = lib_path
|
|
12
|
+
|
|
13
|
+
def get_manifest(self) -> dict[str, object]:
|
|
14
|
+
return {
|
|
15
|
+
"name": "fake-rust",
|
|
16
|
+
"description": "fake",
|
|
17
|
+
"actions": [{"name": "DO", "description": "do it"}],
|
|
18
|
+
"providers": [{"name": "PROV", "description": "p"}],
|
|
19
|
+
"evaluators": [{"name": "EVAL", "description": "e"}],
|
|
20
|
+
}
|
|
21
|
+
|
|
22
|
+
def init(self, config: dict[str, str]) -> None:
|
|
23
|
+
return None
|
|
24
|
+
|
|
25
|
+
def validate_action(self, name: str, memory: object, state: object) -> bool:
|
|
26
|
+
return name == "DO"
|
|
27
|
+
|
|
28
|
+
def invoke_action(self, name: str, memory: object, state: object, options: object) -> object:
|
|
29
|
+
return ActionResult(success=True, text=f"ok:{name}")
|
|
30
|
+
|
|
31
|
+
def get_provider(self, name: str, memory: object, state: object) -> object:
|
|
32
|
+
return ProviderResult(text=f"prov:{name}")
|
|
33
|
+
|
|
34
|
+
def validate_evaluator(self, name: str, memory: object, state: object) -> bool:
|
|
35
|
+
return name == "EVAL"
|
|
36
|
+
|
|
37
|
+
def invoke_evaluator(self, name: str, memory: object, state: object) -> object:
|
|
38
|
+
return ActionResult(success=True, text=f"eval:{name}")
|
|
39
|
+
|
|
40
|
+
|
|
41
|
+
@pytest.mark.asyncio
|
|
42
|
+
async def test_load_rust_plugin_produces_awaitable_handlers(monkeypatch: pytest.MonkeyPatch) -> None:
|
|
43
|
+
monkeypatch.setattr(rust_ffi, "RustPluginFFI", _FakeRustPluginFFI)
|
|
44
|
+
|
|
45
|
+
plugin = rust_ffi.load_rust_plugin("fake.so")
|
|
46
|
+
assert plugin.actions is not None
|
|
47
|
+
assert plugin.providers is not None
|
|
48
|
+
assert plugin.evaluators is not None
|
|
49
|
+
|
|
50
|
+
action = plugin.actions[0]
|
|
51
|
+
provider = plugin.providers[0]
|
|
52
|
+
evaluator = plugin.evaluators[0]
|
|
53
|
+
|
|
54
|
+
valid = await action.validate_fn(None, {"content": {}}, None) # type: ignore[arg-type]
|
|
55
|
+
assert valid is True
|
|
56
|
+
|
|
57
|
+
result = await action.handler(None, {"content": {}}, None, None, None, None) # type: ignore[arg-type]
|
|
58
|
+
assert result is not None
|
|
59
|
+
assert result.success is True
|
|
60
|
+
assert result.text == "ok:DO"
|
|
61
|
+
|
|
62
|
+
prov_result = await provider.get(None, {"content": {}}, {"values": {}, "data": {}, "text": ""}) # type: ignore[arg-type]
|
|
63
|
+
assert prov_result.text == "prov:PROV"
|
|
64
|
+
|
|
65
|
+
eval_valid = await evaluator.validate_fn(None, {"content": {}}, None) # type: ignore[arg-type]
|
|
66
|
+
assert eval_valid is True
|
|
67
|
+
|
|
68
|
+
eval_result = await evaluator.handler(None, {"content": {}}, None, None, None, None) # type: ignore[arg-type]
|
|
69
|
+
assert eval_result is not None
|
|
70
|
+
assert eval_result.text == "eval:EVAL"
|
|
71
|
+
|
|
@@ -0,0 +1,452 @@
|
|
|
1
|
+
"""
|
|
2
|
+
TypeScript Plugin Bridge for elizaOS
|
|
3
|
+
|
|
4
|
+
This module provides utilities for loading TypeScript plugins into the Python runtime
|
|
5
|
+
via subprocess IPC communication.
|
|
6
|
+
"""
|
|
7
|
+
|
|
8
|
+
from __future__ import annotations
|
|
9
|
+
|
|
10
|
+
import asyncio
|
|
11
|
+
import json
|
|
12
|
+
import os
|
|
13
|
+
import subprocess
|
|
14
|
+
from pathlib import Path
|
|
15
|
+
from typing import Callable, Awaitable
|
|
16
|
+
|
|
17
|
+
from elizaos.types.plugin import Plugin
|
|
18
|
+
from elizaos.types.memory import Memory
|
|
19
|
+
from elizaos.types.state import State
|
|
20
|
+
from elizaos.types.components import (
|
|
21
|
+
Action,
|
|
22
|
+
ActionResult,
|
|
23
|
+
Provider,
|
|
24
|
+
ProviderResult,
|
|
25
|
+
Evaluator,
|
|
26
|
+
HandlerOptions,
|
|
27
|
+
)
|
|
28
|
+
|
|
29
|
+
|
|
30
|
+
class TypeScriptPluginBridge:
|
|
31
|
+
"""
|
|
32
|
+
IPC bridge for loading TypeScript plugins in Python.
|
|
33
|
+
|
|
34
|
+
Spawns a Node.js subprocess that loads the TypeScript plugin and
|
|
35
|
+
communicates via JSON-RPC over stdin/stdout.
|
|
36
|
+
"""
|
|
37
|
+
|
|
38
|
+
def __init__(
|
|
39
|
+
self,
|
|
40
|
+
plugin_path: str | Path,
|
|
41
|
+
*,
|
|
42
|
+
node_path: str = "node",
|
|
43
|
+
cwd: str | Path | None = None,
|
|
44
|
+
env: dict[str, str] | None = None,
|
|
45
|
+
timeout: float = 30.0,
|
|
46
|
+
inherit_env: bool = True,
|
|
47
|
+
env_denylist: list[str] | None = None,
|
|
48
|
+
max_pending_requests: int = 1000,
|
|
49
|
+
max_message_bytes: int = 1_000_000,
|
|
50
|
+
max_buffer_bytes: int = 2_000_000,
|
|
51
|
+
) -> None:
|
|
52
|
+
"""
|
|
53
|
+
Initialize the TypeScript plugin bridge.
|
|
54
|
+
|
|
55
|
+
Args:
|
|
56
|
+
plugin_path: Path to the TypeScript plugin (directory or entry file).
|
|
57
|
+
node_path: Path to Node.js executable (defaults to 'node').
|
|
58
|
+
cwd: Working directory for the subprocess.
|
|
59
|
+
env: Additional environment variables.
|
|
60
|
+
timeout: Request timeout in seconds.
|
|
61
|
+
"""
|
|
62
|
+
self.plugin_path = Path(plugin_path)
|
|
63
|
+
self.node_path = node_path
|
|
64
|
+
self.cwd = Path(cwd) if cwd else self.plugin_path.parent
|
|
65
|
+
base_env: dict[str, str] = dict(os.environ) if inherit_env else {}
|
|
66
|
+
if env_denylist:
|
|
67
|
+
for key in env_denylist:
|
|
68
|
+
base_env.pop(key, None)
|
|
69
|
+
if env:
|
|
70
|
+
base_env.update(env)
|
|
71
|
+
# Also pass sizing limits to the runner so it can fail-closed.
|
|
72
|
+
base_env.setdefault("ELIZA_INTEROP_MAX_MESSAGE_BYTES", str(max_message_bytes))
|
|
73
|
+
base_env.setdefault("ELIZA_INTEROP_MAX_BUFFER_BYTES", str(max_buffer_bytes))
|
|
74
|
+
self.env = base_env
|
|
75
|
+
self.timeout = timeout
|
|
76
|
+
self.max_pending_requests = max_pending_requests
|
|
77
|
+
self.max_message_bytes = max_message_bytes
|
|
78
|
+
self.max_buffer_bytes = max_buffer_bytes
|
|
79
|
+
|
|
80
|
+
self.process: subprocess.Popen[bytes] | None = None
|
|
81
|
+
self.manifest: dict[str, object] | None = None
|
|
82
|
+
self._request_counter = 0
|
|
83
|
+
self._pending_requests: dict[str, asyncio.Future[dict[str, object]]] = {}
|
|
84
|
+
self._reader_task: asyncio.Task[None] | None = None
|
|
85
|
+
self._stderr_task: asyncio.Task[None] | None = None
|
|
86
|
+
self._buffer = ""
|
|
87
|
+
|
|
88
|
+
async def start(self) -> None:
|
|
89
|
+
"""Start the TypeScript bridge subprocess."""
|
|
90
|
+
bridge_script = self._get_bridge_script()
|
|
91
|
+
|
|
92
|
+
self.process = subprocess.Popen(
|
|
93
|
+
[self.node_path, bridge_script, str(self.plugin_path)],
|
|
94
|
+
stdin=subprocess.PIPE,
|
|
95
|
+
stdout=subprocess.PIPE,
|
|
96
|
+
stderr=subprocess.PIPE,
|
|
97
|
+
cwd=str(self.cwd),
|
|
98
|
+
env=self.env,
|
|
99
|
+
)
|
|
100
|
+
|
|
101
|
+
# Start the reader task
|
|
102
|
+
self._reader_task = asyncio.create_task(self._read_responses())
|
|
103
|
+
self._stderr_task = asyncio.create_task(self._drain_stderr())
|
|
104
|
+
|
|
105
|
+
# Wait for ready message with manifest
|
|
106
|
+
await self._wait_for_ready()
|
|
107
|
+
|
|
108
|
+
def _get_bridge_script(self) -> str:
|
|
109
|
+
"""Get the path to the TypeScript bridge script."""
|
|
110
|
+
script_dir = Path(__file__).parent
|
|
111
|
+
bridge_path = script_dir / "ts_bridge_runner.mjs"
|
|
112
|
+
if not bridge_path.exists():
|
|
113
|
+
raise FileNotFoundError(f"Missing bridge runner: {bridge_path}")
|
|
114
|
+
return str(bridge_path)
|
|
115
|
+
|
|
116
|
+
async def _read_responses(self) -> None:
|
|
117
|
+
"""Read responses from the subprocess stdout."""
|
|
118
|
+
if not self.process or not self.process.stdout:
|
|
119
|
+
return
|
|
120
|
+
|
|
121
|
+
loop = asyncio.get_event_loop()
|
|
122
|
+
|
|
123
|
+
while True:
|
|
124
|
+
try:
|
|
125
|
+
line = await loop.run_in_executor(
|
|
126
|
+
None, self.process.stdout.readline
|
|
127
|
+
)
|
|
128
|
+
if not line:
|
|
129
|
+
break
|
|
130
|
+
|
|
131
|
+
line_str = line.decode("utf-8").strip()
|
|
132
|
+
if not line_str:
|
|
133
|
+
continue
|
|
134
|
+
if len(line) > self.max_message_bytes:
|
|
135
|
+
raise RuntimeError("Subprocess output exceeded maximum message size")
|
|
136
|
+
|
|
137
|
+
message = json.loads(line_str)
|
|
138
|
+
self._handle_message(message)
|
|
139
|
+
except json.JSONDecodeError as e:
|
|
140
|
+
# Protocol violation: fail closed.
|
|
141
|
+
await self.stop()
|
|
142
|
+
raise RuntimeError("Invalid JSON received from subprocess") from e
|
|
143
|
+
|
|
144
|
+
async def _drain_stderr(self) -> None:
|
|
145
|
+
if not self.process or not self.process.stderr:
|
|
146
|
+
return
|
|
147
|
+
loop = asyncio.get_event_loop()
|
|
148
|
+
while True:
|
|
149
|
+
line = await loop.run_in_executor(None, self.process.stderr.readline)
|
|
150
|
+
if not line:
|
|
151
|
+
break
|
|
152
|
+
# Drain to avoid deadlock; stderr content may be sensitive.
|
|
153
|
+
# In production, route to a controlled logger sink if desired.
|
|
154
|
+
_ = line
|
|
155
|
+
|
|
156
|
+
def _handle_message(self, message: dict[str, object]) -> None:
|
|
157
|
+
"""Handle an incoming message from the subprocess."""
|
|
158
|
+
msg_id = message.get("id")
|
|
159
|
+
|
|
160
|
+
if msg_id and msg_id in self._pending_requests:
|
|
161
|
+
future = self._pending_requests.pop(msg_id)
|
|
162
|
+
if not future.done():
|
|
163
|
+
if message.get("type") == "error":
|
|
164
|
+
future.set_exception(Exception(message.get("error", "Unknown error")))
|
|
165
|
+
else:
|
|
166
|
+
future.set_result(message)
|
|
167
|
+
|
|
168
|
+
async def _wait_for_ready(self) -> None:
|
|
169
|
+
"""Wait for the ready message from the subprocess."""
|
|
170
|
+
if not self.process or not self.process.stdout:
|
|
171
|
+
raise RuntimeError("Process not started")
|
|
172
|
+
|
|
173
|
+
loop = asyncio.get_event_loop()
|
|
174
|
+
|
|
175
|
+
try:
|
|
176
|
+
line = await asyncio.wait_for(
|
|
177
|
+
loop.run_in_executor(None, self.process.stdout.readline),
|
|
178
|
+
timeout=self.timeout,
|
|
179
|
+
)
|
|
180
|
+
if not line:
|
|
181
|
+
raise RuntimeError("Process exited before sending ready message")
|
|
182
|
+
|
|
183
|
+
message = json.loads(line.decode("utf-8"))
|
|
184
|
+
if message.get("type") != "ready":
|
|
185
|
+
raise RuntimeError(f"Unexpected first message: {message.get('type')}")
|
|
186
|
+
|
|
187
|
+
self.manifest = message.get("manifest")
|
|
188
|
+
except asyncio.TimeoutError:
|
|
189
|
+
raise RuntimeError(f"Plugin startup timeout after {self.timeout}s")
|
|
190
|
+
|
|
191
|
+
async def send_request(self, request: dict[str, object]) -> dict[str, object]:
|
|
192
|
+
"""Send a request and wait for the response."""
|
|
193
|
+
if not self.process or not self.process.stdin:
|
|
194
|
+
raise RuntimeError("Bridge not started")
|
|
195
|
+
|
|
196
|
+
if len(self._pending_requests) >= self.max_pending_requests:
|
|
197
|
+
raise RuntimeError("Too many pending requests")
|
|
198
|
+
|
|
199
|
+
self._request_counter += 1
|
|
200
|
+
request_id = f"req_{self._request_counter}"
|
|
201
|
+
request["id"] = request_id
|
|
202
|
+
|
|
203
|
+
future: asyncio.Future[dict[str, object]] = asyncio.get_event_loop().create_future()
|
|
204
|
+
self._pending_requests[request_id] = future
|
|
205
|
+
|
|
206
|
+
json_line = json.dumps(request) + "\n"
|
|
207
|
+
self.process.stdin.write(json_line.encode("utf-8"))
|
|
208
|
+
self.process.stdin.flush()
|
|
209
|
+
|
|
210
|
+
try:
|
|
211
|
+
return await asyncio.wait_for(future, timeout=self.timeout)
|
|
212
|
+
except asyncio.TimeoutError:
|
|
213
|
+
self._pending_requests.pop(request_id, None)
|
|
214
|
+
raise RuntimeError(f"Request timeout for {request.get('type')}")
|
|
215
|
+
|
|
216
|
+
async def stop(self) -> None:
|
|
217
|
+
"""Stop the TypeScript bridge subprocess."""
|
|
218
|
+
if self._reader_task:
|
|
219
|
+
self._reader_task.cancel()
|
|
220
|
+
try:
|
|
221
|
+
await self._reader_task
|
|
222
|
+
except asyncio.CancelledError:
|
|
223
|
+
pass
|
|
224
|
+
|
|
225
|
+
if self._stderr_task:
|
|
226
|
+
self._stderr_task.cancel()
|
|
227
|
+
try:
|
|
228
|
+
await self._stderr_task
|
|
229
|
+
except asyncio.CancelledError:
|
|
230
|
+
pass
|
|
231
|
+
|
|
232
|
+
if self.process:
|
|
233
|
+
self.process.terminate()
|
|
234
|
+
try:
|
|
235
|
+
self.process.wait(timeout=5)
|
|
236
|
+
except subprocess.TimeoutExpired:
|
|
237
|
+
self.process.kill()
|
|
238
|
+
|
|
239
|
+
for fut in self._pending_requests.values():
|
|
240
|
+
if not fut.done():
|
|
241
|
+
fut.set_exception(RuntimeError("Bridge stopped"))
|
|
242
|
+
self._pending_requests.clear()
|
|
243
|
+
|
|
244
|
+
def get_manifest(self) -> dict[str, Any] | None:
|
|
245
|
+
"""Get the plugin manifest."""
|
|
246
|
+
return self.manifest
|
|
247
|
+
|
|
248
|
+
|
|
249
|
+
async def load_typescript_plugin(
|
|
250
|
+
plugin_path: str | Path,
|
|
251
|
+
*,
|
|
252
|
+
node_path: str = "node",
|
|
253
|
+
cwd: str | Path | None = None,
|
|
254
|
+
timeout: float = 30.0,
|
|
255
|
+
) -> Plugin:
|
|
256
|
+
"""
|
|
257
|
+
Load a TypeScript plugin and return an elizaOS Plugin interface.
|
|
258
|
+
|
|
259
|
+
Args:
|
|
260
|
+
plugin_path: Path to the TypeScript plugin.
|
|
261
|
+
node_path: Path to Node.js executable.
|
|
262
|
+
cwd: Working directory for the subprocess.
|
|
263
|
+
timeout: Request timeout in seconds.
|
|
264
|
+
|
|
265
|
+
Returns:
|
|
266
|
+
elizaOS Plugin instance.
|
|
267
|
+
"""
|
|
268
|
+
bridge = TypeScriptPluginBridge(
|
|
269
|
+
plugin_path,
|
|
270
|
+
node_path=node_path,
|
|
271
|
+
cwd=cwd,
|
|
272
|
+
timeout=timeout,
|
|
273
|
+
)
|
|
274
|
+
await bridge.start()
|
|
275
|
+
|
|
276
|
+
manifest = bridge.get_manifest()
|
|
277
|
+
if not manifest:
|
|
278
|
+
raise RuntimeError("Failed to get plugin manifest")
|
|
279
|
+
|
|
280
|
+
# Create action wrappers
|
|
281
|
+
actions: list[Action] = []
|
|
282
|
+
for action_def in manifest.get("actions", []):
|
|
283
|
+
action_name = action_def["name"]
|
|
284
|
+
|
|
285
|
+
def make_validate(name: str, b: TypeScriptPluginBridge) -> Callable[..., Awaitable[bool]]:
|
|
286
|
+
async def validate(runtime: Any, message: Memory, state: State | None) -> bool:
|
|
287
|
+
response = await b.send_request({
|
|
288
|
+
"type": "action.validate",
|
|
289
|
+
"action": name,
|
|
290
|
+
"memory": message.model_dump() if hasattr(message, "model_dump") else message,
|
|
291
|
+
"state": state.model_dump() if state and hasattr(state, "model_dump") else state,
|
|
292
|
+
})
|
|
293
|
+
return response.get("valid", False)
|
|
294
|
+
return validate
|
|
295
|
+
|
|
296
|
+
def make_handler(
|
|
297
|
+
name: str, b: TypeScriptPluginBridge
|
|
298
|
+
) -> Callable[..., Awaitable[ActionResult | None]]:
|
|
299
|
+
async def handler(
|
|
300
|
+
runtime: Any,
|
|
301
|
+
message: Memory,
|
|
302
|
+
state: State | None,
|
|
303
|
+
options: HandlerOptions | None,
|
|
304
|
+
callback: Any,
|
|
305
|
+
responses: Any,
|
|
306
|
+
) -> ActionResult | None:
|
|
307
|
+
response = await b.send_request({
|
|
308
|
+
"type": "action.invoke",
|
|
309
|
+
"action": name,
|
|
310
|
+
"memory": message.model_dump() if hasattr(message, "model_dump") else message,
|
|
311
|
+
"state": state.model_dump() if state and hasattr(state, "model_dump") else state,
|
|
312
|
+
"options": options.model_dump() if options else None,
|
|
313
|
+
})
|
|
314
|
+
result = response.get("result")
|
|
315
|
+
if not result:
|
|
316
|
+
return None
|
|
317
|
+
return ActionResult(**result)
|
|
318
|
+
return handler
|
|
319
|
+
|
|
320
|
+
validate_fn = make_validate(action_name, bridge)
|
|
321
|
+
handler_fn = make_handler(action_name, bridge)
|
|
322
|
+
|
|
323
|
+
actions.append(
|
|
324
|
+
Action(
|
|
325
|
+
name=action_name,
|
|
326
|
+
description=action_def.get("description", ""),
|
|
327
|
+
similes=action_def.get("similes"),
|
|
328
|
+
validate=validate_fn, # type: ignore
|
|
329
|
+
handler=handler_fn, # type: ignore
|
|
330
|
+
)
|
|
331
|
+
)
|
|
332
|
+
|
|
333
|
+
# Create provider wrappers
|
|
334
|
+
providers: list[Provider] = []
|
|
335
|
+
for provider_def in manifest.get("providers", []):
|
|
336
|
+
provider_name = provider_def["name"]
|
|
337
|
+
|
|
338
|
+
def make_get(name: str, b: TypeScriptPluginBridge) -> Callable[..., Awaitable[ProviderResult]]:
|
|
339
|
+
async def get(runtime: Any, message: Memory, state: State) -> ProviderResult:
|
|
340
|
+
response = await b.send_request({
|
|
341
|
+
"type": "provider.get",
|
|
342
|
+
"provider": name,
|
|
343
|
+
"memory": message.model_dump() if hasattr(message, "model_dump") else message,
|
|
344
|
+
"state": state.model_dump() if hasattr(state, "model_dump") else state,
|
|
345
|
+
})
|
|
346
|
+
result = response.get("result", {})
|
|
347
|
+
return ProviderResult(**result)
|
|
348
|
+
return get
|
|
349
|
+
|
|
350
|
+
get_fn = make_get(provider_name, bridge)
|
|
351
|
+
|
|
352
|
+
providers.append(
|
|
353
|
+
Provider(
|
|
354
|
+
name=provider_name,
|
|
355
|
+
description=provider_def.get("description"),
|
|
356
|
+
dynamic=provider_def.get("dynamic"),
|
|
357
|
+
position=provider_def.get("position"),
|
|
358
|
+
private=provider_def.get("private"),
|
|
359
|
+
get=get_fn, # type: ignore
|
|
360
|
+
)
|
|
361
|
+
)
|
|
362
|
+
|
|
363
|
+
# Create evaluator wrappers
|
|
364
|
+
evaluators: list[Evaluator] = []
|
|
365
|
+
for eval_def in manifest.get("evaluators", []):
|
|
366
|
+
eval_name = eval_def["name"]
|
|
367
|
+
|
|
368
|
+
def make_eval_validate(name: str, b: TypeScriptPluginBridge) -> Callable[..., Awaitable[bool]]:
|
|
369
|
+
async def validate(runtime: Any, message: Memory, state: State | None) -> bool:
|
|
370
|
+
response = await b.send_request({
|
|
371
|
+
"type": "action.validate",
|
|
372
|
+
"action": name,
|
|
373
|
+
"memory": message.model_dump() if hasattr(message, "model_dump") else message,
|
|
374
|
+
"state": state.model_dump() if state and hasattr(state, "model_dump") else state,
|
|
375
|
+
})
|
|
376
|
+
return response.get("valid", False)
|
|
377
|
+
return validate
|
|
378
|
+
|
|
379
|
+
def make_eval_handler(
|
|
380
|
+
name: str, b: TypeScriptPluginBridge
|
|
381
|
+
) -> Callable[..., Awaitable[ActionResult | None]]:
|
|
382
|
+
async def handler(
|
|
383
|
+
runtime: Any,
|
|
384
|
+
message: Memory,
|
|
385
|
+
state: State | None,
|
|
386
|
+
options: HandlerOptions | None,
|
|
387
|
+
callback: Any,
|
|
388
|
+
responses: Any,
|
|
389
|
+
) -> ActionResult | None:
|
|
390
|
+
response = await b.send_request({
|
|
391
|
+
"type": "evaluator.invoke",
|
|
392
|
+
"evaluator": name,
|
|
393
|
+
"memory": message.model_dump() if hasattr(message, "model_dump") else message,
|
|
394
|
+
"state": state.model_dump() if state and hasattr(state, "model_dump") else state,
|
|
395
|
+
})
|
|
396
|
+
result = response.get("result")
|
|
397
|
+
if not result:
|
|
398
|
+
return None
|
|
399
|
+
return ActionResult(**result)
|
|
400
|
+
return handler
|
|
401
|
+
|
|
402
|
+
validate_fn = make_eval_validate(eval_name, bridge)
|
|
403
|
+
handler_fn = make_eval_handler(eval_name, bridge)
|
|
404
|
+
|
|
405
|
+
evaluators.append(
|
|
406
|
+
Evaluator(
|
|
407
|
+
name=eval_name,
|
|
408
|
+
description=eval_def.get("description", ""),
|
|
409
|
+
always_run=eval_def.get("alwaysRun"),
|
|
410
|
+
similes=eval_def.get("similes"),
|
|
411
|
+
examples=[],
|
|
412
|
+
validate=validate_fn, # type: ignore
|
|
413
|
+
handler=handler_fn, # type: ignore
|
|
414
|
+
)
|
|
415
|
+
)
|
|
416
|
+
|
|
417
|
+
# Create init function
|
|
418
|
+
async def init(config: dict[str, str], runtime: Any) -> None:
|
|
419
|
+
await bridge.send_request({
|
|
420
|
+
"type": "plugin.init",
|
|
421
|
+
"config": config,
|
|
422
|
+
})
|
|
423
|
+
|
|
424
|
+
# Store bridge reference for cleanup
|
|
425
|
+
plugin = Plugin(
|
|
426
|
+
name=manifest["name"],
|
|
427
|
+
description=manifest["description"],
|
|
428
|
+
init=init,
|
|
429
|
+
config=manifest.get("config"),
|
|
430
|
+
dependencies=manifest.get("dependencies"),
|
|
431
|
+
actions=actions if actions else None,
|
|
432
|
+
providers=providers if providers else None,
|
|
433
|
+
evaluators=evaluators if evaluators else None,
|
|
434
|
+
)
|
|
435
|
+
|
|
436
|
+
# Attach bridge for cleanup
|
|
437
|
+
setattr(plugin, "_bridge", bridge)
|
|
438
|
+
|
|
439
|
+
return plugin
|
|
440
|
+
|
|
441
|
+
|
|
442
|
+
async def stop_typescript_plugin(plugin: Plugin) -> None:
|
|
443
|
+
"""Stop a TypeScript plugin bridge."""
|
|
444
|
+
bridge = getattr(plugin, "_bridge", None)
|
|
445
|
+
if bridge:
|
|
446
|
+
await bridge.stop()
|
|
447
|
+
|
|
448
|
+
|
|
449
|
+
|
|
450
|
+
|
|
451
|
+
|
|
452
|
+
|