@elizaos/plugin-workflow 2.0.0-beta.1

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 (294) hide show
  1. package/README.md +71 -0
  2. package/auto-enable.ts +18 -0
  3. package/dist/actions/index.d.ts +2 -0
  4. package/dist/actions/index.d.ts.map +1 -0
  5. package/dist/actions/index.js +2 -0
  6. package/dist/actions/index.js.map +1 -0
  7. package/dist/actions/workflow.d.ts +23 -0
  8. package/dist/actions/workflow.d.ts.map +1 -0
  9. package/dist/actions/workflow.js +425 -0
  10. package/dist/actions/workflow.js.map +1 -0
  11. package/dist/data/defaultNodes.json +9887 -0
  12. package/dist/data/schemaIndex.json +1 -0
  13. package/dist/data/triggerSchemaIndex.json +1 -0
  14. package/dist/db/index.d.ts +2 -0
  15. package/dist/db/index.d.ts.map +1 -0
  16. package/dist/db/index.js +2 -0
  17. package/dist/db/index.js.map +1 -0
  18. package/dist/db/schema.d.ts +588 -0
  19. package/dist/db/schema.d.ts.map +1 -0
  20. package/dist/db/schema.js +59 -0
  21. package/dist/db/schema.js.map +1 -0
  22. package/dist/index.d.ts +34 -0
  23. package/dist/index.d.ts.map +1 -0
  24. package/dist/index.js +126 -0
  25. package/dist/index.js.map +1 -0
  26. package/dist/lib/automations-builder.d.ts +21 -0
  27. package/dist/lib/automations-builder.d.ts.map +1 -0
  28. package/dist/lib/automations-builder.js +557 -0
  29. package/dist/lib/automations-builder.js.map +1 -0
  30. package/dist/lib/automations-types.d.ts +153 -0
  31. package/dist/lib/automations-types.d.ts.map +1 -0
  32. package/dist/lib/automations-types.js +191 -0
  33. package/dist/lib/automations-types.js.map +1 -0
  34. package/dist/lib/index.d.ts +3 -0
  35. package/dist/lib/index.d.ts.map +1 -0
  36. package/dist/lib/index.js +3 -0
  37. package/dist/lib/index.js.map +1 -0
  38. package/dist/lib/legacy-task-migration.d.ts +20 -0
  39. package/dist/lib/legacy-task-migration.d.ts.map +1 -0
  40. package/dist/lib/legacy-task-migration.js +110 -0
  41. package/dist/lib/legacy-task-migration.js.map +1 -0
  42. package/dist/lib/legacy-text-trigger-migration.d.ts +18 -0
  43. package/dist/lib/legacy-text-trigger-migration.d.ts.map +1 -0
  44. package/dist/lib/legacy-text-trigger-migration.js +131 -0
  45. package/dist/lib/legacy-text-trigger-migration.js.map +1 -0
  46. package/dist/lib/workflow-clarification.d.ts +113 -0
  47. package/dist/lib/workflow-clarification.d.ts.map +1 -0
  48. package/dist/lib/workflow-clarification.js +425 -0
  49. package/dist/lib/workflow-clarification.js.map +1 -0
  50. package/dist/plugin-routes.d.ts +9 -0
  51. package/dist/plugin-routes.d.ts.map +1 -0
  52. package/dist/plugin-routes.js +147 -0
  53. package/dist/plugin-routes.js.map +1 -0
  54. package/dist/providers/activeWorkflows.d.ts +11 -0
  55. package/dist/providers/activeWorkflows.d.ts.map +1 -0
  56. package/dist/providers/activeWorkflows.js +72 -0
  57. package/dist/providers/activeWorkflows.js.map +1 -0
  58. package/dist/providers/index.d.ts +4 -0
  59. package/dist/providers/index.d.ts.map +1 -0
  60. package/dist/providers/index.js +4 -0
  61. package/dist/providers/index.js.map +1 -0
  62. package/dist/providers/pendingDraft.d.ts +9 -0
  63. package/dist/providers/pendingDraft.d.ts.map +1 -0
  64. package/dist/providers/pendingDraft.js +48 -0
  65. package/dist/providers/pendingDraft.js.map +1 -0
  66. package/dist/providers/workflowStatus.d.ts +3 -0
  67. package/dist/providers/workflowStatus.d.ts.map +1 -0
  68. package/dist/providers/workflowStatus.js +69 -0
  69. package/dist/providers/workflowStatus.js.map +1 -0
  70. package/dist/register-routes.d.ts +2 -0
  71. package/dist/register-routes.d.ts.map +1 -0
  72. package/dist/register-routes.js +6 -0
  73. package/dist/register-routes.js.map +1 -0
  74. package/dist/routes/_helpers.d.ts +11 -0
  75. package/dist/routes/_helpers.d.ts.map +1 -0
  76. package/dist/routes/_helpers.js +22 -0
  77. package/dist/routes/_helpers.js.map +1 -0
  78. package/dist/routes/automations.d.ts +19 -0
  79. package/dist/routes/automations.d.ts.map +1 -0
  80. package/dist/routes/automations.js +32 -0
  81. package/dist/routes/automations.js.map +1 -0
  82. package/dist/routes/embedded-webhooks.d.ts +3 -0
  83. package/dist/routes/embedded-webhooks.d.ts.map +1 -0
  84. package/dist/routes/embedded-webhooks.js +47 -0
  85. package/dist/routes/embedded-webhooks.js.map +1 -0
  86. package/dist/routes/executions.d.ts +3 -0
  87. package/dist/routes/executions.d.ts.map +1 -0
  88. package/dist/routes/executions.js +58 -0
  89. package/dist/routes/executions.js.map +1 -0
  90. package/dist/routes/index.d.ts +4 -0
  91. package/dist/routes/index.d.ts.map +1 -0
  92. package/dist/routes/index.js +14 -0
  93. package/dist/routes/index.js.map +1 -0
  94. package/dist/routes/nodes.d.ts +3 -0
  95. package/dist/routes/nodes.d.ts.map +1 -0
  96. package/dist/routes/nodes.js +168 -0
  97. package/dist/routes/nodes.js.map +1 -0
  98. package/dist/routes/validation.d.ts +3 -0
  99. package/dist/routes/validation.d.ts.map +1 -0
  100. package/dist/routes/validation.js +41 -0
  101. package/dist/routes/validation.js.map +1 -0
  102. package/dist/routes/workflow-routes.d.ts +27 -0
  103. package/dist/routes/workflow-routes.d.ts.map +1 -0
  104. package/dist/routes/workflow-routes.js +326 -0
  105. package/dist/routes/workflow-routes.js.map +1 -0
  106. package/dist/routes/workflows.d.ts +3 -0
  107. package/dist/routes/workflows.d.ts.map +1 -0
  108. package/dist/routes/workflows.js +252 -0
  109. package/dist/routes/workflows.js.map +1 -0
  110. package/dist/schemas/draftIntent.d.ts +22 -0
  111. package/dist/schemas/draftIntent.d.ts.map +1 -0
  112. package/dist/schemas/draftIntent.js +22 -0
  113. package/dist/schemas/draftIntent.js.map +1 -0
  114. package/dist/schemas/feasibility.d.ts +13 -0
  115. package/dist/schemas/feasibility.d.ts.map +1 -0
  116. package/dist/schemas/feasibility.js +9 -0
  117. package/dist/schemas/feasibility.js.map +1 -0
  118. package/dist/schemas/index.d.ts +5 -0
  119. package/dist/schemas/index.d.ts.map +1 -0
  120. package/dist/schemas/index.js +5 -0
  121. package/dist/schemas/index.js.map +1 -0
  122. package/dist/schemas/keywordExtraction.d.ts +14 -0
  123. package/dist/schemas/keywordExtraction.d.ts.map +1 -0
  124. package/dist/schemas/keywordExtraction.js +12 -0
  125. package/dist/schemas/keywordExtraction.js.map +1 -0
  126. package/dist/schemas/workflowMatching.d.ts +36 -0
  127. package/dist/schemas/workflowMatching.d.ts.map +1 -0
  128. package/dist/schemas/workflowMatching.js +30 -0
  129. package/dist/schemas/workflowMatching.js.map +1 -0
  130. package/dist/services/embedded-workflow-service.d.ts +106 -0
  131. package/dist/services/embedded-workflow-service.d.ts.map +1 -0
  132. package/dist/services/embedded-workflow-service.js +1900 -0
  133. package/dist/services/embedded-workflow-service.js.map +1 -0
  134. package/dist/services/index.d.ts +5 -0
  135. package/dist/services/index.d.ts.map +1 -0
  136. package/dist/services/index.js +5 -0
  137. package/dist/services/index.js.map +1 -0
  138. package/dist/services/workflow-credential-store.d.ts +27 -0
  139. package/dist/services/workflow-credential-store.d.ts.map +1 -0
  140. package/dist/services/workflow-credential-store.js +92 -0
  141. package/dist/services/workflow-credential-store.js.map +1 -0
  142. package/dist/services/workflow-dispatch.d.ts +41 -0
  143. package/dist/services/workflow-dispatch.d.ts.map +1 -0
  144. package/dist/services/workflow-dispatch.js +86 -0
  145. package/dist/services/workflow-dispatch.js.map +1 -0
  146. package/dist/services/workflow-service.d.ts +63 -0
  147. package/dist/services/workflow-service.d.ts.map +1 -0
  148. package/dist/services/workflow-service.js +492 -0
  149. package/dist/services/workflow-service.js.map +1 -0
  150. package/dist/trigger-routes.d.ts +153 -0
  151. package/dist/trigger-routes.d.ts.map +1 -0
  152. package/dist/trigger-routes.js +424 -0
  153. package/dist/trigger-routes.js.map +1 -0
  154. package/dist/types/index.d.ts +457 -0
  155. package/dist/types/index.d.ts.map +1 -0
  156. package/dist/types/index.js +59 -0
  157. package/dist/types/index.js.map +1 -0
  158. package/dist/utils/catalog.d.ts +16 -0
  159. package/dist/utils/catalog.d.ts.map +1 -0
  160. package/dist/utils/catalog.js +211 -0
  161. package/dist/utils/catalog.js.map +1 -0
  162. package/dist/utils/clarification.d.ts +17 -0
  163. package/dist/utils/clarification.d.ts.map +1 -0
  164. package/dist/utils/clarification.js +46 -0
  165. package/dist/utils/clarification.js.map +1 -0
  166. package/dist/utils/context.d.ts +4 -0
  167. package/dist/utils/context.d.ts.map +1 -0
  168. package/dist/utils/context.js +18 -0
  169. package/dist/utils/context.js.map +1 -0
  170. package/dist/utils/credentialResolver.d.ts +22 -0
  171. package/dist/utils/credentialResolver.d.ts.map +1 -0
  172. package/dist/utils/credentialResolver.js +146 -0
  173. package/dist/utils/credentialResolver.js.map +1 -0
  174. package/dist/utils/generation.d.ts +36 -0
  175. package/dist/utils/generation.d.ts.map +1 -0
  176. package/dist/utils/generation.js +701 -0
  177. package/dist/utils/generation.js.map +1 -0
  178. package/dist/utils/host-capabilities.d.ts +27 -0
  179. package/dist/utils/host-capabilities.d.ts.map +1 -0
  180. package/dist/utils/host-capabilities.js +59 -0
  181. package/dist/utils/host-capabilities.js.map +1 -0
  182. package/dist/utils/inferSyntheticOutputSchema.d.ts +20 -0
  183. package/dist/utils/inferSyntheticOutputSchema.d.ts.map +1 -0
  184. package/dist/utils/inferSyntheticOutputSchema.js +151 -0
  185. package/dist/utils/inferSyntheticOutputSchema.js.map +1 -0
  186. package/dist/utils/outputSchema.d.ts +26 -0
  187. package/dist/utils/outputSchema.d.ts.map +1 -0
  188. package/dist/utils/outputSchema.js +297 -0
  189. package/dist/utils/outputSchema.js.map +1 -0
  190. package/dist/utils/validateAndRepair.d.ts +41 -0
  191. package/dist/utils/validateAndRepair.d.ts.map +1 -0
  192. package/dist/utils/validateAndRepair.js +483 -0
  193. package/dist/utils/validateAndRepair.js.map +1 -0
  194. package/dist/utils/workflow-prompts/actionResponse.d.ts +2 -0
  195. package/dist/utils/workflow-prompts/actionResponse.d.ts.map +1 -0
  196. package/dist/utils/workflow-prompts/actionResponse.js +17 -0
  197. package/dist/utils/workflow-prompts/actionResponse.js.map +1 -0
  198. package/dist/utils/workflow-prompts/draftIntent.d.ts +2 -0
  199. package/dist/utils/workflow-prompts/draftIntent.d.ts.map +1 -0
  200. package/dist/utils/workflow-prompts/draftIntent.js +23 -0
  201. package/dist/utils/workflow-prompts/draftIntent.js.map +1 -0
  202. package/dist/utils/workflow-prompts/feasibilityCheck.d.ts +2 -0
  203. package/dist/utils/workflow-prompts/feasibilityCheck.d.ts.map +1 -0
  204. package/dist/utils/workflow-prompts/feasibilityCheck.js +21 -0
  205. package/dist/utils/workflow-prompts/feasibilityCheck.js.map +1 -0
  206. package/dist/utils/workflow-prompts/fieldCorrection.d.ts +3 -0
  207. package/dist/utils/workflow-prompts/fieldCorrection.d.ts.map +1 -0
  208. package/dist/utils/workflow-prompts/fieldCorrection.js +20 -0
  209. package/dist/utils/workflow-prompts/fieldCorrection.js.map +1 -0
  210. package/dist/utils/workflow-prompts/index.d.ts +8 -0
  211. package/dist/utils/workflow-prompts/index.d.ts.map +1 -0
  212. package/dist/utils/workflow-prompts/index.js +8 -0
  213. package/dist/utils/workflow-prompts/index.js.map +1 -0
  214. package/dist/utils/workflow-prompts/keywordExtraction.d.ts +2 -0
  215. package/dist/utils/workflow-prompts/keywordExtraction.d.ts.map +1 -0
  216. package/dist/utils/workflow-prompts/keywordExtraction.js +21 -0
  217. package/dist/utils/workflow-prompts/keywordExtraction.js.map +1 -0
  218. package/dist/utils/workflow-prompts/parameterCorrection.d.ts +3 -0
  219. package/dist/utils/workflow-prompts/parameterCorrection.d.ts.map +1 -0
  220. package/dist/utils/workflow-prompts/parameterCorrection.js +29 -0
  221. package/dist/utils/workflow-prompts/parameterCorrection.js.map +1 -0
  222. package/dist/utils/workflow-prompts/workflowGeneration.d.ts +2 -0
  223. package/dist/utils/workflow-prompts/workflowGeneration.d.ts.map +1 -0
  224. package/dist/utils/workflow-prompts/workflowGeneration.js +529 -0
  225. package/dist/utils/workflow-prompts/workflowGeneration.js.map +1 -0
  226. package/dist/utils/workflow-prompts/workflowMatching.d.ts +2 -0
  227. package/dist/utils/workflow-prompts/workflowMatching.d.ts.map +1 -0
  228. package/dist/utils/workflow-prompts/workflowMatching.js +23 -0
  229. package/dist/utils/workflow-prompts/workflowMatching.js.map +1 -0
  230. package/dist/utils/workflow.d.ts +62 -0
  231. package/dist/utils/workflow.d.ts.map +1 -0
  232. package/dist/utils/workflow.js +712 -0
  233. package/dist/utils/workflow.js.map +1 -0
  234. package/package.json +87 -0
  235. package/src/actions/index.ts +1 -0
  236. package/src/actions/workflow.ts +494 -0
  237. package/src/data/defaultNodes.json +9887 -0
  238. package/src/data/schemaIndex.json +1 -0
  239. package/src/data/triggerSchemaIndex.json +1 -0
  240. package/src/db/index.ts +8 -0
  241. package/src/db/schema.ts +94 -0
  242. package/src/index.ts +179 -0
  243. package/src/lib/automations-builder.ts +679 -0
  244. package/src/lib/automations-types.ts +391 -0
  245. package/src/lib/index.ts +8 -0
  246. package/src/lib/legacy-task-migration.ts +143 -0
  247. package/src/lib/legacy-text-trigger-migration.ts +178 -0
  248. package/src/lib/workflow-clarification.ts +497 -0
  249. package/src/plugin-routes.ts +164 -0
  250. package/src/providers/activeWorkflows.ts +81 -0
  251. package/src/providers/index.ts +3 -0
  252. package/src/providers/pendingDraft.ts +55 -0
  253. package/src/providers/workflowStatus.ts +88 -0
  254. package/src/register-routes.ts +6 -0
  255. package/src/routes/_helpers.ts +27 -0
  256. package/src/routes/automations.ts +46 -0
  257. package/src/routes/embedded-webhooks.ts +64 -0
  258. package/src/routes/executions.ts +75 -0
  259. package/src/routes/index.ts +16 -0
  260. package/src/routes/nodes.ts +211 -0
  261. package/src/routes/validation.ts +51 -0
  262. package/src/routes/workflow-routes.ts +469 -0
  263. package/src/routes/workflows.ts +310 -0
  264. package/src/schemas/draftIntent.ts +21 -0
  265. package/src/schemas/feasibility.ts +8 -0
  266. package/src/schemas/index.ts +4 -0
  267. package/src/schemas/keywordExtraction.ts +11 -0
  268. package/src/schemas/workflowMatching.ts +29 -0
  269. package/src/services/embedded-workflow-service.ts +2224 -0
  270. package/src/services/index.ts +17 -0
  271. package/src/services/workflow-credential-store.ts +132 -0
  272. package/src/services/workflow-dispatch.ts +121 -0
  273. package/src/services/workflow-service.ts +839 -0
  274. package/src/trigger-routes.ts +714 -0
  275. package/src/types/index.ts +562 -0
  276. package/src/utils/catalog.ts +260 -0
  277. package/src/utils/clarification.ts +52 -0
  278. package/src/utils/context.ts +22 -0
  279. package/src/utils/credentialResolver.ts +234 -0
  280. package/src/utils/generation.ts +987 -0
  281. package/src/utils/host-capabilities.ts +81 -0
  282. package/src/utils/inferSyntheticOutputSchema.ts +163 -0
  283. package/src/utils/outputSchema.ts +372 -0
  284. package/src/utils/validateAndRepair.ts +610 -0
  285. package/src/utils/workflow-prompts/actionResponse.ts +16 -0
  286. package/src/utils/workflow-prompts/draftIntent.ts +22 -0
  287. package/src/utils/workflow-prompts/feasibilityCheck.ts +20 -0
  288. package/src/utils/workflow-prompts/fieldCorrection.ts +20 -0
  289. package/src/utils/workflow-prompts/index.ts +10 -0
  290. package/src/utils/workflow-prompts/keywordExtraction.ts +20 -0
  291. package/src/utils/workflow-prompts/parameterCorrection.ts +29 -0
  292. package/src/utils/workflow-prompts/workflowGeneration.ts +528 -0
  293. package/src/utils/workflow-prompts/workflowMatching.ts +22 -0
  294. package/src/utils/workflow.ts +895 -0
