@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,517 @@
|
|
|
1
|
+
"""
|
|
2
|
+
WASM Plugin Loader for elizaOS
|
|
3
|
+
|
|
4
|
+
This module provides utilities for loading WASM plugins into the Python runtime
|
|
5
|
+
via wasmtime or similar WASM runtimes.
|
|
6
|
+
"""
|
|
7
|
+
|
|
8
|
+
from __future__ import annotations
|
|
9
|
+
|
|
10
|
+
import json
|
|
11
|
+
from pathlib import Path
|
|
12
|
+
from typing import Any, Callable, Awaitable
|
|
13
|
+
|
|
14
|
+
# Try to import wasmtime, provide fallback if not available
|
|
15
|
+
try:
|
|
16
|
+
import wasmtime
|
|
17
|
+
HAS_WASMTIME = True
|
|
18
|
+
except ImportError:
|
|
19
|
+
HAS_WASMTIME = False
|
|
20
|
+
wasmtime = None # type: ignore
|
|
21
|
+
|
|
22
|
+
from elizaos.types.plugin import Plugin
|
|
23
|
+
from elizaos.types.memory import Memory
|
|
24
|
+
from elizaos.types.state import State
|
|
25
|
+
from elizaos.types.components import (
|
|
26
|
+
Action,
|
|
27
|
+
ActionResult,
|
|
28
|
+
Provider,
|
|
29
|
+
ProviderResult,
|
|
30
|
+
Evaluator,
|
|
31
|
+
HandlerOptions,
|
|
32
|
+
)
|
|
33
|
+
|
|
34
|
+
|
|
35
|
+
class WasmPluginLoader:
|
|
36
|
+
"""
|
|
37
|
+
WASM plugin loader for elizaOS.
|
|
38
|
+
|
|
39
|
+
Loads Rust or TypeScript plugins compiled to WASM and adapts them
|
|
40
|
+
to the Python Plugin interface.
|
|
41
|
+
|
|
42
|
+
Requires wasmtime-py: pip install wasmtime
|
|
43
|
+
|
|
44
|
+
The WASM plugin must export these functions:
|
|
45
|
+
- get_manifest() -> ptr (JSON string)
|
|
46
|
+
- init(config_ptr: i32, config_len: i32)
|
|
47
|
+
- validate_action(name_ptr, name_len, memory_ptr, memory_len, state_ptr, state_len) -> i32
|
|
48
|
+
- invoke_action(name_ptr, name_len, memory_ptr, memory_len, state_ptr, state_len, options_ptr, options_len) -> ptr
|
|
49
|
+
- get_provider(name_ptr, name_len, memory_ptr, memory_len, state_ptr, state_len) -> ptr
|
|
50
|
+
- validate_evaluator(name_ptr, name_len, memory_ptr, memory_len, state_ptr, state_len) -> i32
|
|
51
|
+
- invoke_evaluator(name_ptr, name_len, memory_ptr, memory_len, state_ptr, state_len) -> ptr
|
|
52
|
+
- alloc(size: i32) -> ptr
|
|
53
|
+
- dealloc(ptr: i32, size: i32)
|
|
54
|
+
"""
|
|
55
|
+
|
|
56
|
+
def __init__(
|
|
57
|
+
self,
|
|
58
|
+
wasm_path: str | Path,
|
|
59
|
+
*,
|
|
60
|
+
max_module_bytes: int | None = None,
|
|
61
|
+
max_memory_bytes: int | None = None,
|
|
62
|
+
fuel: int | None = None,
|
|
63
|
+
max_string_bytes: int = 1_000_000,
|
|
64
|
+
) -> None:
|
|
65
|
+
"""
|
|
66
|
+
Initialize the WASM plugin loader.
|
|
67
|
+
|
|
68
|
+
Args:
|
|
69
|
+
wasm_path: Path to the WASM file.
|
|
70
|
+
|
|
71
|
+
Raises:
|
|
72
|
+
ImportError: If wasmtime is not installed.
|
|
73
|
+
FileNotFoundError: If the WASM file doesn't exist.
|
|
74
|
+
"""
|
|
75
|
+
if not HAS_WASMTIME:
|
|
76
|
+
raise ImportError(
|
|
77
|
+
"wasmtime is required for WASM plugin loading. "
|
|
78
|
+
"Install with: pip install wasmtime"
|
|
79
|
+
)
|
|
80
|
+
|
|
81
|
+
self.wasm_path = Path(wasm_path)
|
|
82
|
+
if not self.wasm_path.exists():
|
|
83
|
+
raise FileNotFoundError(f"WASM file not found: {wasm_path}")
|
|
84
|
+
|
|
85
|
+
if max_module_bytes is not None:
|
|
86
|
+
size = self.wasm_path.stat().st_size
|
|
87
|
+
if size > max_module_bytes:
|
|
88
|
+
raise ValueError(
|
|
89
|
+
f"WASM module too large ({size} bytes > {max_module_bytes} bytes)"
|
|
90
|
+
)
|
|
91
|
+
|
|
92
|
+
self.max_memory_bytes = max_memory_bytes
|
|
93
|
+
self.max_string_bytes = max_string_bytes
|
|
94
|
+
|
|
95
|
+
# Initialize wasmtime engine and store
|
|
96
|
+
if fuel is not None:
|
|
97
|
+
config = wasmtime.Config()
|
|
98
|
+
config.consume_fuel = True
|
|
99
|
+
self.engine = wasmtime.Engine(config)
|
|
100
|
+
else:
|
|
101
|
+
self.engine = wasmtime.Engine()
|
|
102
|
+
self.store = wasmtime.Store(self.engine)
|
|
103
|
+
if fuel is not None:
|
|
104
|
+
self.store.add_fuel(fuel)
|
|
105
|
+
self.linker = wasmtime.Linker(self.engine)
|
|
106
|
+
|
|
107
|
+
# Load and instantiate the module
|
|
108
|
+
self._setup_imports()
|
|
109
|
+
self._load_module()
|
|
110
|
+
self.manifest: dict[str, Any] | None = None
|
|
111
|
+
|
|
112
|
+
def _setup_imports(self) -> None:
|
|
113
|
+
"""Set up the import functions for the WASM module."""
|
|
114
|
+
# WASI imports (minimal stubs)
|
|
115
|
+
wasi_config = wasmtime.WasiConfig()
|
|
116
|
+
wasi_config.inherit_stdout()
|
|
117
|
+
wasi_config.inherit_stderr()
|
|
118
|
+
self.store.set_wasi(wasi_config)
|
|
119
|
+
self.linker.define_wasi()
|
|
120
|
+
|
|
121
|
+
# Environment imports for console logging
|
|
122
|
+
@wasmtime.Func(self.store, wasmtime.FuncType([wasmtime.ValType.i32(), wasmtime.ValType.i32()], []))
|
|
123
|
+
def console_log(ptr: int, len_: int) -> None:
|
|
124
|
+
data = self._read_memory(ptr, len_)
|
|
125
|
+
print(data.decode("utf-8"))
|
|
126
|
+
|
|
127
|
+
@wasmtime.Func(self.store, wasmtime.FuncType([wasmtime.ValType.i32(), wasmtime.ValType.i32()], []))
|
|
128
|
+
def console_error(ptr: int, len_: int) -> None:
|
|
129
|
+
data = self._read_memory(ptr, len_)
|
|
130
|
+
print(f"[ERROR] {data.decode('utf-8')}")
|
|
131
|
+
|
|
132
|
+
self.linker.define(self.store, "env", "console_log", console_log)
|
|
133
|
+
self.linker.define(self.store, "env", "console_error", console_error)
|
|
134
|
+
|
|
135
|
+
def _load_module(self) -> None:
|
|
136
|
+
"""Load and instantiate the WASM module."""
|
|
137
|
+
with open(self.wasm_path, "rb") as f:
|
|
138
|
+
wasm_bytes = f.read()
|
|
139
|
+
|
|
140
|
+
self.module = wasmtime.Module(self.engine, wasm_bytes)
|
|
141
|
+
self.instance = self.linker.instantiate(self.store, self.module)
|
|
142
|
+
|
|
143
|
+
# Get memory export
|
|
144
|
+
memory_export = self.instance.exports(self.store).get("memory")
|
|
145
|
+
if memory_export is None:
|
|
146
|
+
raise RuntimeError("WASM module does not export 'memory'")
|
|
147
|
+
self.memory = memory_export
|
|
148
|
+
|
|
149
|
+
if self.max_memory_bytes is not None:
|
|
150
|
+
mem_data = self.memory.data_ptr(self.store)
|
|
151
|
+
if len(mem_data) > self.max_memory_bytes:
|
|
152
|
+
raise RuntimeError(
|
|
153
|
+
f"WASM memory too large ({len(mem_data)} bytes > {self.max_memory_bytes} bytes)"
|
|
154
|
+
)
|
|
155
|
+
|
|
156
|
+
def _read_memory(self, ptr: int, length: int) -> bytes:
|
|
157
|
+
"""Read bytes from WASM memory."""
|
|
158
|
+
data = self.memory.data_ptr(self.store)
|
|
159
|
+
return bytes(data[ptr:ptr + length])
|
|
160
|
+
|
|
161
|
+
def _write_memory(self, ptr: int, data: bytes) -> None:
|
|
162
|
+
"""Write bytes to WASM memory."""
|
|
163
|
+
mem_data = self.memory.data_ptr(self.store)
|
|
164
|
+
for i, byte in enumerate(data):
|
|
165
|
+
mem_data[ptr + i] = byte
|
|
166
|
+
|
|
167
|
+
def _read_string(self, ptr: int) -> str:
|
|
168
|
+
"""Read a null-terminated string from WASM memory."""
|
|
169
|
+
mem_data = self.memory.data_ptr(self.store)
|
|
170
|
+
end = ptr
|
|
171
|
+
limit = ptr + self.max_string_bytes
|
|
172
|
+
while end < len(mem_data) and end < limit and mem_data[end] != 0:
|
|
173
|
+
end += 1
|
|
174
|
+
if end >= len(mem_data) or end >= limit:
|
|
175
|
+
raise RuntimeError("WASM string exceeded maximum length or memory bounds")
|
|
176
|
+
return bytes(mem_data[ptr:end]).decode("utf-8")
|
|
177
|
+
|
|
178
|
+
def _call_with_string(self, func_name: str, *strings: str) -> str | None:
|
|
179
|
+
"""Call a WASM function with string arguments and get string result."""
|
|
180
|
+
exports = self.instance.exports(self.store)
|
|
181
|
+
func = exports.get(func_name)
|
|
182
|
+
if func is None:
|
|
183
|
+
raise RuntimeError(f"WASM function '{func_name}' not found")
|
|
184
|
+
|
|
185
|
+
alloc = exports.get("alloc")
|
|
186
|
+
dealloc = exports.get("dealloc")
|
|
187
|
+
if alloc is None or dealloc is None:
|
|
188
|
+
raise RuntimeError("WASM module must export 'alloc' and 'dealloc'")
|
|
189
|
+
|
|
190
|
+
# Allocate and write each string
|
|
191
|
+
ptrs_and_lens: list[tuple[int, int]] = []
|
|
192
|
+
for s in strings:
|
|
193
|
+
encoded = s.encode("utf-8")
|
|
194
|
+
ptr = alloc(self.store, len(encoded))
|
|
195
|
+
self._write_memory(ptr, encoded)
|
|
196
|
+
ptrs_and_lens.append((ptr, len(encoded)))
|
|
197
|
+
|
|
198
|
+
# Build arguments (alternating ptr, len for each string)
|
|
199
|
+
args = []
|
|
200
|
+
for ptr, length in ptrs_and_lens:
|
|
201
|
+
args.extend([ptr, length])
|
|
202
|
+
|
|
203
|
+
# Call the function
|
|
204
|
+
result_ptr = func(self.store, *args)
|
|
205
|
+
|
|
206
|
+
# Cleanup input strings
|
|
207
|
+
for ptr, length in ptrs_and_lens:
|
|
208
|
+
dealloc(self.store, ptr, length)
|
|
209
|
+
|
|
210
|
+
if result_ptr == 0:
|
|
211
|
+
return None
|
|
212
|
+
|
|
213
|
+
return self._read_string(result_ptr)
|
|
214
|
+
|
|
215
|
+
def get_manifest(self) -> dict[str, Any]:
|
|
216
|
+
"""Get the plugin manifest from the WASM module."""
|
|
217
|
+
if self.manifest is not None:
|
|
218
|
+
return self.manifest
|
|
219
|
+
|
|
220
|
+
exports = self.instance.exports(self.store)
|
|
221
|
+
get_manifest_fn = exports.get("get_manifest")
|
|
222
|
+
if get_manifest_fn is None:
|
|
223
|
+
raise RuntimeError("WASM module must export 'get_manifest'")
|
|
224
|
+
|
|
225
|
+
result_ptr = get_manifest_fn(self.store)
|
|
226
|
+
if result_ptr == 0:
|
|
227
|
+
raise RuntimeError("get_manifest returned null")
|
|
228
|
+
|
|
229
|
+
json_str = self._read_string(result_ptr)
|
|
230
|
+
self.manifest = json.loads(json_str)
|
|
231
|
+
return self.manifest
|
|
232
|
+
|
|
233
|
+
def init(self, config: dict[str, str]) -> None:
|
|
234
|
+
"""Initialize the plugin with configuration."""
|
|
235
|
+
exports = self.instance.exports(self.store)
|
|
236
|
+
init_fn = exports.get("init")
|
|
237
|
+
if init_fn is None:
|
|
238
|
+
return # init is optional
|
|
239
|
+
|
|
240
|
+
alloc = exports.get("alloc")
|
|
241
|
+
if alloc is None:
|
|
242
|
+
return
|
|
243
|
+
|
|
244
|
+
config_json = json.dumps(config).encode("utf-8")
|
|
245
|
+
ptr = alloc(self.store, len(config_json))
|
|
246
|
+
self._write_memory(ptr, config_json)
|
|
247
|
+
init_fn(self.store, ptr, len(config_json))
|
|
248
|
+
|
|
249
|
+
def validate_action(self, name: str, memory: Memory, state: State | None) -> bool:
|
|
250
|
+
"""Validate an action."""
|
|
251
|
+
result = self._call_validate("validate_action", name, memory, state)
|
|
252
|
+
return result != 0
|
|
253
|
+
|
|
254
|
+
def invoke_action(
|
|
255
|
+
self, name: str, memory: Memory, state: State | None, options: dict[str, Any] | None
|
|
256
|
+
) -> ActionResult:
|
|
257
|
+
"""Invoke an action."""
|
|
258
|
+
memory_json = json.dumps(memory.model_dump() if hasattr(memory, "model_dump") else memory)
|
|
259
|
+
state_json = json.dumps(
|
|
260
|
+
state.model_dump() if state and hasattr(state, "model_dump") else state
|
|
261
|
+
)
|
|
262
|
+
options_json = json.dumps(options or {})
|
|
263
|
+
|
|
264
|
+
result_json = self._call_with_string(
|
|
265
|
+
"invoke_action", name, memory_json, state_json, options_json
|
|
266
|
+
)
|
|
267
|
+
if not result_json:
|
|
268
|
+
return ActionResult(success=False, error="No result from WASM plugin")
|
|
269
|
+
|
|
270
|
+
result_data = json.loads(result_json)
|
|
271
|
+
return ActionResult(**result_data)
|
|
272
|
+
|
|
273
|
+
def get_provider(self, name: str, memory: Memory, state: State) -> ProviderResult:
|
|
274
|
+
"""Get provider data."""
|
|
275
|
+
memory_json = json.dumps(memory.model_dump() if hasattr(memory, "model_dump") else memory)
|
|
276
|
+
state_json = json.dumps(state.model_dump() if hasattr(state, "model_dump") else state)
|
|
277
|
+
|
|
278
|
+
result_json = self._call_with_string("get_provider", name, memory_json, state_json)
|
|
279
|
+
if not result_json:
|
|
280
|
+
return ProviderResult()
|
|
281
|
+
|
|
282
|
+
result_data = json.loads(result_json)
|
|
283
|
+
return ProviderResult(**result_data)
|
|
284
|
+
|
|
285
|
+
def validate_evaluator(self, name: str, memory: Memory, state: State | None) -> bool:
|
|
286
|
+
"""Validate an evaluator."""
|
|
287
|
+
result = self._call_validate("validate_evaluator", name, memory, state)
|
|
288
|
+
return result != 0
|
|
289
|
+
|
|
290
|
+
def invoke_evaluator(self, name: str, memory: Memory, state: State | None) -> ActionResult | None:
|
|
291
|
+
"""Invoke an evaluator."""
|
|
292
|
+
memory_json = json.dumps(memory.model_dump() if hasattr(memory, "model_dump") else memory)
|
|
293
|
+
state_json = json.dumps(
|
|
294
|
+
state.model_dump() if state and hasattr(state, "model_dump") else state
|
|
295
|
+
)
|
|
296
|
+
|
|
297
|
+
result_json = self._call_with_string("invoke_evaluator", name, memory_json, state_json)
|
|
298
|
+
if not result_json or result_json == "null":
|
|
299
|
+
return None
|
|
300
|
+
|
|
301
|
+
result_data = json.loads(result_json)
|
|
302
|
+
return ActionResult(**result_data)
|
|
303
|
+
|
|
304
|
+
def _call_validate(
|
|
305
|
+
self, func_name: str, name: str, memory: Memory, state: State | None
|
|
306
|
+
) -> int:
|
|
307
|
+
"""Call a validation function."""
|
|
308
|
+
exports = self.instance.exports(self.store)
|
|
309
|
+
func = exports.get(func_name)
|
|
310
|
+
alloc = exports.get("alloc")
|
|
311
|
+
dealloc = exports.get("dealloc")
|
|
312
|
+
|
|
313
|
+
if func is None or alloc is None or dealloc is None:
|
|
314
|
+
return 0
|
|
315
|
+
|
|
316
|
+
name_bytes = name.encode("utf-8")
|
|
317
|
+
memory_json = json.dumps(
|
|
318
|
+
memory.model_dump() if hasattr(memory, "model_dump") else memory
|
|
319
|
+
).encode("utf-8")
|
|
320
|
+
state_json = json.dumps(
|
|
321
|
+
state.model_dump() if state and hasattr(state, "model_dump") else state
|
|
322
|
+
).encode("utf-8")
|
|
323
|
+
|
|
324
|
+
name_ptr = alloc(self.store, len(name_bytes))
|
|
325
|
+
mem_ptr = alloc(self.store, len(memory_json))
|
|
326
|
+
state_ptr = alloc(self.store, len(state_json))
|
|
327
|
+
|
|
328
|
+
self._write_memory(name_ptr, name_bytes)
|
|
329
|
+
self._write_memory(mem_ptr, memory_json)
|
|
330
|
+
self._write_memory(state_ptr, state_json)
|
|
331
|
+
|
|
332
|
+
result = func(
|
|
333
|
+
self.store,
|
|
334
|
+
name_ptr, len(name_bytes),
|
|
335
|
+
mem_ptr, len(memory_json),
|
|
336
|
+
state_ptr, len(state_json),
|
|
337
|
+
)
|
|
338
|
+
|
|
339
|
+
dealloc(self.store, name_ptr, len(name_bytes))
|
|
340
|
+
dealloc(self.store, mem_ptr, len(memory_json))
|
|
341
|
+
dealloc(self.store, state_ptr, len(state_json))
|
|
342
|
+
|
|
343
|
+
return result
|
|
344
|
+
|
|
345
|
+
|
|
346
|
+
def load_wasm_plugin(
|
|
347
|
+
wasm_path: str | Path,
|
|
348
|
+
manifest_path: str | Path | None = None,
|
|
349
|
+
*,
|
|
350
|
+
max_module_bytes: int | None = None,
|
|
351
|
+
max_memory_bytes: int | None = None,
|
|
352
|
+
fuel: int | None = None,
|
|
353
|
+
) -> Plugin:
|
|
354
|
+
"""
|
|
355
|
+
Load a WASM plugin and return an elizaOS Plugin interface.
|
|
356
|
+
|
|
357
|
+
Args:
|
|
358
|
+
wasm_path: Path to the WASM file.
|
|
359
|
+
manifest_path: Optional path to external manifest JSON (uses embedded if not provided).
|
|
360
|
+
|
|
361
|
+
Returns:
|
|
362
|
+
elizaOS Plugin instance.
|
|
363
|
+
"""
|
|
364
|
+
loader = WasmPluginLoader(
|
|
365
|
+
wasm_path,
|
|
366
|
+
max_module_bytes=max_module_bytes,
|
|
367
|
+
max_memory_bytes=max_memory_bytes,
|
|
368
|
+
fuel=fuel,
|
|
369
|
+
)
|
|
370
|
+
|
|
371
|
+
# Get manifest
|
|
372
|
+
if manifest_path:
|
|
373
|
+
with open(manifest_path) as f:
|
|
374
|
+
manifest = json.load(f)
|
|
375
|
+
else:
|
|
376
|
+
manifest = loader.get_manifest()
|
|
377
|
+
|
|
378
|
+
# Create action wrappers
|
|
379
|
+
actions: list[Action] = []
|
|
380
|
+
for action_def in manifest.get("actions", []):
|
|
381
|
+
action_name = action_def["name"]
|
|
382
|
+
|
|
383
|
+
def make_validate(name: str) -> Callable[..., Awaitable[bool]]:
|
|
384
|
+
async def validate(runtime: Any, message: Memory, state: State | None) -> bool:
|
|
385
|
+
return loader.validate_action(name, message, state)
|
|
386
|
+
return validate
|
|
387
|
+
|
|
388
|
+
def make_handler(name: str) -> Callable[..., Awaitable[ActionResult | None]]:
|
|
389
|
+
async def handler(
|
|
390
|
+
runtime: Any,
|
|
391
|
+
message: Memory,
|
|
392
|
+
state: State | None,
|
|
393
|
+
options: HandlerOptions | None,
|
|
394
|
+
callback: Any,
|
|
395
|
+
responses: Any,
|
|
396
|
+
) -> ActionResult | None:
|
|
397
|
+
return loader.invoke_action(
|
|
398
|
+
name, message, state, options.model_dump() if options else None
|
|
399
|
+
)
|
|
400
|
+
return handler
|
|
401
|
+
|
|
402
|
+
validate_fn = make_validate(action_name)
|
|
403
|
+
handler_fn = make_handler(action_name)
|
|
404
|
+
|
|
405
|
+
actions.append(
|
|
406
|
+
Action(
|
|
407
|
+
name=action_name,
|
|
408
|
+
description=action_def.get("description", ""),
|
|
409
|
+
similes=action_def.get("similes"),
|
|
410
|
+
examples=action_def.get("examples"),
|
|
411
|
+
validate=validate_fn, # type: ignore
|
|
412
|
+
handler=handler_fn, # type: ignore
|
|
413
|
+
)
|
|
414
|
+
)
|
|
415
|
+
|
|
416
|
+
# Create provider wrappers
|
|
417
|
+
providers: list[Provider] = []
|
|
418
|
+
for provider_def in manifest.get("providers", []):
|
|
419
|
+
provider_name = provider_def["name"]
|
|
420
|
+
|
|
421
|
+
def make_get(name: str) -> Callable[..., Awaitable[ProviderResult]]:
|
|
422
|
+
async def get(runtime: Any, message: Memory, state: State) -> ProviderResult:
|
|
423
|
+
return loader.get_provider(name, message, state)
|
|
424
|
+
return get
|
|
425
|
+
|
|
426
|
+
get_fn = make_get(provider_name)
|
|
427
|
+
|
|
428
|
+
providers.append(
|
|
429
|
+
Provider(
|
|
430
|
+
name=provider_name,
|
|
431
|
+
description=provider_def.get("description"),
|
|
432
|
+
dynamic=provider_def.get("dynamic"),
|
|
433
|
+
position=provider_def.get("position"),
|
|
434
|
+
private=provider_def.get("private"),
|
|
435
|
+
get=get_fn, # type: ignore
|
|
436
|
+
)
|
|
437
|
+
)
|
|
438
|
+
|
|
439
|
+
# Create evaluator wrappers
|
|
440
|
+
evaluators: list[Evaluator] = []
|
|
441
|
+
for eval_def in manifest.get("evaluators", []):
|
|
442
|
+
eval_name = eval_def["name"]
|
|
443
|
+
|
|
444
|
+
def make_eval_validate(name: str) -> Callable[..., Awaitable[bool]]:
|
|
445
|
+
async def validate(runtime: Any, message: Memory, state: State | None) -> bool:
|
|
446
|
+
return loader.validate_evaluator(name, message, state)
|
|
447
|
+
return validate
|
|
448
|
+
|
|
449
|
+
def make_eval_handler(name: str) -> Callable[..., Awaitable[ActionResult | None]]:
|
|
450
|
+
async def handler(
|
|
451
|
+
runtime: Any,
|
|
452
|
+
message: Memory,
|
|
453
|
+
state: State | None,
|
|
454
|
+
options: HandlerOptions | None,
|
|
455
|
+
callback: Any,
|
|
456
|
+
responses: Any,
|
|
457
|
+
) -> ActionResult | None:
|
|
458
|
+
return loader.invoke_evaluator(name, message, state)
|
|
459
|
+
return handler
|
|
460
|
+
|
|
461
|
+
validate_fn = make_eval_validate(eval_name)
|
|
462
|
+
handler_fn = make_eval_handler(eval_name)
|
|
463
|
+
|
|
464
|
+
evaluators.append(
|
|
465
|
+
Evaluator(
|
|
466
|
+
name=eval_name,
|
|
467
|
+
description=eval_def.get("description", ""),
|
|
468
|
+
always_run=eval_def.get("alwaysRun"),
|
|
469
|
+
similes=eval_def.get("similes"),
|
|
470
|
+
examples=[],
|
|
471
|
+
validate=validate_fn, # type: ignore
|
|
472
|
+
handler=handler_fn, # type: ignore
|
|
473
|
+
)
|
|
474
|
+
)
|
|
475
|
+
|
|
476
|
+
# Create init function
|
|
477
|
+
async def init(config: dict[str, str], runtime: Any) -> None:
|
|
478
|
+
loader.init(config)
|
|
479
|
+
|
|
480
|
+
return Plugin(
|
|
481
|
+
name=manifest["name"],
|
|
482
|
+
description=manifest["description"],
|
|
483
|
+
init=init,
|
|
484
|
+
config=manifest.get("config"),
|
|
485
|
+
dependencies=manifest.get("dependencies"),
|
|
486
|
+
actions=actions if actions else None,
|
|
487
|
+
providers=providers if providers else None,
|
|
488
|
+
evaluators=evaluators if evaluators else None,
|
|
489
|
+
)
|
|
490
|
+
|
|
491
|
+
|
|
492
|
+
def validate_wasm_plugin(wasm_path: str | Path) -> dict[str, Any]:
|
|
493
|
+
"""
|
|
494
|
+
Validate a WASM plugin without fully loading it.
|
|
495
|
+
|
|
496
|
+
Args:
|
|
497
|
+
wasm_path: Path to the WASM file.
|
|
498
|
+
|
|
499
|
+
Returns:
|
|
500
|
+
Dict with 'valid', 'manifest', and optionally 'error' keys.
|
|
501
|
+
"""
|
|
502
|
+
try:
|
|
503
|
+
loader = WasmPluginLoader(wasm_path)
|
|
504
|
+
manifest = loader.get_manifest()
|
|
505
|
+
|
|
506
|
+
if not manifest.get("name") or not manifest.get("description"):
|
|
507
|
+
return {"valid": False, "error": "Missing required manifest fields"}
|
|
508
|
+
|
|
509
|
+
return {"valid": True, "manifest": manifest}
|
|
510
|
+
except Exception as e:
|
|
511
|
+
return {"valid": False, "error": str(e)}
|
|
512
|
+
|
|
513
|
+
|
|
514
|
+
|
|
515
|
+
|
|
516
|
+
|
|
517
|
+
|