@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,467 @@
1
+ //! TypeScript Plugin Loader for elizaOS Rust Runtime
2
+ //!
3
+ //! This module provides utilities for loading TypeScript plugins into the Rust runtime
4
+ //! via subprocess IPC communication.
5
+
6
+ use anyhow::{anyhow, Result};
7
+ use serde::{Deserialize, Serialize};
8
+ use std::collections::HashMap;
9
+ use std::io::{BufRead, BufReader, Write};
10
+ use std::path::Path;
11
+ use std::process::{Child, Command, Stdio};
12
+ use std::sync::atomic::{AtomicU64, Ordering};
13
+ use std::sync::Mutex;
14
+
15
+ /// Plugin manifest from TypeScript
16
+ #[derive(Clone, Debug, Serialize, Deserialize)]
17
+ #[serde(rename_all = "camelCase")]
18
+ pub struct TypeScriptManifest {
19
+ pub name: String,
20
+ pub description: String,
21
+ #[serde(default)]
22
+ pub version: Option<String>,
23
+ #[serde(default)]
24
+ pub config: Option<HashMap<String, serde_json::Value>>,
25
+ #[serde(default)]
26
+ pub dependencies: Option<Vec<String>>,
27
+ #[serde(default)]
28
+ pub actions: Vec<ActionManifest>,
29
+ #[serde(default)]
30
+ pub providers: Vec<ProviderManifest>,
31
+ #[serde(default)]
32
+ pub evaluators: Vec<EvaluatorManifest>,
33
+ }
34
+
35
+ #[derive(Clone, Debug, Serialize, Deserialize)]
36
+ #[serde(rename_all = "camelCase")]
37
+ pub struct ActionManifest {
38
+ pub name: String,
39
+ #[serde(default)]
40
+ pub description: Option<String>,
41
+ #[serde(default)]
42
+ pub similes: Option<Vec<String>>,
43
+ }
44
+
45
+ #[derive(Clone, Debug, Serialize, Deserialize)]
46
+ #[serde(rename_all = "camelCase")]
47
+ pub struct ProviderManifest {
48
+ pub name: String,
49
+ #[serde(default)]
50
+ pub description: Option<String>,
51
+ #[serde(default)]
52
+ pub dynamic: Option<bool>,
53
+ #[serde(default)]
54
+ pub position: Option<i32>,
55
+ #[serde(default)]
56
+ pub private: Option<bool>,
57
+ }
58
+
59
+ #[derive(Clone, Debug, Serialize, Deserialize)]
60
+ #[serde(rename_all = "camelCase")]
61
+ pub struct EvaluatorManifest {
62
+ pub name: String,
63
+ #[serde(default)]
64
+ pub description: Option<String>,
65
+ #[serde(default)]
66
+ pub always_run: Option<bool>,
67
+ #[serde(default)]
68
+ pub similes: Option<Vec<String>>,
69
+ }
70
+
71
+ /// Action result from TypeScript
72
+ #[derive(Clone, Debug, Default, Serialize, Deserialize)]
73
+ #[serde(rename_all = "camelCase")]
74
+ pub struct ActionResult {
75
+ pub success: bool,
76
+ #[serde(skip_serializing_if = "Option::is_none")]
77
+ pub text: Option<String>,
78
+ #[serde(skip_serializing_if = "Option::is_none")]
79
+ pub error: Option<String>,
80
+ #[serde(skip_serializing_if = "Option::is_none")]
81
+ pub data: Option<HashMap<String, serde_json::Value>>,
82
+ #[serde(skip_serializing_if = "Option::is_none")]
83
+ pub values: Option<HashMap<String, serde_json::Value>>,
84
+ }
85
+
86
+ /// Provider result from TypeScript
87
+ #[derive(Clone, Debug, Default, Serialize, Deserialize)]
88
+ #[serde(rename_all = "camelCase")]
89
+ pub struct ProviderResult {
90
+ #[serde(skip_serializing_if = "Option::is_none")]
91
+ pub text: Option<String>,
92
+ #[serde(skip_serializing_if = "Option::is_none")]
93
+ pub values: Option<HashMap<String, serde_json::Value>>,
94
+ #[serde(skip_serializing_if = "Option::is_none")]
95
+ pub data: Option<HashMap<String, serde_json::Value>>,
96
+ }
97
+
98
+ /// IPC Request types
99
+ #[derive(Clone, Debug, Serialize, Deserialize)]
100
+ #[serde(tag = "type", rename_all = "snake_case")]
101
+ pub enum IpcRequest {
102
+ #[serde(rename = "plugin.init")]
103
+ PluginInit {
104
+ id: String,
105
+ config: HashMap<String, String>,
106
+ },
107
+ #[serde(rename = "action.validate")]
108
+ ActionValidate {
109
+ id: String,
110
+ action: String,
111
+ memory: serde_json::Value,
112
+ state: Option<serde_json::Value>,
113
+ },
114
+ #[serde(rename = "action.invoke")]
115
+ ActionInvoke {
116
+ id: String,
117
+ action: String,
118
+ memory: serde_json::Value,
119
+ state: Option<serde_json::Value>,
120
+ options: Option<serde_json::Value>,
121
+ },
122
+ #[serde(rename = "provider.get")]
123
+ ProviderGet {
124
+ id: String,
125
+ provider: String,
126
+ memory: serde_json::Value,
127
+ state: serde_json::Value,
128
+ },
129
+ #[serde(rename = "evaluator.invoke")]
130
+ EvaluatorInvoke {
131
+ id: String,
132
+ evaluator: String,
133
+ memory: serde_json::Value,
134
+ state: Option<serde_json::Value>,
135
+ },
136
+ }
137
+
138
+ /// IPC Response types
139
+ #[derive(Clone, Debug, Serialize, Deserialize)]
140
+ #[serde(tag = "type", rename_all = "snake_case")]
141
+ pub enum IpcResponse {
142
+ Ready {
143
+ manifest: TypeScriptManifest,
144
+ },
145
+ #[serde(rename = "plugin.init.result")]
146
+ PluginInitResult {
147
+ id: String,
148
+ success: bool,
149
+ },
150
+ #[serde(rename = "validate.result")]
151
+ ValidateResult {
152
+ id: String,
153
+ valid: bool,
154
+ },
155
+ #[serde(rename = "action.result")]
156
+ ActionResult {
157
+ id: String,
158
+ result: Option<ActionResult>,
159
+ },
160
+ #[serde(rename = "provider.result")]
161
+ ProviderResult {
162
+ id: String,
163
+ result: ProviderResult,
164
+ },
165
+ Error {
166
+ id: String,
167
+ error: String,
168
+ },
169
+ }
170
+
171
+ /// TypeScript plugin bridge that communicates via subprocess
172
+ pub struct TypeScriptPluginBridge {
173
+ process: Child,
174
+ manifest: TypeScriptManifest,
175
+ request_counter: AtomicU64,
176
+ stdin_mutex: Mutex<()>,
177
+ }
178
+
179
+ impl TypeScriptPluginBridge {
180
+ /// Create a new TypeScript plugin bridge
181
+ ///
182
+ /// # Arguments
183
+ /// * `plugin_path` - Path to the TypeScript plugin (directory or entry file)
184
+ /// * `node_path` - Path to Node.js executable (defaults to "node")
185
+ ///
186
+ /// # Returns
187
+ /// The bridge instance with the plugin loaded
188
+ pub fn new<P: AsRef<Path>>(plugin_path: P, node_path: Option<&str>) -> Result<Self> {
189
+ let node = node_path.unwrap_or("node");
190
+ let bridge_script = Self::get_bridge_script()?;
191
+
192
+ let mut process = Command::new(node)
193
+ .arg(&bridge_script)
194
+ .arg(plugin_path.as_ref())
195
+ .stdin(Stdio::piped())
196
+ .stdout(Stdio::piped())
197
+ .stderr(Stdio::inherit())
198
+ .spawn()?;
199
+
200
+ // Wait for ready message
201
+ let stdout = process.stdout.take().ok_or_else(|| anyhow!("No stdout"))?;
202
+ let mut reader = BufReader::new(stdout);
203
+ let mut line = String::new();
204
+ reader.read_line(&mut line)?;
205
+
206
+ let response: IpcResponse = serde_json::from_str(&line)?;
207
+ let manifest = match response {
208
+ IpcResponse::Ready { manifest } => manifest,
209
+ _ => return Err(anyhow!("Expected ready message, got: {:?}", response)),
210
+ };
211
+
212
+ // Put stdout back for later reads
213
+ process.stdout = Some(reader.into_inner());
214
+
215
+ Ok(Self {
216
+ process,
217
+ manifest,
218
+ request_counter: AtomicU64::new(0),
219
+ stdin_mutex: Mutex::new(()),
220
+ })
221
+ }
222
+
223
+ fn get_bridge_script() -> Result<String> {
224
+ // For now, use inline script via node -e
225
+ // In production, this would be a separate file
226
+ let script = r#"
227
+ const { createInterface } = require('readline');
228
+
229
+ const pluginPath = process.argv[2];
230
+
231
+ (async () => {
232
+ const module = require(pluginPath);
233
+ const plugin = module.default || module.plugin || module;
234
+
235
+ const actions = {};
236
+ const providers = {};
237
+ const evaluators = {};
238
+
239
+ for (const a of plugin.actions || []) actions[a.name] = a;
240
+ for (const p of plugin.providers || []) providers[p.name] = p;
241
+ for (const e of plugin.evaluators || []) evaluators[e.name] = e;
242
+
243
+ const manifest = {
244
+ name: plugin.name,
245
+ description: plugin.description,
246
+ version: plugin.version || '1.0.0',
247
+ actions: Object.values(actions).map(a => ({ name: a.name, description: a.description, similes: a.similes })),
248
+ providers: Object.values(providers).map(p => ({ name: p.name, description: p.description, dynamic: p.dynamic, position: p.position, private: p.private })),
249
+ evaluators: Object.values(evaluators).map(e => ({ name: e.name, description: e.description, alwaysRun: e.alwaysRun, similes: e.similes })),
250
+ };
251
+
252
+ console.log(JSON.stringify({ type: 'ready', manifest }));
253
+
254
+ const rl = createInterface({ input: process.stdin });
255
+ rl.on('line', async (line) => {
256
+ try {
257
+ const req = JSON.parse(line);
258
+ let res;
259
+ switch (req.type) {
260
+ case 'plugin.init':
261
+ if (plugin.init) await plugin.init(req.config, null);
262
+ res = { type: 'plugin.init.result', id: req.id, success: true };
263
+ break;
264
+ case 'action.validate':
265
+ const a = actions[req.action];
266
+ res = { type: 'validate.result', id: req.id, valid: a ? await a.validate(null, req.memory, req.state) : false };
267
+ break;
268
+ case 'action.invoke':
269
+ const act = actions[req.action];
270
+ if (!act) { res = { type: 'action.result', id: req.id, result: { success: false, error: 'Not found' }}; break; }
271
+ const r = await act.handler(null, req.memory, req.state, req.options);
272
+ res = { type: 'action.result', id: req.id, result: r ? { success: r.success, text: r.text, error: r.error?.message || r.error, data: r.data, values: r.values } : { success: true } };
273
+ break;
274
+ case 'provider.get':
275
+ const prov = providers[req.provider];
276
+ res = { type: 'provider.result', id: req.id, result: prov ? await prov.get(null, req.memory, req.state) : {} };
277
+ break;
278
+ case 'evaluator.invoke':
279
+ const ev = evaluators[req.evaluator];
280
+ if (!ev) { res = { type: 'action.result', id: req.id, result: null }; break; }
281
+ const er = await ev.handler(null, req.memory, req.state);
282
+ res = { type: 'action.result', id: req.id, result: er ? { success: er.success, text: er.text, data: er.data, values: er.values } : null };
283
+ break;
284
+ default:
285
+ res = { type: 'error', id: req.id, error: 'Unknown type' };
286
+ }
287
+ console.log(JSON.stringify(res));
288
+ } catch (e) {
289
+ console.log(JSON.stringify({ type: 'error', id: '', error: e.message }));
290
+ }
291
+ });
292
+ })();
293
+ "#;
294
+ // Write to temp file
295
+ let temp_path = std::env::temp_dir().join("elizaos_ts_bridge.js");
296
+ std::fs::write(&temp_path, script)?;
297
+ Ok(temp_path.to_string_lossy().to_string())
298
+ }
299
+
300
+ /// Get the plugin manifest
301
+ pub fn manifest(&self) -> &TypeScriptManifest {
302
+ &self.manifest
303
+ }
304
+
305
+ /// Send a request and get response
306
+ fn send_request(&mut self, request: IpcRequest) -> Result<IpcResponse> {
307
+ let _lock = self.stdin_mutex.lock().unwrap();
308
+
309
+ let stdin = self.process.stdin.as_mut().ok_or_else(|| anyhow!("No stdin"))?;
310
+ let json = serde_json::to_string(&request)?;
311
+ writeln!(stdin, "{}", json)?;
312
+ stdin.flush()?;
313
+
314
+ let stdout = self.process.stdout.as_mut().ok_or_else(|| anyhow!("No stdout"))?;
315
+ let mut reader = BufReader::new(stdout);
316
+ let mut line = String::new();
317
+ reader.read_line(&mut line)?;
318
+
319
+ Ok(serde_json::from_str(&line)?)
320
+ }
321
+
322
+ fn next_id(&self) -> String {
323
+ format!("req_{}", self.request_counter.fetch_add(1, Ordering::SeqCst))
324
+ }
325
+
326
+ /// Initialize the plugin
327
+ pub fn init(&mut self, config: HashMap<String, String>) -> Result<()> {
328
+ let id = self.next_id();
329
+ let response = self.send_request(IpcRequest::PluginInit { id, config })?;
330
+
331
+ match response {
332
+ IpcResponse::PluginInitResult { success, .. } if success => Ok(()),
333
+ IpcResponse::Error { error, .. } => Err(anyhow!("Init failed: {}", error)),
334
+ _ => Err(anyhow!("Unexpected response")),
335
+ }
336
+ }
337
+
338
+ /// Validate an action
339
+ pub fn validate_action(
340
+ &mut self,
341
+ name: &str,
342
+ memory: &serde_json::Value,
343
+ state: Option<&serde_json::Value>,
344
+ ) -> Result<bool> {
345
+ let id = self.next_id();
346
+ let response = self.send_request(IpcRequest::ActionValidate {
347
+ id,
348
+ action: name.to_string(),
349
+ memory: memory.clone(),
350
+ state: state.cloned(),
351
+ })?;
352
+
353
+ match response {
354
+ IpcResponse::ValidateResult { valid, .. } => Ok(valid),
355
+ IpcResponse::Error { error, .. } => Err(anyhow!("Validate failed: {}", error)),
356
+ _ => Err(anyhow!("Unexpected response")),
357
+ }
358
+ }
359
+
360
+ /// Invoke an action
361
+ pub fn invoke_action(
362
+ &mut self,
363
+ name: &str,
364
+ memory: &serde_json::Value,
365
+ state: Option<&serde_json::Value>,
366
+ options: Option<&serde_json::Value>,
367
+ ) -> Result<Option<ActionResult>> {
368
+ let id = self.next_id();
369
+ let response = self.send_request(IpcRequest::ActionInvoke {
370
+ id,
371
+ action: name.to_string(),
372
+ memory: memory.clone(),
373
+ state: state.cloned(),
374
+ options: options.cloned(),
375
+ })?;
376
+
377
+ match response {
378
+ IpcResponse::ActionResult { result, .. } => Ok(result),
379
+ IpcResponse::Error { error, .. } => Err(anyhow!("Invoke failed: {}", error)),
380
+ _ => Err(anyhow!("Unexpected response")),
381
+ }
382
+ }
383
+
384
+ /// Get provider data
385
+ pub fn get_provider(
386
+ &mut self,
387
+ name: &str,
388
+ memory: &serde_json::Value,
389
+ state: &serde_json::Value,
390
+ ) -> Result<ProviderResult> {
391
+ let id = self.next_id();
392
+ let response = self.send_request(IpcRequest::ProviderGet {
393
+ id,
394
+ provider: name.to_string(),
395
+ memory: memory.clone(),
396
+ state: state.clone(),
397
+ })?;
398
+
399
+ match response {
400
+ IpcResponse::ProviderResult { result, .. } => Ok(result),
401
+ IpcResponse::Error { error, .. } => Err(anyhow!("Provider failed: {}", error)),
402
+ _ => Err(anyhow!("Unexpected response")),
403
+ }
404
+ }
405
+
406
+ /// Invoke an evaluator
407
+ pub fn invoke_evaluator(
408
+ &mut self,
409
+ name: &str,
410
+ memory: &serde_json::Value,
411
+ state: Option<&serde_json::Value>,
412
+ ) -> Result<Option<ActionResult>> {
413
+ let id = self.next_id();
414
+ let response = self.send_request(IpcRequest::EvaluatorInvoke {
415
+ id,
416
+ evaluator: name.to_string(),
417
+ memory: memory.clone(),
418
+ state: state.cloned(),
419
+ })?;
420
+
421
+ match response {
422
+ IpcResponse::ActionResult { result, .. } => Ok(result),
423
+ IpcResponse::Error { error, .. } => Err(anyhow!("Evaluator failed: {}", error)),
424
+ _ => Err(anyhow!("Unexpected response")),
425
+ }
426
+ }
427
+ }
428
+
429
+ impl Drop for TypeScriptPluginBridge {
430
+ fn drop(&mut self) {
431
+ let _ = self.process.kill();
432
+ }
433
+ }
434
+
435
+ #[cfg(test)]
436
+ mod tests {
437
+ use super::*;
438
+
439
+ #[test]
440
+ fn test_manifest_deserialize() {
441
+ let json = r#"{
442
+ "name": "test-plugin",
443
+ "description": "A test plugin",
444
+ "actions": [{"name": "test_action"}],
445
+ "providers": [],
446
+ "evaluators": []
447
+ }"#;
448
+
449
+ let manifest: TypeScriptManifest = serde_json::from_str(json).unwrap();
450
+ assert_eq!(manifest.name, "test-plugin");
451
+ assert_eq!(manifest.actions.len(), 1);
452
+ }
453
+
454
+ #[test]
455
+ fn test_action_result_deserialize() {
456
+ let json = r#"{"success": true, "text": "Done"}"#;
457
+ let result: ActionResult = serde_json::from_str(json).unwrap();
458
+ assert!(result.success);
459
+ assert_eq!(result.text, Some("Done".to_string()));
460
+ }
461
+ }
462
+
463
+
464
+
465
+
466
+
467
+