@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.
Files changed (42) hide show
  1. package/LICENSE +21 -0
  2. package/README.md +436 -0
  3. package/dist/packages/interop/tsconfig.tsbuildinfo +1 -0
  4. package/dist/tsconfig.tsbuildinfo +1 -0
  5. package/dist/typescript/index.d.ts +33 -0
  6. package/dist/typescript/index.d.ts.map +1 -0
  7. package/dist/typescript/index.js +121 -0
  8. package/dist/typescript/python-bridge.d.ts +80 -0
  9. package/dist/typescript/python-bridge.d.ts.map +1 -0
  10. package/dist/typescript/python-bridge.js +334 -0
  11. package/dist/typescript/types.d.ts +301 -0
  12. package/dist/typescript/types.d.ts.map +1 -0
  13. package/dist/typescript/types.js +10 -0
  14. package/dist/typescript/wasm-loader.d.ts +32 -0
  15. package/dist/typescript/wasm-loader.d.ts.map +1 -0
  16. package/dist/typescript/wasm-loader.js +269 -0
  17. package/package.json +43 -0
  18. package/python/__init__.py +50 -0
  19. package/python/__pycache__/__init__.cpython-313.pyc +0 -0
  20. package/python/__pycache__/rust_ffi.cpython-313.pyc +0 -0
  21. package/python/__pycache__/ts_bridge.cpython-313.pyc +0 -0
  22. package/python/__pycache__/wasm_loader.cpython-313.pyc +0 -0
  23. package/python/bridge_server.py +505 -0
  24. package/python/rust_ffi.py +418 -0
  25. package/python/tests/__init__.py +1 -0
  26. package/python/tests/__pycache__/__init__.cpython-313.pyc +0 -0
  27. package/python/tests/__pycache__/test_bridge_server.cpython-313-pytest-9.0.2.pyc +0 -0
  28. package/python/tests/__pycache__/test_interop.cpython-313-pytest-9.0.2.pyc +0 -0
  29. package/python/tests/__pycache__/test_rust_ffi.cpython-313-pytest-9.0.2.pyc +0 -0
  30. package/python/tests/__pycache__/test_rust_ffi_loader.cpython-313-pytest-9.0.2.pyc +0 -0
  31. package/python/tests/test_bridge_server.py +525 -0
  32. package/python/tests/test_interop.py +319 -0
  33. package/python/tests/test_rust_ffi.py +352 -0
  34. package/python/tests/test_rust_ffi_loader.py +71 -0
  35. package/python/ts_bridge.py +452 -0
  36. package/python/ts_bridge_runner.mjs +284 -0
  37. package/python/wasm_loader.py +517 -0
  38. package/rust/ffi_exports.rs +362 -0
  39. package/rust/mod.rs +21 -0
  40. package/rust/py_loader.rs +412 -0
  41. package/rust/ts_loader.rs +467 -0
  42. package/rust/wasm_plugin.rs +320 -0
