@elizaos/plugin-workflow 2.0.0-beta.1 → 2.0.3-beta.5

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 (170) hide show
  1. package/LICENSE +21 -0
  2. package/README.md +28 -26
  3. package/dist/actions/eval-code.d.ts +12 -0
  4. package/dist/actions/eval-code.d.ts.map +1 -0
  5. package/dist/actions/eval-code.js +59 -0
  6. package/dist/actions/eval-code.js.map +1 -0
  7. package/dist/actions/index.d.ts +1 -0
  8. package/dist/actions/index.d.ts.map +1 -1
  9. package/dist/actions/index.js +1 -0
  10. package/dist/actions/index.js.map +1 -1
  11. package/dist/actions/workflow.d.ts +7 -0
  12. package/dist/actions/workflow.d.ts.map +1 -1
  13. package/dist/actions/workflow.js +462 -10
  14. package/dist/actions/workflow.js.map +1 -1
  15. package/dist/db/schema.d.ts +196 -0
  16. package/dist/db/schema.d.ts.map +1 -1
  17. package/dist/db/schema.js +23 -0
  18. package/dist/db/schema.js.map +1 -1
  19. package/dist/index.d.ts.map +1 -1
  20. package/dist/index.js +9 -64
  21. package/dist/index.js.map +1 -1
  22. package/dist/lib/automations-builder.d.ts.map +1 -1
  23. package/dist/lib/automations-builder.js +10 -35
  24. package/dist/lib/automations-builder.js.map +1 -1
  25. package/dist/lib/automations-types.d.ts +2 -2
  26. package/dist/lib/automations-types.d.ts.map +1 -1
  27. package/dist/lib/automations-types.js.map +1 -1
  28. package/dist/lib/index.d.ts +0 -2
  29. package/dist/lib/index.d.ts.map +1 -1
  30. package/dist/lib/index.js +1 -2
  31. package/dist/lib/index.js.map +1 -1
  32. package/dist/lib/workflow-clarification.d.ts +2 -2
  33. package/dist/lib/workflow-clarification.d.ts.map +1 -1
  34. package/dist/lib/workflow-clarification.js +15 -11
  35. package/dist/lib/workflow-clarification.js.map +1 -1
  36. package/dist/plugin-routes.d.ts.map +1 -1
  37. package/dist/plugin-routes.js +6 -0
  38. package/dist/plugin-routes.js.map +1 -1
  39. package/dist/providers/activeWorkflows.js +2 -2
  40. package/dist/providers/activeWorkflows.js.map +1 -1
  41. package/dist/providers/workflowStatus.js +1 -1
  42. package/dist/providers/workflowStatus.js.map +1 -1
  43. package/dist/routes/workflow-routes.d.ts.map +1 -1
  44. package/dist/routes/workflow-routes.js +68 -2
  45. package/dist/routes/workflow-routes.js.map +1 -1
  46. package/dist/routes/workflows.d.ts.map +1 -1
  47. package/dist/routes/workflows.js +5 -1
  48. package/dist/routes/workflows.js.map +1 -1
  49. package/dist/services/embedded-workflow-service.d.ts +74 -17
  50. package/dist/services/embedded-workflow-service.d.ts.map +1 -1
  51. package/dist/services/embedded-workflow-service.js +343 -149
  52. package/dist/services/embedded-workflow-service.js.map +1 -1
  53. package/dist/services/smithers-runtime.d.ts +47 -0
  54. package/dist/services/smithers-runtime.d.ts.map +1 -0
  55. package/dist/services/smithers-runtime.js +444 -0
  56. package/dist/services/smithers-runtime.js.map +1 -0
  57. package/dist/services/workflow-credential-store.js +1 -1
  58. package/dist/services/workflow-credential-store.js.map +1 -1
  59. package/dist/services/workflow-dispatch.d.ts +31 -1
  60. package/dist/services/workflow-dispatch.d.ts.map +1 -1
  61. package/dist/services/workflow-dispatch.js +75 -10
  62. package/dist/services/workflow-dispatch.js.map +1 -1
  63. package/dist/services/workflow-service.d.ts +27 -1
  64. package/dist/services/workflow-service.d.ts.map +1 -1
  65. package/dist/services/workflow-service.js +133 -11
  66. package/dist/services/workflow-service.js.map +1 -1
  67. package/dist/trigger-routes.d.ts +2 -18
  68. package/dist/trigger-routes.d.ts.map +1 -1
  69. package/dist/trigger-routes.js +11 -39
  70. package/dist/trigger-routes.js.map +1 -1
  71. package/dist/types/index.d.ts +82 -2
  72. package/dist/types/index.d.ts.map +1 -1
  73. package/dist/types/index.js.map +1 -1
  74. package/dist/types/workflow-contracts.d.ts +118 -0
  75. package/dist/types/workflow-contracts.d.ts.map +1 -0
  76. package/dist/types/workflow-contracts.js +2 -0
  77. package/dist/types/workflow-contracts.js.map +1 -0
  78. package/dist/utils/catalog.js +2 -2
  79. package/dist/utils/catalog.js.map +1 -1
  80. package/dist/utils/clarification.d.ts +1 -1
  81. package/dist/utils/clarification.d.ts.map +1 -1
  82. package/dist/utils/clarification.js +15 -4
  83. package/dist/utils/clarification.js.map +1 -1
  84. package/dist/utils/context.js +1 -1
  85. package/dist/utils/context.js.map +1 -1
  86. package/dist/utils/evaluation-samples.d.ts +6 -0
  87. package/dist/utils/evaluation-samples.d.ts.map +1 -0
  88. package/dist/utils/evaluation-samples.js +216 -0
  89. package/dist/utils/evaluation-samples.js.map +1 -0
  90. package/dist/utils/execution-diagnostics.d.ts +26 -0
  91. package/dist/utils/execution-diagnostics.d.ts.map +1 -0
  92. package/dist/utils/execution-diagnostics.js +159 -0
  93. package/dist/utils/execution-diagnostics.js.map +1 -0
  94. package/dist/utils/generation.d.ts.map +1 -1
  95. package/dist/utils/generation.js +134 -19
  96. package/dist/utils/generation.js.map +1 -1
  97. package/dist/utils/host-capabilities.d.ts.map +1 -1
  98. package/dist/utils/host-capabilities.js +20 -5
  99. package/dist/utils/host-capabilities.js.map +1 -1
  100. package/dist/utils/inferSyntheticOutputSchema.js +3 -3
  101. package/dist/utils/inferSyntheticOutputSchema.js.map +1 -1
  102. package/dist/utils/outputSchema.js +1 -1
  103. package/dist/utils/outputSchema.js.map +1 -1
  104. package/dist/utils/validateAndRepair.js +10 -10
  105. package/dist/utils/validateAndRepair.js.map +1 -1
  106. package/dist/utils/workflow-prompts/draftIntent.d.ts +1 -1
  107. package/dist/utils/workflow-prompts/draftIntent.d.ts.map +1 -1
  108. package/dist/utils/workflow-prompts/draftIntent.js +1 -1
  109. package/dist/utils/workflow-prompts/keywordExtraction.d.ts +1 -1
  110. package/dist/utils/workflow-prompts/keywordExtraction.d.ts.map +1 -1
  111. package/dist/utils/workflow-prompts/keywordExtraction.js +1 -1
  112. package/dist/utils/workflow-prompts/workflowGeneration.d.ts +1 -1
  113. package/dist/utils/workflow-prompts/workflowGeneration.d.ts.map +1 -1
  114. package/dist/utils/workflow-prompts/workflowGeneration.js +4 -4
  115. package/dist/utils/workflow-prompts/workflowMatching.d.ts +1 -1
  116. package/dist/utils/workflow-prompts/workflowMatching.d.ts.map +1 -1
  117. package/dist/utils/workflow-prompts/workflowMatching.js +1 -1
  118. package/dist/utils/workflow.d.ts +1 -0
  119. package/dist/utils/workflow.d.ts.map +1 -1
  120. package/dist/utils/workflow.js +44 -8
  121. package/dist/utils/workflow.js.map +1 -1
  122. package/package.json +27 -8
  123. package/registry-entry.json +25 -0
  124. package/src/actions/eval-code.ts +81 -0
  125. package/src/actions/index.ts +1 -0
  126. package/src/actions/workflow.ts +518 -10
  127. package/src/db/schema.ts +31 -0
  128. package/src/index.ts +9 -82
  129. package/src/lib/automations-builder.ts +11 -35
  130. package/src/lib/automations-types.ts +1 -2
  131. package/src/lib/index.ts +0 -8
  132. package/src/lib/workflow-clarification.ts +18 -13
  133. package/src/plugin-routes.ts +6 -0
  134. package/src/providers/activeWorkflows.ts +2 -2
  135. package/src/providers/workflowStatus.ts +1 -1
  136. package/src/routes/workflow-routes.ts +100 -2
  137. package/src/routes/workflows.ts +5 -1
  138. package/src/services/embedded-workflow-service.ts +447 -172
  139. package/src/services/smithers-runtime.ts +526 -0
  140. package/src/services/workflow-credential-store.ts +1 -1
  141. package/src/services/workflow-dispatch.ts +116 -13
  142. package/src/services/workflow-service.ts +186 -10
  143. package/src/trigger-routes.ts +12 -70
  144. package/src/types/index.ts +94 -2
  145. package/src/types/workflow-contracts.ts +166 -0
  146. package/src/utils/catalog.ts +2 -2
  147. package/src/utils/clarification.ts +19 -5
  148. package/src/utils/context.ts +1 -1
  149. package/src/utils/evaluation-samples.ts +239 -0
  150. package/src/utils/execution-diagnostics.ts +192 -0
  151. package/src/utils/generation.ts +224 -32
  152. package/src/utils/host-capabilities.ts +21 -5
  153. package/src/utils/inferSyntheticOutputSchema.ts +3 -3
  154. package/src/utils/outputSchema.ts +1 -1
  155. package/src/utils/validateAndRepair.ts +10 -10
  156. package/src/utils/workflow-prompts/draftIntent.ts +1 -1
  157. package/src/utils/workflow-prompts/keywordExtraction.ts +1 -1
  158. package/src/utils/workflow-prompts/workflowGeneration.ts +4 -4
  159. package/src/utils/workflow-prompts/workflowMatching.ts +1 -1
  160. package/src/utils/workflow.ts +56 -8
  161. package/dist/lib/legacy-task-migration.d.ts +0 -20
  162. package/dist/lib/legacy-task-migration.d.ts.map +0 -1
  163. package/dist/lib/legacy-task-migration.js +0 -110
  164. package/dist/lib/legacy-task-migration.js.map +0 -1
  165. package/dist/lib/legacy-text-trigger-migration.d.ts +0 -18
  166. package/dist/lib/legacy-text-trigger-migration.d.ts.map +0 -1
  167. package/dist/lib/legacy-text-trigger-migration.js +0 -131
  168. package/dist/lib/legacy-text-trigger-migration.js.map +0 -1
  169. package/src/lib/legacy-task-migration.ts +0 -143
  170. package/src/lib/legacy-text-trigger-migration.ts +0 -178
