@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,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
|
+
|