@@ -0,0 +1,418 @@
1
+ """
2
+ Rust Plugin FFI Loader for elizaOS
3
+
4
+ This module provides utilities for loading Rust plugins into the Python runtime
5
+ via ctypes/cffi FFI bindings.
6
+ """
7
+
8
+ from __future__ import annotations
9
+
10
+ import ctypes
11
+ import json
12
+ import platform
13
+ from pathlib import Path
14
+ from typing import Any, Callable, Awaitable
15
+
16
+ from elizaos.types.plugin import Plugin
17
+ from elizaos.types.memory import Memory
18
+ from elizaos.types.state import State
19
+ from elizaos.types.components import (
20
+ Action,
21
+ ActionResult,
22
+ Provider,
23
+ ProviderResult,
24
+ Evaluator,
25
+ HandlerOptions,
26
+ )
27
+
28
+
29
+ def get_lib_extension() -> str:
30
+ """Get the shared library extension for the current platform."""
31
+ system = platform.system()
32
+ if system == "Darwin":
33
+ return ".dylib"
34
+ elif system == "Windows":
35
+ return ".dll"
36
+ else:
37
+ return ".so"
38
+
39
+
40
+ def get_lib_prefix() -> str:
41
+ """Get the shared library prefix for the current platform."""
42
+ system = platform.system()
43
+ if system == "Windows":
44
+ return ""
45
+ else:
46
+ return "lib"
47
+
48
+
49
+ class RustPluginFFI:
50
+ """
51
+ FFI wrapper for Rust plugins.
52
+
53
+ The Rust plugin must export these functions:
54
+ - elizaos_get_manifest() -> *const c_char
55
+ - elizaos_init(config_json: *const c_char) -> c_int
56
+ - elizaos_validate_action(name: *const c_char, memory: *const c_char, state: *const c_char) -> c_int
57
+ - elizaos_invoke_action(name: *const c_char, memory: *const c_char, state: *const c_char, options: *const c_char) -> *const c_char
58
+ - elizaos_get_provider(name: *const c_char, memory: *const c_char, state: *const c_char) -> *const c_char
59
+ - elizaos_validate_evaluator(name: *const c_char, memory: *const c_char, state: *const c_char) -> c_int
60
+ - elizaos_invoke_evaluator(name: *const c_char, memory: *const c_char, state: *const c_char) -> *const c_char
61
+ - elizaos_free_string(ptr: *const c_char) -> void
62
+ """
63
+
64
+ def __init__(self, lib_path: str | Path) -> None:
65
+ """
66
+ Initialize the FFI wrapper.
67
+
68
+ Args:
69
+ lib_path: Path to the shared library (.so/.dylib/.dll)
70
+ """
71
+ self.lib_path = Path(lib_path)
72
+ if not self.lib_path.exists():
73
+ raise FileNotFoundError(f"Shared library not found: {lib_path}")
74
+
75
+ # Load the library
76
+ self.lib = ctypes.CDLL(str(self.lib_path))
77
+ self._setup_bindings()
78
+ self.manifest: dict[str, Any] | None = None
79
+
80
+ def _setup_bindings(self) -> None:
81
+ """Set up ctypes function bindings."""
82
+ # elizaos_get_manifest() -> *const c_char
83
+ self.lib.elizaos_get_manifest.argtypes = []
84
+ self.lib.elizaos_get_manifest.restype = ctypes.c_char_p
85
+
86
+ # elizaos_init(config_json: *const c_char) -> c_int
87
+ self.lib.elizaos_init.argtypes = [ctypes.c_char_p]
88
+ self.lib.elizaos_init.restype = ctypes.c_int
89
+
90
+ # elizaos_validate_action(...)
91
+ self.lib.elizaos_validate_action.argtypes = [
92
+ ctypes.c_char_p, # action name
93
+ ctypes.c_char_p, # memory json
94
+ ctypes.c_char_p, # state json
95
+ ]
96
+ self.lib.elizaos_validate_action.restype = ctypes.c_int
97
+
98
+ # elizaos_invoke_action(...)
99
+ self.lib.elizaos_invoke_action.argtypes = [
100
+ ctypes.c_char_p, # action name
101
+ ctypes.c_char_p, # memory json
102
+ ctypes.c_char_p, # state json
103
+ ctypes.c_char_p, # options json
104
+ ]
105
+ self.lib.elizaos_invoke_action.restype = ctypes.c_char_p
106
+
107
+ # elizaos_get_provider(...)
108
+ self.lib.elizaos_get_provider.argtypes = [
109
+ ctypes.c_char_p, # provider name
110
+ ctypes.c_char_p, # memory json
111
+ ctypes.c_char_p, # state json
112
+ ]
113
+ self.lib.elizaos_get_provider.restype = ctypes.c_char_p
114
+
115
+ # elizaos_validate_evaluator(...)
116
+ self.lib.elizaos_validate_evaluator.argtypes = [
117
+ ctypes.c_char_p, # evaluator name
118
+ ctypes.c_char_p, # memory json
119
+ ctypes.c_char_p, # state json
120
+ ]
121
+ self.lib.elizaos_validate_evaluator.restype = ctypes.c_int
122
+
123
+ # elizaos_invoke_evaluator(...)
124
+ self.lib.elizaos_invoke_evaluator.argtypes = [
125
+ ctypes.c_char_p, # evaluator name
126
+ ctypes.c_char_p, # memory json
127
+ ctypes.c_char_p, # state json
128
+ ]
129
+ self.lib.elizaos_invoke_evaluator.restype = ctypes.c_char_p
130
+
131
+ # elizaos_free_string(ptr: *const c_char)
132
+ self.lib.elizaos_free_string.argtypes = [ctypes.c_char_p]
133
+ self.lib.elizaos_free_string.restype = None
134
+
135
+ def _to_c_string(self, s: str | None) -> ctypes.c_char_p:
136
+ """Convert Python string to C string."""
137
+ if s is None:
138
+ return ctypes.c_char_p(None)
139
+ return ctypes.c_char_p(s.encode("utf-8"))
140
+
141
+ def _from_c_string(self, ptr: ctypes.c_char_p) -> str | None:
142
+ """Convert C string to Python string and free the C memory."""
143
+ if not ptr:
144
+ return None
145
+ result = ptr.decode("utf-8")
146
+ self.lib.elizaos_free_string(ptr)
147
+ return result
148
+
149
+ def get_manifest(self) -> dict[str, Any]:
150
+ """Get the plugin manifest."""
151
+ if self.manifest:
152
+ return self.manifest
153
+
154
+ ptr = self.lib.elizaos_get_manifest()
155
+ json_str = self._from_c_string(ptr)
156
+ if not json_str:
157
+ raise RuntimeError("Failed to get manifest from Rust plugin")
158
+
159
+ self.manifest = json.loads(json_str)
160
+ return self.manifest
161
+
162
+ def init(self, config: dict[str, str]) -> None:
163
+ """Initialize the plugin with configuration."""
164
+ config_json = json.dumps(config)
165
+ result = self.lib.elizaos_init(self._to_c_string(config_json))
166
+ if result != 0:
167
+ raise RuntimeError(f"Plugin initialization failed with code {result}")
168
+
169
+ def validate_action(self, name: str, memory: Memory, state: State | None) -> bool:
170
+ """Validate an action."""
171
+ memory_json = json.dumps(memory.model_dump() if hasattr(memory, "model_dump") else memory)
172
+ state_json = json.dumps(
173
+ state.model_dump() if state and hasattr(state, "model_dump") else state
174
+ )
175
+
176
+ result = self.lib.elizaos_validate_action(
177
+ self._to_c_string(name),
178
+ self._to_c_string(memory_json),
179
+ self._to_c_string(state_json),
180
+ )
181
+ return result != 0
182
+
183
+ def invoke_action(
184
+ self, name: str, memory: Memory, state: State | None, options: dict[str, Any] | None
185
+ ) -> ActionResult:
186
+ """Invoke an action."""
187
+ memory_json = json.dumps(memory.model_dump() if hasattr(memory, "model_dump") else memory)
188
+ state_json = json.dumps(
189
+ state.model_dump() if state and hasattr(state, "model_dump") else state
190
+ )
191
+ options_json = json.dumps(options or {})
192
+
193
+ result_ptr = self.lib.elizaos_invoke_action(
194
+ self._to_c_string(name),
195
+ self._to_c_string(memory_json),
196
+ self._to_c_string(state_json),
197
+ self._to_c_string(options_json),
198
+ )
199
+ result_json = self._from_c_string(result_ptr)
200
+ if not result_json:
201
+ return ActionResult(success=False, error="No result from Rust plugin")
202
+
203
+ result_data = json.loads(result_json)
204
+ return ActionResult(**result_data)
205
+
206
+ def get_provider(self, name: str, memory: Memory, state: State) -> ProviderResult:
207
+ """Get provider data."""
208
+ memory_json = json.dumps(memory.model_dump() if hasattr(memory, "model_dump") else memory)
209
+ state_json = json.dumps(state.model_dump() if hasattr(state, "model_dump") else state)
210
+
211
+ result_ptr = self.lib.elizaos_get_provider(
212
+ self._to_c_string(name),
213
+ self._to_c_string(memory_json),
214
+ self._to_c_string(state_json),
215
+ )
216
+ result_json = self._from_c_string(result_ptr)
217
+ if not result_json:
218
+ return ProviderResult()
219
+
220
+ result_data = json.loads(result_json)
221
+ return ProviderResult(**result_data)
222
+
223
+ def validate_evaluator(self, name: str, memory: Memory, state: State | None) -> bool:
224
+ """Validate an evaluator."""
225
+ memory_json = json.dumps(memory.model_dump() if hasattr(memory, "model_dump") else memory)
226
+ state_json = json.dumps(
227
+ state.model_dump() if state and hasattr(state, "model_dump") else state
228
+ )
229
+
230
+ result = self.lib.elizaos_validate_evaluator(
231
+ self._to_c_string(name),
232
+ self._to_c_string(memory_json),
233
+ self._to_c_string(state_json),
234
+ )
235
+ return result != 0
236
+
237
+ def invoke_evaluator(self, name: str, memory: Memory, state: State | None) -> ActionResult | None:
238
+ """Invoke an evaluator."""
239
+ memory_json = json.dumps(memory.model_dump() if hasattr(memory, "model_dump") else memory)
240
+ state_json = json.dumps(
241
+ state.model_dump() if state and hasattr(state, "model_dump") else state
242
+ )
243
+
244
+ result_ptr = self.lib.elizaos_invoke_evaluator(
245
+ self._to_c_string(name),
246
+ self._to_c_string(memory_json),
247
+ self._to_c_string(state_json),
248
+ )
249
+ result_json = self._from_c_string(result_ptr)
250
+ if not result_json or result_json == "null":
251
+ return None
252
+
253
+ result_data = json.loads(result_json)
254
+ return ActionResult(**result_data)
255
+
256
+
257
+ def load_rust_plugin(lib_path: str | Path) -> Plugin:
258
+ """
259
+ Load a Rust plugin from a shared library.
260
+
261
+ Args:
262
+ lib_path: Path to the shared library
263
+
264
+ Returns:
265
+ elizaOS Plugin instance
266
+ """
267
+ ffi = RustPluginFFI(lib_path)
268
+ manifest = ffi.get_manifest()
269
+
270
+ # Create action wrappers
271
+ actions: list[Action] = []
272
+ for action_def in manifest.get("actions", []):
273
+ action_name = action_def["name"]
274
+
275
+ def make_validate(name: str) -> Callable[..., Awaitable[bool]]:
276
+ async def validate(runtime: Any, message: Memory, state: State | None) -> bool:
277
+ return ffi.validate_action(name, message, state)
278
+
279
+ return validate
280
+
281
+ def make_handler(name: str) -> Callable[..., Awaitable[ActionResult | None]]:
282
+ async def handler(
283
+ runtime: Any,
284
+ message: Memory,
285
+ state: State | None,
286
+ options: HandlerOptions | None,
287
+ callback: Any,
288
+ responses: Any,
289
+ ) -> ActionResult | None:
290
+ return ffi.invoke_action(
291
+ name, message, state, options.model_dump() if options else None
292
+ )
293
+
294
+ return handler
295
+
296
+ validate_fn = make_validate(action_name)
297
+ handler_fn = make_handler(action_name)
298
+
299
+ actions.append(
300
+ Action(
301
+ name=action_name,
302
+ description=action_def.get("description", ""),
303
+ similes=action_def.get("similes"),
304
+ examples=action_def.get("examples"),
305
+ validate=validate_fn, # type: ignore
306
+ handler=handler_fn, # type: ignore
307
+ )
308
+ )
309
+
310
+ # Create provider wrappers
311
+ providers: list[Provider] = []
312
+ for provider_def in manifest.get("providers", []):
313
+ provider_name = provider_def["name"]
314
+
315
+ def make_get(name: str) -> Callable[..., Awaitable[ProviderResult]]:
316
+ async def get(runtime: Any, message: Memory, state: State) -> ProviderResult:
317
+ return ffi.get_provider(name, message, state)
318
+
319
+ return get
320
+
321
+ get_fn = make_get(provider_name)
322
+
323
+ providers.append(
324
+ Provider(
325
+ name=provider_name,
326
+ description=provider_def.get("description"),
327
+ dynamic=provider_def.get("dynamic"),
328
+ position=provider_def.get("position"),
329
+ private=provider_def.get("private"),
330
+ get=get_fn, # type: ignore
331
+ )
332
+ )
333
+
334
+ # Create evaluator wrappers
335
+ evaluators: list[Evaluator] = []
336
+ for eval_def in manifest.get("evaluators", []):
337
+ eval_name = eval_def["name"]
338
+
339
+ def make_eval_validate(name: str) -> Callable[..., Awaitable[bool]]:
340
+ async def validate(runtime: Any, message: Memory, state: State | None) -> bool:
341
+ return ffi.validate_evaluator(name, message, state)
342
+
343
+ return validate
344
+
345
+ def make_eval_handler(name: str) -> Callable[..., Awaitable[ActionResult | None]]:
346
+ async def handler(
347
+ runtime: Any,
348
+ message: Memory,
349
+ state: State | None,
350
+ options: HandlerOptions | None,
351
+ callback: Any,
352
+ responses: Any,
353
+ ) -> ActionResult | None:
354
+ return ffi.invoke_evaluator(name, message, state)
355
+
356
+ return handler
357
+
358
+ validate_fn = make_eval_validate(eval_name)
359
+ handler_fn = make_eval_handler(eval_name)
360
+
361
+ evaluators.append(
362
+ Evaluator(
363
+ name=eval_name,
364
+ description=eval_def.get("description", ""),
365
+ always_run=eval_def.get("alwaysRun"),
366
+ similes=eval_def.get("similes"),
367
+ examples=[],
368
+ validate=validate_fn, # type: ignore
369
+ handler=handler_fn, # type: ignore
370
+ )
371
+ )
372
+
373
+ # Create init function
374
+ async def init(config: dict[str, str], runtime: Any) -> None:
375
+ ffi.init(config)
376
+
377
+ return Plugin(
378
+ name=manifest["name"],
379
+ description=manifest["description"],
380
+ init=init,
381
+ config=manifest.get("config"),
382
+ dependencies=manifest.get("dependencies"),
383
+ actions=actions if actions else None,
384
+ providers=providers if providers else None,
385
+ evaluators=evaluators if evaluators else None,
386
+ )
387
+
388
+
389
+ def find_rust_plugin(name: str, search_paths: list[str | Path] | None = None) -> Path | None:
390
+ """
391
+ Find a Rust plugin by name in common locations.
392
+
393
+ Args:
394
+ name: Plugin name (without lib prefix or extension)
395
+ search_paths: Additional paths to search
396
+
397
+ Returns:
398
+ Path to the shared library, or None if not found
399
+ """
400
+ prefix = get_lib_prefix()
401
+ ext = get_lib_extension()
402
+ lib_name = f"{prefix}{name}{ext}"
403
+
404
+ paths_to_search = search_paths or []
405
+ paths_to_search.extend([
406
+ Path.cwd() / "target" / "release",
407
+ Path.cwd() / "target" / "debug",
408
+ Path.cwd() / "dist",
409
+ Path.home() / ".elizaos" / "plugins",
410
+ ])
411
+
412
+ for path in paths_to_search:
413
+ lib_path = Path(path) / lib_name
414
+ if lib_path.exists():
415
+ return lib_path
416
+
417
+ return None
418
+
@@ -0,0 +1 @@
1
+ """Test suite for elizaOS Python interop."""