@elizaos/interop 2.0.0-alpha.51 → 2.0.0-alpha.511

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.
@@ -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
+ }