stagent 0.1.0

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 (333) hide show
  1. package/LICENSE +191 -0
  2. package/README.md +399 -0
  3. package/components.json +21 -0
  4. package/dist/cli.js +171 -0
  5. package/drizzle.config.ts +12 -0
  6. package/next.config.mjs +15 -0
  7. package/package.json +114 -0
  8. package/postcss.config.mjs +8 -0
  9. package/public/icon-512.png +0 -0
  10. package/public/icon.svg +13 -0
  11. package/public/readme/home-workspace.png +0 -0
  12. package/public/readme/inbox-approvals.png +0 -0
  13. package/public/readme/workflow-blueprints.png +0 -0
  14. package/public/stagent-s-128.png +0 -0
  15. package/public/stagent-s-64.png +0 -0
  16. package/src/app/api/blueprints/[id]/instantiate/route.ts +27 -0
  17. package/src/app/api/blueprints/[id]/route.ts +39 -0
  18. package/src/app/api/blueprints/import/route.ts +68 -0
  19. package/src/app/api/blueprints/route.ts +29 -0
  20. package/src/app/api/command-palette/recent/route.ts +31 -0
  21. package/src/app/api/data/clear/route.ts +22 -0
  22. package/src/app/api/data/seed/route.ts +22 -0
  23. package/src/app/api/documents/[id]/file/route.ts +44 -0
  24. package/src/app/api/documents/[id]/route.ts +123 -0
  25. package/src/app/api/documents/route.ts +59 -0
  26. package/src/app/api/logs/stream/route.ts +101 -0
  27. package/src/app/api/notifications/[id]/route.ts +36 -0
  28. package/src/app/api/notifications/mark-all-read/route.ts +13 -0
  29. package/src/app/api/notifications/pending-approvals/route.ts +10 -0
  30. package/src/app/api/notifications/pending-approvals/stream/route.ts +101 -0
  31. package/src/app/api/notifications/route.ts +34 -0
  32. package/src/app/api/permissions/route.ts +46 -0
  33. package/src/app/api/profiles/[id]/route.ts +79 -0
  34. package/src/app/api/profiles/[id]/test/route.ts +42 -0
  35. package/src/app/api/profiles/import/route.ts +108 -0
  36. package/src/app/api/profiles/route.ts +50 -0
  37. package/src/app/api/projects/[id]/route.ts +72 -0
  38. package/src/app/api/projects/route.ts +53 -0
  39. package/src/app/api/schedules/[id]/route.ts +185 -0
  40. package/src/app/api/schedules/route.ts +117 -0
  41. package/src/app/api/settings/budgets/route.ts +24 -0
  42. package/src/app/api/settings/openai/route.ts +24 -0
  43. package/src/app/api/settings/route.ts +21 -0
  44. package/src/app/api/settings/test/route.ts +26 -0
  45. package/src/app/api/tasks/[id]/cancel/route.ts +21 -0
  46. package/src/app/api/tasks/[id]/execute/route.ts +90 -0
  47. package/src/app/api/tasks/[id]/logs/route.ts +95 -0
  48. package/src/app/api/tasks/[id]/output/route.ts +47 -0
  49. package/src/app/api/tasks/[id]/respond/route.ts +64 -0
  50. package/src/app/api/tasks/[id]/resume/route.ts +76 -0
  51. package/src/app/api/tasks/[id]/route.ts +77 -0
  52. package/src/app/api/tasks/assist/route.ts +35 -0
  53. package/src/app/api/tasks/route.ts +82 -0
  54. package/src/app/api/uploads/[id]/route.ts +81 -0
  55. package/src/app/api/uploads/cleanup/route.ts +7 -0
  56. package/src/app/api/uploads/route.ts +66 -0
  57. package/src/app/api/workflows/[id]/execute/route.ts +82 -0
  58. package/src/app/api/workflows/[id]/route.ts +133 -0
  59. package/src/app/api/workflows/[id]/status/route.ts +54 -0
  60. package/src/app/api/workflows/[id]/steps/[stepId]/retry/route.ts +22 -0
  61. package/src/app/api/workflows/route.ts +61 -0
  62. package/src/app/apple-icon.tsx +31 -0
  63. package/src/app/costs/page.tsx +256 -0
  64. package/src/app/dashboard/page.tsx +44 -0
  65. package/src/app/documents/[id]/page.tsx +46 -0
  66. package/src/app/documents/page.tsx +45 -0
  67. package/src/app/error.tsx +26 -0
  68. package/src/app/global-error.tsx +23 -0
  69. package/src/app/globals.css +733 -0
  70. package/src/app/icon.tsx +30 -0
  71. package/src/app/inbox/loading.tsx +15 -0
  72. package/src/app/inbox/page.tsx +35 -0
  73. package/src/app/layout.tsx +78 -0
  74. package/src/app/manifest.ts +32 -0
  75. package/src/app/monitor/page.tsx +37 -0
  76. package/src/app/page.tsx +162 -0
  77. package/src/app/profiles/[id]/edit/page.tsx +39 -0
  78. package/src/app/profiles/[id]/page.tsx +33 -0
  79. package/src/app/profiles/new/page.tsx +22 -0
  80. package/src/app/profiles/page.tsx +19 -0
  81. package/src/app/projects/[id]/page.tsx +134 -0
  82. package/src/app/projects/loading.tsx +17 -0
  83. package/src/app/projects/page.tsx +32 -0
  84. package/src/app/schedules/[id]/page.tsx +47 -0
  85. package/src/app/schedules/page.tsx +18 -0
  86. package/src/app/settings/loading.tsx +24 -0
  87. package/src/app/settings/page.tsx +27 -0
  88. package/src/app/tasks/[id]/page.tsx +45 -0
  89. package/src/app/tasks/new/page.tsx +27 -0
  90. package/src/app/workflows/[id]/edit/page.tsx +66 -0
  91. package/src/app/workflows/[id]/page.tsx +37 -0
  92. package/src/app/workflows/blueprints/[id]/page.tsx +40 -0
  93. package/src/app/workflows/blueprints/new/page.tsx +20 -0
  94. package/src/app/workflows/blueprints/page.tsx +11 -0
  95. package/src/app/workflows/new/page.tsx +36 -0
  96. package/src/app/workflows/page.tsx +18 -0
  97. package/src/components/charts/donut-ring.tsx +64 -0
  98. package/src/components/charts/mini-bar.tsx +75 -0
  99. package/src/components/charts/sparkline.tsx +107 -0
  100. package/src/components/costs/cost-dashboard.tsx +877 -0
  101. package/src/components/costs/cost-filters.tsx +179 -0
  102. package/src/components/dashboard/activity-feed.tsx +95 -0
  103. package/src/components/dashboard/greeting.tsx +30 -0
  104. package/src/components/dashboard/priority-queue.tsx +79 -0
  105. package/src/components/dashboard/quick-actions.tsx +62 -0
  106. package/src/components/dashboard/recent-projects.tsx +79 -0
  107. package/src/components/dashboard/stats-cards.tsx +114 -0
  108. package/src/components/documents/document-browser.tsx +235 -0
  109. package/src/components/documents/document-detail-view.tsx +367 -0
  110. package/src/components/documents/document-grid.tsx +78 -0
  111. package/src/components/documents/document-preview.tsx +68 -0
  112. package/src/components/documents/document-table.tsx +119 -0
  113. package/src/components/documents/document-upload-dialog.tsx +153 -0
  114. package/src/components/documents/types.ts +6 -0
  115. package/src/components/documents/utils.ts +57 -0
  116. package/src/components/monitoring/connection-indicator.tsx +14 -0
  117. package/src/components/monitoring/log-entry.tsx +79 -0
  118. package/src/components/monitoring/log-filters.tsx +57 -0
  119. package/src/components/monitoring/log-stream.tsx +144 -0
  120. package/src/components/monitoring/monitor-overview-wrapper.tsx +64 -0
  121. package/src/components/monitoring/monitor-overview.tsx +119 -0
  122. package/src/components/notifications/failure-action.tsx +38 -0
  123. package/src/components/notifications/inbox-list.tsx +165 -0
  124. package/src/components/notifications/message-response.tsx +196 -0
  125. package/src/components/notifications/notification-item.tsx +250 -0
  126. package/src/components/notifications/pending-approval-host.tsx +478 -0
  127. package/src/components/notifications/permission-action.tsx +37 -0
  128. package/src/components/notifications/permission-response-actions.tsx +126 -0
  129. package/src/components/notifications/unread-badge.tsx +35 -0
  130. package/src/components/profiles/profile-browser.tsx +117 -0
  131. package/src/components/profiles/profile-card.tsx +78 -0
  132. package/src/components/profiles/profile-detail-view.tsx +564 -0
  133. package/src/components/profiles/profile-form-view.tsx +480 -0
  134. package/src/components/profiles/profile-import-dialog.tsx +113 -0
  135. package/src/components/projects/project-card.tsx +58 -0
  136. package/src/components/projects/project-create-dialog.tsx +140 -0
  137. package/src/components/projects/project-detail.tsx +68 -0
  138. package/src/components/projects/project-edit-dialog.tsx +219 -0
  139. package/src/components/projects/project-list.tsx +108 -0
  140. package/src/components/schedules/schedule-create-dialog.tsx +403 -0
  141. package/src/components/schedules/schedule-detail-view.tsx +274 -0
  142. package/src/components/schedules/schedule-list.tsx +242 -0
  143. package/src/components/schedules/schedule-status-badge.tsx +16 -0
  144. package/src/components/settings/api-key-form.tsx +141 -0
  145. package/src/components/settings/auth-config-section.tsx +141 -0
  146. package/src/components/settings/auth-method-selector.tsx +67 -0
  147. package/src/components/settings/auth-status-badge.tsx +40 -0
  148. package/src/components/settings/auth-status-dot.tsx +59 -0
  149. package/src/components/settings/budget-guardrails-section.tsx +842 -0
  150. package/src/components/settings/data-management-section.tsx +141 -0
  151. package/src/components/settings/openai-runtime-section.tsx +104 -0
  152. package/src/components/settings/permissions-section.tsx +91 -0
  153. package/src/components/shared/app-sidebar.tsx +123 -0
  154. package/src/components/shared/card-skeleton.tsx +42 -0
  155. package/src/components/shared/command-palette.tsx +250 -0
  156. package/src/components/shared/confirm-dialog.tsx +52 -0
  157. package/src/components/shared/empty-state.tsx +24 -0
  158. package/src/components/shared/error-state.tsx +32 -0
  159. package/src/components/shared/form-section-card.tsx +33 -0
  160. package/src/components/shared/section-heading.tsx +14 -0
  161. package/src/components/shared/stagent-logo.tsx +21 -0
  162. package/src/components/shared/theme-toggle.tsx +46 -0
  163. package/src/components/tasks/ai-assist-panel.tsx +210 -0
  164. package/src/components/tasks/content-preview.tsx +89 -0
  165. package/src/components/tasks/empty-board.tsx +12 -0
  166. package/src/components/tasks/file-upload.tsx +120 -0
  167. package/src/components/tasks/kanban-board.tsx +275 -0
  168. package/src/components/tasks/kanban-column.tsx +75 -0
  169. package/src/components/tasks/skeleton-board.tsx +21 -0
  170. package/src/components/tasks/task-attachments.tsx +114 -0
  171. package/src/components/tasks/task-card.tsx +101 -0
  172. package/src/components/tasks/task-create-panel.tsx +360 -0
  173. package/src/components/tasks/task-detail-view.tsx +356 -0
  174. package/src/components/ui/alert-dialog.tsx +196 -0
  175. package/src/components/ui/badge.tsx +50 -0
  176. package/src/components/ui/button.tsx +71 -0
  177. package/src/components/ui/card.tsx +92 -0
  178. package/src/components/ui/checkbox.tsx +32 -0
  179. package/src/components/ui/command.tsx +184 -0
  180. package/src/components/ui/dialog.tsx +158 -0
  181. package/src/components/ui/dropdown-menu.tsx +257 -0
  182. package/src/components/ui/form.tsx +167 -0
  183. package/src/components/ui/input.tsx +21 -0
  184. package/src/components/ui/label.tsx +24 -0
  185. package/src/components/ui/popover.tsx +89 -0
  186. package/src/components/ui/progress.tsx +31 -0
  187. package/src/components/ui/radio-group.tsx +45 -0
  188. package/src/components/ui/scroll-area.tsx +58 -0
  189. package/src/components/ui/select.tsx +190 -0
  190. package/src/components/ui/separator.tsx +28 -0
  191. package/src/components/ui/sheet.tsx +143 -0
  192. package/src/components/ui/sidebar.tsx +726 -0
  193. package/src/components/ui/skeleton.tsx +13 -0
  194. package/src/components/ui/slider.tsx +63 -0
  195. package/src/components/ui/sonner.tsx +36 -0
  196. package/src/components/ui/switch.tsx +35 -0
  197. package/src/components/ui/table.tsx +116 -0
  198. package/src/components/ui/tabs.tsx +91 -0
  199. package/src/components/ui/textarea.tsx +18 -0
  200. package/src/components/ui/tooltip.tsx +57 -0
  201. package/src/components/workflows/blueprint-editor.tsx +109 -0
  202. package/src/components/workflows/blueprint-gallery.tsx +155 -0
  203. package/src/components/workflows/blueprint-preview.tsx +240 -0
  204. package/src/components/workflows/loop-status-view.tsx +272 -0
  205. package/src/components/workflows/swarm-dashboard.tsx +185 -0
  206. package/src/components/workflows/workflow-form-view.tsx +1376 -0
  207. package/src/components/workflows/workflow-list.tsx +230 -0
  208. package/src/components/workflows/workflow-status-view.tsx +477 -0
  209. package/src/hooks/use-mobile.ts +19 -0
  210. package/src/instrumentation.ts +7 -0
  211. package/src/lib/agents/claude-agent.ts +737 -0
  212. package/src/lib/agents/execution-manager.ts +27 -0
  213. package/src/lib/agents/profiles/assignment-validation.ts +75 -0
  214. package/src/lib/agents/profiles/builtins/code-reviewer/SKILL.md +21 -0
  215. package/src/lib/agents/profiles/builtins/code-reviewer/profile.yaml +28 -0
  216. package/src/lib/agents/profiles/builtins/data-analyst/SKILL.md +25 -0
  217. package/src/lib/agents/profiles/builtins/data-analyst/profile.yaml +27 -0
  218. package/src/lib/agents/profiles/builtins/devops-engineer/SKILL.md +34 -0
  219. package/src/lib/agents/profiles/builtins/devops-engineer/profile.yaml +27 -0
  220. package/src/lib/agents/profiles/builtins/document-writer/SKILL.md +16 -0
  221. package/src/lib/agents/profiles/builtins/document-writer/profile.yaml +27 -0
  222. package/src/lib/agents/profiles/builtins/general/SKILL.md +13 -0
  223. package/src/lib/agents/profiles/builtins/general/profile.yaml +18 -0
  224. package/src/lib/agents/profiles/builtins/health-fitness-coach/SKILL.md +34 -0
  225. package/src/lib/agents/profiles/builtins/health-fitness-coach/profile.yaml +26 -0
  226. package/src/lib/agents/profiles/builtins/learning-coach/SKILL.md +35 -0
  227. package/src/lib/agents/profiles/builtins/learning-coach/profile.yaml +26 -0
  228. package/src/lib/agents/profiles/builtins/project-manager/SKILL.md +26 -0
  229. package/src/lib/agents/profiles/builtins/project-manager/profile.yaml +26 -0
  230. package/src/lib/agents/profiles/builtins/researcher/SKILL.md +15 -0
  231. package/src/lib/agents/profiles/builtins/researcher/profile.yaml +27 -0
  232. package/src/lib/agents/profiles/builtins/shopping-assistant/SKILL.md +34 -0
  233. package/src/lib/agents/profiles/builtins/shopping-assistant/profile.yaml +26 -0
  234. package/src/lib/agents/profiles/builtins/technical-writer/SKILL.md +31 -0
  235. package/src/lib/agents/profiles/builtins/technical-writer/profile.yaml +29 -0
  236. package/src/lib/agents/profiles/builtins/travel-planner/SKILL.md +23 -0
  237. package/src/lib/agents/profiles/builtins/travel-planner/profile.yaml +26 -0
  238. package/src/lib/agents/profiles/builtins/wealth-manager/SKILL.md +24 -0
  239. package/src/lib/agents/profiles/builtins/wealth-manager/profile.yaml +26 -0
  240. package/src/lib/agents/profiles/compatibility.ts +109 -0
  241. package/src/lib/agents/profiles/registry.ts +293 -0
  242. package/src/lib/agents/profiles/test-runner.ts +18 -0
  243. package/src/lib/agents/profiles/test-types.ts +20 -0
  244. package/src/lib/agents/profiles/types.ts +43 -0
  245. package/src/lib/agents/router.ts +56 -0
  246. package/src/lib/agents/runtime/catalog.ts +85 -0
  247. package/src/lib/agents/runtime/claude-sdk.ts +12 -0
  248. package/src/lib/agents/runtime/claude.ts +370 -0
  249. package/src/lib/agents/runtime/codex-app-server-client.ts +289 -0
  250. package/src/lib/agents/runtime/index.ts +167 -0
  251. package/src/lib/agents/runtime/openai-codex.ts +1089 -0
  252. package/src/lib/agents/runtime/task-assist-types.ts +8 -0
  253. package/src/lib/agents/runtime/types.ts +30 -0
  254. package/src/lib/constants/settings.ts +13 -0
  255. package/src/lib/constants/status-colors.ts +44 -0
  256. package/src/lib/constants/task-status.ts +49 -0
  257. package/src/lib/data/clear.ts +63 -0
  258. package/src/lib/data/seed-data/documents.ts +715 -0
  259. package/src/lib/data/seed-data/logs.ts +195 -0
  260. package/src/lib/data/seed-data/notifications.ts +141 -0
  261. package/src/lib/data/seed-data/profiles.ts +175 -0
  262. package/src/lib/data/seed-data/projects.ts +61 -0
  263. package/src/lib/data/seed-data/schedules.ts +108 -0
  264. package/src/lib/data/seed-data/tasks.ts +341 -0
  265. package/src/lib/data/seed-data/usage-ledger.ts +130 -0
  266. package/src/lib/data/seed-data/workflows.ts +213 -0
  267. package/src/lib/data/seed.ts +129 -0
  268. package/src/lib/db/index.ts +221 -0
  269. package/src/lib/db/migrations/0000_aromatic_gargoyle.sql +59 -0
  270. package/src/lib/db/migrations/0001_first_iron_patriot.sql +6 -0
  271. package/src/lib/db/migrations/0002_add_resume_count.sql +1 -0
  272. package/src/lib/db/migrations/0003_add_settings.sql +5 -0
  273. package/src/lib/db/migrations/0004_add_documents.sql +20 -0
  274. package/src/lib/db/migrations/0005_add_document_preprocessing.sql +4 -0
  275. package/src/lib/db/migrations/0006_add_agent_profile.sql +2 -0
  276. package/src/lib/db/migrations/0007_add_usage_metering_ledger.sql +30 -0
  277. package/src/lib/db/migrations/0008_add_document_version.sql +1 -0
  278. package/src/lib/db/migrations/meta/0000_snapshot.json +416 -0
  279. package/src/lib/db/migrations/meta/0001_snapshot.json +461 -0
  280. package/src/lib/db/migrations/meta/0002_snapshot.json +469 -0
  281. package/src/lib/db/migrations/meta/_journal.json +27 -0
  282. package/src/lib/db/schema.ts +227 -0
  283. package/src/lib/documents/cleanup.ts +50 -0
  284. package/src/lib/documents/context-builder.ts +75 -0
  285. package/src/lib/documents/output-scanner.ts +166 -0
  286. package/src/lib/documents/processor.ts +120 -0
  287. package/src/lib/documents/processors/image.ts +21 -0
  288. package/src/lib/documents/processors/office.ts +36 -0
  289. package/src/lib/documents/processors/pdf.ts +12 -0
  290. package/src/lib/documents/processors/spreadsheet.ts +18 -0
  291. package/src/lib/documents/processors/text.ts +8 -0
  292. package/src/lib/documents/registry.ts +25 -0
  293. package/src/lib/notifications/actionable.ts +108 -0
  294. package/src/lib/notifications/permissions.ts +169 -0
  295. package/src/lib/queries/chart-data.ts +184 -0
  296. package/src/lib/schedules/interval-parser.ts +110 -0
  297. package/src/lib/schedules/scheduler.ts +220 -0
  298. package/src/lib/settings/auth.ts +98 -0
  299. package/src/lib/settings/budget-guardrails.ts +590 -0
  300. package/src/lib/settings/helpers.ts +23 -0
  301. package/src/lib/settings/openai-auth.ts +80 -0
  302. package/src/lib/settings/permissions.ts +102 -0
  303. package/src/lib/usage/ledger.ts +489 -0
  304. package/src/lib/usage/pricing.ts +68 -0
  305. package/src/lib/utils/crypto.ts +90 -0
  306. package/src/lib/utils/format-timestamp.ts +46 -0
  307. package/src/lib/utils/session-cleanup.ts +26 -0
  308. package/src/lib/utils/stagent-paths.ts +18 -0
  309. package/src/lib/utils.ts +6 -0
  310. package/src/lib/validators/blueprint.ts +43 -0
  311. package/src/lib/validators/profile.ts +64 -0
  312. package/src/lib/validators/project.ts +17 -0
  313. package/src/lib/validators/settings.ts +57 -0
  314. package/src/lib/validators/task.ts +30 -0
  315. package/src/lib/workflows/blueprints/builtins/code-review-pipeline.yaml +72 -0
  316. package/src/lib/workflows/blueprints/builtins/documentation-generation.yaml +62 -0
  317. package/src/lib/workflows/blueprints/builtins/investment-research.yaml +81 -0
  318. package/src/lib/workflows/blueprints/builtins/meal-planning.yaml +73 -0
  319. package/src/lib/workflows/blueprints/builtins/product-research.yaml +72 -0
  320. package/src/lib/workflows/blueprints/builtins/research-report.yaml +77 -0
  321. package/src/lib/workflows/blueprints/builtins/sprint-planning.yaml +77 -0
  322. package/src/lib/workflows/blueprints/builtins/travel-planning.yaml +80 -0
  323. package/src/lib/workflows/blueprints/instantiator.ts +131 -0
  324. package/src/lib/workflows/blueprints/registry.ts +128 -0
  325. package/src/lib/workflows/blueprints/template.ts +58 -0
  326. package/src/lib/workflows/blueprints/types.ts +38 -0
  327. package/src/lib/workflows/definition-validation.ts +121 -0
  328. package/src/lib/workflows/engine.ts +1113 -0
  329. package/src/lib/workflows/loop-executor.ts +270 -0
  330. package/src/lib/workflows/parallel.ts +55 -0
  331. package/src/lib/workflows/swarm.ts +97 -0
  332. package/src/lib/workflows/types.ts +112 -0
  333. package/tsconfig.json +41 -0
