@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,505 @@
1
+ #!/usr/bin/env python3
2
+ """
3
+ elizaOS Python Plugin Bridge Server
4
+
5
+ This server is spawned by the TypeScript runtime to load and execute
6
+ Python plugins via JSON-RPC over stdin/stdout.
7
+ """
8
+
9
+ from __future__ import annotations
10
+
11
+ import argparse
12
+ import asyncio
13
+ import importlib
14
+ import json
15
+ import os
16
+ import sys
17
+ import traceback
18
+ from typing import Any
19
+
20
+ # Import elizaos types (required). Fail closed if unavailable.
21
+ try:
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 ActionResult, ProviderResult, HandlerOptions
26
+ except ImportError as e:
27
+ sys.stderr.write(
28
+ "elizaOS interop bridge_server requires the 'elizaos' python package in the environment.\n"
29
+ )
30
+ sys.stderr.write(f"ImportError: {e}\n")
31
+ sys.exit(1)
32
+
33
+
34
+ def _include_error_details() -> bool:
35
+ """
36
+ Whether to include stack traces in error responses.
37
+
38
+ Default is off to avoid leaking secrets/PII via tracebacks over IPC.
39
+ """
40
+ value = os.environ.get("ELIZA_INTEROP_DEBUG") or os.environ.get("LOG_DIAGNOSTIC") or ""
41
+ normalized = value.strip().lower()
42
+ return normalized in {"1", "true", "yes", "on"}
43
+
44
+
45
+ class PluginBridgeServer:
46
+ """JSON-RPC bridge server for Python plugins."""
47
+
48
+ def __init__(self, module_name: str) -> None:
49
+ self.module_name = module_name
50
+ self.plugin: Plugin | None = None
51
+ self.actions: dict[str, Any] = {}
52
+ self.providers: dict[str, Any] = {}
53
+ self.evaluators: dict[str, Any] = {}
54
+ self.services: dict[str, Any] = {}
55
+ self.routes: dict[str, Any] = {}
56
+ self.initialized = False
57
+
58
+ async def load_plugin(self) -> None:
59
+ """Load the Python plugin module."""
60
+ try:
61
+ module = importlib.import_module(self.module_name)
62
+
63
+ # Look for plugin export
64
+ if hasattr(module, "plugin"):
65
+ self.plugin = module.plugin
66
+ elif hasattr(module, "default"):
67
+ self.plugin = module.default
68
+ else:
69
+ raise RuntimeError(f"Module {self.module_name} has no plugin export")
70
+
71
+ # Index components for fast lookup
72
+ if hasattr(self.plugin, "actions") and self.plugin.actions:
73
+ for action in self.plugin.actions:
74
+ self.actions[action.name] = action
75
+
76
+ if hasattr(self.plugin, "providers") and self.plugin.providers:
77
+ for provider in self.plugin.providers:
78
+ self.providers[provider.name] = provider
79
+
80
+ if hasattr(self.plugin, "evaluators") and self.plugin.evaluators:
81
+ for evaluator in self.plugin.evaluators:
82
+ self.evaluators[evaluator.name] = evaluator
83
+
84
+ if hasattr(self.plugin, "services") and self.plugin.services:
85
+ for service in self.plugin.services:
86
+ service_name = getattr(service, "service_type", None) or getattr(service, "name", str(service))
87
+ self.services[service_name] = service
88
+
89
+ if hasattr(self.plugin, "routes") and self.plugin.routes:
90
+ for route in self.plugin.routes:
91
+ route_path = route.path if hasattr(route, "path") else route.get("path", "")
92
+ self.routes[route_path] = route
93
+
94
+ except Exception as e:
95
+ raise RuntimeError(f"Failed to load plugin: {e}") from e
96
+
97
+ def get_manifest(self) -> dict[str, Any]:
98
+ """Get the plugin manifest as a dictionary."""
99
+ if not self.plugin:
100
+ raise RuntimeError("Plugin not loaded")
101
+
102
+ manifest: dict[str, Any] = {
103
+ "name": self.plugin.name,
104
+ "description": self.plugin.description,
105
+ "version": getattr(self.plugin, "version", "1.0.0"),
106
+ "language": "python",
107
+ }
108
+
109
+ if hasattr(self.plugin, "config") and self.plugin.config:
110
+ manifest["config"] = self.plugin.config
111
+
112
+ if hasattr(self.plugin, "dependencies") and self.plugin.dependencies:
113
+ manifest["dependencies"] = self.plugin.dependencies
114
+
115
+ if hasattr(self.plugin, "actions") and self.plugin.actions:
116
+ manifest["actions"] = [
117
+ {
118
+ "name": a.name,
119
+ "description": a.description,
120
+ "similes": getattr(a, "similes", None),
121
+ }
122
+ for a in self.plugin.actions
123
+ ]
124
+
125
+ if hasattr(self.plugin, "providers") and self.plugin.providers:
126
+ manifest["providers"] = [
127
+ {
128
+ "name": p.name,
129
+ "description": getattr(p, "description", None),
130
+ "dynamic": getattr(p, "dynamic", None),
131
+ "position": getattr(p, "position", None),
132
+ "private": getattr(p, "private", None),
133
+ }
134
+ for p in self.plugin.providers
135
+ ]
136
+
137
+ if hasattr(self.plugin, "evaluators") and self.plugin.evaluators:
138
+ manifest["evaluators"] = [
139
+ {
140
+ "name": e.name,
141
+ "description": e.description,
142
+ "alwaysRun": getattr(e, "always_run", None),
143
+ "similes": getattr(e, "similes", None),
144
+ }
145
+ for e in self.plugin.evaluators
146
+ ]
147
+
148
+ if hasattr(self.plugin, "services") and self.plugin.services:
149
+ manifest["services"] = [
150
+ {
151
+ "type": getattr(s, "service_type", None) or getattr(s, "name", str(s)),
152
+ "description": getattr(s, "description", None),
153
+ }
154
+ for s in self.plugin.services
155
+ ]
156
+
157
+ if hasattr(self.plugin, "routes") and self.plugin.routes:
158
+ manifest["routes"] = [
159
+ {
160
+ "path": r.path if hasattr(r, "path") else r.get("path", ""),
161
+ "type": r.type if hasattr(r, "type") else r.get("type", "GET"),
162
+ "public": r.public if hasattr(r, "public") else r.get("public", False),
163
+ "name": getattr(r, "name", None) or r.get("name"),
164
+ }
165
+ for r in self.plugin.routes
166
+ ]
167
+
168
+ return manifest
169
+
170
+ async def handle_request(self, request: dict[str, Any]) -> dict[str, Any]:
171
+ """Handle an incoming JSON-RPC request."""
172
+ req_type = request.get("type", "")
173
+ req_id = request.get("id", "")
174
+
175
+ try:
176
+ if req_type == "plugin.init":
177
+ config = request.get("config", {})
178
+ if self.plugin and hasattr(self.plugin, "init") and self.plugin.init:
179
+ await self.plugin.init(config, None) # type: ignore
180
+ self.initialized = True
181
+ return {"type": "plugin.init.result", "id": req_id, "success": True}
182
+
183
+ elif req_type == "action.validate":
184
+ action_name = request.get("action", "")
185
+ action = self.actions.get(action_name)
186
+ if not action:
187
+ return {"type": "validate.result", "id": req_id, "valid": False}
188
+
189
+ memory = self._parse_memory(request.get("memory"))
190
+ state = self._parse_state(request.get("state"))
191
+
192
+ # Call validate function
193
+ valid = await action.validate(None, memory, state) # type: ignore
194
+ return {"type": "validate.result", "id": req_id, "valid": valid}
195
+
196
+ elif req_type == "action.invoke":
197
+ action_name = request.get("action", "")
198
+ action = self.actions.get(action_name)
199
+ if not action:
200
+ return {
201
+ "type": "action.result",
202
+ "id": req_id,
203
+ "result": {"success": False, "error": f"Action not found: {action_name}"},
204
+ }
205
+
206
+ memory = self._parse_memory(request.get("memory"))
207
+ state = self._parse_state(request.get("state"))
208
+ options = request.get("options") or {}
209
+
210
+ # Call handler
211
+ result = await action.handler(
212
+ None, # runtime
213
+ memory,
214
+ state,
215
+ HandlerOptions(**options) if isinstance(options, dict) else None,
216
+ None, # callback
217
+ None, # responses
218
+ )
219
+
220
+ return {
221
+ "type": "action.result",
222
+ "id": req_id,
223
+ "result": self._serialize_action_result(result),
224
+ }
225
+
226
+ elif req_type == "provider.get":
227
+ provider_name = request.get("provider", "")
228
+ provider = self.providers.get(provider_name)
229
+ if not provider:
230
+ return {
231
+ "type": "provider.result",
232
+ "id": req_id,
233
+ "result": {"text": None, "values": None, "data": None},
234
+ }
235
+
236
+ memory = self._parse_memory(request.get("memory"))
237
+ state = self._parse_state(request.get("state"))
238
+
239
+ result = await provider.get(None, memory, state) # type: ignore
240
+
241
+ return {
242
+ "type": "provider.result",
243
+ "id": req_id,
244
+ "result": self._serialize_provider_result(result),
245
+ }
246
+
247
+ elif req_type == "evaluator.invoke":
248
+ evaluator_name = request.get("evaluator", "")
249
+ evaluator = self.evaluators.get(evaluator_name)
250
+ if not evaluator:
251
+ return {
252
+ "type": "action.result",
253
+ "id": req_id,
254
+ "result": None,
255
+ }
256
+
257
+ memory = self._parse_memory(request.get("memory"))
258
+ state = self._parse_state(request.get("state"))
259
+
260
+ result = await evaluator.handler(
261
+ None, # runtime
262
+ memory,
263
+ state,
264
+ None, # options
265
+ None, # callback
266
+ None, # responses
267
+ )
268
+
269
+ return {
270
+ "type": "action.result",
271
+ "id": req_id,
272
+ "result": self._serialize_action_result(result) if result else None,
273
+ }
274
+
275
+ elif req_type == "service.start":
276
+ service_type = request.get("serviceType", "")
277
+ service_class = self.services.get(service_type)
278
+ if not service_class:
279
+ return {
280
+ "type": "service.result",
281
+ "id": req_id,
282
+ "success": False,
283
+ "error": f"Service not found: {service_type}",
284
+ }
285
+
286
+ # Start the service
287
+ try:
288
+ if hasattr(service_class, "start"):
289
+ service_instance = await service_class.start(None) # runtime
290
+ elif callable(service_class):
291
+ service_instance = service_class(None) # runtime
292
+ if hasattr(service_instance, "start"):
293
+ await service_instance.start()
294
+
295
+ return {
296
+ "type": "service.result",
297
+ "id": req_id,
298
+ "success": True,
299
+ "serviceType": service_type,
300
+ }
301
+ except Exception as e:
302
+ return {
303
+ "type": "service.result",
304
+ "id": req_id,
305
+ "success": False,
306
+ "error": str(e),
307
+ }
308
+
309
+ elif req_type == "service.stop":
310
+ service_type = request.get("serviceType", "")
311
+ # Note: In a real implementation, we'd track running service instances
312
+ return {
313
+ "type": "service.result",
314
+ "id": req_id,
315
+ "success": True,
316
+ "serviceType": service_type,
317
+ }
318
+
319
+ elif req_type == "route.handle":
320
+ route_path = request.get("path", "")
321
+ route = self.routes.get(route_path)
322
+ if not route:
323
+ return {
324
+ "type": "route.result",
325
+ "id": req_id,
326
+ "status": 404,
327
+ "body": {"error": f"Route not found: {route_path}"},
328
+ }
329
+
330
+ handler = getattr(route, "handler", None) or route.get("handler")
331
+ if not handler:
332
+ return {
333
+ "type": "route.result",
334
+ "id": req_id,
335
+ "status": 501,
336
+ "body": {"error": "Route has no handler"},
337
+ }
338
+
339
+ # Create mock request/response objects
340
+ req_data = request.get("request", {})
341
+ mock_req = {
342
+ "body": req_data.get("body", {}),
343
+ "params": req_data.get("params", {}),
344
+ "query": req_data.get("query", {}),
345
+ "headers": req_data.get("headers", {}),
346
+ "method": req_data.get("method", "GET"),
347
+ "path": route_path,
348
+ }
349
+
350
+ response_data: dict[str, Any] = {"status": 200, "body": None, "headers": {}}
351
+
352
+ class MockResponse:
353
+ def status(self, code: int) -> "MockResponse":
354
+ response_data["status"] = code
355
+ return self
356
+
357
+ def json(self, data: Any) -> "MockResponse":
358
+ response_data["body"] = data
359
+ return self
360
+
361
+ def send(self, data: Any) -> "MockResponse":
362
+ response_data["body"] = data
363
+ return self
364
+
365
+ def end(self) -> "MockResponse":
366
+ return self
367
+
368
+ def setHeader(self, name: str, value: str) -> "MockResponse":
369
+ response_data["headers"][name] = value
370
+ return self
371
+
372
+ try:
373
+ if asyncio.iscoroutinefunction(handler):
374
+ await handler(mock_req, MockResponse(), None) # runtime
375
+ else:
376
+ handler(mock_req, MockResponse(), None) # runtime
377
+
378
+ return {
379
+ "type": "route.result",
380
+ "id": req_id,
381
+ "status": response_data["status"],
382
+ "body": response_data["body"],
383
+ "headers": response_data["headers"],
384
+ }
385
+ except Exception as e:
386
+ return {
387
+ "type": "route.result",
388
+ "id": req_id,
389
+ "status": 500,
390
+ "body": {"error": str(e)},
391
+ }
392
+
393
+ else:
394
+ return {
395
+ "type": "error",
396
+ "id": req_id,
397
+ "error": f"Unknown request type: {req_type}",
398
+ }
399
+
400
+ except Exception as e:
401
+ error_response: dict[str, Any] = {
402
+ "type": "error",
403
+ "id": req_id,
404
+ "error": str(e),
405
+ }
406
+ if _include_error_details():
407
+ error_response["details"] = traceback.format_exc()
408
+ return error_response
409
+
410
+ def _parse_memory(self, data: dict[str, Any] | None) -> Memory:
411
+ """Parse memory from JSON."""
412
+ if data is None:
413
+ return Memory() # type: ignore
414
+ if isinstance(data, dict):
415
+ return Memory(**data) # type: ignore
416
+ return data # type: ignore
417
+
418
+ def _parse_state(self, data: dict[str, Any] | None) -> State | None:
419
+ """Parse state from JSON."""
420
+ if data is None:
421
+ return None
422
+ if isinstance(data, dict):
423
+ return State(**data) # type: ignore
424
+ return data # type: ignore
425
+
426
+ def _serialize_action_result(self, result: ActionResult | None) -> dict[str, Any] | None:
427
+ """Serialize action result to JSON-compatible dict."""
428
+ if result is None:
429
+ return None
430
+
431
+ if isinstance(result, dict):
432
+ return result
433
+
434
+ return {
435
+ "success": result.success,
436
+ "text": result.text,
437
+ "error": str(result.error) if result.error else None,
438
+ "data": result.data,
439
+ "values": result.values,
440
+ }
441
+
442
+ def _serialize_provider_result(self, result: ProviderResult) -> dict[str, Any]:
443
+ """Serialize provider result to JSON-compatible dict."""
444
+ if isinstance(result, dict):
445
+ return result
446
+
447
+ return {
448
+ "text": result.text,
449
+ "values": result.values,
450
+ "data": result.data,
451
+ }
452
+
453
+
454
+ async def main(module_name: str) -> None:
455
+ """Main entry point for the bridge server."""
456
+ server = PluginBridgeServer(module_name)
457
+
458
+ # Load the plugin
459
+ await server.load_plugin()
460
+
461
+ # Send ready message with manifest
462
+ manifest = server.get_manifest()
463
+ ready_msg = {"type": "ready", "manifest": manifest}
464
+ print(json.dumps(ready_msg), flush=True)
465
+
466
+ # Process requests from stdin
467
+ reader = asyncio.StreamReader()
468
+ protocol = asyncio.StreamReaderProtocol(reader)
469
+ await asyncio.get_event_loop().connect_read_pipe(lambda: protocol, sys.stdin)
470
+
471
+ while True:
472
+ try:
473
+ line = await reader.readline()
474
+ if not line:
475
+ break
476
+
477
+ line_str = line.decode("utf-8").strip()
478
+ if not line_str:
479
+ continue
480
+
481
+ request = json.loads(line_str)
482
+ response = await server.handle_request(request)
483
+ print(json.dumps(response), flush=True)
484
+
485
+ except json.JSONDecodeError as e:
486
+ error_response = {"type": "error", "id": "", "error": f"JSON parse error: {e}"}
487
+ print(json.dumps(error_response), flush=True)
488
+ except Exception as e:
489
+ error_response: dict[str, Any] = {
490
+ "type": "error",
491
+ "id": "",
492
+ "error": str(e),
493
+ }
494
+ if _include_error_details():
495
+ error_response["details"] = traceback.format_exc()
496
+ print(json.dumps(error_response), flush=True)
497
+
498
+
499
+ if __name__ == "__main__":
500
+ parser = argparse.ArgumentParser(description="elizaOS Python Plugin Bridge Server")
501
+ parser.add_argument("--module", "-m", required=True, help="Python module name to load")
502
+ args = parser.parse_args()
503
+
504
+ asyncio.run(main(args.module))
505
+