@@ -0,0 +1,178 @@
1
+ /**
2
+ * Idempotent migration: legacy `kind: "text"` trigger Tasks → workflow triggers
3
+ *
4
+ * Walks every TRIGGER_DISPATCH task for the running agent. For each task whose
5
+ * stored `metadata.trigger.kind` is `"text"` (or omitted, which is the legacy
6
+ * default), deploy a one-node workflow built around
7
+ * `workflows-nodes-base.respondToEvent` and rewrite the trigger metadata so
8
+ * `kind = "workflow"` plus a pointer to the new workflow. A `migratedFromText`
9
+ * marker prevents double-conversion on subsequent boots.
10
+ */
11
+
12
+ import {
13
+ type IAgentRuntime,
14
+ logger,
15
+ type Task,
16
+ type TaskMetadata,
17
+ type TriggerConfig,
18
+ } from '@elizaos/core';
19
+ import type { WorkflowService } from '../services/workflow-service';
20
+ import { WORKFLOW_SERVICE_TYPE } from '../services/workflow-service';
21
+ import type { WorkflowDefinition } from '../types/index';
22
+
23
+ const TRIGGER_TASK_NAME = 'TRIGGER_DISPATCH';
24
+ const RESPOND_TO_EVENT_NODE_TYPE = 'workflows-nodes-base.respondToEvent';
25
+
26
+ export interface LegacyTextTriggerMigrationSummary {
27
+ migrated: number;
28
+ skipped: number;
29
+ failed: number;
30
+ }
31
+
32
+ function readWorkflowService(runtime: IAgentRuntime): WorkflowService | null {
33
+ const svc = runtime.getService(WORKFLOW_SERVICE_TYPE) as WorkflowService | null;
34
+ return svc ?? null;
35
+ }
36
+
37
+ function readTriggerFromMetadata(task: Task): TriggerConfig | null {
38
+ const trigger = task.metadata?.trigger;
39
+ if (!trigger || typeof trigger !== 'object' || Array.isArray(trigger)) return null;
40
+ if (typeof trigger.triggerId !== 'string') return null;
41
+ return trigger;
42
+ }
43
+
44
+ function buildRespondToEventWorkflow(
45
+ trigger: TriggerConfig,
46
+ fallbackName: string
47
+ ): WorkflowDefinition {
48
+ const displayName =
49
+ typeof trigger.displayName === 'string' && trigger.displayName.trim().length > 0
50
+ ? trigger.displayName
51
+ : fallbackName;
52
+ const instructions =
53
+ typeof trigger.instructions === 'string' && trigger.instructions.trim().length > 0
54
+ ? trigger.instructions
55
+ : displayName;
56
+
57
+ return {
58
+ name: displayName,
59
+ nodes: [
60
+ {
61
+ id: 'respond-to-event',
62
+ name: 'Respond To Event',
63
+ type: RESPOND_TO_EVENT_NODE_TYPE,
64
+ typeVersion: 1,
65
+ position: [0, 0],
66
+ parameters: {
67
+ instructions,
68
+ displayName,
69
+ wakeMode: 'inject_now',
70
+ },
71
+ },
72
+ ],
73
+ connections: {},
74
+ };
75
+ }
76
+
77
+ export async function migrateLegacyTextTriggers(
78
+ runtime: IAgentRuntime
79
+ ): Promise<LegacyTextTriggerMigrationSummary> {
80
+ const summary: LegacyTextTriggerMigrationSummary = { migrated: 0, skipped: 0, failed: 0 };
81
+
82
+ const service = readWorkflowService(runtime);
83
+ if (!service) {
84
+ logger.debug(
85
+ { src: 'plugin:workflow:migration:text-trigger' },
86
+ 'WorkflowService not registered; skipping text-trigger migration'
87
+ );
88
+ return summary;
89
+ }
90
+
91
+ let tasks: Task[];
92
+ try {
93
+ tasks = await runtime.getTasksByName(TRIGGER_TASK_NAME);
94
+ } catch (err) {
95
+ logger.warn(
96
+ {
97
+ src: 'plugin:workflow:migration:text-trigger',
98
+ err: err instanceof Error ? err.message : String(err),
99
+ },
100
+ 'Failed to list trigger dispatch tasks; aborting migration'
101
+ );
102
+ return summary;
103
+ }
104
+
105
+ for (const task of tasks) {
106
+ if (!task.id || task.agentId !== runtime.agentId) {
107
+ summary.skipped += 1;
108
+ continue;
109
+ }
110
+
111
+ const metadata: TaskMetadata = task.metadata ?? {};
112
+ if (metadata.migratedFromText === true) {
113
+ summary.skipped += 1;
114
+ continue;
115
+ }
116
+
117
+ const trigger = readTriggerFromMetadata(task);
118
+ if (!trigger) {
119
+ summary.skipped += 1;
120
+ continue;
121
+ }
122
+
123
+ // `kind` is optional in the legacy schema; absence implies "text".
124
+ const isTextKind = trigger.kind === 'text' || trigger.kind === undefined;
125
+ if (!isTextKind) {
126
+ summary.skipped += 1;
127
+ continue;
128
+ }
129
+
130
+ try {
131
+ const draft = buildRespondToEventWorkflow(trigger, task.name ?? 'Trigger');
132
+ const deployed = await service.deployWorkflow(draft, runtime.agentId);
133
+
134
+ if (!deployed.id) {
135
+ summary.failed += 1;
136
+ logger.warn(
137
+ {
138
+ src: 'plugin:workflow:migration:text-trigger',
139
+ taskId: task.id,
140
+ triggerId: trigger.triggerId,
141
+ },
142
+ 'deployWorkflow returned no id; will retry on next boot'
143
+ );
144
+ continue;
145
+ }
146
+
147
+ const updatedTrigger: TriggerConfig = {
148
+ ...trigger,
149
+ kind: 'workflow',
150
+ workflowId: deployed.id,
151
+ workflowName: deployed.name,
152
+ };
153
+
154
+ const nextMetadata: TaskMetadata = {
155
+ ...metadata,
156
+ trigger: updatedTrigger,
157
+ migratedFromText: true,
158
+ migratedAt: Date.now(),
159
+ };
160
+
161
+ await runtime.updateTask(task.id, { metadata: nextMetadata });
162
+ summary.migrated += 1;
163
+ } catch (err) {
164
+ summary.failed += 1;
165
+ logger.warn(
166
+ {
167
+ src: 'plugin:workflow:migration:text-trigger',
168
+ taskId: task.id,
169
+ triggerId: trigger.triggerId,
170
+ err: err instanceof Error ? err.message : String(err),
171
+ },
172
+ 'Failed to migrate text trigger to workflow trigger'
173
+ );
174
+ }
175
+ }
176
+
177
+ return summary;
178
+ }
@@ -0,0 +1,497 @@
1
+ /**
2
+ * Clarification helpers for workflow generation routes.
3
+ *
4
+ * - `coerceClarifications`: normalizes the plugin's mixed-shape
5
+ * `_meta.requiresClarification` (legacy strings + structured objects)
6
+ * into typed `WorkflowClarificationRequest[]`.
7
+ * - `setByDotPath`: applies `{paramPath, value}` resolutions to a draft
8
+ * workflow JSON in place. Supports dot segments and bracketed-string
9
+ * segments (`nodes["Discord Send"].parameters.channelId`).
10
+ *
11
+ * Kept out of `workflows-routes.ts` so the handlers stay focused on transport.
12
+ */
13
+
14
+ import { logger } from '@elizaos/core';
15
+
16
+ export interface WorkflowClarificationRequest {
17
+ kind: 'target_channel' | 'target_server' | 'recipient' | 'value' | 'free_text';
18
+ platform?: string;
19
+ scope?: { guildId?: string };
20
+ question: string;
21
+ paramPath: string;
22
+ }
23
+
24
+ export interface WorkflowClarificationResolution {
25
+ paramPath: string;
26
+ value: string;
27
+ }
28
+
29
+ export interface WorkflowClarificationTargetGroup {
30
+ platform: string;
31
+ groupId: string;
32
+ groupName: string;
33
+ targets: Array<{
34
+ id: string;
35
+ name: string;
36
+ kind: 'channel' | 'recipient' | 'chat';
37
+ }>;
38
+ }
39
+
40
+ type RawStructuredClarification = Partial<WorkflowClarificationRequest> & {
41
+ question: string;
42
+ };
43
+
44
+ const VALID_KINDS: ReadonlySet<WorkflowClarificationRequest['kind']> = new Set([
45
+ 'target_channel',
46
+ 'target_server',
47
+ 'recipient',
48
+ 'value',
49
+ 'free_text',
50
+ ]);
51
+
52
+ /**
53
+ * Stable sort priority for clarification kinds. Lower number = asked first.
54
+ *
55
+ * `target_server` MUST come before `target_channel` because the channel
56
+ * picker reads `scope.guildId` from the server pick to narrow its options.
57
+ * If the LLM emits them in reverse order (which it sometimes does), the
58
+ * user picks a channel first against an unscoped catalog, which is bad UX
59
+ * (every channel from every guild they belong to) and can land the wrong
60
+ * id when channel names collide across guilds.
61
+ *
62
+ * `recipient` shares the server-scoped concern — DMs/contacts belong to a
63
+ * platform context — so it sorts after `target_server` too. `value` and
64
+ * `free_text` don't depend on prior picks; relative order is preserved
65
+ * because Array.prototype.sort is stable as of ES2019.
66
+ */
67
+ const KIND_SORT_PRIORITY: Readonly<Record<WorkflowClarificationRequest['kind'], number>> = {
68
+ target_server: 0,
69
+ target_channel: 1,
70
+ recipient: 1,
71
+ value: 2,
72
+ free_text: 3,
73
+ };
74
+
75
+ function isStructuredClarification(v: unknown): v is RawStructuredClarification {
76
+ if (!v || typeof v !== 'object') {
77
+ return false;
78
+ }
79
+ const o = v as Record<string, unknown>;
80
+ if (typeof o.question !== 'string' || o.question.trim().length === 0) {
81
+ return false;
82
+ }
83
+ // `kind` and `paramPath` may be missing on partial / older payloads — we
84
+ // default them here rather than reject the item outright.
85
+ return true;
86
+ }
87
+
88
+ export function coerceClarifications(raw: unknown): WorkflowClarificationRequest[] {
89
+ if (!Array.isArray(raw) || raw.length === 0) {
90
+ return [];
91
+ }
92
+ const out: WorkflowClarificationRequest[] = [];
93
+ for (const item of raw) {
94
+ if (typeof item === 'string') {
95
+ const trimmed = item.trim();
96
+ if (trimmed.length === 0) {
97
+ continue;
98
+ }
99
+ out.push({ kind: 'free_text', question: trimmed, paramPath: '' });
100
+ continue;
101
+ }
102
+ if (!isStructuredClarification(item)) {
103
+ continue;
104
+ }
105
+ const kindRaw = typeof item.kind === 'string' ? item.kind : 'free_text';
106
+ const kind = (
107
+ VALID_KINDS.has(kindRaw as WorkflowClarificationRequest['kind']) ? kindRaw : 'free_text'
108
+ ) as WorkflowClarificationRequest['kind'];
109
+ const platform = typeof item.platform === 'string' ? item.platform : undefined;
110
+ let scope: { guildId?: string } | undefined;
111
+ if (item.scope && typeof item.scope === 'object' && typeof item.scope.guildId === 'string') {
112
+ scope = {
113
+ guildId: item.scope.guildId,
114
+ };
115
+ }
116
+ const paramPath = typeof item.paramPath === 'string' ? item.paramPath : '';
117
+ out.push({
118
+ kind,
119
+ platform,
120
+ scope,
121
+ question: item.question.trim(),
122
+ paramPath,
123
+ });
124
+ }
125
+ // Stable-sort so dependency-bearing kinds come first. Within the same
126
+ // priority bucket the LLM's emission order is preserved.
127
+ out.sort((a, b) => KIND_SORT_PRIORITY[a.kind] - KIND_SORT_PRIORITY[b.kind]);
128
+ return out;
129
+ }
130
+
131
+ /**
132
+ * Tokenizer for paramPath. Handles three segment forms:
133
+ * - dot identifier: `parameters`
134
+ * - bracketed quoted key: `["Discord Send"]` or `['k']`
135
+ * - bracketed numeric: `[0]`
136
+ */
137
+ export function parseParamPath(path: string): string[] {
138
+ const segments: string[] = [];
139
+ let i = 0;
140
+ const n = path.length;
141
+ while (i < n) {
142
+ const ch = path[i];
143
+ if (ch === '.') {
144
+ i += 1;
145
+ continue;
146
+ }
147
+ if (ch === '[') {
148
+ const close = path.indexOf(']', i);
149
+ if (close < 0) {
150
+ throw new Error(`unterminated bracket at index ${i}`);
151
+ }
152
+ const inner = path.slice(i + 1, close).trim();
153
+ if (inner.length === 0) {
154
+ throw new Error(`empty bracket at index ${i}`);
155
+ }
156
+ const first = inner[0];
157
+ const last = inner[inner.length - 1];
158
+ if ((first === '"' && last === '"') || (first === "'" && last === "'")) {
159
+ segments.push(inner.slice(1, -1));
160
+ } else if (/^[0-9]+$/.test(inner)) {
161
+ segments.push(inner);
162
+ } else {
163
+ // Unquoted bare identifier inside brackets — accept to be lenient
164
+ // with LLM output (e.g. `[channelId]`).
165
+ segments.push(inner);
166
+ }
167
+ i = close + 1;
168
+ continue;
169
+ }
170
+ // Identifier run: read until next `.` or `[`.
171
+ let j = i;
172
+ while (j < n && path[j] !== '.' && path[j] !== '[') {
173
+ j += 1;
174
+ }
175
+ const ident = path.slice(i, j).trim();
176
+ if (ident.length === 0) {
177
+ throw new Error(`empty identifier at index ${i}`);
178
+ }
179
+ segments.push(ident);
180
+ i = j;
181
+ }
182
+ if (segments.length === 0) {
183
+ throw new Error('paramPath has no segments');
184
+ }
185
+ return segments;
186
+ }
187
+
188
+ /**
189
+ * Find the index of a named entry in an array of objects, matching against
190
+ * `.name` first then `.id`. Returns -1 if no match. Used by `setByDotPath`
191
+ * to resolve `nodes["My Node"]`-style segments — the LLM consistently
192
+ * addresses workflow nodes by their human name even though `workflow.nodes`
193
+ * is an array, so we map name → index here rather than rejecting the path.
194
+ */
195
+ function findArrayIndexByNameOrId(arr: unknown[], key: string): number {
196
+ for (let i = 0; i < arr.length; i += 1) {
197
+ const entry = arr[i];
198
+ if (entry === null || typeof entry !== 'object') continue;
199
+ const obj = entry as Record<string, unknown>;
200
+ if (obj.name === key || obj.id === key) {
201
+ return i;
202
+ }
203
+ }
204
+ return -1;
205
+ }
206
+
207
+ /**
208
+ * Mutate `obj` so that its value at `paramPath` becomes `value`. Creates
209
+ * intermediate plain objects as needed; never replaces an existing
210
+ * non-object intermediate (those throw, since the path is invalid).
211
+ *
212
+ * Segments that hit an array can be:
213
+ * - numeric → direct index
214
+ * - non-numeric (string) → looked up against the array's `.name` or `.id`
215
+ * field. Common in LLM output: the model writes `nodes["Post to Slack"]`
216
+ * rather than `nodes[2]`. Treating that as a hard failure forced every
217
+ * clarification resolution through a 400.
218
+ *
219
+ * If the segment expects an array but the existing intermediate is a non-
220
+ * array object, we treat it as an object key (workflow shapes mix arrays and
221
+ * objects fairly freely; we err on the side of preserving structure).
222
+ *
223
+ * Terminal-segment guard: refuses to overwrite an existing object with a
224
+ * non-object value. The LLM sometimes emits a paramPath that points at a
225
+ * parent scope rather than a leaf (e.g.
226
+ * `nodes["Hourly Trigger"].parameters` for a question whose answer is a
227
+ * channel name); naively writing the string there replaces the entire
228
+ * `parameters` object and the workflow runner then rejects the workflow with
229
+ * `parameters must be object`. Throwing here gives `applyResolutions` a
230
+ * chance to fall back to the userNotes path.
231
+ */
232
+ export function setByDotPath(
233
+ obj: Record<string, unknown>,
234
+ paramPath: string,
235
+ value: unknown
236
+ ): void {
237
+ const segments = parseParamPath(paramPath);
238
+ let cur: Record<string, unknown> | unknown[] = obj;
239
+ for (let i = 0; i < segments.length - 1; i += 1) {
240
+ const seg = segments[i];
241
+ const isArrayIndex = /^[0-9]+$/.test(seg);
242
+ if (Array.isArray(cur)) {
243
+ let idx: number;
244
+ if (isArrayIndex) {
245
+ idx = Number(seg);
246
+ } else {
247
+ idx = findArrayIndexByNameOrId(cur, seg);
248
+ if (idx < 0) {
249
+ throw new Error(
250
+ `paramPath segment "${seg}" did not match any element by name/id at depth ${i}`
251
+ );
252
+ }
253
+ }
254
+ let next = cur[idx];
255
+ if (next === undefined || next === null) {
256
+ next = /^[0-9]+$/.test(segments[i + 1]) ? [] : {};
257
+ cur[idx] = next;
258
+ }
259
+ if (typeof next !== 'object') {
260
+ throw new Error(`paramPath cannot descend into non-object at "${seg}" (depth ${i})`);
261
+ }
262
+ cur = next as Record<string, unknown> | unknown[];
263
+ continue;
264
+ }
265
+ let next = (cur as Record<string, unknown>)[seg];
266
+ if (next === undefined || next === null) {
267
+ next = /^[0-9]+$/.test(segments[i + 1]) ? [] : {};
268
+ (cur as Record<string, unknown>)[seg] = next;
269
+ }
270
+ if (typeof next !== 'object') {
271
+ throw new Error(`paramPath cannot descend into non-object at "${seg}" (depth ${i})`);
272
+ }
273
+ cur = next as Record<string, unknown> | unknown[];
274
+ }
275
+ const last = segments[segments.length - 1];
276
+ const isNonNullObject = (v: unknown): boolean => v !== null && typeof v === 'object';
277
+ if (Array.isArray(cur)) {
278
+ let idx: number;
279
+ if (/^[0-9]+$/.test(last)) {
280
+ idx = Number(last);
281
+ } else {
282
+ idx = findArrayIndexByNameOrId(cur, last);
283
+ if (idx < 0) {
284
+ throw new Error(
285
+ `paramPath terminal segment "${last}" did not match any element by name/id at array`
286
+ );
287
+ }
288
+ }
289
+ if (isNonNullObject(cur[idx]) && !isNonNullObject(value)) {
290
+ throw new Error(
291
+ `paramPath terminal "${last}" currently holds an object; refusing to overwrite with non-object value (path likely points at a parent scope rather than a leaf field)`
292
+ );
293
+ }
294
+ cur[idx] = value;
295
+ } else {
296
+ const existing = (cur as Record<string, unknown>)[last];
297
+ if (isNonNullObject(existing) && !isNonNullObject(value)) {
298
+ throw new Error(
299
+ `paramPath terminal "${last}" currently holds an object; refusing to overwrite with non-object value (path likely points at a parent scope rather than a leaf field)`
300
+ );
301
+ }
302
+ (cur as Record<string, unknown>)[last] = value;
303
+ }
304
+ }
305
+
306
+ /**
307
+ * Append a free-form answer to `draft._meta.userNotes`. Used for
308
+ * clarifications with no `paramPath` AND as the fallback when
309
+ * `setByDotPath` can't resolve a paramPath against the current draft.
310
+ * Subsequent LLM regeneration rounds read these notes from `_meta` so the
311
+ * user's answer is preserved across the failure rather than discarded.
312
+ */
313
+ function appendUserNote(draft: Record<string, unknown>, value: string): void {
314
+ const existingMeta = draft._meta;
315
+ const meta =
316
+ existingMeta && typeof existingMeta === 'object'
317
+ ? (existingMeta as Record<string, unknown>)
318
+ : {};
319
+ draft._meta = meta;
320
+
321
+ let notes: string[];
322
+ if (Array.isArray(meta.userNotes)) {
323
+ notes = meta.userNotes as string[];
324
+ } else {
325
+ notes = meta.userNotes !== null && meta.userNotes !== undefined ? [String(meta.userNotes)] : [];
326
+ meta.userNotes = notes;
327
+ }
328
+ notes.push(value);
329
+ }
330
+
331
+ export function applyResolutions(
332
+ draft: Record<string, unknown>,
333
+ resolutions: ReadonlyArray<WorkflowClarificationResolution>
334
+ ): { ok: true } | { ok: false; error: string; paramPath?: string } {
335
+ for (const r of resolutions) {
336
+ if (!r || typeof r.paramPath !== 'string') {
337
+ return { ok: false, error: 'resolution missing paramPath' };
338
+ }
339
+ if (typeof r.value !== 'string') {
340
+ return {
341
+ ok: false,
342
+ error: 'resolution value must be a string',
343
+ paramPath: r.paramPath,
344
+ };
345
+ }
346
+ if (r.paramPath.length === 0) {
347
+ // Free-form clarification with no field to wire into. Record the user's
348
+ // answer under draft._meta.userNotes so subsequent LLM iterations can
349
+ // consume the context, but don't mutate the workflow itself.
350
+ appendUserNote(draft, r.value);
351
+ continue;
352
+ }
353
+ // Surface structural parse errors (unterminated bracket, empty
354
+ // identifier, etc.) up to the caller as a 400 — these signal a
355
+ // malformed LLM emission and cannot be silently recovered into
356
+ // userNotes without losing the failure mode in the metrics pipeline.
357
+ try {
358
+ parseParamPath(r.paramPath);
359
+ } catch (err) {
360
+ return {
361
+ ok: false,
362
+ error: `paramPath is structurally invalid: ${
363
+ err instanceof Error ? err.message : String(err)
364
+ }`,
365
+ paramPath: r.paramPath,
366
+ };
367
+ }
368
+ try {
369
+ setByDotPath(draft, r.paramPath, r.value);
370
+ } catch (err) {
371
+ // Lookup-time failure: the path parsed cleanly but didn't resolve
372
+ // against the current draft (e.g. references a node the LLM didn't
373
+ // actually create, or points at a parent scope rather than a leaf
374
+ // field). Failing the whole resolution batch with a 400 is a
375
+ // dead-end — the user has no way to recover without re-prompting
376
+ // from scratch. Log a warn and record the answer as a free-form
377
+ // note so the next regeneration round can use it.
378
+ const errMsg = err instanceof Error ? err.message : String(err);
379
+ logger.warn(
380
+ {
381
+ src: 'plugin:workflow:clarification:applyResolutions',
382
+ err: errMsg,
383
+ paramPath: r.paramPath,
384
+ },
385
+ `setByDotPath failed for paramPath "${r.paramPath}"; recording "${r.value}" as a free-form note instead`
386
+ );
387
+ appendUserNote(draft, r.value);
388
+ }
389
+ }
390
+ return { ok: true };
391
+ }
392
+
393
+ /**
394
+ * Drop the resolved clarifications from the draft's `_meta` so the next
395
+ * read of the draft does not re-prompt the user for the same parameter.
396
+ *
397
+ * Two pruning paths:
398
+ * 1. Object-form clarifications with paramPath → prune by paramPath match.
399
+ * 2. String-form clarifications (LLM emits free-form questions without a
400
+ * paramPath) and object-form clarifications with empty paramPath →
401
+ * prune positionally by `freeFormCount` (UI presents them in order, so
402
+ * each free-form resolution consumes the next one).
403
+ *
404
+ * Positional-pruning contract: free-form items are dropped from the head of
405
+ * the stored list in order. The UI must therefore submit answers in the
406
+ * order they were presented, with no skipped or out-of-order items in a
407
+ * single batch — otherwise the wrong question gets pruned. If we ever need
408
+ * to support partial/interleaved submissions, switch the resolution payload
409
+ * to send the answered question text and match by value here instead.
410
+ */
411
+ export function pruneResolvedClarifications(
412
+ draft: Record<string, unknown>,
413
+ resolved: ReadonlySet<string>,
414
+ freeFormCount = 0
415
+ ): void {
416
+ const meta = (draft as { _meta?: Record<string, unknown> })._meta;
417
+ if (!meta || typeof meta !== 'object') {
418
+ return;
419
+ }
420
+ const list = meta.requiresClarification;
421
+ if (!Array.isArray(list)) {
422
+ return;
423
+ }
424
+ let toDropFreeForm = freeFormCount;
425
+ const remaining = list.filter((item) => {
426
+ if (typeof item === 'string') {
427
+ if (toDropFreeForm > 0) {
428
+ toDropFreeForm -= 1;
429
+ return false;
430
+ }
431
+ return true;
432
+ }
433
+ if (item && typeof item === 'object') {
434
+ const path = (item as { paramPath?: unknown }).paramPath;
435
+ if (typeof path === 'string' && path.length > 0 && resolved.has(path)) {
436
+ return false;
437
+ }
438
+ // Empty-paramPath object-form: also positional.
439
+ if ((typeof path !== 'string' || path.length === 0) && toDropFreeForm > 0) {
440
+ toDropFreeForm -= 1;
441
+ return false;
442
+ }
443
+ }
444
+ return true;
445
+ });
446
+ if (remaining.length === 0) {
447
+ delete meta.requiresClarification;
448
+ } else {
449
+ meta.requiresClarification = remaining;
450
+ }
451
+ }
452
+
453
+ /**
454
+ * Subset of `ElizaConnectorTargetCatalog` used by the route. Declared here
455
+ * (vs. imported from the service) so route tests can stub it without
456
+ * spinning up the full service.
457
+ */
458
+ export interface CatalogLike {
459
+ listGroups(opts?: {
460
+ platform?: string;
461
+ groupId?: string;
462
+ }): Promise<WorkflowClarificationTargetGroup[]>;
463
+ }
464
+
465
+ /**
466
+ * Build a catalog snapshot for the platforms referenced by `clarifications`.
467
+ * If multiple clarifications reference the same platform, we union their
468
+ * groupId scopes — broader queries (no scope) always win.
469
+ */
470
+ export async function buildCatalogSnapshot(
471
+ catalog: CatalogLike,
472
+ clarifications: ReadonlyArray<WorkflowClarificationRequest>
473
+ ): Promise<WorkflowClarificationTargetGroup[]> {
474
+ const platforms = new Set<string>();
475
+ for (const c of clarifications) {
476
+ if (c.platform) {
477
+ platforms.add(c.platform);
478
+ }
479
+ }
480
+ if (platforms.size === 0) {
481
+ return [];
482
+ }
483
+ const out: WorkflowClarificationTargetGroup[] = [];
484
+ const seen = new Set<string>();
485
+ for (const platform of platforms) {
486
+ const groups = await catalog.listGroups({ platform });
487
+ for (const g of groups) {
488
+ const key = `${g.platform}::${g.groupId}`;
489
+ if (seen.has(key)) {
490
+ continue;
491
+ }
492
+ seen.add(key);
493
+ out.push(g);
494
+ }
495
+ }
496
+ return out;
497
+ }