@@ -0,0 +1,1113 @@
1
+ import { db } from "@/lib/db";
2
+ import { workflows, tasks, agentLogs, notifications } from "@/lib/db/schema";
3
+ import { eq } from "drizzle-orm";
4
+ import { executeTaskWithRuntime } from "@/lib/agents/runtime";
5
+ import type { WorkflowDefinition, WorkflowState, StepState, LoopState } from "./types";
6
+ import { createInitialState } from "./types";
7
+ import { executeLoop } from "./loop-executor";
8
+ import {
9
+ buildParallelSynthesisPrompt,
10
+ getParallelWorkflowStructure,
11
+ PARALLEL_BRANCH_CONCURRENCY_LIMIT,
12
+ } from "./parallel";
13
+ import {
14
+ buildSwarmRefineryPrompt,
15
+ buildSwarmWorkerPrompt,
16
+ getSwarmWorkflowStructure,
17
+ } from "./swarm";
18
+
19
+ /**
20
+ * Execute a workflow by advancing through its steps according to the pattern.
21
+ * Fire-and-forget — call this from the API route and don't await.
22
+ */
23
+ export async function executeWorkflow(workflowId: string): Promise<void> {
24
+ const [workflow] = await db
25
+ .select()
26
+ .from(workflows)
27
+ .where(eq(workflows.id, workflowId));
28
+
29
+ if (!workflow) throw new Error(`Workflow ${workflowId} not found`);
30
+
31
+ const definition: WorkflowDefinition = JSON.parse(workflow.definition);
32
+ const state = createInitialState(definition);
33
+
34
+ await updateWorkflowState(workflowId, state, "active");
35
+
36
+ await db.insert(agentLogs).values({
37
+ id: crypto.randomUUID(),
38
+ taskId: null,
39
+ agentType: "workflow-engine",
40
+ event: "workflow_started",
41
+ payload: JSON.stringify({ workflowId, pattern: definition.pattern }),
42
+ timestamp: new Date(),
43
+ });
44
+
45
+ // Loop pattern manages its own lifecycle — delegate fully
46
+ if (definition.pattern === "loop") {
47
+ try {
48
+ await executeLoop(workflowId, definition);
49
+
50
+ await db.insert(agentLogs).values({
51
+ id: crypto.randomUUID(),
52
+ taskId: null,
53
+ agentType: "workflow-engine",
54
+ event: "workflow_completed",
55
+ payload: JSON.stringify({ workflowId }),
56
+ timestamp: new Date(),
57
+ });
58
+ } catch (error) {
59
+ await db.insert(agentLogs).values({
60
+ id: crypto.randomUUID(),
61
+ taskId: null,
62
+ agentType: "workflow-engine",
63
+ event: "workflow_failed",
64
+ payload: JSON.stringify({
65
+ workflowId,
66
+ error: error instanceof Error ? error.message : String(error),
67
+ }),
68
+ timestamp: new Date(),
69
+ });
70
+ }
71
+ return;
72
+ }
73
+
74
+ try {
75
+ switch (definition.pattern) {
76
+ case "sequence":
77
+ await executeSequence(workflowId, definition, state);
78
+ break;
79
+ case "planner-executor":
80
+ await executePlannerExecutor(workflowId, definition, state);
81
+ break;
82
+ case "checkpoint":
83
+ await executeCheckpoint(workflowId, definition, state);
84
+ break;
85
+ case "parallel":
86
+ await executeParallel(workflowId, definition, state);
87
+ break;
88
+ case "swarm":
89
+ await executeSwarm(workflowId, definition, state);
90
+ break;
91
+ }
92
+
93
+ state.status = "completed";
94
+ state.completedAt = new Date().toISOString();
95
+ await updateWorkflowState(workflowId, state, "completed");
96
+
97
+ await db.insert(agentLogs).values({
98
+ id: crypto.randomUUID(),
99
+ taskId: null,
100
+ agentType: "workflow-engine",
101
+ event: "workflow_completed",
102
+ payload: JSON.stringify({ workflowId }),
103
+ timestamp: new Date(),
104
+ });
105
+ } catch (error) {
106
+ state.status = "failed";
107
+ await updateWorkflowState(workflowId, state, "failed");
108
+
109
+ await db.insert(agentLogs).values({
110
+ id: crypto.randomUUID(),
111
+ taskId: null,
112
+ agentType: "workflow-engine",
113
+ event: "workflow_failed",
114
+ payload: JSON.stringify({
115
+ workflowId,
116
+ error: error instanceof Error ? error.message : String(error),
117
+ }),
118
+ timestamp: new Date(),
119
+ });
120
+ }
121
+ }
122
+
123
+ /**
124
+ * Sequence pattern: execute steps one after another, passing output forward.
125
+ */
126
+ async function executeSequence(
127
+ workflowId: string,
128
+ definition: WorkflowDefinition,
129
+ state: WorkflowState
130
+ ): Promise<void> {
131
+ let previousOutput = "";
132
+
133
+ for (let i = 0; i < definition.steps.length; i++) {
134
+ const step = definition.steps[i];
135
+ state.currentStepIndex = i;
136
+
137
+ // Build prompt with context from previous step
138
+ const contextPrompt = previousOutput
139
+ ? `Previous step output:\n${previousOutput}\n\n---\n\n${step.prompt}`
140
+ : step.prompt;
141
+
142
+ const result = await executeStep(
143
+ workflowId,
144
+ step.id,
145
+ step.name,
146
+ contextPrompt,
147
+ state,
148
+ step.assignedAgent,
149
+ step.agentProfile
150
+ );
151
+
152
+ if (result.status === "failed") {
153
+ throw new Error(`Step "${step.name}" failed: ${result.error}`);
154
+ }
155
+
156
+ previousOutput = result.result ?? "";
157
+ }
158
+ }
159
+
160
+ /**
161
+ * Planner-Executor pattern: first step generates a plan, subsequent steps execute it.
162
+ */
163
+ async function executePlannerExecutor(
164
+ workflowId: string,
165
+ definition: WorkflowDefinition,
166
+ state: WorkflowState
167
+ ): Promise<void> {
168
+ if (definition.steps.length < 2) {
169
+ throw new Error("Planner-Executor requires at least 2 steps (planner + executor)");
170
+ }
171
+
172
+ // Step 1: Planner
173
+ const plannerStep = definition.steps[0];
174
+ state.currentStepIndex = 0;
175
+ const planResult = await executeStep(
176
+ workflowId,
177
+ plannerStep.id,
178
+ plannerStep.name,
179
+ plannerStep.prompt,
180
+ state,
181
+ plannerStep.assignedAgent,
182
+ plannerStep.agentProfile
183
+ );
184
+
185
+ if (planResult.status === "failed") {
186
+ throw new Error(`Planner step failed: ${planResult.error}`);
187
+ }
188
+
189
+ // Execute remaining steps with plan context
190
+ for (let i = 1; i < definition.steps.length; i++) {
191
+ const step = definition.steps[i];
192
+ state.currentStepIndex = i;
193
+
194
+ const contextPrompt = `Plan from planner:\n${planResult.result}\n\n---\n\n${step.prompt}`;
195
+ const result = await executeStep(
196
+ workflowId,
197
+ step.id,
198
+ step.name,
199
+ contextPrompt,
200
+ state,
201
+ step.assignedAgent,
202
+ step.agentProfile
203
+ );
204
+
205
+ if (result.status === "failed") {
206
+ throw new Error(`Executor step "${step.name}" failed: ${result.error}`);
207
+ }
208
+ }
209
+ }
210
+
211
+ /**
212
+ * Checkpoint pattern: execute steps with human approval gates between them.
213
+ */
214
+ async function executeCheckpoint(
215
+ workflowId: string,
216
+ definition: WorkflowDefinition,
217
+ state: WorkflowState
218
+ ): Promise<void> {
219
+ let previousOutput = "";
220
+
221
+ for (let i = 0; i < definition.steps.length; i++) {
222
+ const step = definition.steps[i];
223
+ state.currentStepIndex = i;
224
+
225
+ // If step requires approval and we have previous output, wait for approval
226
+ if (step.requiresApproval && i > 0) {
227
+ state.stepStates[i].status = "waiting_approval";
228
+ await updateWorkflowState(workflowId, state, "active");
229
+
230
+ const approved = await waitForApproval(workflowId, step.name, previousOutput);
231
+ if (!approved) {
232
+ state.stepStates[i].status = "failed";
233
+ state.stepStates[i].error = "Approval denied by user";
234
+ throw new Error(`Step "${step.name}" was denied approval`);
235
+ }
236
+ }
237
+
238
+ const contextPrompt = previousOutput
239
+ ? `Previous step output:\n${previousOutput}\n\n---\n\n${step.prompt}`
240
+ : step.prompt;
241
+
242
+ const result = await executeStep(
243
+ workflowId,
244
+ step.id,
245
+ step.name,
246
+ contextPrompt,
247
+ state,
248
+ step.assignedAgent,
249
+ step.agentProfile
250
+ );
251
+
252
+ if (result.status === "failed") {
253
+ throw new Error(`Step "${step.name}" failed: ${result.error}`);
254
+ }
255
+
256
+ previousOutput = result.result ?? "";
257
+ }
258
+ }
259
+
260
+ /**
261
+ * Parallel pattern: execute branch steps concurrently, then run a synthesis step.
262
+ */
263
+ async function executeParallel(
264
+ workflowId: string,
265
+ definition: WorkflowDefinition,
266
+ state: WorkflowState
267
+ ): Promise<void> {
268
+ const structure = getParallelWorkflowStructure(definition);
269
+ if (!structure) {
270
+ throw new Error(
271
+ "Parallel workflows require branch steps and exactly one synthesis step"
272
+ );
273
+ }
274
+
275
+ const { branchSteps, synthesisStep } = structure;
276
+ const synthesisIndex = definition.steps.findIndex(
277
+ (step) => step.id === synthesisStep.id
278
+ );
279
+
280
+ if (synthesisIndex === -1) {
281
+ throw new Error(`Synthesis step "${synthesisStep.id}" not found`);
282
+ }
283
+
284
+ let stateWriteQueue = Promise.resolve();
285
+ const commitState = (
286
+ mutate: (draft: WorkflowState) => void,
287
+ status: "draft" | "active" | "paused" | "completed" = "active"
288
+ ) => {
289
+ stateWriteQueue = stateWriteQueue.then(async () => {
290
+ mutate(state);
291
+ await updateWorkflowState(workflowId, state, status);
292
+ });
293
+ return stateWriteQueue;
294
+ };
295
+
296
+ await commitState((draft) => {
297
+ draft.currentStepIndex = 0;
298
+ const joinState = draft.stepStates[synthesisIndex];
299
+ joinState.status = "waiting_dependencies";
300
+ joinState.error = undefined;
301
+ joinState.result = undefined;
302
+ });
303
+
304
+ const branchResults = await mapWithConcurrency(
305
+ branchSteps,
306
+ PARALLEL_BRANCH_CONCURRENCY_LIMIT,
307
+ async (step) => {
308
+ const stepIndex = definition.steps.findIndex(
309
+ (candidate) => candidate.id === step.id
310
+ );
311
+ if (stepIndex === -1) {
312
+ throw new Error(`Parallel branch "${step.id}" not found`);
313
+ }
314
+
315
+ const startedAt = new Date().toISOString();
316
+ await commitState((draft) => {
317
+ const stepState = draft.stepStates[stepIndex];
318
+ stepState.status = "running";
319
+ stepState.startedAt = startedAt;
320
+ stepState.completedAt = undefined;
321
+ stepState.error = undefined;
322
+ stepState.result = undefined;
323
+ });
324
+
325
+ const result = await executeChildTask(
326
+ workflowId,
327
+ step.name,
328
+ step.prompt,
329
+ step.assignedAgent,
330
+ step.agentProfile
331
+ );
332
+
333
+ const completedAt = new Date().toISOString();
334
+ await commitState((draft) => {
335
+ const stepState = draft.stepStates[stepIndex];
336
+ stepState.taskId = result.taskId;
337
+ stepState.completedAt = completedAt;
338
+
339
+ if (result.status === "completed") {
340
+ stepState.status = "completed";
341
+ stepState.result = result.result ?? "";
342
+ } else {
343
+ stepState.status = "failed";
344
+ stepState.error =
345
+ result.error ?? "Task did not complete successfully";
346
+ }
347
+ });
348
+
349
+ return { step, result };
350
+ }
351
+ );
352
+
353
+ await stateWriteQueue;
354
+
355
+ const failedBranches = branchResults.filter(
356
+ (branch) => branch.result.status !== "completed"
357
+ );
358
+
359
+ if (failedBranches.length > 0) {
360
+ const failureSummary = failedBranches
361
+ .map(
362
+ (branch) =>
363
+ `${branch.step.name}: ${
364
+ branch.result.error ?? "Task did not complete successfully"
365
+ }`
366
+ )
367
+ .join("; ");
368
+
369
+ await commitState((draft) => {
370
+ const joinState = draft.stepStates[synthesisIndex];
371
+ joinState.status = "failed";
372
+ joinState.error = `Blocked by failed branches: ${failureSummary}`;
373
+ });
374
+ await stateWriteQueue;
375
+
376
+ throw new Error(`Parallel branches failed: ${failureSummary}`);
377
+ }
378
+
379
+ const synthesisStartedAt = new Date().toISOString();
380
+ await commitState((draft) => {
381
+ draft.currentStepIndex = synthesisIndex;
382
+ const joinState = draft.stepStates[synthesisIndex];
383
+ joinState.status = "running";
384
+ joinState.startedAt = synthesisStartedAt;
385
+ joinState.completedAt = undefined;
386
+ joinState.error = undefined;
387
+ joinState.result = undefined;
388
+ });
389
+
390
+ const synthesisPrompt = buildParallelSynthesisPrompt({
391
+ branchOutputs: branchResults.map((branch) => ({
392
+ stepName: branch.step.name,
393
+ result: branch.result.result ?? "",
394
+ })),
395
+ synthesisPrompt: synthesisStep.prompt,
396
+ });
397
+
398
+ const synthesisResult = await executeChildTask(
399
+ workflowId,
400
+ synthesisStep.name,
401
+ synthesisPrompt,
402
+ synthesisStep.assignedAgent,
403
+ synthesisStep.agentProfile
404
+ );
405
+
406
+ await commitState((draft) => {
407
+ const joinState = draft.stepStates[synthesisIndex];
408
+ joinState.taskId = synthesisResult.taskId;
409
+ joinState.completedAt = new Date().toISOString();
410
+
411
+ if (synthesisResult.status === "completed") {
412
+ joinState.status = "completed";
413
+ joinState.result = synthesisResult.result ?? "";
414
+ } else {
415
+ joinState.status = "failed";
416
+ joinState.error =
417
+ synthesisResult.error ?? "Task did not complete successfully";
418
+ }
419
+ });
420
+ await stateWriteQueue;
421
+
422
+ if (synthesisResult.status !== "completed") {
423
+ throw new Error(
424
+ `Synthesis step "${synthesisStep.name}" failed: ${
425
+ synthesisResult.error ?? "Task did not complete successfully"
426
+ }`
427
+ );
428
+ }
429
+ }
430
+
431
+ /**
432
+ * Swarm pattern: run a mayor planning step, execute worker steps in parallel,
433
+ * then merge the results through a refinery step.
434
+ */
435
+ async function executeSwarm(
436
+ workflowId: string,
437
+ definition: WorkflowDefinition,
438
+ state: WorkflowState
439
+ ): Promise<void> {
440
+ const structure = getSwarmWorkflowStructure(definition);
441
+ if (!structure) {
442
+ throw new Error(
443
+ "Swarm workflows require a mayor step, 2-5 worker steps, and a refinery step"
444
+ );
445
+ }
446
+
447
+ const { mayorStep, workerSteps, refineryStep, workerConcurrencyLimit } =
448
+ structure;
449
+ const refineryIndex = definition.steps.findIndex(
450
+ (step) => step.id === refineryStep.id
451
+ );
452
+
453
+ if (refineryIndex === -1) {
454
+ throw new Error(`Refinery step "${refineryStep.id}" not found`);
455
+ }
456
+
457
+ const mayorResult = await executeStep(
458
+ workflowId,
459
+ mayorStep.id,
460
+ mayorStep.name,
461
+ mayorStep.prompt,
462
+ state,
463
+ mayorStep.assignedAgent,
464
+ mayorStep.agentProfile
465
+ );
466
+
467
+ if (mayorResult.status === "failed") {
468
+ throw new Error(`Mayor step "${mayorStep.name}" failed: ${mayorResult.error}`);
469
+ }
470
+
471
+ let stateWriteQueue = Promise.resolve();
472
+ const commitState = (
473
+ mutate: (draft: WorkflowState) => void,
474
+ status: "draft" | "active" | "paused" | "completed" = "active"
475
+ ) => {
476
+ stateWriteQueue = stateWriteQueue.then(async () => {
477
+ mutate(state);
478
+ await updateWorkflowState(workflowId, state, status);
479
+ });
480
+ return stateWriteQueue;
481
+ };
482
+
483
+ await commitState((draft) => {
484
+ draft.currentStepIndex = 1;
485
+ const refineryState = draft.stepStates[refineryIndex];
486
+ refineryState.status = "waiting_dependencies";
487
+ refineryState.error = undefined;
488
+ refineryState.result = undefined;
489
+ refineryState.startedAt = undefined;
490
+ refineryState.completedAt = undefined;
491
+ });
492
+
493
+ const workerResults = await mapWithConcurrency(
494
+ workerSteps,
495
+ workerConcurrencyLimit,
496
+ async (step) => {
497
+ const stepIndex = definition.steps.findIndex(
498
+ (candidate) => candidate.id === step.id
499
+ );
500
+ if (stepIndex === -1) {
501
+ throw new Error(`Swarm worker "${step.id}" not found`);
502
+ }
503
+
504
+ const workerPrompt = buildSwarmWorkerPrompt({
505
+ mayorName: mayorStep.name,
506
+ mayorResult: mayorResult.result ?? "",
507
+ workerName: step.name,
508
+ workerPrompt: step.prompt,
509
+ });
510
+
511
+ const startedAt = new Date().toISOString();
512
+ await commitState((draft) => {
513
+ const stepState = draft.stepStates[stepIndex];
514
+ stepState.status = "running";
515
+ stepState.startedAt = startedAt;
516
+ stepState.completedAt = undefined;
517
+ stepState.error = undefined;
518
+ stepState.result = undefined;
519
+ });
520
+
521
+ const result = await executeChildTask(
522
+ workflowId,
523
+ step.name,
524
+ workerPrompt,
525
+ step.assignedAgent,
526
+ step.agentProfile
527
+ );
528
+
529
+ const completedAt = new Date().toISOString();
530
+ await commitState((draft) => {
531
+ const stepState = draft.stepStates[stepIndex];
532
+ stepState.taskId = result.taskId;
533
+ stepState.completedAt = completedAt;
534
+
535
+ if (result.status === "completed") {
536
+ stepState.status = "completed";
537
+ stepState.result = result.result ?? "";
538
+ } else {
539
+ stepState.status = "failed";
540
+ stepState.error =
541
+ result.error ?? "Task did not complete successfully";
542
+ }
543
+ });
544
+
545
+ return { step, result };
546
+ }
547
+ );
548
+
549
+ await stateWriteQueue;
550
+
551
+ const failedWorkers = workerResults.filter(
552
+ (worker) => worker.result.status !== "completed"
553
+ );
554
+ if (failedWorkers.length > 0) {
555
+ const failureSummary = summarizeFailedWorkers(failedWorkers);
556
+
557
+ await commitState((draft) => {
558
+ const refineryState = draft.stepStates[refineryIndex];
559
+ refineryState.status = "failed";
560
+ refineryState.error = `Blocked by failed workers: ${failureSummary}`;
561
+ });
562
+ await stateWriteQueue;
563
+
564
+ throw new Error(`Swarm workers failed: ${failureSummary}`);
565
+ }
566
+
567
+ await runSwarmRefinery({
568
+ workflowId,
569
+ state,
570
+ mayorStep,
571
+ mayorResult: mayorResult.result ?? "",
572
+ refineryStep,
573
+ refineryIndex,
574
+ workerOutputs: workerResults.map((worker) => ({
575
+ stepName: worker.step.name,
576
+ result: worker.result.result ?? "",
577
+ })),
578
+ });
579
+ }
580
+
581
+ function summarizeFailedWorkers(
582
+ failedWorkers: Array<{
583
+ step: { name: string };
584
+ result: { error?: string };
585
+ }>
586
+ ): string {
587
+ return failedWorkers
588
+ .map(
589
+ (worker) =>
590
+ `${worker.step.name}: ${
591
+ worker.result.error ?? "Task did not complete successfully"
592
+ }`
593
+ )
594
+ .join("; ");
595
+ }
596
+
597
+ async function runSwarmRefinery(input: {
598
+ workflowId: string;
599
+ state: WorkflowState;
600
+ mayorStep: { name: string };
601
+ mayorResult: string;
602
+ refineryStep: {
603
+ id: string;
604
+ name: string;
605
+ prompt: string;
606
+ assignedAgent?: string;
607
+ agentProfile?: string;
608
+ };
609
+ refineryIndex: number;
610
+ workerOutputs: Array<{ stepName: string; result: string }>;
611
+ }): Promise<void> {
612
+ const {
613
+ workflowId,
614
+ state,
615
+ mayorStep,
616
+ mayorResult,
617
+ refineryStep,
618
+ refineryIndex,
619
+ workerOutputs,
620
+ } = input;
621
+
622
+ state.currentStepIndex = refineryIndex;
623
+ const refineryState = state.stepStates[refineryIndex];
624
+ refineryState.status = "running";
625
+ refineryState.startedAt = new Date().toISOString();
626
+ refineryState.completedAt = undefined;
627
+ refineryState.error = undefined;
628
+ refineryState.result = undefined;
629
+ await updateWorkflowState(workflowId, state, "active");
630
+
631
+ const refineryPrompt = buildSwarmRefineryPrompt({
632
+ mayorName: mayorStep.name,
633
+ mayorResult,
634
+ workerOutputs,
635
+ refineryPrompt: refineryStep.prompt,
636
+ });
637
+
638
+ const refineryResult = await executeChildTask(
639
+ workflowId,
640
+ refineryStep.name,
641
+ refineryPrompt,
642
+ refineryStep.assignedAgent,
643
+ refineryStep.agentProfile
644
+ );
645
+
646
+ refineryState.taskId = refineryResult.taskId;
647
+ refineryState.completedAt = new Date().toISOString();
648
+
649
+ if (refineryResult.status === "completed") {
650
+ refineryState.status = "completed";
651
+ refineryState.result = refineryResult.result ?? "";
652
+ } else {
653
+ refineryState.status = "failed";
654
+ refineryState.error =
655
+ refineryResult.error ?? "Task did not complete successfully";
656
+ }
657
+
658
+ await updateWorkflowState(workflowId, state, "active");
659
+
660
+ if (refineryResult.status !== "completed") {
661
+ throw new Error(
662
+ `Refinery step "${refineryStep.name}" failed: ${
663
+ refineryResult.error ?? "Task did not complete successfully"
664
+ }`
665
+ );
666
+ }
667
+ }
668
+
669
+ /**
670
+ * Create and execute a child task, returning its result.
671
+ * Shared by step-based patterns and the loop executor.
672
+ */
673
+ export async function executeChildTask(
674
+ workflowId: string,
675
+ name: string,
676
+ prompt: string,
677
+ assignedAgent?: string,
678
+ agentProfile?: string
679
+ ): Promise<{ taskId: string; status: string; result?: string; error?: string }> {
680
+ const [workflow] = await db
681
+ .select()
682
+ .from(workflows)
683
+ .where(eq(workflows.id, workflowId));
684
+
685
+ const taskId = crypto.randomUUID();
686
+ await db.insert(tasks).values({
687
+ id: taskId,
688
+ projectId: workflow?.projectId ?? null,
689
+ workflowId,
690
+ scheduleId: null,
691
+ title: `[Workflow] ${name}`,
692
+ description: prompt,
693
+ status: "queued",
694
+ priority: 1,
695
+ assignedAgent: assignedAgent ?? null,
696
+ agentProfile: agentProfile ?? null,
697
+ createdAt: new Date(),
698
+ updatedAt: new Date(),
699
+ });
700
+
701
+ await db
702
+ .update(tasks)
703
+ .set({ status: "running", updatedAt: new Date() })
704
+ .where(eq(tasks.id, taskId));
705
+
706
+ try {
707
+ await executeTaskWithRuntime(taskId);
708
+ } catch {
709
+ // Runtime adapter handles its own error logging
710
+ }
711
+
712
+ const [completedTask] = await db
713
+ .select()
714
+ .from(tasks)
715
+ .where(eq(tasks.id, taskId));
716
+
717
+ if (completedTask?.status === "completed") {
718
+ return { taskId, status: "completed", result: completedTask.result ?? "" };
719
+ }
720
+ return {
721
+ taskId,
722
+ status: "failed",
723
+ error: completedTask?.result ?? "Task did not complete successfully",
724
+ };
725
+ }
726
+
727
+ /**
728
+ * Execute a single workflow step by creating a task and waiting for completion.
729
+ */
730
+ async function executeStep(
731
+ workflowId: string,
732
+ stepId: string,
733
+ stepName: string,
734
+ prompt: string,
735
+ state: WorkflowState,
736
+ assignedAgent?: string,
737
+ agentProfile?: string
738
+ ): Promise<StepState> {
739
+ const stepState = state.stepStates.find((s) => s.stepId === stepId);
740
+ if (!stepState) throw new Error(`Step ${stepId} not found in state`);
741
+
742
+ stepState.status = "running";
743
+ stepState.startedAt = new Date().toISOString();
744
+ await updateWorkflowState(workflowId, state, "active");
745
+
746
+ const result = await executeChildTask(
747
+ workflowId,
748
+ stepName,
749
+ prompt,
750
+ assignedAgent,
751
+ agentProfile
752
+ );
753
+
754
+ stepState.taskId = result.taskId;
755
+ if (result.status === "completed") {
756
+ stepState.status = "completed";
757
+ stepState.result = result.result ?? "";
758
+ stepState.completedAt = new Date().toISOString();
759
+ } else {
760
+ stepState.status = "failed";
761
+ stepState.error = result.error ?? "Task did not complete successfully";
762
+ }
763
+
764
+ await updateWorkflowState(workflowId, state, "active");
765
+ return stepState;
766
+ }
767
+
768
+ /**
769
+ * Wait for human approval via the notifications system.
770
+ */
771
+ async function waitForApproval(
772
+ workflowId: string,
773
+ stepName: string,
774
+ previousOutput: string
775
+ ): Promise<boolean> {
776
+ const notificationId = crypto.randomUUID();
777
+
778
+ await db.insert(notifications).values({
779
+ id: notificationId,
780
+ taskId: null,
781
+ type: "permission_required",
782
+ title: `Workflow checkpoint: ${stepName}`,
783
+ body: `Previous step output:\n${previousOutput.slice(0, 500)}`,
784
+ toolName: "WorkflowCheckpoint",
785
+ toolInput: JSON.stringify({ workflowId, stepName }),
786
+ createdAt: new Date(),
787
+ });
788
+
789
+ // Poll for response with 5-minute timeout for human approval
790
+ const deadline = Date.now() + 5 * 60 * 1000;
791
+ const pollInterval = 2000;
792
+
793
+ while (Date.now() < deadline) {
794
+ const [notification] = await db
795
+ .select()
796
+ .from(notifications)
797
+ .where(eq(notifications.id, notificationId));
798
+
799
+ if (notification?.response) {
800
+ try {
801
+ const parsed = JSON.parse(notification.response);
802
+ return parsed.behavior === "allow";
803
+ } catch {
804
+ return false;
805
+ }
806
+ }
807
+
808
+ await new Promise((resolve) => setTimeout(resolve, pollInterval));
809
+ }
810
+
811
+ return false; // Timeout — treat as denied
812
+ }
813
+
814
+ /**
815
+ * Update workflow state in the database.
816
+ */
817
+ export async function updateWorkflowState(
818
+ workflowId: string,
819
+ state: WorkflowState,
820
+ status: "draft" | "active" | "paused" | "completed" | "failed"
821
+ ): Promise<void> {
822
+ // Store state in the definition field as a combined object
823
+ const [workflow] = await db
824
+ .select()
825
+ .from(workflows)
826
+ .where(eq(workflows.id, workflowId));
827
+
828
+ if (!workflow) return;
829
+
830
+ const definition = JSON.parse(workflow.definition);
831
+ const combined = { ...definition, _state: state };
832
+
833
+ await db
834
+ .update(workflows)
835
+ .set({
836
+ definition: JSON.stringify(combined),
837
+ status,
838
+ updatedAt: new Date(),
839
+ })
840
+ .where(eq(workflows.id, workflowId));
841
+ }
842
+
843
+ /**
844
+ * Get the current state of a workflow.
845
+ */
846
+ export function parseWorkflowState(
847
+ definitionJson: string
848
+ ): { definition: WorkflowDefinition; state: WorkflowState | null; loopState: LoopState | null } {
849
+ const parsed = JSON.parse(definitionJson);
850
+ const { _state, _loopState, ...definition } = parsed;
851
+ return { definition, state: _state ?? null, loopState: _loopState ?? null };
852
+ }
853
+
854
+ /**
855
+ * Retry a failed step in a workflow.
856
+ */
857
+ export async function retryWorkflowStep(
858
+ workflowId: string,
859
+ stepId: string
860
+ ): Promise<void> {
861
+ const [workflow] = await db
862
+ .select()
863
+ .from(workflows)
864
+ .where(eq(workflows.id, workflowId));
865
+
866
+ if (!workflow) throw new Error(`Workflow ${workflowId} not found`);
867
+
868
+ const { definition, state } = parseWorkflowState(workflow.definition);
869
+ if (!state) throw new Error("Workflow has no execution state");
870
+
871
+ const stepIndex = state.stepStates.findIndex((s) => s.stepId === stepId);
872
+ if (stepIndex === -1) throw new Error(`Step ${stepId} not found`);
873
+
874
+ const stepState = state.stepStates[stepIndex];
875
+ if (stepState.status !== "failed") {
876
+ throw new Error(`Step ${stepId} is not in failed state`);
877
+ }
878
+
879
+ if (workflow.status === "active") {
880
+ throw new Error("Cannot retry a step while the workflow is active");
881
+ }
882
+
883
+ if (definition.pattern === "swarm") {
884
+ await retrySwarmStep(workflowId, definition, state, stepIndex);
885
+ return;
886
+ }
887
+
888
+ // Reset step state
889
+ stepState.status = "pending";
890
+ stepState.error = undefined;
891
+ stepState.taskId = undefined;
892
+ state.status = "running";
893
+ state.currentStepIndex = stepIndex;
894
+ await updateWorkflowState(workflowId, state, "active");
895
+
896
+ // Re-execute from this step
897
+ const step = definition.steps[stepIndex];
898
+ const result = await executeStep(
899
+ workflowId,
900
+ step.id,
901
+ step.name,
902
+ step.prompt,
903
+ state,
904
+ step.assignedAgent,
905
+ step.agentProfile
906
+ );
907
+
908
+ if (result.status === "completed") {
909
+ // Continue with remaining steps if this was a sequence
910
+ if (definition.pattern === "sequence") {
911
+ let previousOutput = result.result ?? "";
912
+ for (let i = stepIndex + 1; i < definition.steps.length; i++) {
913
+ const nextStep = definition.steps[i];
914
+ state.currentStepIndex = i;
915
+ const contextPrompt = `Previous step output:\n${previousOutput}\n\n---\n\n${nextStep.prompt}`;
916
+ const nextResult = await executeStep(
917
+ workflowId,
918
+ nextStep.id,
919
+ nextStep.name,
920
+ contextPrompt,
921
+ state,
922
+ nextStep.assignedAgent,
923
+ nextStep.agentProfile
924
+ );
925
+ if (nextResult.status === "failed") break;
926
+ previousOutput = nextResult.result ?? "";
927
+ }
928
+ }
929
+
930
+ const allCompleted = state.stepStates.every((s) => s.status === "completed");
931
+ state.status = allCompleted ? "completed" : "failed";
932
+ state.completedAt = allCompleted ? new Date().toISOString() : undefined;
933
+ await updateWorkflowState(workflowId, state, allCompleted ? "completed" : "failed");
934
+ }
935
+ }
936
+
937
+ function resetStepState(stepState: StepState): void {
938
+ stepState.status = "pending";
939
+ stepState.error = undefined;
940
+ stepState.result = undefined;
941
+ stepState.taskId = undefined;
942
+ stepState.startedAt = undefined;
943
+ stepState.completedAt = undefined;
944
+ }
945
+
946
+ async function retrySwarmStep(
947
+ workflowId: string,
948
+ definition: WorkflowDefinition,
949
+ state: WorkflowState,
950
+ stepIndex: number
951
+ ): Promise<void> {
952
+ const structure = getSwarmWorkflowStructure(definition);
953
+ if (!structure) {
954
+ throw new Error(
955
+ "Swarm workflows require a mayor step, 2-5 worker steps, and a refinery step"
956
+ );
957
+ }
958
+
959
+ const { mayorStep, workerSteps, refineryStep } = structure;
960
+ const refineryIndex = definition.steps.length - 1;
961
+ const mayorState = state.stepStates[0];
962
+ const refineryState = state.stepStates[refineryIndex];
963
+ const targetStep = definition.steps[stepIndex];
964
+ const targetState = state.stepStates[stepIndex];
965
+
966
+ if (stepIndex === 0) {
967
+ for (const currentStepState of state.stepStates) {
968
+ resetStepState(currentStepState);
969
+ }
970
+
971
+ state.status = "running";
972
+ state.currentStepIndex = 0;
973
+ state.completedAt = undefined;
974
+ await updateWorkflowState(workflowId, state, "active");
975
+ await executeSwarm(workflowId, definition, state);
976
+ return;
977
+ }
978
+
979
+ if (mayorState.status !== "completed" || !mayorState.result) {
980
+ throw new Error("Swarm mayor output must complete before retrying downstream steps");
981
+ }
982
+
983
+ if (stepIndex === refineryIndex) {
984
+ const incompleteWorkers = workerSteps.filter((_, workerIndex) => {
985
+ const workerState = state.stepStates[workerIndex + 1];
986
+ return workerState.status !== "completed" || !workerState.result;
987
+ });
988
+
989
+ if (incompleteWorkers.length > 0) {
990
+ throw new Error("All swarm workers must complete before retrying the refinery");
991
+ }
992
+
993
+ resetStepState(refineryState);
994
+ state.status = "running";
995
+ state.currentStepIndex = refineryIndex;
996
+ state.completedAt = undefined;
997
+ await updateWorkflowState(workflowId, state, "active");
998
+
999
+ await runSwarmRefinery({
1000
+ workflowId,
1001
+ state,
1002
+ mayorStep,
1003
+ mayorResult: mayorState.result,
1004
+ refineryStep,
1005
+ refineryIndex,
1006
+ workerOutputs: workerSteps.map((worker, workerIndex) => ({
1007
+ stepName: worker.name,
1008
+ result: state.stepStates[workerIndex + 1].result ?? "",
1009
+ })),
1010
+ });
1011
+
1012
+ state.status = "completed";
1013
+ state.completedAt = new Date().toISOString();
1014
+ await updateWorkflowState(workflowId, state, "completed");
1015
+ return;
1016
+ }
1017
+
1018
+ resetStepState(targetState);
1019
+ resetStepState(refineryState);
1020
+ refineryState.status = "waiting_dependencies";
1021
+ state.status = "running";
1022
+ state.currentStepIndex = stepIndex;
1023
+ state.completedAt = undefined;
1024
+ await updateWorkflowState(workflowId, state, "active");
1025
+
1026
+ const retriedWorker = await executeStep(
1027
+ workflowId,
1028
+ targetStep.id,
1029
+ targetStep.name,
1030
+ buildSwarmWorkerPrompt({
1031
+ mayorName: mayorStep.name,
1032
+ mayorResult: mayorState.result,
1033
+ workerName: targetStep.name,
1034
+ workerPrompt: targetStep.prompt,
1035
+ }),
1036
+ state,
1037
+ targetStep.assignedAgent,
1038
+ targetStep.agentProfile
1039
+ );
1040
+
1041
+ if (retriedWorker.status !== "completed") {
1042
+ state.status = "failed";
1043
+ await updateWorkflowState(workflowId, state, "failed");
1044
+ throw new Error(
1045
+ `Swarm worker "${targetStep.name}" failed: ${
1046
+ retriedWorker.error ?? "Task did not complete successfully"
1047
+ }`
1048
+ );
1049
+ }
1050
+
1051
+ const failedWorkers = workerSteps
1052
+ .map((worker, workerIndex) => ({
1053
+ step: worker,
1054
+ result: state.stepStates[workerIndex + 1],
1055
+ }))
1056
+ .filter((worker) => worker.result.status !== "completed");
1057
+
1058
+ if (failedWorkers.length > 0) {
1059
+ refineryState.status = "failed";
1060
+ refineryState.error = `Blocked by failed workers: ${summarizeFailedWorkers(
1061
+ failedWorkers.map((worker) => ({
1062
+ step: worker.step,
1063
+ result: { error: worker.result.error },
1064
+ }))
1065
+ )}`;
1066
+ state.status = "failed";
1067
+ await updateWorkflowState(workflowId, state, "failed");
1068
+ return;
1069
+ }
1070
+
1071
+ await runSwarmRefinery({
1072
+ workflowId,
1073
+ state,
1074
+ mayorStep,
1075
+ mayorResult: mayorState.result,
1076
+ refineryStep,
1077
+ refineryIndex,
1078
+ workerOutputs: workerSteps.map((worker, workerIndex) => ({
1079
+ stepName: worker.name,
1080
+ result: state.stepStates[workerIndex + 1].result ?? "",
1081
+ })),
1082
+ });
1083
+
1084
+ state.status = "completed";
1085
+ state.completedAt = new Date().toISOString();
1086
+ await updateWorkflowState(workflowId, state, "completed");
1087
+ }
1088
+
1089
+ async function mapWithConcurrency<T, TResult>(
1090
+ items: T[],
1091
+ limit: number,
1092
+ worker: (item: T) => Promise<TResult>
1093
+ ): Promise<TResult[]> {
1094
+ if (items.length === 0) {
1095
+ return [];
1096
+ }
1097
+
1098
+ const results = new Array<TResult>(items.length);
1099
+ let nextIndex = 0;
1100
+ const workerCount = Math.min(limit, items.length);
1101
+
1102
+ await Promise.all(
1103
+ Array.from({ length: workerCount }, async () => {
1104
+ while (nextIndex < items.length) {
1105
+ const currentIndex = nextIndex;
1106
+ nextIndex += 1;
1107
+ results[currentIndex] = await worker(items[currentIndex]);
1108
+ }
1109
+ })
1110
+ );
1111
+
1112
+ return results;
1113
+ }