@@ -0,0 +1,526 @@
1
+ import { spawn } from 'node:child_process';
2
+ import { mkdir, readFile } from 'node:fs/promises';
3
+ import { dirname, join } from 'node:path';
4
+ import { fileURLToPath } from 'node:url';
5
+ import { logger } from '@elizaos/core';
6
+ import type {
7
+ WorkflowDefinition,
8
+ WorkflowExecution,
9
+ WorkflowExecutionEngineMetrics,
10
+ WorkflowNode,
11
+ } from '../types/index';
12
+
13
+ interface SmithersNodeExecutionData {
14
+ json: Record<string, unknown>;
15
+ binary?: Record<string, unknown>;
16
+ pairedItem?: { item: number } | Array<{ item: number }>;
17
+ }
18
+
19
+ interface SmithersIncomingConnection {
20
+ source: string;
21
+ sourceOutputIndex: number;
22
+ destinationInputIndex: number;
23
+ }
24
+
25
+ export interface SmithersExecutionPlan {
26
+ enabledNodes: WorkflowNode[];
27
+ startNodes: string[];
28
+ incoming: Record<string, SmithersIncomingConnection[]>;
29
+ }
30
+
31
+ export interface SmithersWorkflowRunOptions {
32
+ workflow: WorkflowDefinition;
33
+ executionId: string;
34
+ pending: WorkflowExecution;
35
+ mode: WorkflowExecution['mode'];
36
+ triggerData?: Record<string, unknown>;
37
+ plan: SmithersExecutionPlan;
38
+ runNode: (
39
+ node: WorkflowNode,
40
+ inputData: SmithersNodeExecutionData[][]
41
+ ) => Promise<SmithersNodeExecutionData[][]>;
42
+ }
43
+
44
+ type SmithersRunMetrics = Omit<WorkflowExecutionEngineMetrics, 'provider'>;
45
+
46
+ interface SmithersProtocolRequest {
47
+ type: 'executeNode';
48
+ requestId: string;
49
+ nodeName: string;
50
+ inputData: SmithersNodeExecutionData[][];
51
+ }
52
+
53
+ interface SmithersProtocolResponse {
54
+ requestId: string;
55
+ ok: boolean;
56
+ outputData?: SmithersNodeExecutionData[][];
57
+ error?: { message: string; stack?: string };
58
+ }
59
+
60
+ interface SmithersProtocolResult {
61
+ type: 'workflowResult';
62
+ execution: WorkflowExecution;
63
+ metrics?: SmithersRunMetrics;
64
+ }
65
+
66
+ function sanitizeWorkflowName(name: string): string {
67
+ return name.replace(/[^a-zA-Z0-9_.:-]+/g, '-').replace(/^-+|-+$/g, '') || 'workflow';
68
+ }
69
+
70
+ function resolveSmithersDbPath(workflowId: string): string {
71
+ const safeId = sanitizeWorkflowName(workflowId || 'anonymous');
72
+ return join(process.cwd(), '.eliza', 'smithers', `${safeId}.sqlite`);
73
+ }
74
+
75
+ function resolveBunBinary(): string {
76
+ if (typeof (globalThis as { Bun?: unknown }).Bun !== 'undefined') return process.execPath;
77
+ return process.env.BUN_BIN || 'bun';
78
+ }
79
+
80
+ /**
81
+ * Resolve the Smithers storage backend configuration from environment variables.
82
+ *
83
+ * SMITHERS_DB_PROVIDER: "sqlite" (default) | "postgres" | "pglite"
84
+ * SMITHERS_DB_URL: PostgreSQL connection string (used when provider = "postgres")
85
+ * SMITHERS_DB_DATA_DIR: PGlite data directory (used when provider = "pglite")
86
+ *
87
+ * The resolved config is threaded through the subprocess payload so the layer
88
+ * selection runs inside the subprocess script string.
89
+ */
90
+ export function resolveSmithersDbConfig(): {
91
+ provider: 'sqlite' | 'postgres' | 'pglite';
92
+ connectionString?: string;
93
+ dataDir?: string;
94
+ } {
95
+ const raw = process.env.SMITHERS_DB_PROVIDER ?? 'sqlite';
96
+ const provider = raw === 'postgres' || raw === 'pglite' ? raw : 'sqlite';
97
+ return {
98
+ provider,
99
+ connectionString: process.env.SMITHERS_DB_URL,
100
+ dataDir: process.env.SMITHERS_DB_DATA_DIR,
101
+ };
102
+ }
103
+
104
+ async function resolvePluginRoot(): Promise<string> {
105
+ let dir = dirname(fileURLToPath(import.meta.url));
106
+ for (let depth = 0; depth < 8; depth += 1) {
107
+ try {
108
+ const manifestPath = join(dir, 'package.json');
109
+ const manifest = JSON.parse(await readFile(manifestPath, 'utf8')) as { name?: string };
110
+ if (manifest.name === '@elizaos/plugin-workflow') return dir;
111
+ } catch {
112
+ // Continue walking upward until the plugin package root is found.
113
+ }
114
+ const parent = dirname(dir);
115
+ if (parent === dir) break;
116
+ dir = parent;
117
+ }
118
+ return process.cwd();
119
+ }
120
+
121
+ function toErrorPayload(error: unknown): { message: string; stack?: string } {
122
+ if (error instanceof Error) return { message: error.message, stack: error.stack };
123
+ return { message: String(error) };
124
+ }
125
+
126
+ function buildSmithersWorkerEnv(payload: string): NodeJS.ProcessEnv {
127
+ const env: NodeJS.ProcessEnv = { ...process.env, ELIZA_SMITHERS_RUN_PAYLOAD: payload };
128
+ for (const key of Object.keys(env)) {
129
+ const normalized = key.toUpperCase();
130
+ if (
131
+ normalized === 'NODE_V8_COVERAGE' ||
132
+ normalized === 'BUN_TEST' ||
133
+ normalized.startsWith('BUN_TEST_') ||
134
+ normalized.startsWith('VITEST') ||
135
+ normalized.startsWith('NYC_') ||
136
+ normalized.includes('COVERAGE')
137
+ ) {
138
+ delete env[key];
139
+ }
140
+ }
141
+ return env;
142
+ }
143
+
144
+ /**
145
+ * Source for the per-run Smithers subprocess. Each workflow run executes in a
146
+ * fresh Bun process so the global Smithers singleton + SQLite state stay isolated
147
+ * (a long-lived singleton degrades across runs) and so Bun's `bun:sqlite` is
148
+ * available (Smithers requires it).
149
+ *
150
+ * The node graph is built with native Smithers control flow: dependency-depth
151
+ * levels become `parallel` groups joined in `sequence`, so independent nodes run
152
+ * concurrently instead of strictly serially. Node execution is delegated back to
153
+ * the parent over a line-delimited stdin/stdout protocol; a map-based response
154
+ * reader lets concurrent in-flight requests from a parallel level resolve without
155
+ * racing. Per-node n8n retry / continue-on-fail is honoured, and per-run metrics
156
+ * are reported back.
157
+ */
158
+ function createSmithersScript(): string {
159
+ return String.raw`
160
+ import { Smithers } from 'smithers-orchestrator';
161
+ import { Effect, Schema } from 'effect';
162
+ import { createInterface } from 'node:readline/promises';
163
+
164
+ const payload = JSON.parse(process.env.ELIZA_SMITHERS_RUN_PAYLOAD ?? '{}');
165
+ const rl = createInterface({ input: process.stdin, crlfDelay: Infinity });
166
+ const pending = new Map();
167
+ let requestSeq = 0;
168
+ const metrics = { nodes: 0, levels: 0, maxConcurrency: 0, started: 0, finished: 0, failed: 0, skipped: 0, retries: 0 };
169
+ let lastNodeError = null;
170
+
171
+ function emit(message) {
172
+ process.stdout.write(JSON.stringify(message) + '\n');
173
+ }
174
+
175
+ (async () => {
176
+ for await (const line of rl) {
177
+ if (!line.trim()) continue;
178
+ let response;
179
+ try { response = JSON.parse(line); } catch { continue; }
180
+ const entry = pending.get(response.requestId);
181
+ if (!entry) continue;
182
+ pending.delete(response.requestId);
183
+ if (!response.ok) {
184
+ const error = new Error(response.error?.message ?? 'Node execution failed');
185
+ if (response.error?.stack) error.stack = response.error.stack;
186
+ entry.reject(error);
187
+ } else {
188
+ entry.resolve(response.outputData ?? [[]]);
189
+ }
190
+ }
191
+ })();
192
+
193
+ function sendNodeRequest(nodeName, inputData) {
194
+ const requestId = String(++requestSeq);
195
+ return new Promise((resolve, reject) => {
196
+ pending.set(requestId, { resolve, reject });
197
+ emit({ type: 'executeNode', requestId, nodeName, inputData });
198
+ });
199
+ }
200
+
201
+ function cloneJson(value) { return JSON.parse(JSON.stringify(value)); }
202
+ function delay(ms) { return new Promise((resolve) => setTimeout(resolve, ms)); }
203
+
204
+ function collectInputData(nodeName, incoming, nodeOutputs) {
205
+ const inputData = [];
206
+ for (const connection of incoming[nodeName] ?? []) {
207
+ const sourceOutputs = nodeOutputs[connection.source] ?? [];
208
+ const sourceItems = sourceOutputs[connection.sourceOutputIndex] ?? [];
209
+ inputData[connection.destinationInputIndex] = [
210
+ ...(inputData[connection.destinationInputIndex] ?? []),
211
+ ...sourceItems,
212
+ ];
213
+ }
214
+ return inputData.length > 0 ? inputData : [[]];
215
+ }
216
+
217
+ function hasInputItems(inputData) { return inputData.some((items) => items.length > 0); }
218
+
219
+ function makeStepId(index, node) {
220
+ const raw = node.id ?? node.name ?? 'node';
221
+ const safe = String(raw).replace(/[^a-zA-Z0-9_.:-]+/g, '-').replace(/^-+|-+$/g, '') || 'node';
222
+ return String(index).padStart(4, '0') + '-' + safe;
223
+ }
224
+
225
+ // Group topologically-ordered nodes into dependency-depth levels; nodes in a
226
+ // level have no data dependency on each other and run concurrently.
227
+ function computeLevels(enabledNodes, incoming, startNodes, nodeByName) {
228
+ const depth = new Map();
229
+ for (const node of enabledNodes) {
230
+ const connections = (incoming[node.name] ?? []).filter((c) => nodeByName.has(c.source));
231
+ if (startNodes.has(node.name) || connections.length === 0) { depth.set(node.name, 0); continue; }
232
+ let nodeDepth = 0;
233
+ for (const connection of connections) nodeDepth = Math.max(nodeDepth, (depth.get(connection.source) ?? 0) + 1);
234
+ depth.set(node.name, nodeDepth);
235
+ }
236
+ const levels = [];
237
+ for (const node of enabledNodes) {
238
+ const nodeDepth = depth.get(node.name) ?? 0;
239
+ (levels[nodeDepth] ??= []).push(node);
240
+ }
241
+ return levels.filter((level) => level && level.length > 0);
242
+ }
243
+
244
+ // Honour the node's own n8n retry / continue-on-fail settings.
245
+ async function runNodeWithPolicy(node, inputData) {
246
+ const maxAttempts = node.retryOnFail ? Math.max(1, node.maxTries ?? 3) : 1;
247
+ let lastError;
248
+ for (let attempt = 1; attempt <= maxAttempts; attempt += 1) {
249
+ try { return await sendNodeRequest(node.name, inputData); }
250
+ catch (error) {
251
+ lastError = error;
252
+ lastNodeError = { nodeName: node.name, message: error?.message ?? String(error) };
253
+ if (attempt < maxAttempts) { metrics.retries += 1; await delay(node.waitBetweenTries ?? 1000); }
254
+ }
255
+ }
256
+ if (node.continueOnFail) return [[{ json: { error: lastError?.message ?? String(lastError) } }]];
257
+ throw lastError;
258
+ }
259
+
260
+ try {
261
+ const enabledNodes = payload.plan.enabledNodes;
262
+ const incoming = payload.plan.incoming;
263
+ const startNodes = new Set(payload.plan.startNodes);
264
+ const nodeByName = new Map(enabledNodes.map((node) => [node.name, node]));
265
+ const levels = computeLevels(enabledNodes, incoming, startNodes, nodeByName);
266
+ const terminalNodeName = enabledNodes[enabledNodes.length - 1]?.name;
267
+ metrics.nodes = enabledNodes.length;
268
+ metrics.levels = levels.length;
269
+ metrics.maxConcurrency = levels.reduce((max, level) => Math.max(max, level.length), 0);
270
+
271
+ const nodeOutputs = {};
272
+ const runData = {};
273
+ const workflow = Smithers.workflow({ name: payload.workflowName, input: Schema.Unknown });
274
+
275
+ const buildStep = (node, index) =>
276
+ workflow.step(makeStepId(index, node), {
277
+ output: Schema.Unknown,
278
+ run: async () => {
279
+ metrics.started += 1;
280
+ const incomingConnections = incoming[node.name] ?? [];
281
+ const isStartNode = startNodes.has(node.name);
282
+ const inputData =
283
+ isStartNode && incomingConnections.length === 0
284
+ ? Object.keys(payload.triggerData ?? {}).length > 0
285
+ ? [[{ json: payload.triggerData }]]
286
+ : [[]]
287
+ : collectInputData(node.name, incoming, nodeOutputs);
288
+ const started = Date.now();
289
+ const shouldSkip = !isStartNode && incomingConnections.length > 0 && !hasInputItems(inputData);
290
+ let outputData;
291
+ if (shouldSkip) { outputData = [[]]; metrics.skipped += 1; }
292
+ else {
293
+ try { outputData = await runNodeWithPolicy(node, inputData); }
294
+ catch (error) { metrics.failed += 1; throw error; }
295
+ }
296
+ nodeOutputs[node.name] = outputData;
297
+ runData[node.name] = [{
298
+ startTime: started,
299
+ executionTime: Date.now() - started,
300
+ data: { main: cloneJson(outputData) },
301
+ source: incomingConnections.map((connection) => ({
302
+ previousNode: connection.source,
303
+ previousNodeOutput: connection.sourceOutputIndex,
304
+ previousNodeRun: 0,
305
+ })),
306
+ }];
307
+ metrics.finished += 1;
308
+ return { nodeName: node.name, outputData };
309
+ },
310
+ });
311
+
312
+ let stepIndex = 0;
313
+ const levelGraphs = levels.map((level) => {
314
+ const handles = level.map((node) => buildStep(node, stepIndex++));
315
+ return handles.length === 1 ? handles[0] : workflow.parallel(...handles);
316
+ });
317
+
318
+ const resultStep = workflow.step('eliza-workflow-result', {
319
+ output: Schema.Unknown,
320
+ run: async () => {
321
+ const stoppedAt = new Date().toISOString();
322
+ return {
323
+ ...payload.pending,
324
+ finished: true,
325
+ status: 'success',
326
+ stoppedAt,
327
+ data: { resultData: { runData, lastNodeExecuted: terminalNodeName } },
328
+ };
329
+ },
330
+ });
331
+
332
+ const graph = workflow.sequence(...levelGraphs, resultStep);
333
+ const built = workflow.from(graph);
334
+ // Select the storage backend based on the provider field threaded through
335
+ // the payload. Feature-detect non-sqlite APIs: smithers-orchestrator@0.22.0
336
+ // does not yet expose Smithers.postgres / Smithers.pglite; if the method is
337
+ // absent we degrade to sqlite so old and new builds both work correctly.
338
+ const dbConfig = payload.dbConfig ?? {};
339
+ const provider = dbConfig.provider ?? 'sqlite';
340
+ let smithersLayer;
341
+ if (provider !== 'sqlite' && typeof Smithers[provider] === 'function') {
342
+ if (provider === 'postgres') {
343
+ smithersLayer = Smithers.postgres({ connectionString: dbConfig.connectionString });
344
+ } else if (provider === 'pglite') {
345
+ smithersLayer = Smithers.pglite({ dataDir: dbConfig.dataDir });
346
+ } else {
347
+ smithersLayer = Smithers.sqlite({ filename: payload.dbPath });
348
+ }
349
+ } else {
350
+ smithersLayer = Smithers.sqlite({ filename: payload.dbPath });
351
+ }
352
+ const execution = await Effect.runPromise(
353
+ built
354
+ .execute(payload.input, {
355
+ runId: payload.executionId,
356
+ force: true,
357
+ rootDir: payload.rootDir ?? process.cwd(),
358
+ allowNetwork: true,
359
+ })
360
+ .pipe(Effect.provide(smithersLayer))
361
+ );
362
+ emit({ type: 'workflowResult', execution, metrics });
363
+ process.exit(0);
364
+ } catch (error) {
365
+ if (lastNodeError) {
366
+ console.error('Node "' + lastNodeError.nodeName + '" failed: ' + lastNodeError.message);
367
+ }
368
+ console.error(error?.stack ?? error?.message ?? String(error));
369
+ process.exit(1);
370
+ }
371
+ `;
372
+ }
373
+
374
+ export async function runWorkflowWithSmithers({
375
+ workflow,
376
+ executionId,
377
+ pending,
378
+ mode,
379
+ triggerData,
380
+ plan,
381
+ runNode,
382
+ }: SmithersWorkflowRunOptions): Promise<WorkflowExecution> {
383
+ const dbPath = resolveSmithersDbPath(workflow.id ?? workflow.name);
384
+ await mkdir(dirname(dbPath), { recursive: true });
385
+ const dbConfig = resolveSmithersDbConfig();
386
+
387
+ const payload = JSON.stringify({
388
+ dbPath,
389
+ dbConfig,
390
+ executionId,
391
+ workflowName: sanitizeWorkflowName(workflow.name),
392
+ input: { mode, triggerData: triggerData ?? {}, workflowId: workflow.id ?? '' },
393
+ pending,
394
+ plan,
395
+ triggerData: triggerData ?? {},
396
+ rootDir: process.cwd(),
397
+ });
398
+ const pluginRoot = await resolvePluginRoot();
399
+ const proc = spawn(resolveBunBinary(), ['-e', createSmithersScript()], {
400
+ cwd: pluginRoot,
401
+ env: buildSmithersWorkerEnv(payload),
402
+ stdio: ['pipe', 'pipe', 'pipe'],
403
+ });
404
+ const byName = new Map(plan.enabledNodes.map((node) => [node.name, node]));
405
+ let executionResult: WorkflowExecution | null = null;
406
+ let runMetrics: SmithersRunMetrics | null = null;
407
+ let stdinEnded = false;
408
+
409
+ const endStdin = (): void => {
410
+ if (stdinEnded) return;
411
+ stdinEnded = true;
412
+ proc.stdin.end();
413
+ };
414
+
415
+ const writeResponse = (response: SmithersProtocolResponse): void => {
416
+ if (proc.stdin.writable) proc.stdin.write(`${JSON.stringify(response)}\n`);
417
+ };
418
+
419
+ // Node executions are dispatched concurrently so a parallel level's nodes
420
+ // actually run in parallel; their promises are drained before completion.
421
+ const inflight: Promise<void>[] = [];
422
+ const handleLine = (line: string): void => {
423
+ // The subprocess shares stdout with Smithers' own logging; only our protocol
424
+ // JSON is relevant, so ignore anything that isn't an object line.
425
+ const trimmed = line.trim();
426
+ if (trimmed?.[0] !== '{') return;
427
+ let message: SmithersProtocolRequest | SmithersProtocolResult;
428
+ try {
429
+ message = JSON.parse(trimmed) as SmithersProtocolRequest | SmithersProtocolResult;
430
+ } catch {
431
+ return;
432
+ }
433
+ if (message.type === 'workflowResult') {
434
+ executionResult = message.execution;
435
+ runMetrics = message.metrics ?? null;
436
+ endStdin();
437
+ return;
438
+ }
439
+ if (message.type !== 'executeNode') return;
440
+ const node = byName.get(message.nodeName);
441
+ if (!node) {
442
+ writeResponse({
443
+ requestId: message.requestId,
444
+ ok: false,
445
+ error: { message: `Smithers requested unknown workflow node "${message.nodeName}"` },
446
+ });
447
+ return;
448
+ }
449
+ inflight.push(
450
+ (async () => {
451
+ try {
452
+ const outputData = await runNode(node, message.inputData);
453
+ writeResponse({ requestId: message.requestId, ok: true, outputData });
454
+ } catch (error) {
455
+ writeResponse({ requestId: message.requestId, ok: false, error: toErrorPayload(error) });
456
+ }
457
+ })()
458
+ );
459
+ };
460
+
461
+ let stdoutBuffer = '';
462
+ let stderr = '';
463
+ proc.stdout.setEncoding('utf8');
464
+ proc.stdout.on('data', (chunk: string) => {
465
+ stdoutBuffer += chunk;
466
+ const lines = stdoutBuffer.split(/\r?\n/);
467
+ stdoutBuffer = lines.pop() ?? '';
468
+ for (const line of lines) handleLine(line);
469
+ });
470
+ proc.stderr.setEncoding('utf8');
471
+ proc.stderr.on('data', (chunk: string) => {
472
+ stderr += chunk;
473
+ });
474
+
475
+ const exitCode = await new Promise<number>((resolve, reject) => {
476
+ proc.on('error', reject);
477
+ proc.on('close', (code) => resolve(code ?? 1));
478
+ });
479
+ if (stdoutBuffer.trim()) handleLine(stdoutBuffer);
480
+ if (exitCode === 0) await Promise.all(inflight);
481
+
482
+ endStdin();
483
+
484
+ if (exitCode !== 0) {
485
+ throw new Error(`Smithers workflow execution failed: ${stderr.trim() || `exit ${exitCode}`}`);
486
+ }
487
+ if (!executionResult) {
488
+ throw new Error('Smithers workflow execution completed without returning a workflow result');
489
+ }
490
+ const completedExecution = executionResult as WorkflowExecution;
491
+ const completedMetrics = runMetrics as SmithersRunMetrics | null;
492
+ const executionWithMetrics: WorkflowExecution = completedMetrics
493
+ ? {
494
+ ...completedExecution,
495
+ data: {
496
+ ...completedExecution.data,
497
+ resultData: {
498
+ ...completedExecution.data?.resultData,
499
+ engine: {
500
+ provider: 'smithers',
501
+ nodes: completedMetrics.nodes,
502
+ levels: completedMetrics.levels,
503
+ maxConcurrency: completedMetrics.maxConcurrency,
504
+ started: completedMetrics.started,
505
+ finished: completedMetrics.finished,
506
+ failed: completedMetrics.failed,
507
+ skipped: completedMetrics.skipped,
508
+ retries: completedMetrics.retries,
509
+ },
510
+ },
511
+ },
512
+ }
513
+ : completedExecution;
514
+
515
+ logger.info(
516
+ {
517
+ src: 'plugin:workflow:smithers',
518
+ workflowId: workflow.id ?? '',
519
+ executionId,
520
+ ...(runMetrics ?? {}),
521
+ },
522
+ 'workflow executed'
523
+ );
524
+
525
+ return executionWithMetrics;
526
+ }
@@ -30,7 +30,7 @@ export class WorkflowCredentialStore extends Service implements WorkflowCredenti
30
30
  private readonly connectorDisconnectedHandler = async (
31
31
  payload: ConnectorDisconnectedPayload
32
32
  ): Promise<void> => {
33
- if (!payload?.userId || !Array.isArray(payload.credTypes)) {
33
+ if (!payload.userId || !Array.isArray(payload.credTypes)) {
34
34
  return;
35
35
  }
36
36
  if (payload.credTypes.length === 0) {