@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,269 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* WASM Plugin Loader for elizaOS
|
|
3
|
+
*
|
|
4
|
+
* Loads Rust (or other) plugins compiled to WebAssembly and adapts them
|
|
5
|
+
* to the TypeScript Plugin interface.
|
|
6
|
+
*/
|
|
7
|
+
/**
|
|
8
|
+
* Text encoder/decoder for string passing
|
|
9
|
+
*/
|
|
10
|
+
const encoder = new TextEncoder();
|
|
11
|
+
const decoder = new TextDecoder();
|
|
12
|
+
/**
|
|
13
|
+
* Load a WASM plugin and return an elizaOS Plugin interface
|
|
14
|
+
*/
|
|
15
|
+
export async function loadWasmPlugin(options) {
|
|
16
|
+
const { wasmPath, manifestPath } = options;
|
|
17
|
+
// Load the WASM module
|
|
18
|
+
const wasmInstance = await loadWasmModule(wasmPath, options.imports);
|
|
19
|
+
// Get the manifest from the WASM module or external file
|
|
20
|
+
let manifest;
|
|
21
|
+
if (manifestPath) {
|
|
22
|
+
manifest = await loadManifest(manifestPath);
|
|
23
|
+
}
|
|
24
|
+
else {
|
|
25
|
+
// Try to get manifest from WASM exports
|
|
26
|
+
const manifestJson = wasmInstance.exports.get_manifest();
|
|
27
|
+
manifest = JSON.parse(manifestJson);
|
|
28
|
+
}
|
|
29
|
+
// Create the plugin adapter
|
|
30
|
+
return createPluginFromWasm(manifest, wasmInstance);
|
|
31
|
+
}
|
|
32
|
+
/**
|
|
33
|
+
* Load a WASM module and instantiate it
|
|
34
|
+
*/
|
|
35
|
+
async function loadWasmModule(wasmPath, customImports) {
|
|
36
|
+
// Default imports for WASM
|
|
37
|
+
const defaultImports = {
|
|
38
|
+
env: {
|
|
39
|
+
// Console logging
|
|
40
|
+
console_log: (_ptr, _len) => {
|
|
41
|
+
// Will be resolved once we have memory
|
|
42
|
+
},
|
|
43
|
+
console_error: (_ptr, _len) => {
|
|
44
|
+
// Will be resolved once we have memory
|
|
45
|
+
},
|
|
46
|
+
// Abort handler
|
|
47
|
+
abort: (_msg, file, line, column) => {
|
|
48
|
+
throw new Error(`WASM abort at ${file}:${line}:${column}`);
|
|
49
|
+
},
|
|
50
|
+
},
|
|
51
|
+
wasi_snapshot_preview1: {
|
|
52
|
+
// Minimal WASI stubs for compatibility
|
|
53
|
+
proc_exit: (code) => {
|
|
54
|
+
throw new Error(`WASM exit with code ${code}`);
|
|
55
|
+
},
|
|
56
|
+
fd_write: () => 0,
|
|
57
|
+
fd_read: () => 0,
|
|
58
|
+
fd_close: () => 0,
|
|
59
|
+
fd_seek: () => 0,
|
|
60
|
+
environ_get: () => 0,
|
|
61
|
+
environ_sizes_get: () => 0,
|
|
62
|
+
clock_time_get: () => 0,
|
|
63
|
+
random_get: (_buf, _len) => {
|
|
64
|
+
// Fill with random bytes
|
|
65
|
+
return 0;
|
|
66
|
+
},
|
|
67
|
+
},
|
|
68
|
+
};
|
|
69
|
+
const imports = { ...defaultImports, ...customImports };
|
|
70
|
+
// Determine if we're in Node.js or browser
|
|
71
|
+
const isNode = typeof globalThis.process !== "undefined" &&
|
|
72
|
+
globalThis.process.versions &&
|
|
73
|
+
globalThis.process.versions.node;
|
|
74
|
+
let wasmModule;
|
|
75
|
+
if (isNode) {
|
|
76
|
+
// Node.js: read file
|
|
77
|
+
const fs = await import("node:fs/promises");
|
|
78
|
+
const wasmBuffer = await fs.readFile(wasmPath);
|
|
79
|
+
wasmModule = await WebAssembly.compile(wasmBuffer);
|
|
80
|
+
}
|
|
81
|
+
else {
|
|
82
|
+
// Browser: fetch
|
|
83
|
+
const response = await fetch(wasmPath);
|
|
84
|
+
const wasmBuffer = await response.arrayBuffer();
|
|
85
|
+
wasmModule = await WebAssembly.compile(wasmBuffer);
|
|
86
|
+
}
|
|
87
|
+
let instance = await WebAssembly.instantiate(wasmModule, defaultImports);
|
|
88
|
+
instance = await WebAssembly.instantiate(wasmModule, imports);
|
|
89
|
+
// Set up console logging now that we have memory
|
|
90
|
+
const memory = instance.exports.memory;
|
|
91
|
+
if (imports.env) {
|
|
92
|
+
const env = imports.env;
|
|
93
|
+
env.console_log = (ptr, len) => {
|
|
94
|
+
const bytes = new Uint8Array(memory.buffer, ptr, len);
|
|
95
|
+
console.log(decoder.decode(bytes));
|
|
96
|
+
};
|
|
97
|
+
env.console_error = (ptr, len) => {
|
|
98
|
+
const bytes = new Uint8Array(memory.buffer, ptr, len);
|
|
99
|
+
console.error(decoder.decode(bytes));
|
|
100
|
+
};
|
|
101
|
+
}
|
|
102
|
+
return {
|
|
103
|
+
exports: instance.exports,
|
|
104
|
+
memory: { buffer: memory.buffer },
|
|
105
|
+
};
|
|
106
|
+
}
|
|
107
|
+
/**
|
|
108
|
+
* Load manifest from external JSON file
|
|
109
|
+
*/
|
|
110
|
+
async function loadManifest(manifestPath) {
|
|
111
|
+
const isNode = typeof globalThis.process !== "undefined" &&
|
|
112
|
+
globalThis.process.versions &&
|
|
113
|
+
globalThis.process.versions.node;
|
|
114
|
+
if (isNode) {
|
|
115
|
+
const fs = await import("node:fs/promises");
|
|
116
|
+
const content = await fs.readFile(manifestPath, "utf-8");
|
|
117
|
+
return JSON.parse(content);
|
|
118
|
+
}
|
|
119
|
+
else {
|
|
120
|
+
const response = await fetch(manifestPath);
|
|
121
|
+
return response.json();
|
|
122
|
+
}
|
|
123
|
+
}
|
|
124
|
+
/**
|
|
125
|
+
* Helper to pass a string to WASM and get result
|
|
126
|
+
*/
|
|
127
|
+
function _callWasmWithString(instance, fn, ...strings) {
|
|
128
|
+
const { exports, memory } = instance;
|
|
129
|
+
const ptrs = [];
|
|
130
|
+
const lens = [];
|
|
131
|
+
// Allocate and write strings
|
|
132
|
+
for (const str of strings) {
|
|
133
|
+
const bytes = encoder.encode(str);
|
|
134
|
+
const ptr = exports.alloc(bytes.length);
|
|
135
|
+
const view = new Uint8Array(memory.buffer, ptr, bytes.length);
|
|
136
|
+
view.set(bytes);
|
|
137
|
+
ptrs.push(ptr);
|
|
138
|
+
lens.push(bytes.length);
|
|
139
|
+
}
|
|
140
|
+
// Call the function
|
|
141
|
+
const args = [];
|
|
142
|
+
for (let i = 0; i < strings.length; i++) {
|
|
143
|
+
args.push(ptrs[i], lens[i]);
|
|
144
|
+
}
|
|
145
|
+
const resultPtr = fn(...args);
|
|
146
|
+
// Read result (assuming null-terminated or length-prefixed)
|
|
147
|
+
// For now, assume it returns a pointer to a null-terminated string
|
|
148
|
+
let resultLen = 0;
|
|
149
|
+
const view = new Uint8Array(memory.buffer);
|
|
150
|
+
while (view[resultPtr + resultLen] !== 0 &&
|
|
151
|
+
resultPtr + resultLen < view.length) {
|
|
152
|
+
resultLen++;
|
|
153
|
+
}
|
|
154
|
+
const result = decoder.decode(new Uint8Array(memory.buffer, resultPtr, resultLen));
|
|
155
|
+
// Deallocate input strings
|
|
156
|
+
for (let i = 0; i < strings.length; i++) {
|
|
157
|
+
exports.dealloc(ptrs[i], lens[i]);
|
|
158
|
+
}
|
|
159
|
+
return result;
|
|
160
|
+
}
|
|
161
|
+
/**
|
|
162
|
+
* Create a Plugin from a WASM instance
|
|
163
|
+
*/
|
|
164
|
+
function createPluginFromWasm(manifest, instance) {
|
|
165
|
+
const { exports } = instance;
|
|
166
|
+
// Create action wrappers
|
|
167
|
+
const actions = (manifest.actions ?? []).map((actionDef) => ({
|
|
168
|
+
name: actionDef.name,
|
|
169
|
+
description: actionDef.description,
|
|
170
|
+
similes: actionDef.similes,
|
|
171
|
+
examples: actionDef.examples,
|
|
172
|
+
validate: async (_runtime, message, state) => {
|
|
173
|
+
const result = exports.validate_action(actionDef.name, JSON.stringify(message), JSON.stringify(state ?? null));
|
|
174
|
+
return typeof result === "boolean" ? result : result !== 0;
|
|
175
|
+
},
|
|
176
|
+
handler: async (_runtime, message, state, options, _callback) => {
|
|
177
|
+
const resultJson = exports.invoke_action(actionDef.name, JSON.stringify(message), JSON.stringify(state ?? null), JSON.stringify(options ?? {}));
|
|
178
|
+
const result = JSON.parse(resultJson);
|
|
179
|
+
return {
|
|
180
|
+
success: result.success,
|
|
181
|
+
text: result.text,
|
|
182
|
+
error: result.error ? new Error(result.error) : undefined,
|
|
183
|
+
data: result.data,
|
|
184
|
+
values: result.values,
|
|
185
|
+
};
|
|
186
|
+
},
|
|
187
|
+
}));
|
|
188
|
+
// Create provider wrappers
|
|
189
|
+
const providers = (manifest.providers ?? []).map((providerDef) => ({
|
|
190
|
+
name: providerDef.name,
|
|
191
|
+
description: providerDef.description,
|
|
192
|
+
dynamic: providerDef.dynamic,
|
|
193
|
+
position: providerDef.position,
|
|
194
|
+
private: providerDef.private,
|
|
195
|
+
get: async (_runtime, message, state) => {
|
|
196
|
+
const resultJson = exports.get_provider(providerDef.name, JSON.stringify(message), JSON.stringify(state));
|
|
197
|
+
const result = JSON.parse(resultJson);
|
|
198
|
+
return {
|
|
199
|
+
text: result.text,
|
|
200
|
+
values: result.values,
|
|
201
|
+
data: result.data,
|
|
202
|
+
};
|
|
203
|
+
},
|
|
204
|
+
}));
|
|
205
|
+
// Create evaluator wrappers
|
|
206
|
+
const evaluators = (manifest.evaluators ?? []).map((evalDef) => ({
|
|
207
|
+
name: evalDef.name,
|
|
208
|
+
description: evalDef.description,
|
|
209
|
+
alwaysRun: evalDef.alwaysRun,
|
|
210
|
+
similes: evalDef.similes,
|
|
211
|
+
examples: [],
|
|
212
|
+
validate: async (_runtime, message, state) => {
|
|
213
|
+
const result = exports.validate_evaluator(evalDef.name, JSON.stringify(message), JSON.stringify(state ?? null));
|
|
214
|
+
return typeof result === "boolean" ? result : result !== 0;
|
|
215
|
+
},
|
|
216
|
+
handler: async (_runtime, message, state) => {
|
|
217
|
+
const resultJson = exports.invoke_evaluator(evalDef.name, JSON.stringify(message), JSON.stringify(state ?? null));
|
|
218
|
+
if (!resultJson || resultJson === "null") {
|
|
219
|
+
return undefined;
|
|
220
|
+
}
|
|
221
|
+
const result = JSON.parse(resultJson);
|
|
222
|
+
return {
|
|
223
|
+
success: result.success,
|
|
224
|
+
text: result.text,
|
|
225
|
+
error: result.error ? new Error(result.error) : undefined,
|
|
226
|
+
data: result.data,
|
|
227
|
+
values: result.values,
|
|
228
|
+
};
|
|
229
|
+
},
|
|
230
|
+
}));
|
|
231
|
+
// Return the plugin
|
|
232
|
+
return {
|
|
233
|
+
name: manifest.name,
|
|
234
|
+
description: manifest.description,
|
|
235
|
+
config: manifest.config ?? {},
|
|
236
|
+
dependencies: manifest.dependencies,
|
|
237
|
+
actions,
|
|
238
|
+
providers,
|
|
239
|
+
evaluators,
|
|
240
|
+
// Routes would need special handling for WASM
|
|
241
|
+
routes: [],
|
|
242
|
+
// Services need special handling
|
|
243
|
+
services: [],
|
|
244
|
+
async init(config) {
|
|
245
|
+
exports.init(JSON.stringify(config));
|
|
246
|
+
},
|
|
247
|
+
};
|
|
248
|
+
}
|
|
249
|
+
/**
|
|
250
|
+
* Preload and validate a WASM plugin without fully loading it
|
|
251
|
+
*/
|
|
252
|
+
export async function validateWasmPlugin(wasmPath) {
|
|
253
|
+
try {
|
|
254
|
+
const instance = await loadWasmModule(wasmPath, {});
|
|
255
|
+
const manifestJson = instance.exports.get_manifest();
|
|
256
|
+
const manifest = JSON.parse(manifestJson);
|
|
257
|
+
// Basic validation
|
|
258
|
+
if (!manifest.name || !manifest.description) {
|
|
259
|
+
return { valid: false, error: "Missing required manifest fields" };
|
|
260
|
+
}
|
|
261
|
+
return { valid: true, manifest };
|
|
262
|
+
}
|
|
263
|
+
catch (error) {
|
|
264
|
+
return {
|
|
265
|
+
valid: false,
|
|
266
|
+
error: error instanceof Error ? error.message : String(error),
|
|
267
|
+
};
|
|
268
|
+
}
|
|
269
|
+
}
|
package/package.json
ADDED
|
@@ -0,0 +1,43 @@
|
|
|
1
|
+
{
|
|
2
|
+
"name": "@elizaos/interop",
|
|
3
|
+
"version": "2.0.0-alpha",
|
|
4
|
+
"description": "Cross-language plugin interoperability for elizaOS",
|
|
5
|
+
"type": "module",
|
|
6
|
+
"main": "dist/index.js",
|
|
7
|
+
"types": "dist/index.d.ts",
|
|
8
|
+
"exports": {
|
|
9
|
+
"./package.json": "./package.json",
|
|
10
|
+
".": {
|
|
11
|
+
"types": "./dist/index.d.ts",
|
|
12
|
+
"import": "./dist/index.js",
|
|
13
|
+
"default": "./dist/index.js"
|
|
14
|
+
}
|
|
15
|
+
},
|
|
16
|
+
"files": [
|
|
17
|
+
"dist",
|
|
18
|
+
"python",
|
|
19
|
+
"rust"
|
|
20
|
+
],
|
|
21
|
+
"scripts": {
|
|
22
|
+
"build": "tsc",
|
|
23
|
+
"test": "vitest run --pool=threads",
|
|
24
|
+
"test:watch": "vitest --pool=threads",
|
|
25
|
+
"test:python": "cd python && pytest tests/ -v -p no:anchorpy --asyncio-mode=auto || echo 'Python tests skipped'",
|
|
26
|
+
"lint": "bunx @biomejs/biome check --write --unsafe .",
|
|
27
|
+
"lint:check": "bunx @biomejs/biome check .",
|
|
28
|
+
"typecheck": "tsc --noEmit"
|
|
29
|
+
},
|
|
30
|
+
"dependencies": {
|
|
31
|
+
"@elizaos/core": "2.0.0-alpha"
|
|
32
|
+
},
|
|
33
|
+
"devDependencies": {
|
|
34
|
+
"@types/node": "^25.0.3",
|
|
35
|
+
"typescript": "^5.9.3"
|
|
36
|
+
},
|
|
37
|
+
"author": "elizaOS",
|
|
38
|
+
"license": "MIT",
|
|
39
|
+
"publishConfig": {
|
|
40
|
+
"access": "public"
|
|
41
|
+
},
|
|
42
|
+
"gitHead": "97cb016b794d01bc07a552427734d2e98a1d674a"
|
|
43
|
+
}
|
|
@@ -0,0 +1,50 @@
|
|
|
1
|
+
"""
|
|
2
|
+
elizaOS Cross-Language Interop - Python
|
|
3
|
+
|
|
4
|
+
This module provides utilities for loading plugins written in other languages
|
|
5
|
+
(Rust, TypeScript) into the Python runtime.
|
|
6
|
+
|
|
7
|
+
This is a standalone package that can be installed separately or used from
|
|
8
|
+
within the main elizaos package.
|
|
9
|
+
"""
|
|
10
|
+
|
|
11
|
+
# Rust FFI loader
|
|
12
|
+
from .rust_ffi import (
|
|
13
|
+
RustPluginFFI,
|
|
14
|
+
find_rust_plugin,
|
|
15
|
+
get_lib_extension,
|
|
16
|
+
get_lib_prefix,
|
|
17
|
+
load_rust_plugin,
|
|
18
|
+
)
|
|
19
|
+
|
|
20
|
+
# WASM loader (for Rust/TypeScript WASM plugins)
|
|
21
|
+
from .wasm_loader import (
|
|
22
|
+
WasmPluginLoader,
|
|
23
|
+
load_wasm_plugin,
|
|
24
|
+
validate_wasm_plugin,
|
|
25
|
+
)
|
|
26
|
+
|
|
27
|
+
# TypeScript bridge (for TypeScript plugins via IPC)
|
|
28
|
+
from .ts_bridge import (
|
|
29
|
+
TypeScriptPluginBridge,
|
|
30
|
+
load_typescript_plugin,
|
|
31
|
+
stop_typescript_plugin,
|
|
32
|
+
)
|
|
33
|
+
|
|
34
|
+
__all__ = [
|
|
35
|
+
# Rust FFI
|
|
36
|
+
"RustPluginFFI",
|
|
37
|
+
"load_rust_plugin",
|
|
38
|
+
"find_rust_plugin",
|
|
39
|
+
"get_lib_extension",
|
|
40
|
+
"get_lib_prefix",
|
|
41
|
+
# WASM Loader
|
|
42
|
+
"WasmPluginLoader",
|
|
43
|
+
"load_wasm_plugin",
|
|
44
|
+
"validate_wasm_plugin",
|
|
45
|
+
# TypeScript Bridge
|
|
46
|
+
"TypeScriptPluginBridge",
|
|
47
|
+
"load_typescript_plugin",
|
|
48
|
+
"stop_typescript_plugin",
|
|
49
|
+
]
|
|
50
|
+
|
|
Binary file
|
|
Binary file
|
|
Binary file
|
|
Binary file
|