@elizaos/interop 2.0.0-alpha.50 → 2.0.0-alpha.502
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/dist/index.d.ts +33 -0
- package/dist/index.d.ts.map +1 -0
- package/dist/index.js +121 -0
- package/dist/python-bridge.d.ts +96 -0
- package/dist/python-bridge.d.ts.map +1 -0
- package/dist/python-bridge.js +391 -0
- package/dist/types.d.ts +301 -0
- package/dist/types.d.ts.map +1 -0
- package/dist/types.js +10 -0
- package/dist/wasm-loader.d.ts +36 -0
- package/dist/wasm-loader.d.ts.map +1 -0
- package/dist/wasm-loader.js +314 -0
- package/package.json +7 -6
- package/python/tests/test_rust_ffi_loader.py +2 -2
- package/dist/packages/interop/tsconfig.tsbuildinfo +0 -1
package/dist/index.d.ts
ADDED
|
@@ -0,0 +1,33 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* elizaOS Cross-Language Interop - TypeScript
|
|
3
|
+
*
|
|
4
|
+
* This module provides utilities for loading plugins written in other languages
|
|
5
|
+
* (Rust, Python) into the TypeScript runtime.
|
|
6
|
+
*/
|
|
7
|
+
export type { PythonBridgeOptions } from "./python-bridge";
|
|
8
|
+
export { loadPythonPlugin, PythonPluginBridge, stopPythonPlugin, } from "./python-bridge";
|
|
9
|
+
export type { ActionInvokeRequest, ActionManifest, ActionResultPayload, ActionResultResponse, EvaluatorManifest, InteropProtocol, IPCMessage, IPCRequest, IPCResponse, PluginInteropConfig, PluginLanguage, PluginManifest, ProviderGetRequest, ProviderManifest, ProviderResultPayload, ProviderResultResponse, RouteManifest, ServiceManifest, WasmPluginExports, WasmPluginInstance, } from "./types";
|
|
10
|
+
export type { WasmLoaderOptions } from "./wasm-loader";
|
|
11
|
+
export { loadWasmPlugin, validateWasmPlugin } from "./wasm-loader";
|
|
12
|
+
/**
|
|
13
|
+
* Universal plugin loader that auto-detects the plugin type
|
|
14
|
+
*/
|
|
15
|
+
import type { Plugin } from "@elizaos/core";
|
|
16
|
+
import type { PluginManifest } from "./types";
|
|
17
|
+
export interface UniversalLoaderOptions {
|
|
18
|
+
/** Path to plugin manifest (plugin.json) */
|
|
19
|
+
manifestPath?: string;
|
|
20
|
+
/** Direct manifest object */
|
|
21
|
+
manifest?: PluginManifest;
|
|
22
|
+
/** Base path for resolving relative paths in manifest */
|
|
23
|
+
basePath?: string;
|
|
24
|
+
/** Python executable path */
|
|
25
|
+
pythonPath?: string;
|
|
26
|
+
/** Connection timeout */
|
|
27
|
+
timeout?: number;
|
|
28
|
+
}
|
|
29
|
+
/**
|
|
30
|
+
* Load any plugin based on its manifest
|
|
31
|
+
*/
|
|
32
|
+
export declare function loadPlugin(options: UniversalLoaderOptions): Promise<Plugin>;
|
|
33
|
+
//# sourceMappingURL=index.d.ts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"index.d.ts","sourceRoot":"","sources":["../typescript/index.ts"],"names":[],"mappings":"AAAA;;;;;GAKG;AAEH,YAAY,EAAE,mBAAmB,EAAE,MAAM,iBAAiB,CAAC;AAE3D,OAAO,EACL,gBAAgB,EAChB,kBAAkB,EAClB,gBAAgB,GACjB,MAAM,iBAAiB,CAAC;AAEzB,YAAY,EACV,mBAAmB,EACnB,cAAc,EACd,mBAAmB,EACnB,oBAAoB,EACpB,iBAAiB,EACjB,eAAe,EAEf,UAAU,EACV,UAAU,EACV,WAAW,EACX,mBAAmB,EACnB,cAAc,EACd,cAAc,EACd,kBAAkB,EAClB,gBAAgB,EAChB,qBAAqB,EACrB,sBAAsB,EACtB,aAAa,EACb,eAAe,EAEf,iBAAiB,EACjB,kBAAkB,GACnB,MAAM,SAAS,CAAC;AACjB,YAAY,EAAE,iBAAiB,EAAE,MAAM,eAAe,CAAC;AAEvD,OAAO,EAAE,cAAc,EAAE,kBAAkB,EAAE,MAAM,eAAe,CAAC;AAEnE;;GAEG;AACH,OAAO,KAAK,EAAE,MAAM,EAAE,MAAM,eAAe,CAAC;AAE5C,OAAO,KAAK,EAAuB,cAAc,EAAE,MAAM,SAAS,CAAC;AAGnE,MAAM,WAAW,sBAAsB;IACrC,4CAA4C;IAC5C,YAAY,CAAC,EAAE,MAAM,CAAC;IACtB,6BAA6B;IAC7B,QAAQ,CAAC,EAAE,cAAc,CAAC;IAC1B,yDAAyD;IACzD,QAAQ,CAAC,EAAE,MAAM,CAAC;IAClB,6BAA6B;IAC7B,UAAU,CAAC,EAAE,MAAM,CAAC;IACpB,yBAAyB;IACzB,OAAO,CAAC,EAAE,MAAM,CAAC;CAClB;AAED;;GAEG;AACH,wBAAsB,UAAU,CAC9B,OAAO,EAAE,sBAAsB,GAC9B,OAAO,CAAC,MAAM,CAAC,CA4CjB"}
|
package/dist/index.js
ADDED
|
@@ -0,0 +1,121 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* elizaOS Cross-Language Interop - TypeScript
|
|
3
|
+
*
|
|
4
|
+
* This module provides utilities for loading plugins written in other languages
|
|
5
|
+
* (Rust, Python) into the TypeScript runtime.
|
|
6
|
+
*/
|
|
7
|
+
// Python Bridge (for Python plugins via IPC)
|
|
8
|
+
export { loadPythonPlugin, PythonPluginBridge, stopPythonPlugin, } from "./python-bridge";
|
|
9
|
+
// WASM Loader (for Rust plugins compiled to WASM)
|
|
10
|
+
export { loadWasmPlugin, validateWasmPlugin } from "./wasm-loader";
|
|
11
|
+
import { loadPythonPlugin } from "./python-bridge";
|
|
12
|
+
import { loadWasmPlugin } from "./wasm-loader";
|
|
13
|
+
/**
|
|
14
|
+
* Load any plugin based on its manifest
|
|
15
|
+
*/
|
|
16
|
+
export async function loadPlugin(options) {
|
|
17
|
+
let manifest;
|
|
18
|
+
if (options.manifest) {
|
|
19
|
+
manifest = options.manifest;
|
|
20
|
+
}
|
|
21
|
+
else if (options.manifestPath) {
|
|
22
|
+
manifest = await loadManifestFile(options.manifestPath);
|
|
23
|
+
}
|
|
24
|
+
else {
|
|
25
|
+
throw new Error("Either manifest or manifestPath is required");
|
|
26
|
+
}
|
|
27
|
+
const basePath = options.basePath ?? (await getBasePath(options.manifestPath));
|
|
28
|
+
const interop = manifest.interop ?? inferInterop(manifest);
|
|
29
|
+
switch (interop.protocol) {
|
|
30
|
+
case "wasm":
|
|
31
|
+
if (!interop.wasmPath) {
|
|
32
|
+
throw new Error("WASM plugin requires wasmPath in interop config");
|
|
33
|
+
}
|
|
34
|
+
return loadWasmPlugin({
|
|
35
|
+
wasmPath: await resolvePath(basePath, interop.wasmPath),
|
|
36
|
+
});
|
|
37
|
+
case "ipc":
|
|
38
|
+
if (manifest.language === "python") {
|
|
39
|
+
return loadPythonPlugin({
|
|
40
|
+
moduleName: manifest.name.replace(/-/g, "_"),
|
|
41
|
+
pythonPath: options.pythonPath,
|
|
42
|
+
cwd: basePath,
|
|
43
|
+
timeout: options.timeout,
|
|
44
|
+
});
|
|
45
|
+
}
|
|
46
|
+
throw new Error(`IPC not supported for language: ${manifest.language}`);
|
|
47
|
+
case "native":
|
|
48
|
+
throw new Error("Native plugins must be loaded directly via import");
|
|
49
|
+
case "ffi":
|
|
50
|
+
throw new Error("FFI loading not yet implemented for TypeScript runtime");
|
|
51
|
+
default:
|
|
52
|
+
throw new Error(`Unknown interop protocol: ${interop.protocol}`);
|
|
53
|
+
}
|
|
54
|
+
}
|
|
55
|
+
/**
|
|
56
|
+
* Load manifest from file
|
|
57
|
+
*/
|
|
58
|
+
async function loadManifestFile(path) {
|
|
59
|
+
const isNode = typeof globalThis.process !== "undefined" &&
|
|
60
|
+
globalThis.process.versions &&
|
|
61
|
+
globalThis.process.versions.node;
|
|
62
|
+
if (isNode) {
|
|
63
|
+
const fs = await import("node:fs/promises");
|
|
64
|
+
const content = await fs.readFile(path, "utf-8");
|
|
65
|
+
return JSON.parse(content);
|
|
66
|
+
}
|
|
67
|
+
else {
|
|
68
|
+
const response = await fetch(path);
|
|
69
|
+
return response.json();
|
|
70
|
+
}
|
|
71
|
+
}
|
|
72
|
+
/**
|
|
73
|
+
* Get base path from manifest path
|
|
74
|
+
*/
|
|
75
|
+
async function getBasePath(manifestPath) {
|
|
76
|
+
if (!manifestPath)
|
|
77
|
+
return ".";
|
|
78
|
+
const isNode = typeof globalThis.process !== "undefined" &&
|
|
79
|
+
globalThis.process.versions &&
|
|
80
|
+
globalThis.process.versions.node;
|
|
81
|
+
if (isNode) {
|
|
82
|
+
const path = await import("node:path");
|
|
83
|
+
return path.dirname(manifestPath);
|
|
84
|
+
}
|
|
85
|
+
else {
|
|
86
|
+
return manifestPath.substring(0, manifestPath.lastIndexOf("/")) || ".";
|
|
87
|
+
}
|
|
88
|
+
}
|
|
89
|
+
/**
|
|
90
|
+
* Resolve a path relative to base
|
|
91
|
+
*/
|
|
92
|
+
async function resolvePath(base, relative) {
|
|
93
|
+
if (relative.startsWith("/") || relative.startsWith("http")) {
|
|
94
|
+
return relative;
|
|
95
|
+
}
|
|
96
|
+
const isNode = typeof globalThis.process !== "undefined" &&
|
|
97
|
+
globalThis.process.versions &&
|
|
98
|
+
globalThis.process.versions.node;
|
|
99
|
+
if (isNode) {
|
|
100
|
+
const path = await import("node:path");
|
|
101
|
+
return path.resolve(base, relative);
|
|
102
|
+
}
|
|
103
|
+
else {
|
|
104
|
+
return `${base}/${relative}`;
|
|
105
|
+
}
|
|
106
|
+
}
|
|
107
|
+
/**
|
|
108
|
+
* Infer interop config from manifest if not provided
|
|
109
|
+
*/
|
|
110
|
+
function inferInterop(manifest) {
|
|
111
|
+
switch (manifest.language) {
|
|
112
|
+
case "rust":
|
|
113
|
+
return { protocol: "wasm", wasmPath: `./dist/${manifest.name}.wasm` };
|
|
114
|
+
case "python":
|
|
115
|
+
return { protocol: "ipc" };
|
|
116
|
+
case "typescript":
|
|
117
|
+
return { protocol: "native" };
|
|
118
|
+
default:
|
|
119
|
+
return { protocol: "native" };
|
|
120
|
+
}
|
|
121
|
+
}
|
|
@@ -0,0 +1,96 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Python Plugin Bridge for elizaOS
|
|
3
|
+
*
|
|
4
|
+
* Loads Python plugins via subprocess IPC and adapts them
|
|
5
|
+
* to the TypeScript Plugin interface.
|
|
6
|
+
*/
|
|
7
|
+
import { EventEmitter } from "node:events";
|
|
8
|
+
import type { Plugin } from "@elizaos/core";
|
|
9
|
+
import type { IPCRequest, IPCResponse, PluginManifest } from "./types";
|
|
10
|
+
/**
|
|
11
|
+
* Options for loading a Python plugin
|
|
12
|
+
*/
|
|
13
|
+
export interface PythonBridgeOptions {
|
|
14
|
+
/** Python module name to import */
|
|
15
|
+
moduleName: string;
|
|
16
|
+
/** Path to Python executable (defaults to 'python3') */
|
|
17
|
+
pythonPath?: string;
|
|
18
|
+
/** Working directory for the subprocess */
|
|
19
|
+
cwd?: string;
|
|
20
|
+
/** Additional environment variables */
|
|
21
|
+
env?: Record<string, string>;
|
|
22
|
+
/**
|
|
23
|
+
* Whether to inherit the parent process environment variables.
|
|
24
|
+
*
|
|
25
|
+
* Defaults to true for compatibility. For tighter isolation, set to false and pass only
|
|
26
|
+
* explicit `env` entries.
|
|
27
|
+
*/
|
|
28
|
+
inheritEnv?: boolean;
|
|
29
|
+
/** Environment variable names to remove when inheriting. */
|
|
30
|
+
envDenylist?: string[];
|
|
31
|
+
/** Path to the bridge script */
|
|
32
|
+
bridgeScriptPath?: string;
|
|
33
|
+
/** Connection timeout in milliseconds */
|
|
34
|
+
timeout?: number;
|
|
35
|
+
/** Maximum number of in-flight IPC requests (prevents unbounded memory growth). */
|
|
36
|
+
maxPendingRequests?: number;
|
|
37
|
+
/** Maximum size (bytes) of a single newline-delimited IPC message. */
|
|
38
|
+
maxMessageBytes?: number;
|
|
39
|
+
/** Maximum size (bytes) of the internal stdout buffer. */
|
|
40
|
+
maxBufferBytes?: number;
|
|
41
|
+
}
|
|
42
|
+
/**
|
|
43
|
+
* Python plugin bridge that communicates via subprocess
|
|
44
|
+
*/
|
|
45
|
+
export declare class PythonPluginBridge extends EventEmitter {
|
|
46
|
+
private options;
|
|
47
|
+
private process;
|
|
48
|
+
private pendingRequests;
|
|
49
|
+
private messageBuffer;
|
|
50
|
+
private manifest;
|
|
51
|
+
private initialized;
|
|
52
|
+
private requestCounter;
|
|
53
|
+
constructor(options: PythonBridgeOptions);
|
|
54
|
+
private buildChildEnv;
|
|
55
|
+
/**
|
|
56
|
+
* Start the Python subprocess
|
|
57
|
+
*/
|
|
58
|
+
start(): Promise<void>;
|
|
59
|
+
/**
|
|
60
|
+
* Wait for the Python process to send ready message
|
|
61
|
+
*/
|
|
62
|
+
private waitForReady;
|
|
63
|
+
/**
|
|
64
|
+
* Handle incoming data from subprocess
|
|
65
|
+
*/
|
|
66
|
+
private handleData;
|
|
67
|
+
/**
|
|
68
|
+
* Handle a parsed IPC message
|
|
69
|
+
*/
|
|
70
|
+
private handleMessage;
|
|
71
|
+
/**
|
|
72
|
+
* Send a request and wait for response
|
|
73
|
+
*/
|
|
74
|
+
sendRequest<T extends IPCResponse>(request: IPCRequest): Promise<T>;
|
|
75
|
+
/**
|
|
76
|
+
* Get the plugin manifest
|
|
77
|
+
*/
|
|
78
|
+
getManifest(): PluginManifest | null;
|
|
79
|
+
/**
|
|
80
|
+
* Stop the Python subprocess
|
|
81
|
+
*/
|
|
82
|
+
stop(): Promise<void>;
|
|
83
|
+
/**
|
|
84
|
+
* Cleanup resources
|
|
85
|
+
*/
|
|
86
|
+
private cleanup;
|
|
87
|
+
}
|
|
88
|
+
/**
|
|
89
|
+
* Load a Python plugin and return an elizaOS Plugin interface
|
|
90
|
+
*/
|
|
91
|
+
export declare function loadPythonPlugin(options: PythonBridgeOptions): Promise<Plugin>;
|
|
92
|
+
/**
|
|
93
|
+
* Stop a Python plugin bridge
|
|
94
|
+
*/
|
|
95
|
+
export declare function stopPythonPlugin(plugin: Plugin): Promise<void>;
|
|
96
|
+
//# sourceMappingURL=python-bridge.d.ts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"python-bridge.d.ts","sourceRoot":"","sources":["../typescript/python-bridge.ts"],"names":[],"mappings":"AAAA;;;;;GAKG;AAEH,OAAO,EAAE,YAAY,EAAE,MAAM,aAAa,CAAC;AAC3C,OAAO,KAAK,EASV,MAAM,EAKP,MAAM,eAAe,CAAC;AAEvB,OAAO,KAAK,EAGV,UAAU,EACV,WAAW,EACX,cAAc,EAGf,MAAM,SAAS,CAAC;AAEjB;;GAEG;AACH,MAAM,WAAW,mBAAmB;IAClC,mCAAmC;IACnC,UAAU,EAAE,MAAM,CAAC;IACnB,wDAAwD;IACxD,UAAU,CAAC,EAAE,MAAM,CAAC;IACpB,2CAA2C;IAC3C,GAAG,CAAC,EAAE,MAAM,CAAC;IACb,uCAAuC;IACvC,GAAG,CAAC,EAAE,MAAM,CAAC,MAAM,EAAE,MAAM,CAAC,CAAC;IAC7B;;;;;OAKG;IACH,UAAU,CAAC,EAAE,OAAO,CAAC;IACrB,4DAA4D;IAC5D,WAAW,CAAC,EAAE,MAAM,EAAE,CAAC;IACvB,gCAAgC;IAChC,gBAAgB,CAAC,EAAE,MAAM,CAAC;IAC1B,yCAAyC;IACzC,OAAO,CAAC,EAAE,MAAM,CAAC;IACjB,mFAAmF;IACnF,kBAAkB,CAAC,EAAE,MAAM,CAAC;IAC5B,sEAAsE;IACtE,eAAe,CAAC,EAAE,MAAM,CAAC;IACzB,0DAA0D;IAC1D,cAAc,CAAC,EAAE,MAAM,CAAC;CACzB;AAWD;;GAEG;AACH,qBAAa,kBAAmB,SAAQ,YAAY;IAStC,OAAO,CAAC,OAAO;IAR3B,OAAO,CAAC,OAAO,CACR;IACP,OAAO,CAAC,eAAe,CAA0C;IACjE,OAAO,CAAC,aAAa,CAAc;IACnC,OAAO,CAAC,QAAQ,CAA+B;IAC/C,OAAO,CAAC,WAAW,CAAkB;IACrC,OAAO,CAAC,cAAc,CAAa;gBAEf,OAAO,EAAE,mBAAmB;IAIhD,OAAO,CAAC,aAAa;IA0BrB;;OAEG;IACG,KAAK,IAAI,OAAO,CAAC,IAAI,CAAC;IAuD5B;;OAEG;YACW,YAAY;IAsB1B;;OAEG;IACH,OAAO,CAAC,UAAU;IAuDlB;;OAEG;IACH,OAAO,CAAC,aAAa;IAkBrB;;OAEG;IACG,WAAW,CAAC,CAAC,SAAS,WAAW,EAAE,OAAO,EAAE,UAAU,GAAG,OAAO,CAAC,CAAC,CAAC;IAkCzE;;OAEG;IACH,WAAW,IAAI,cAAc,GAAG,IAAI;IAIpC;;OAEG;IACG,IAAI,IAAI,OAAO,CAAC,IAAI,CAAC;IA0B3B;;OAEG;IACH,OAAO,CAAC,OAAO;CAWhB;AAED;;GAEG;AACH,wBAAsB,gBAAgB,CACpC,OAAO,EAAE,mBAAmB,GAC3B,OAAO,CAAC,MAAM,CAAC,CAUjB;AA8KD;;GAEG;AACH,wBAAsB,gBAAgB,CAAC,MAAM,EAAE,MAAM,GAAG,OAAO,CAAC,IAAI,CAAC,CAOpE"}
|
|
@@ -0,0 +1,391 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Python Plugin Bridge for elizaOS
|
|
3
|
+
*
|
|
4
|
+
* Loads Python plugins via subprocess IPC and adapts them
|
|
5
|
+
* to the TypeScript Plugin interface.
|
|
6
|
+
*/
|
|
7
|
+
import { EventEmitter } from "node:events";
|
|
8
|
+
import { logger } from "@elizaos/core";
|
|
9
|
+
/**
|
|
10
|
+
* Python plugin bridge that communicates via subprocess
|
|
11
|
+
*/
|
|
12
|
+
export class PythonPluginBridge extends EventEmitter {
|
|
13
|
+
options;
|
|
14
|
+
process = null;
|
|
15
|
+
pendingRequests = new Map();
|
|
16
|
+
messageBuffer = "";
|
|
17
|
+
manifest = null;
|
|
18
|
+
initialized = false;
|
|
19
|
+
requestCounter = 0;
|
|
20
|
+
constructor(options) {
|
|
21
|
+
super();
|
|
22
|
+
this.options = options;
|
|
23
|
+
}
|
|
24
|
+
buildChildEnv() {
|
|
25
|
+
const inherit = this.options.inheritEnv !== false;
|
|
26
|
+
const base = {};
|
|
27
|
+
if (inherit) {
|
|
28
|
+
for (const [k, v] of Object.entries(process.env)) {
|
|
29
|
+
if (typeof v === "string") {
|
|
30
|
+
base[k] = v;
|
|
31
|
+
}
|
|
32
|
+
}
|
|
33
|
+
}
|
|
34
|
+
const deny = new Set(this.options.envDenylist ?? []);
|
|
35
|
+
for (const key of deny) {
|
|
36
|
+
delete base[key];
|
|
37
|
+
}
|
|
38
|
+
if (this.options.env) {
|
|
39
|
+
for (const [k, v] of Object.entries(this.options.env)) {
|
|
40
|
+
base[k] = v;
|
|
41
|
+
}
|
|
42
|
+
}
|
|
43
|
+
return base;
|
|
44
|
+
}
|
|
45
|
+
/**
|
|
46
|
+
* Start the Python subprocess
|
|
47
|
+
*/
|
|
48
|
+
async start() {
|
|
49
|
+
const { spawn } = await import("node:child_process");
|
|
50
|
+
const pythonPath = this.options.pythonPath ?? "python3";
|
|
51
|
+
const bridgeScript = this.options.bridgeScriptPath ??
|
|
52
|
+
new URL("../python/bridge_server.py", import.meta.url).pathname;
|
|
53
|
+
this.process = spawn(pythonPath, ["-u", bridgeScript, "--module", this.options.moduleName], {
|
|
54
|
+
cwd: this.options.cwd,
|
|
55
|
+
env: this.buildChildEnv(),
|
|
56
|
+
stdio: ["pipe", "pipe", "pipe"],
|
|
57
|
+
});
|
|
58
|
+
// Handle stdout (JSON-RPC messages)
|
|
59
|
+
if (this.process.stdout) {
|
|
60
|
+
this.process.stdout.on("data", (data) => {
|
|
61
|
+
this.handleData(data.toString());
|
|
62
|
+
});
|
|
63
|
+
}
|
|
64
|
+
// Handle stderr (logging)
|
|
65
|
+
if (this.process.stderr) {
|
|
66
|
+
this.process.stderr.on("data", (data) => {
|
|
67
|
+
logger.error({
|
|
68
|
+
src: "interop:python-bridge",
|
|
69
|
+
event: "interop.ipc.stderr",
|
|
70
|
+
moduleName: this.options.moduleName,
|
|
71
|
+
stream: "stderr",
|
|
72
|
+
}, data.toString());
|
|
73
|
+
});
|
|
74
|
+
}
|
|
75
|
+
// Handle process exit
|
|
76
|
+
this.process.on("exit", (code, signal) => {
|
|
77
|
+
this.emit("exit", { code, signal });
|
|
78
|
+
this.cleanup();
|
|
79
|
+
});
|
|
80
|
+
this.process.on("error", (error) => {
|
|
81
|
+
this.emit("error", error);
|
|
82
|
+
});
|
|
83
|
+
// Wait for the ready message with manifest
|
|
84
|
+
await this.waitForReady();
|
|
85
|
+
this.initialized = true;
|
|
86
|
+
}
|
|
87
|
+
/**
|
|
88
|
+
* Wait for the Python process to send ready message
|
|
89
|
+
*/
|
|
90
|
+
async waitForReady() {
|
|
91
|
+
const timeout = this.options.timeout ?? 30000;
|
|
92
|
+
return new Promise((resolve, reject) => {
|
|
93
|
+
const timer = setTimeout(() => {
|
|
94
|
+
reject(new Error(`Python plugin startup timeout after ${timeout}ms`));
|
|
95
|
+
}, timeout);
|
|
96
|
+
const handler = (msg) => {
|
|
97
|
+
if (msg.type === "ready" && "manifest" in msg) {
|
|
98
|
+
clearTimeout(timer);
|
|
99
|
+
this.manifest = msg.manifest;
|
|
100
|
+
resolve();
|
|
101
|
+
}
|
|
102
|
+
};
|
|
103
|
+
this.once("message", handler);
|
|
104
|
+
});
|
|
105
|
+
}
|
|
106
|
+
/**
|
|
107
|
+
* Handle incoming data from subprocess
|
|
108
|
+
*/
|
|
109
|
+
handleData(data) {
|
|
110
|
+
const maxBufferBytes = this.options.maxBufferBytes ?? 2_000_000;
|
|
111
|
+
const maxMessageBytes = this.options.maxMessageBytes ?? 1_000_000;
|
|
112
|
+
this.messageBuffer += data;
|
|
113
|
+
if (Buffer.byteLength(this.messageBuffer, "utf8") > maxBufferBytes) {
|
|
114
|
+
logger.error({
|
|
115
|
+
src: "interop:python-bridge",
|
|
116
|
+
event: "interop.ipc.stdout_buffer_exceeded",
|
|
117
|
+
moduleName: this.options.moduleName,
|
|
118
|
+
}, `IPC stdout buffer exceeded limit (${maxBufferBytes} bytes); terminating bridge`);
|
|
119
|
+
this.process?.kill("SIGKILL");
|
|
120
|
+
this.cleanup();
|
|
121
|
+
return;
|
|
122
|
+
}
|
|
123
|
+
// Process complete JSON messages (newline-delimited)
|
|
124
|
+
const lines = this.messageBuffer.split("\n");
|
|
125
|
+
this.messageBuffer = lines.pop() ?? "";
|
|
126
|
+
for (const line of lines) {
|
|
127
|
+
if (line.trim()) {
|
|
128
|
+
if (Buffer.byteLength(line, "utf8") > maxMessageBytes) {
|
|
129
|
+
logger.error({
|
|
130
|
+
src: "interop:python-bridge",
|
|
131
|
+
event: "interop.ipc.message_exceeded",
|
|
132
|
+
moduleName: this.options.moduleName,
|
|
133
|
+
}, `IPC message exceeded limit (${maxMessageBytes} bytes); terminating bridge`);
|
|
134
|
+
this.process?.kill("SIGKILL");
|
|
135
|
+
this.cleanup();
|
|
136
|
+
return;
|
|
137
|
+
}
|
|
138
|
+
try {
|
|
139
|
+
const message = JSON.parse(line);
|
|
140
|
+
this.handleMessage(message);
|
|
141
|
+
}
|
|
142
|
+
catch (_error) {
|
|
143
|
+
logger.error({
|
|
144
|
+
src: "interop:python-bridge",
|
|
145
|
+
event: "interop.ipc.parse_failed",
|
|
146
|
+
moduleName: this.options.moduleName,
|
|
147
|
+
}, `Failed to parse IPC message: ${line}`);
|
|
148
|
+
}
|
|
149
|
+
}
|
|
150
|
+
}
|
|
151
|
+
}
|
|
152
|
+
/**
|
|
153
|
+
* Handle a parsed IPC message
|
|
154
|
+
*/
|
|
155
|
+
handleMessage(message) {
|
|
156
|
+
this.emit("message", message);
|
|
157
|
+
// Check if this is a response to a pending request
|
|
158
|
+
if (message.id && this.pendingRequests.has(message.id)) {
|
|
159
|
+
const pending = this.pendingRequests.get(message.id);
|
|
160
|
+
if (!pending)
|
|
161
|
+
return;
|
|
162
|
+
this.pendingRequests.delete(message.id);
|
|
163
|
+
clearTimeout(pending.timeout);
|
|
164
|
+
if (message.type === "error") {
|
|
165
|
+
pending.reject(new Error(message.error));
|
|
166
|
+
}
|
|
167
|
+
else {
|
|
168
|
+
pending.resolve(message);
|
|
169
|
+
}
|
|
170
|
+
}
|
|
171
|
+
}
|
|
172
|
+
/**
|
|
173
|
+
* Send a request and wait for response
|
|
174
|
+
*/
|
|
175
|
+
async sendRequest(request) {
|
|
176
|
+
if (!this.process || !this.initialized) {
|
|
177
|
+
throw new Error("Python bridge not started");
|
|
178
|
+
}
|
|
179
|
+
const maxPendingRequests = this.options.maxPendingRequests ?? 1000;
|
|
180
|
+
if (this.pendingRequests.size >= maxPendingRequests) {
|
|
181
|
+
throw new Error(`Too many pending IPC requests (max=${maxPendingRequests})`);
|
|
182
|
+
}
|
|
183
|
+
const id = `req_${++this.requestCounter}`;
|
|
184
|
+
const requestWithId = { ...request, id };
|
|
185
|
+
return new Promise((resolve, reject) => {
|
|
186
|
+
const timeout = setTimeout(() => {
|
|
187
|
+
this.pendingRequests.delete(id);
|
|
188
|
+
reject(new Error(`Request timeout for ${request.type}`));
|
|
189
|
+
}, this.options.timeout ?? 30000);
|
|
190
|
+
this.pendingRequests.set(id, {
|
|
191
|
+
resolve: resolve,
|
|
192
|
+
reject,
|
|
193
|
+
timeout,
|
|
194
|
+
});
|
|
195
|
+
const json = `${JSON.stringify(requestWithId)}\n`;
|
|
196
|
+
if (this.process?.stdin) {
|
|
197
|
+
this.process.stdin.write(json);
|
|
198
|
+
}
|
|
199
|
+
});
|
|
200
|
+
}
|
|
201
|
+
/**
|
|
202
|
+
* Get the plugin manifest
|
|
203
|
+
*/
|
|
204
|
+
getManifest() {
|
|
205
|
+
return this.manifest;
|
|
206
|
+
}
|
|
207
|
+
/**
|
|
208
|
+
* Stop the Python subprocess
|
|
209
|
+
*/
|
|
210
|
+
async stop() {
|
|
211
|
+
if (this.process) {
|
|
212
|
+
this.process.kill("SIGTERM");
|
|
213
|
+
// Wait for graceful shutdown
|
|
214
|
+
await new Promise((resolve) => {
|
|
215
|
+
const timeout = setTimeout(() => {
|
|
216
|
+
if (this.process) {
|
|
217
|
+
this.process.kill("SIGKILL");
|
|
218
|
+
}
|
|
219
|
+
resolve();
|
|
220
|
+
}, 5000);
|
|
221
|
+
if (this.process) {
|
|
222
|
+
this.process.on("exit", () => {
|
|
223
|
+
clearTimeout(timeout);
|
|
224
|
+
resolve();
|
|
225
|
+
});
|
|
226
|
+
}
|
|
227
|
+
else {
|
|
228
|
+
resolve();
|
|
229
|
+
}
|
|
230
|
+
});
|
|
231
|
+
}
|
|
232
|
+
this.cleanup();
|
|
233
|
+
}
|
|
234
|
+
/**
|
|
235
|
+
* Cleanup resources
|
|
236
|
+
*/
|
|
237
|
+
cleanup() {
|
|
238
|
+
this.process = null;
|
|
239
|
+
this.initialized = false;
|
|
240
|
+
// Reject all pending requests
|
|
241
|
+
for (const pending of this.pendingRequests.values()) {
|
|
242
|
+
clearTimeout(pending.timeout);
|
|
243
|
+
pending.reject(new Error("Bridge closed"));
|
|
244
|
+
}
|
|
245
|
+
this.pendingRequests.clear();
|
|
246
|
+
}
|
|
247
|
+
}
|
|
248
|
+
/**
|
|
249
|
+
* Load a Python plugin and return an elizaOS Plugin interface
|
|
250
|
+
*/
|
|
251
|
+
export async function loadPythonPlugin(options) {
|
|
252
|
+
const bridge = new PythonPluginBridge(options);
|
|
253
|
+
await bridge.start();
|
|
254
|
+
const manifest = bridge.getManifest();
|
|
255
|
+
if (!manifest) {
|
|
256
|
+
throw new Error("Failed to get plugin manifest");
|
|
257
|
+
}
|
|
258
|
+
return createPluginFromBridge(manifest, bridge);
|
|
259
|
+
}
|
|
260
|
+
/**
|
|
261
|
+
* Create a Plugin from a Python bridge
|
|
262
|
+
*/
|
|
263
|
+
function createPluginFromBridge(manifest, bridge) {
|
|
264
|
+
// Create action wrappers
|
|
265
|
+
const actions = (manifest.actions ?? []).map((actionDef) => ({
|
|
266
|
+
name: actionDef.name,
|
|
267
|
+
description: actionDef.description,
|
|
268
|
+
similes: actionDef.similes,
|
|
269
|
+
examples: actionDef.examples,
|
|
270
|
+
validate: async (_runtime, message, state) => {
|
|
271
|
+
const response = await bridge.sendRequest({
|
|
272
|
+
type: "action.validate",
|
|
273
|
+
id: "",
|
|
274
|
+
action: actionDef.name,
|
|
275
|
+
memory: message,
|
|
276
|
+
state: state ?? null,
|
|
277
|
+
});
|
|
278
|
+
return response.valid;
|
|
279
|
+
},
|
|
280
|
+
handler: async (_runtime, message, state, options, _callback) => {
|
|
281
|
+
const response = await bridge.sendRequest({
|
|
282
|
+
type: "action.invoke",
|
|
283
|
+
id: "",
|
|
284
|
+
action: actionDef.name,
|
|
285
|
+
memory: message,
|
|
286
|
+
state: state ?? null,
|
|
287
|
+
options: options ?? null,
|
|
288
|
+
});
|
|
289
|
+
const result = response.result;
|
|
290
|
+
return {
|
|
291
|
+
success: result.success,
|
|
292
|
+
text: result.text,
|
|
293
|
+
error: result.error ? new Error(result.error) : undefined,
|
|
294
|
+
data: result.data,
|
|
295
|
+
values: result.values,
|
|
296
|
+
};
|
|
297
|
+
},
|
|
298
|
+
}));
|
|
299
|
+
// Create provider wrappers
|
|
300
|
+
const providers = (manifest.providers ?? []).map((providerDef) => ({
|
|
301
|
+
name: providerDef.name,
|
|
302
|
+
description: providerDef.description,
|
|
303
|
+
dynamic: providerDef.dynamic,
|
|
304
|
+
position: providerDef.position,
|
|
305
|
+
private: providerDef.private,
|
|
306
|
+
get: async (_runtime, message, state) => {
|
|
307
|
+
const response = await bridge.sendRequest({
|
|
308
|
+
type: "provider.get",
|
|
309
|
+
id: "",
|
|
310
|
+
provider: providerDef.name,
|
|
311
|
+
memory: message,
|
|
312
|
+
state: state,
|
|
313
|
+
});
|
|
314
|
+
return {
|
|
315
|
+
text: response.result.text,
|
|
316
|
+
values: response.result.values,
|
|
317
|
+
data: response.result.data,
|
|
318
|
+
};
|
|
319
|
+
},
|
|
320
|
+
}));
|
|
321
|
+
// Create evaluator wrappers
|
|
322
|
+
const evaluators = (manifest.evaluators ?? []).map((evalDef) => ({
|
|
323
|
+
name: evalDef.name,
|
|
324
|
+
description: evalDef.description,
|
|
325
|
+
alwaysRun: evalDef.alwaysRun,
|
|
326
|
+
similes: evalDef.similes,
|
|
327
|
+
examples: [],
|
|
328
|
+
validate: async (_runtime, message, state) => {
|
|
329
|
+
const response = await bridge.sendRequest({
|
|
330
|
+
type: "action.validate",
|
|
331
|
+
id: "",
|
|
332
|
+
action: evalDef.name,
|
|
333
|
+
memory: message,
|
|
334
|
+
state: state ?? null,
|
|
335
|
+
});
|
|
336
|
+
return response.valid;
|
|
337
|
+
},
|
|
338
|
+
handler: async (_runtime, message, state) => {
|
|
339
|
+
const response = await bridge.sendRequest({
|
|
340
|
+
type: "evaluator.invoke",
|
|
341
|
+
id: "",
|
|
342
|
+
evaluator: evalDef.name,
|
|
343
|
+
memory: message,
|
|
344
|
+
state: state ?? null,
|
|
345
|
+
});
|
|
346
|
+
if (!response.result) {
|
|
347
|
+
return undefined;
|
|
348
|
+
}
|
|
349
|
+
return {
|
|
350
|
+
success: response.result.success,
|
|
351
|
+
text: response.result.text,
|
|
352
|
+
error: response.result.error
|
|
353
|
+
? new Error(response.result.error)
|
|
354
|
+
: undefined,
|
|
355
|
+
data: response.result.data,
|
|
356
|
+
values: response.result.values,
|
|
357
|
+
};
|
|
358
|
+
},
|
|
359
|
+
}));
|
|
360
|
+
// Store bridge reference for cleanup
|
|
361
|
+
const bridgeRef = { current: bridge };
|
|
362
|
+
return {
|
|
363
|
+
name: manifest.name,
|
|
364
|
+
description: manifest.description,
|
|
365
|
+
config: manifest.config ?? {},
|
|
366
|
+
dependencies: manifest.dependencies,
|
|
367
|
+
actions,
|
|
368
|
+
providers,
|
|
369
|
+
evaluators,
|
|
370
|
+
routes: [],
|
|
371
|
+
services: [],
|
|
372
|
+
async init(config) {
|
|
373
|
+
await bridge.sendRequest({
|
|
374
|
+
type: "plugin.init",
|
|
375
|
+
id: "",
|
|
376
|
+
config,
|
|
377
|
+
});
|
|
378
|
+
},
|
|
379
|
+
// Extension for cleanup
|
|
380
|
+
_bridge: bridgeRef,
|
|
381
|
+
};
|
|
382
|
+
}
|
|
383
|
+
/**
|
|
384
|
+
* Stop a Python plugin bridge
|
|
385
|
+
*/
|
|
386
|
+
export async function stopPythonPlugin(plugin) {
|
|
387
|
+
const extended = plugin;
|
|
388
|
+
if (extended._bridge?.current) {
|
|
389
|
+
await extended._bridge.current.stop();
|
|
390
|
+
}
|
|
391
|
+
}
|