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,1376 @@
1
+ "use client";
2
+
3
+ import { useState, useEffect } from "react";
4
+ import { useRouter } from "next/navigation";
5
+ import { Button } from "@/components/ui/button";
6
+ import { Input } from "@/components/ui/input";
7
+ import { Textarea } from "@/components/ui/textarea";
8
+ import { Label } from "@/components/ui/label";
9
+ import { Badge } from "@/components/ui/badge";
10
+ import { Slider } from "@/components/ui/slider";
11
+ import { Switch } from "@/components/ui/switch";
12
+ import {
13
+ Select,
14
+ SelectContent,
15
+ SelectItem,
16
+ SelectTrigger,
17
+ SelectValue,
18
+ } from "@/components/ui/select";
19
+ import {
20
+ Plus,
21
+ Trash2,
22
+ GitBranch,
23
+ RefreshCw,
24
+ Bot,
25
+ ListOrdered,
26
+ MessageSquare,
27
+ ArrowDown,
28
+ Brain,
29
+ ShieldCheck,
30
+ } from "lucide-react";
31
+ import { toast } from "sonner";
32
+ import { FormSectionCard } from "@/components/shared/form-section-card";
33
+ import type {
34
+ WorkflowStep,
35
+ WorkflowDefinition,
36
+ WorkflowPattern,
37
+ } from "@/lib/workflows/types";
38
+ import {
39
+ type AgentRuntimeId,
40
+ DEFAULT_AGENT_RUNTIME,
41
+ listRuntimeCatalog,
42
+ } from "@/lib/agents/runtime/catalog";
43
+ import { profileSupportsRuntime } from "@/lib/agents/profiles/compatibility";
44
+ import type { AgentProfile } from "@/lib/agents/profiles/types";
45
+ import { validateWorkflowDefinition } from "@/lib/workflows/definition-validation";
46
+ import {
47
+ MAX_PARALLEL_BRANCHES,
48
+ MIN_PARALLEL_BRANCHES,
49
+ } from "@/lib/workflows/parallel";
50
+ import {
51
+ DEFAULT_SWARM_CONCURRENCY_LIMIT,
52
+ MAX_SWARM_WORKERS,
53
+ MIN_SWARM_WORKERS,
54
+ } from "@/lib/workflows/swarm";
55
+
56
+ interface WorkflowData {
57
+ id: string;
58
+ name: string;
59
+ projectId: string | null;
60
+ definition: string;
61
+ }
62
+
63
+ interface WorkflowFormViewProps {
64
+ workflow?: WorkflowData;
65
+ projects: { id: string; name: string }[];
66
+ profiles: Pick<AgentProfile, "id" | "name" | "supportedRuntimes">[];
67
+ clone?: boolean;
68
+ }
69
+
70
+ function createEmptyStep(): WorkflowStep {
71
+ return {
72
+ id: crypto.randomUUID(),
73
+ name: "",
74
+ prompt: "",
75
+ requiresApproval: false,
76
+ };
77
+ }
78
+
79
+ function parseDefinition(json: string): WorkflowDefinition | null {
80
+ try {
81
+ return JSON.parse(json) as WorkflowDefinition;
82
+ } catch {
83
+ return null;
84
+ }
85
+ }
86
+
87
+ function createParallelBranchStep(index: number): WorkflowStep {
88
+ return {
89
+ id: crypto.randomUUID(),
90
+ name: `Research Branch ${index}`,
91
+ prompt: "",
92
+ };
93
+ }
94
+
95
+ function createParallelSynthesisStep(branchIds: string[]): WorkflowStep {
96
+ return {
97
+ id: crypto.randomUUID(),
98
+ name: "Synthesize findings",
99
+ prompt: "",
100
+ dependsOn: branchIds,
101
+ };
102
+ }
103
+
104
+ function isSynthesisStep(step: WorkflowStep): boolean {
105
+ return !!step.dependsOn?.length;
106
+ }
107
+
108
+ function buildParallelSteps(
109
+ branches: WorkflowStep[],
110
+ synthesis?: WorkflowStep
111
+ ): WorkflowStep[] {
112
+ const normalizedBranches = branches.map((branch) => ({
113
+ ...branch,
114
+ requiresApproval: false,
115
+ dependsOn: undefined,
116
+ }));
117
+ const branchIds = normalizedBranches.map((branch) => branch.id);
118
+ const joinStep = synthesis
119
+ ? {
120
+ ...synthesis,
121
+ requiresApproval: false,
122
+ dependsOn: branchIds,
123
+ }
124
+ : createParallelSynthesisStep(branchIds);
125
+
126
+ return [...normalizedBranches, joinStep];
127
+ }
128
+
129
+ function createDefaultParallelSteps(): WorkflowStep[] {
130
+ return buildParallelSteps(
131
+ Array.from({ length: MIN_PARALLEL_BRANCHES }, (_, index) =>
132
+ createParallelBranchStep(index + 1)
133
+ )
134
+ );
135
+ }
136
+
137
+ function normalizeParallelSteps(
138
+ input: WorkflowStep[],
139
+ options?: { cloneIds?: boolean }
140
+ ): WorkflowStep[] {
141
+ const rawBranches = input.filter((step) => !isSynthesisStep(step)).slice(
142
+ 0,
143
+ MAX_PARALLEL_BRANCHES
144
+ );
145
+ const rawSynthesis = input.find(isSynthesisStep);
146
+
147
+ const branches = [...rawBranches];
148
+ while (branches.length < MIN_PARALLEL_BRANCHES) {
149
+ branches.push(createParallelBranchStep(branches.length + 1));
150
+ }
151
+
152
+ const normalizedBranches = branches.map((branch, index) => ({
153
+ ...branch,
154
+ id: options?.cloneIds ? crypto.randomUUID() : branch.id,
155
+ name: branch.name || `Research Branch ${index + 1}`,
156
+ }));
157
+
158
+ const normalizedSynthesis = rawSynthesis
159
+ ? {
160
+ ...rawSynthesis,
161
+ id: options?.cloneIds ? crypto.randomUUID() : rawSynthesis.id,
162
+ name: rawSynthesis.name || "Synthesize findings",
163
+ }
164
+ : undefined;
165
+
166
+ return buildParallelSteps(normalizedBranches, normalizedSynthesis);
167
+ }
168
+
169
+ function getParallelParts(steps: WorkflowStep[]) {
170
+ return {
171
+ branchSteps: steps.filter((step) => !isSynthesisStep(step)),
172
+ synthesisStep: steps.find(isSynthesisStep) ?? null,
173
+ };
174
+ }
175
+
176
+ function createSwarmMayorStep(): WorkflowStep {
177
+ return {
178
+ id: crypto.randomUUID(),
179
+ name: "Mayor plan",
180
+ prompt:
181
+ "Break the goal into a concise swarm plan. Assign a distinct focus area to each worker by name, call out dependencies or overlap risks, and define what the refinery should merge at the end.",
182
+ };
183
+ }
184
+
185
+ function createSwarmWorkerStep(index: number): WorkflowStep {
186
+ return {
187
+ id: crypto.randomUUID(),
188
+ name: `Worker ${index}`,
189
+ prompt:
190
+ "Own one slice of the mayor plan. Produce concrete findings, decisions, or deliverables the refinery can merge with sibling worker output.",
191
+ };
192
+ }
193
+
194
+ function createSwarmRefineryStep(): WorkflowStep {
195
+ return {
196
+ id: crypto.randomUUID(),
197
+ name: "Refine and merge",
198
+ prompt:
199
+ "Merge the mayor plan and worker outputs into one final result. Resolve overlaps, call out conflicts, and produce the final deliverable with a short rationale.",
200
+ };
201
+ }
202
+
203
+ function buildSwarmSteps(
204
+ mayorStep: WorkflowStep,
205
+ workerSteps: WorkflowStep[],
206
+ refineryStep: WorkflowStep
207
+ ): WorkflowStep[] {
208
+ return [
209
+ {
210
+ ...mayorStep,
211
+ requiresApproval: false,
212
+ dependsOn: undefined,
213
+ },
214
+ ...workerSteps.map((worker) => ({
215
+ ...worker,
216
+ requiresApproval: false,
217
+ dependsOn: undefined,
218
+ })),
219
+ {
220
+ ...refineryStep,
221
+ requiresApproval: false,
222
+ dependsOn: undefined,
223
+ },
224
+ ];
225
+ }
226
+
227
+ function createDefaultSwarmSteps(): WorkflowStep[] {
228
+ return buildSwarmSteps(
229
+ createSwarmMayorStep(),
230
+ Array.from({ length: MIN_SWARM_WORKERS }, (_, index) =>
231
+ createSwarmWorkerStep(index + 1)
232
+ ),
233
+ createSwarmRefineryStep()
234
+ );
235
+ }
236
+
237
+ function getSwarmParts(steps: WorkflowStep[]) {
238
+ return {
239
+ mayorStep: steps[0] ?? null,
240
+ workerSteps: steps.length > 2 ? steps.slice(1, -1) : [],
241
+ refineryStep: steps.length > 1 ? (steps.at(-1) ?? null) : null,
242
+ };
243
+ }
244
+
245
+ function normalizeSwarmSteps(
246
+ input: WorkflowStep[],
247
+ options?: { cloneIds?: boolean }
248
+ ): WorkflowStep[] {
249
+ const { mayorStep, workerSteps, refineryStep } = getSwarmParts(input);
250
+
251
+ const normalizedMayor = {
252
+ ...(mayorStep ?? createSwarmMayorStep()),
253
+ id:
254
+ options?.cloneIds && mayorStep
255
+ ? crypto.randomUUID()
256
+ : (mayorStep?.id ?? crypto.randomUUID()),
257
+ name: mayorStep?.name || "Mayor plan",
258
+ };
259
+
260
+ const nextWorkers = [...workerSteps].slice(0, MAX_SWARM_WORKERS);
261
+ while (nextWorkers.length < MIN_SWARM_WORKERS) {
262
+ nextWorkers.push(createSwarmWorkerStep(nextWorkers.length + 1));
263
+ }
264
+
265
+ const normalizedWorkers = nextWorkers.map((worker, index) => ({
266
+ ...worker,
267
+ id: options?.cloneIds ? crypto.randomUUID() : worker.id,
268
+ name: worker.name || `Worker ${index + 1}`,
269
+ }));
270
+
271
+ const normalizedRefinery = {
272
+ ...(refineryStep ?? createSwarmRefineryStep()),
273
+ id:
274
+ options?.cloneIds && refineryStep
275
+ ? crypto.randomUUID()
276
+ : (refineryStep?.id ?? crypto.randomUUID()),
277
+ name: refineryStep?.name || "Refine and merge",
278
+ };
279
+
280
+ return buildSwarmSteps(
281
+ normalizedMayor,
282
+ normalizedWorkers,
283
+ normalizedRefinery
284
+ );
285
+ }
286
+
287
+ const PATTERN_ICONS: Record<string, React.ReactNode> = {
288
+ sequence: <ArrowDown className="h-3.5 w-3.5 text-muted-foreground" />,
289
+ "planner-executor": <Brain className="h-3.5 w-3.5 text-muted-foreground" />,
290
+ checkpoint: <ShieldCheck className="h-3.5 w-3.5 text-muted-foreground" />,
291
+ loop: <RefreshCw className="h-3.5 w-3.5 text-muted-foreground" />,
292
+ parallel: <GitBranch className="h-3.5 w-3.5 text-muted-foreground" />,
293
+ swarm: <Bot className="h-3.5 w-3.5 text-muted-foreground" />,
294
+ };
295
+
296
+ export function WorkflowFormView({
297
+ workflow,
298
+ projects,
299
+ profiles,
300
+ clone = false,
301
+ }: WorkflowFormViewProps) {
302
+ const runtimeOptions = listRuntimeCatalog();
303
+ const runtimeLabelMap = new Map(
304
+ runtimeOptions.map((runtime) => [runtime.id, runtime.label])
305
+ );
306
+ const router = useRouter();
307
+ const mode = workflow ? (clone ? "clone" : "edit") : "create";
308
+
309
+ const [name, setName] = useState("");
310
+ const [pattern, setPattern] = useState<WorkflowPattern>("sequence");
311
+ const [projectId, setProjectId] = useState("");
312
+ const [steps, setSteps] = useState<WorkflowStep[]>([createEmptyStep()]);
313
+ const [loading, setLoading] = useState(false);
314
+ const [error, setError] = useState<string | null>(null);
315
+
316
+ // Loop-specific state
317
+ const [loopPrompt, setLoopPrompt] = useState("");
318
+ const [maxIterations, setMaxIterations] = useState(5);
319
+ const [timeBudgetMinutes, setTimeBudgetMinutes] = useState<number | "">(
320
+ ""
321
+ );
322
+ const [loopAssignedAgent, setLoopAssignedAgent] = useState("");
323
+ const [loopAgentProfile, setLoopAgentProfile] = useState("");
324
+ const [swarmConcurrencyLimit, setSwarmConcurrencyLimit] = useState(
325
+ DEFAULT_SWARM_CONCURRENCY_LIMIT
326
+ );
327
+
328
+ // Pre-populate form for edit/clone
329
+ useEffect(() => {
330
+ if (!workflow) return;
331
+
332
+ const def = parseDefinition(workflow.definition);
333
+ setName(clone ? `${workflow.name} (Copy)` : workflow.name);
334
+ setProjectId(workflow.projectId ?? "");
335
+
336
+ if (def) {
337
+ setPattern(def.pattern);
338
+ if (def.pattern === "loop") {
339
+ setLoopPrompt(def.steps[0]?.prompt ?? "");
340
+ setMaxIterations(def.loopConfig?.maxIterations ?? 5);
341
+ setTimeBudgetMinutes(
342
+ def.loopConfig?.timeBudgetMs
343
+ ? def.loopConfig.timeBudgetMs / 60000
344
+ : ""
345
+ );
346
+ setLoopAssignedAgent(def.loopConfig?.assignedAgent ?? "");
347
+ setLoopAgentProfile(def.loopConfig?.agentProfile ?? "");
348
+ } else {
349
+ if (def.pattern === "swarm") {
350
+ setSwarmConcurrencyLimit(
351
+ def.swarmConfig?.workerConcurrencyLimit ??
352
+ DEFAULT_SWARM_CONCURRENCY_LIMIT
353
+ );
354
+ }
355
+
356
+ setSteps(
357
+ clone
358
+ ? def.pattern === "parallel"
359
+ ? normalizeParallelSteps(def.steps, { cloneIds: true })
360
+ : def.pattern === "swarm"
361
+ ? normalizeSwarmSteps(def.steps, { cloneIds: true })
362
+ : def.steps.map((s) => ({ ...s, id: crypto.randomUUID() }))
363
+ : def.pattern === "parallel"
364
+ ? normalizeParallelSteps(def.steps)
365
+ : def.pattern === "swarm"
366
+ ? normalizeSwarmSteps(def.steps)
367
+ : def.steps
368
+ );
369
+ }
370
+ }
371
+ }, [workflow, clone]);
372
+
373
+ useEffect(() => {
374
+ if (pattern !== "parallel") {
375
+ return;
376
+ }
377
+
378
+ setSteps((prev) => {
379
+ const { branchSteps, synthesisStep } = getParallelParts(prev);
380
+ const hasValidShape =
381
+ branchSteps.length >= MIN_PARALLEL_BRANCHES && synthesisStep !== null;
382
+
383
+ return hasValidShape ? buildParallelSteps(branchSteps, synthesisStep) : createDefaultParallelSteps();
384
+ });
385
+ }, [pattern]);
386
+
387
+ useEffect(() => {
388
+ if (pattern !== "swarm") {
389
+ return;
390
+ }
391
+
392
+ setSteps((prev) => {
393
+ const { mayorStep, workerSteps, refineryStep } = getSwarmParts(prev);
394
+ const hasValidShape =
395
+ mayorStep !== null &&
396
+ refineryStep !== null &&
397
+ workerSteps.length >= MIN_SWARM_WORKERS;
398
+
399
+ return hasValidShape
400
+ ? buildSwarmSteps(mayorStep, workerSteps, refineryStep)
401
+ : createDefaultSwarmSteps();
402
+ });
403
+ }, [pattern]);
404
+
405
+ useEffect(() => {
406
+ if (pattern !== "swarm") {
407
+ return;
408
+ }
409
+
410
+ setSwarmConcurrencyLimit((prev) =>
411
+ Math.min(Math.max(prev, 1), Math.max(getSwarmParts(steps).workerSteps.length, 1))
412
+ );
413
+ }, [pattern, steps]);
414
+
415
+ function addStep() {
416
+ setSteps((prev) => {
417
+ if (pattern === "parallel") {
418
+ const { branchSteps, synthesisStep } = getParallelParts(prev);
419
+ if (branchSteps.length >= MAX_PARALLEL_BRANCHES) {
420
+ return prev;
421
+ }
422
+
423
+ return buildParallelSteps(
424
+ [...branchSteps, createParallelBranchStep(branchSteps.length + 1)],
425
+ synthesisStep ?? undefined
426
+ );
427
+ }
428
+
429
+ if (pattern === "swarm") {
430
+ const { mayorStep, workerSteps, refineryStep } = getSwarmParts(prev);
431
+ if (
432
+ !mayorStep ||
433
+ !refineryStep ||
434
+ workerSteps.length >= MAX_SWARM_WORKERS
435
+ ) {
436
+ return prev;
437
+ }
438
+
439
+ return buildSwarmSteps(
440
+ mayorStep,
441
+ [...workerSteps, createSwarmWorkerStep(workerSteps.length + 1)],
442
+ refineryStep
443
+ );
444
+ }
445
+
446
+ return [...prev, createEmptyStep()];
447
+ });
448
+ }
449
+
450
+ function removeStep(index: number) {
451
+ setSteps((prev) => {
452
+ if (pattern === "parallel") {
453
+ const { branchSteps, synthesisStep } = getParallelParts(prev);
454
+ if (index >= branchSteps.length || branchSteps.length <= MIN_PARALLEL_BRANCHES) {
455
+ return prev;
456
+ }
457
+
458
+ return buildParallelSteps(
459
+ branchSteps.filter((_, branchIndex) => branchIndex !== index),
460
+ synthesisStep ?? undefined
461
+ );
462
+ }
463
+
464
+ if (pattern === "swarm") {
465
+ const { mayorStep, workerSteps, refineryStep } = getSwarmParts(prev);
466
+ const workerIndex = index - 1;
467
+
468
+ if (
469
+ !mayorStep ||
470
+ !refineryStep ||
471
+ workerIndex < 0 ||
472
+ workerIndex >= workerSteps.length ||
473
+ workerSteps.length <= MIN_SWARM_WORKERS
474
+ ) {
475
+ return prev;
476
+ }
477
+
478
+ return buildSwarmSteps(
479
+ mayorStep,
480
+ workerSteps.filter((_, currentWorkerIndex) => currentWorkerIndex !== workerIndex),
481
+ refineryStep
482
+ );
483
+ }
484
+
485
+ if (prev.length <= 1) {
486
+ return prev;
487
+ }
488
+
489
+ return prev.filter((_, i) => i !== index);
490
+ });
491
+ }
492
+
493
+ function updateStep(index: number, updates: Partial<WorkflowStep>) {
494
+ setSteps((prev) => {
495
+ if (pattern === "parallel") {
496
+ const { branchSteps, synthesisStep } = getParallelParts(prev);
497
+
498
+ if (index < branchSteps.length) {
499
+ const nextBranches = branchSteps.map((step, branchIndex) =>
500
+ branchIndex === index
501
+ ? { ...step, ...updates, dependsOn: undefined, requiresApproval: false }
502
+ : step
503
+ );
504
+ return buildParallelSteps(nextBranches, synthesisStep ?? undefined);
505
+ }
506
+
507
+ if (synthesisStep) {
508
+ return buildParallelSteps(branchSteps, {
509
+ ...synthesisStep,
510
+ ...updates,
511
+ requiresApproval: false,
512
+ });
513
+ }
514
+ }
515
+
516
+ if (pattern === "swarm") {
517
+ const { mayorStep, workerSteps, refineryStep } = getSwarmParts(prev);
518
+ if (!mayorStep || !refineryStep) {
519
+ return createDefaultSwarmSteps();
520
+ }
521
+
522
+ if (index === 0) {
523
+ return buildSwarmSteps(
524
+ {
525
+ ...mayorStep,
526
+ ...updates,
527
+ dependsOn: undefined,
528
+ requiresApproval: false,
529
+ },
530
+ workerSteps,
531
+ refineryStep
532
+ );
533
+ }
534
+
535
+ if (index === workerSteps.length + 1) {
536
+ return buildSwarmSteps(mayorStep, workerSteps, {
537
+ ...refineryStep,
538
+ ...updates,
539
+ dependsOn: undefined,
540
+ requiresApproval: false,
541
+ });
542
+ }
543
+
544
+ const workerIndex = index - 1;
545
+ if (workerIndex >= 0 && workerIndex < workerSteps.length) {
546
+ return buildSwarmSteps(
547
+ mayorStep,
548
+ workerSteps.map((worker, currentWorkerIndex) =>
549
+ currentWorkerIndex === workerIndex
550
+ ? {
551
+ ...worker,
552
+ ...updates,
553
+ dependsOn: undefined,
554
+ requiresApproval: false,
555
+ }
556
+ : worker
557
+ ),
558
+ refineryStep
559
+ );
560
+ }
561
+ }
562
+
563
+ return prev.map((step, i) => (i === index ? { ...step, ...updates } : step));
564
+ });
565
+ }
566
+
567
+ function getProfileCompatibilityError(
568
+ profileId?: string,
569
+ runtimeId?: string
570
+ ): string | null {
571
+ if (!profileId) {
572
+ return null;
573
+ }
574
+
575
+ const profile = profiles.find((candidate) => candidate.id === profileId);
576
+ if (!profile) {
577
+ return `Profile "${profileId}" was not found`;
578
+ }
579
+
580
+ const selectedRuntimeId = (runtimeId ||
581
+ DEFAULT_AGENT_RUNTIME) as AgentRuntimeId;
582
+ if (profileSupportsRuntime(profile, selectedRuntimeId)) {
583
+ return null;
584
+ }
585
+
586
+ return `${profile.name} does not support ${
587
+ runtimeLabelMap.get(selectedRuntimeId) ?? selectedRuntimeId
588
+ }`;
589
+ }
590
+
591
+ async function handleSubmit(e: React.FormEvent) {
592
+ e.preventDefault();
593
+ if (!name.trim()) return;
594
+
595
+ const isLoop = pattern === "loop";
596
+ const isParallel = pattern === "parallel";
597
+ const isSwarm = pattern === "swarm";
598
+
599
+ if (isLoop) {
600
+ if (!loopPrompt.trim()) {
601
+ setError("Loop prompt is required");
602
+ return;
603
+ }
604
+ if (maxIterations < 1 || maxIterations > 100) {
605
+ setError("Max iterations must be between 1 and 100");
606
+ return;
607
+ }
608
+ const loopCompatibilityError = getProfileCompatibilityError(
609
+ loopAgentProfile || undefined,
610
+ loopAssignedAgent || undefined
611
+ );
612
+ if (loopCompatibilityError) {
613
+ setError(loopCompatibilityError);
614
+ return;
615
+ }
616
+ } else {
617
+ if (steps.some((s) => !s.name.trim() || !s.prompt.trim())) {
618
+ setError("All steps must have a name and prompt");
619
+ return;
620
+ }
621
+ for (const [index, step] of steps.entries()) {
622
+ const compatibilityError = getProfileCompatibilityError(
623
+ step.agentProfile,
624
+ step.assignedAgent
625
+ );
626
+ if (compatibilityError) {
627
+ setError(`Step ${index + 1}: ${compatibilityError}`);
628
+ return;
629
+ }
630
+ }
631
+
632
+ if (isParallel) {
633
+ const { branchSteps } = getParallelParts(steps);
634
+ if (branchSteps.length < MIN_PARALLEL_BRANCHES) {
635
+ setError(
636
+ `Parallel workflows require at least ${MIN_PARALLEL_BRANCHES} research branches`
637
+ );
638
+ return;
639
+ }
640
+ }
641
+
642
+ if (isSwarm) {
643
+ const { workerSteps } = getSwarmParts(steps);
644
+ if (workerSteps.length < MIN_SWARM_WORKERS) {
645
+ setError(
646
+ `Swarm workflows require at least ${MIN_SWARM_WORKERS} worker agents`
647
+ );
648
+ return;
649
+ }
650
+ }
651
+ }
652
+
653
+ setLoading(true);
654
+ setError(null);
655
+
656
+ try {
657
+ const normalizedSwarmSteps = isSwarm ? normalizeSwarmSteps(steps) : null;
658
+ const definition: WorkflowDefinition = isLoop
659
+ ? {
660
+ pattern,
661
+ steps: [
662
+ {
663
+ id: crypto.randomUUID(),
664
+ name: "Loop",
665
+ prompt: loopPrompt.trim(),
666
+ },
667
+ ],
668
+ loopConfig: {
669
+ maxIterations,
670
+ ...(timeBudgetMinutes
671
+ ? { timeBudgetMs: Number(timeBudgetMinutes) * 60 * 1000 }
672
+ : {}),
673
+ ...(loopAssignedAgent ? { assignedAgent: loopAssignedAgent } : {}),
674
+ ...(loopAgentProfile ? { agentProfile: loopAgentProfile }
675
+ : {}),
676
+ },
677
+ }
678
+ : {
679
+ pattern,
680
+ steps: isParallel
681
+ ? normalizeParallelSteps(steps)
682
+ : isSwarm
683
+ ? (normalizedSwarmSteps ?? normalizeSwarmSteps(steps))
684
+ : steps,
685
+ ...(isSwarm
686
+ ? {
687
+ swarmConfig: {
688
+ workerConcurrencyLimit: Math.max(
689
+ 1,
690
+ Math.min(
691
+ swarmConcurrencyLimit,
692
+ getSwarmParts(
693
+ normalizedSwarmSteps ?? normalizeSwarmSteps(steps)
694
+ ).workerSteps.length
695
+ )
696
+ ),
697
+ },
698
+ }
699
+ : {}),
700
+ };
701
+
702
+ const definitionError = validateWorkflowDefinition(definition);
703
+ if (definitionError) {
704
+ setError(definitionError);
705
+ setLoading(false);
706
+ return;
707
+ }
708
+
709
+ const isEdit = mode === "edit" && workflow;
710
+
711
+ const url = isEdit
712
+ ? `/api/workflows/${workflow.id}`
713
+ : "/api/workflows";
714
+
715
+ const res = await fetch(url, {
716
+ method: isEdit ? "PATCH" : "POST",
717
+ headers: { "Content-Type": "application/json" },
718
+ body: JSON.stringify({
719
+ name: name.trim(),
720
+ projectId: projectId || undefined,
721
+ definition,
722
+ }),
723
+ });
724
+
725
+ if (res.ok) {
726
+ toast.success(
727
+ mode === "edit"
728
+ ? "Workflow updated"
729
+ : mode === "clone"
730
+ ? "Workflow cloned"
731
+ : "Workflow created"
732
+ );
733
+
734
+ if (isEdit) {
735
+ router.push(`/workflows/${workflow.id}`);
736
+ } else {
737
+ const data = await res.json().catch(() => null);
738
+ if (data?.id) {
739
+ router.push(`/workflows/${data.id}`);
740
+ } else {
741
+ router.push("/workflows");
742
+ }
743
+ }
744
+ } else {
745
+ const data = await res.json().catch(() => null);
746
+ setError(
747
+ data?.error ??
748
+ `Failed to ${mode === "edit" ? "update" : "create"} workflow (${res.status})`
749
+ );
750
+ }
751
+ } catch {
752
+ setError("Network error — could not reach server");
753
+ } finally {
754
+ setLoading(false);
755
+ }
756
+ }
757
+
758
+ const titles: Record<string, string> = {
759
+ create: "Create Workflow",
760
+ edit: "Edit Workflow",
761
+ clone: "Clone Workflow",
762
+ };
763
+
764
+ const submitLabels: Record<string, [string, string]> = {
765
+ create: ["Creating...", "Create Workflow"],
766
+ edit: ["Saving...", "Save Changes"],
767
+ clone: ["Cloning...", "Clone Workflow"],
768
+ };
769
+
770
+ const isLoop = pattern === "loop";
771
+ const isParallel = pattern === "parallel";
772
+ const isSwarm = pattern === "swarm";
773
+ const { branchSteps, synthesisStep } = getParallelParts(steps);
774
+ const {
775
+ mayorStep,
776
+ workerSteps: swarmWorkerSteps,
777
+ refineryStep,
778
+ } = getSwarmParts(steps);
779
+
780
+ function renderStepEditor(
781
+ step: WorkflowStep,
782
+ index: number,
783
+ options?: {
784
+ title: string;
785
+ icon?: typeof ListOrdered;
786
+ hint?: string;
787
+ removable?: boolean;
788
+ badgeLabel?: string;
789
+ }
790
+ ) {
791
+ return (
792
+ <FormSectionCard
793
+ key={step.id}
794
+ icon={options?.icon ?? ListOrdered}
795
+ title={options?.title ?? `Step ${index + 1}`}
796
+ hint={options?.hint}
797
+ >
798
+ <div className="space-y-3">
799
+ <div className="flex items-center gap-2">
800
+ <Badge variant="secondary" className="text-xs shrink-0">
801
+ {options?.badgeLabel ?? `#${index + 1}`}
802
+ </Badge>
803
+ <Input
804
+ value={step.name}
805
+ onChange={(e) => updateStep(index, { name: e.target.value })}
806
+ placeholder="Step name"
807
+ className="flex-1"
808
+ />
809
+ {options?.removable && (
810
+ <Button
811
+ type="button"
812
+ variant="ghost"
813
+ size="icon"
814
+ className="h-8 w-8 shrink-0"
815
+ onClick={() => removeStep(index)}
816
+ aria-label={`Remove step ${index + 1}`}
817
+ >
818
+ <Trash2 className="h-3 w-3" />
819
+ </Button>
820
+ )}
821
+ </div>
822
+ <div className="space-y-1.5">
823
+ <Textarea
824
+ value={step.prompt}
825
+ onChange={(e) => updateStep(index, { prompt: e.target.value })}
826
+ placeholder="Instructions for the agent"
827
+ rows={3}
828
+ />
829
+ <p className="text-xs text-muted-foreground">Agent prompt for this step</p>
830
+ </div>
831
+ <div className="flex flex-col gap-3 md:flex-row md:items-start">
832
+ {profiles.length > 0 && (
833
+ <div className="flex-1">
834
+ <Select
835
+ value={step.agentProfile || "auto"}
836
+ onValueChange={(v) =>
837
+ updateStep(index, {
838
+ agentProfile: v === "auto" ? undefined : v,
839
+ })
840
+ }
841
+ >
842
+ <SelectTrigger className="h-8 text-xs">
843
+ <SelectValue placeholder="Profile: Auto-detect" />
844
+ </SelectTrigger>
845
+ <SelectContent>
846
+ <SelectItem value="auto">Auto-detect</SelectItem>
847
+ {profiles.map((p) => (
848
+ <SelectItem
849
+ key={p.id}
850
+ value={p.id}
851
+ disabled={
852
+ !profileSupportsRuntime(
853
+ p,
854
+ step.assignedAgent || DEFAULT_AGENT_RUNTIME
855
+ )
856
+ }
857
+ >
858
+ {p.name}
859
+ </SelectItem>
860
+ ))}
861
+ </SelectContent>
862
+ </Select>
863
+ {step.agentProfile &&
864
+ getProfileCompatibilityError(
865
+ step.agentProfile,
866
+ step.assignedAgent
867
+ ) && (
868
+ <p className="mt-1 text-xs text-destructive">
869
+ {getProfileCompatibilityError(
870
+ step.agentProfile,
871
+ step.assignedAgent
872
+ )}
873
+ </p>
874
+ )}
875
+ </div>
876
+ )}
877
+ <div className="flex-1">
878
+ <Select
879
+ value={step.assignedAgent || "default"}
880
+ onValueChange={(value) =>
881
+ updateStep(index, {
882
+ assignedAgent: value === "default" ? undefined : value,
883
+ })
884
+ }
885
+ >
886
+ <SelectTrigger className="h-8 text-xs">
887
+ <SelectValue placeholder="Runtime: Default" />
888
+ </SelectTrigger>
889
+ <SelectContent>
890
+ <SelectItem value="default">Default runtime</SelectItem>
891
+ {runtimeOptions.map((runtime) => (
892
+ <SelectItem key={runtime.id} value={runtime.id}>
893
+ {runtime.label}
894
+ </SelectItem>
895
+ ))}
896
+ </SelectContent>
897
+ </Select>
898
+ </div>
899
+ {pattern === "checkpoint" && (
900
+ <div className="flex items-center gap-2 pt-1">
901
+ <Switch
902
+ id={`approval-${step.id}`}
903
+ checked={step.requiresApproval ?? false}
904
+ onCheckedChange={(checked) =>
905
+ updateStep(index, {
906
+ requiresApproval: checked,
907
+ })
908
+ }
909
+ />
910
+ <Label htmlFor={`approval-${step.id}`} className="text-xs">
911
+ Requires approval
912
+ </Label>
913
+ </div>
914
+ )}
915
+ </div>
916
+ </div>
917
+ </FormSectionCard>
918
+ );
919
+ }
920
+
921
+ return (
922
+ <div className="space-y-4">
923
+ <h2 className="text-xl font-semibold">{titles[mode]}</h2>
924
+
925
+ <form onSubmit={handleSubmit}>
926
+ <div className="grid grid-cols-1 lg:grid-cols-[1fr_2fr] gap-4">
927
+ {/* Left: Config sidebar */}
928
+ <div className="space-y-4">
929
+ <FormSectionCard icon={GitBranch} title="Workflow Identity">
930
+ <div className="space-y-3">
931
+ <div className="space-y-1.5">
932
+ <Label htmlFor="wf-name">Name</Label>
933
+ <Input
934
+ id="wf-name"
935
+ value={name}
936
+ onChange={(e) => setName(e.target.value)}
937
+ placeholder="Workflow name"
938
+ required
939
+ />
940
+ <p className="text-xs text-muted-foreground">Short descriptive name</p>
941
+ </div>
942
+ <div className="space-y-1.5">
943
+ <Label>Pattern</Label>
944
+ <Select
945
+ value={pattern}
946
+ onValueChange={(value) =>
947
+ setPattern(value as WorkflowPattern)
948
+ }
949
+ disabled={mode === "edit"}
950
+ >
951
+ <SelectTrigger>
952
+ <SelectValue />
953
+ </SelectTrigger>
954
+ <SelectContent>
955
+ <SelectItem value="sequence">
956
+ <span className="flex items-center gap-1.5">
957
+ {PATTERN_ICONS.sequence}
958
+ Sequence
959
+ </span>
960
+ </SelectItem>
961
+ <SelectItem value="planner-executor">
962
+ <span className="flex items-center gap-1.5">
963
+ {PATTERN_ICONS["planner-executor"]}
964
+ Planner → Executor
965
+ </span>
966
+ </SelectItem>
967
+ <SelectItem value="checkpoint">
968
+ <span className="flex items-center gap-1.5">
969
+ {PATTERN_ICONS.checkpoint}
970
+ Checkpoint
971
+ </span>
972
+ </SelectItem>
973
+ <SelectItem value="loop">
974
+ <span className="flex items-center gap-1.5">
975
+ {PATTERN_ICONS.loop}
976
+ Autonomous Loop
977
+ </span>
978
+ </SelectItem>
979
+ <SelectItem value="parallel">
980
+ <span className="flex items-center gap-1.5">
981
+ {PATTERN_ICONS.parallel}
982
+ Parallel Research
983
+ </span>
984
+ </SelectItem>
985
+ <SelectItem value="swarm">
986
+ <span className="flex items-center gap-1.5">
987
+ {PATTERN_ICONS.swarm}
988
+ Multi-Agent Swarm
989
+ </span>
990
+ </SelectItem>
991
+ </SelectContent>
992
+ </Select>
993
+ <p className="text-xs text-muted-foreground">How steps execute</p>
994
+ </div>
995
+ {projects.length > 0 && (
996
+ <div className="space-y-1.5">
997
+ <Label>Project</Label>
998
+ <Select
999
+ value={projectId || "none"}
1000
+ onValueChange={(value) =>
1001
+ setProjectId(value === "none" ? "" : value)
1002
+ }
1003
+ >
1004
+ <SelectTrigger>
1005
+ <SelectValue placeholder="None" />
1006
+ </SelectTrigger>
1007
+ <SelectContent>
1008
+ <SelectItem value="none">None</SelectItem>
1009
+ {projects.map((p) => (
1010
+ <SelectItem key={p.id} value={p.id}>
1011
+ {p.name}
1012
+ </SelectItem>
1013
+ ))}
1014
+ </SelectContent>
1015
+ </Select>
1016
+ <p className="text-xs text-muted-foreground">Associates working directory</p>
1017
+ </div>
1018
+ )}
1019
+ </div>
1020
+ </FormSectionCard>
1021
+
1022
+ {isLoop && (
1023
+ <FormSectionCard icon={RefreshCw} title="Loop Config">
1024
+ <div className="space-y-4">
1025
+ <div className="space-y-2">
1026
+ <div className="flex items-center justify-between">
1027
+ <Label>Max Iterations</Label>
1028
+ <Badge variant="secondary" className="tabular-nums text-xs">
1029
+ {maxIterations}
1030
+ </Badge>
1031
+ </div>
1032
+ <Slider
1033
+ min={1}
1034
+ max={100}
1035
+ step={1}
1036
+ value={[maxIterations]}
1037
+ onValueChange={([v]) => setMaxIterations(v)}
1038
+ />
1039
+ <p className="text-xs text-muted-foreground">Safety limit for loops</p>
1040
+ </div>
1041
+ <div className="space-y-2">
1042
+ <div className="flex items-center justify-between">
1043
+ <Label>Time Budget (min)</Label>
1044
+ <Badge variant="secondary" className="tabular-nums text-xs">
1045
+ {timeBudgetMinutes || "None"}
1046
+ </Badge>
1047
+ </div>
1048
+ <Slider
1049
+ min={0}
1050
+ max={120}
1051
+ step={1}
1052
+ value={[typeof timeBudgetMinutes === "number" ? timeBudgetMinutes : 0]}
1053
+ onValueChange={([v]) => setTimeBudgetMinutes(v === 0 ? "" : v)}
1054
+ />
1055
+ <p className="text-xs text-muted-foreground">Optional time cap (0 = no limit)</p>
1056
+ </div>
1057
+ {profiles.length > 0 && (
1058
+ <div className="space-y-1.5">
1059
+ <Label>Agent Profile</Label>
1060
+ <Select
1061
+ value={loopAgentProfile || "auto"}
1062
+ onValueChange={(value) =>
1063
+ setLoopAgentProfile(value === "auto" ? "" : value)
1064
+ }
1065
+ >
1066
+ <SelectTrigger>
1067
+ <SelectValue placeholder="Auto-detect" />
1068
+ </SelectTrigger>
1069
+ <SelectContent>
1070
+ <SelectItem value="auto">Auto-detect</SelectItem>
1071
+ {profiles.map((p) => (
1072
+ <SelectItem
1073
+ key={p.id}
1074
+ value={p.id}
1075
+ disabled={
1076
+ !profileSupportsRuntime(
1077
+ p,
1078
+ loopAssignedAgent || DEFAULT_AGENT_RUNTIME
1079
+ )
1080
+ }
1081
+ >
1082
+ {p.name}
1083
+ </SelectItem>
1084
+ ))}
1085
+ </SelectContent>
1086
+ </Select>
1087
+ <p className="text-xs text-muted-foreground">Which agent to use per iteration</p>
1088
+ {loopAgentProfile &&
1089
+ getProfileCompatibilityError(
1090
+ loopAgentProfile,
1091
+ loopAssignedAgent || undefined
1092
+ ) && (
1093
+ <p className="text-xs text-destructive">
1094
+ {getProfileCompatibilityError(
1095
+ loopAgentProfile,
1096
+ loopAssignedAgent || undefined
1097
+ )}
1098
+ </p>
1099
+ )}
1100
+ </div>
1101
+ )}
1102
+ <div className="space-y-1.5">
1103
+ <Label>Runtime</Label>
1104
+ <Select
1105
+ value={loopAssignedAgent || "default"}
1106
+ onValueChange={(value) =>
1107
+ setLoopAssignedAgent(value === "default" ? "" : value)
1108
+ }
1109
+ >
1110
+ <SelectTrigger>
1111
+ <SelectValue placeholder="Default runtime" />
1112
+ </SelectTrigger>
1113
+ <SelectContent>
1114
+ <SelectItem value="default">Default runtime</SelectItem>
1115
+ {runtimeOptions.map((runtime) => (
1116
+ <SelectItem key={runtime.id} value={runtime.id}>
1117
+ {runtime.label}
1118
+ </SelectItem>
1119
+ ))}
1120
+ </SelectContent>
1121
+ </Select>
1122
+ <p className="text-xs text-muted-foreground">
1123
+ Which provider runtime to use per iteration
1124
+ </p>
1125
+ </div>
1126
+ </div>
1127
+ </FormSectionCard>
1128
+ )}
1129
+
1130
+ {isSwarm && (
1131
+ <FormSectionCard
1132
+ icon={Bot}
1133
+ title="Swarm Config"
1134
+ hint="Mayor runs first, workers fan out in parallel, then the refinery merges the results."
1135
+ >
1136
+ <div className="space-y-2">
1137
+ <div className="flex items-center justify-between">
1138
+ <Label>Worker Concurrency</Label>
1139
+ <Badge variant="secondary" className="tabular-nums text-xs">
1140
+ {Math.max(
1141
+ 1,
1142
+ Math.min(
1143
+ swarmConcurrencyLimit,
1144
+ Math.max(swarmWorkerSteps.length, 1)
1145
+ )
1146
+ )}
1147
+ </Badge>
1148
+ </div>
1149
+ <Slider
1150
+ min={1}
1151
+ max={Math.max(swarmWorkerSteps.length, 1)}
1152
+ step={1}
1153
+ value={[
1154
+ Math.max(
1155
+ 1,
1156
+ Math.min(
1157
+ swarmConcurrencyLimit,
1158
+ Math.max(swarmWorkerSteps.length, 1)
1159
+ )
1160
+ ),
1161
+ ]}
1162
+ onValueChange={([value]) => setSwarmConcurrencyLimit(value)}
1163
+ />
1164
+ <p className="text-xs text-muted-foreground">
1165
+ How many workers can run at once
1166
+ </p>
1167
+ </div>
1168
+ </FormSectionCard>
1169
+ )}
1170
+
1171
+ {!isLoop && (
1172
+ <FormSectionCard
1173
+ icon={isParallel ? GitBranch : isSwarm ? Bot : ListOrdered}
1174
+ title={
1175
+ isParallel
1176
+ ? "Parallel Overview"
1177
+ : isSwarm
1178
+ ? "Swarm Overview"
1179
+ : "Step Overview"
1180
+ }
1181
+ hint={
1182
+ isParallel
1183
+ ? "Launch 2-5 research branches, then merge them in one synthesis step."
1184
+ : isSwarm
1185
+ ? "Run one mayor, 2-5 workers, and one refinery step on the existing workflow engine."
1186
+ : undefined
1187
+ }
1188
+ >
1189
+ <div className="space-y-2">
1190
+ <div className="flex items-center justify-between">
1191
+ <Badge variant="secondary" className="text-xs">
1192
+ {isParallel
1193
+ ? `${branchSteps.length} branch${branchSteps.length === 1 ? "" : "es"}`
1194
+ : isSwarm
1195
+ ? `${swarmWorkerSteps.length} worker${swarmWorkerSteps.length === 1 ? "" : "s"}`
1196
+ : `${steps.length} step${steps.length === 1 ? "" : "s"}`}
1197
+ </Badge>
1198
+ <Button
1199
+ type="button"
1200
+ variant="outline"
1201
+ size="sm"
1202
+ onClick={addStep}
1203
+ disabled={
1204
+ (isParallel && branchSteps.length >= MAX_PARALLEL_BRANCHES) ||
1205
+ (isSwarm && swarmWorkerSteps.length >= MAX_SWARM_WORKERS)
1206
+ }
1207
+ >
1208
+ <Plus className="h-3 w-3 mr-1" />
1209
+ {isParallel ? "Add Branch" : isSwarm ? "Add Worker" : "Add Step"}
1210
+ </Button>
1211
+ </div>
1212
+ <div className="space-y-1">
1213
+ {isParallel ? (
1214
+ <>
1215
+ {branchSteps.map((step, i) => (
1216
+ <p
1217
+ key={step.id}
1218
+ className="text-xs text-muted-foreground truncate"
1219
+ >
1220
+ Branch {i + 1}: {step.name || "(unnamed)"}
1221
+ </p>
1222
+ ))}
1223
+ <p className="text-xs text-muted-foreground truncate">
1224
+ Join: {synthesisStep?.name || "(unnamed synthesis step)"}
1225
+ </p>
1226
+ </>
1227
+ ) : isSwarm ? (
1228
+ <>
1229
+ <p className="text-xs text-muted-foreground truncate">
1230
+ Mayor: {mayorStep?.name || "(unnamed mayor step)"}
1231
+ </p>
1232
+ {swarmWorkerSteps.map((step, i) => (
1233
+ <p
1234
+ key={step.id}
1235
+ className="text-xs text-muted-foreground truncate"
1236
+ >
1237
+ Worker {i + 1}: {step.name || "(unnamed)"}
1238
+ </p>
1239
+ ))}
1240
+ <p className="text-xs text-muted-foreground truncate">
1241
+ Refinery: {refineryStep?.name || "(unnamed refinery step)"}
1242
+ </p>
1243
+ </>
1244
+ ) : (
1245
+ steps.map((step, i) => (
1246
+ <p
1247
+ key={step.id}
1248
+ className="text-xs text-muted-foreground truncate"
1249
+ >
1250
+ #{i + 1} {step.name || "(unnamed)"}
1251
+ </p>
1252
+ ))
1253
+ )}
1254
+ </div>
1255
+ </div>
1256
+ </FormSectionCard>
1257
+ )}
1258
+ </div>
1259
+
1260
+ {/* Right: Main content */}
1261
+ <div className="space-y-4">
1262
+ {isLoop ? (
1263
+ <FormSectionCard icon={MessageSquare} title="Loop Prompt">
1264
+ <div className="space-y-1.5">
1265
+ <Textarea
1266
+ id="loop-prompt"
1267
+ value={loopPrompt}
1268
+ onChange={(e) => setLoopPrompt(e.target.value)}
1269
+ placeholder="The prompt the agent will iterate on..."
1270
+ rows={8}
1271
+ required
1272
+ />
1273
+ <p className="text-xs text-muted-foreground">
1274
+ Each iteration receives previous output as context
1275
+ </p>
1276
+ </div>
1277
+ </FormSectionCard>
1278
+ ) : isParallel ? (
1279
+ <>
1280
+ <FormSectionCard
1281
+ icon={GitBranch}
1282
+ title="Research Branches"
1283
+ hint="Each branch runs independently before Stagent unlocks the join step."
1284
+ >
1285
+ <div className="space-y-4">
1286
+ {branchSteps.map((step, index) =>
1287
+ renderStepEditor(step, index, {
1288
+ title: `Branch ${index + 1}`,
1289
+ icon: GitBranch,
1290
+ removable: branchSteps.length > MIN_PARALLEL_BRANCHES,
1291
+ badgeLabel: `B${index + 1}`,
1292
+ })
1293
+ )}
1294
+ </div>
1295
+ </FormSectionCard>
1296
+
1297
+ {synthesisStep &&
1298
+ renderStepEditor(synthesisStep, branchSteps.length, {
1299
+ title: "Synthesis Step",
1300
+ icon: MessageSquare,
1301
+ hint: "This step receives labeled outputs from every branch as context.",
1302
+ badgeLabel: "JOIN",
1303
+ })}
1304
+ </>
1305
+ ) : isSwarm ? (
1306
+ <>
1307
+ {mayorStep &&
1308
+ renderStepEditor(mayorStep, 0, {
1309
+ title: "Mayor",
1310
+ icon: Brain,
1311
+ hint: "Plans the swarm, assigns each worker a lane, and defines the merge objective.",
1312
+ badgeLabel: "MAYOR",
1313
+ })}
1314
+
1315
+ <FormSectionCard
1316
+ icon={Bot}
1317
+ title="Worker Agents"
1318
+ hint="Workers run in parallel after the mayor step completes."
1319
+ >
1320
+ <div className="space-y-4">
1321
+ {swarmWorkerSteps.map((step, index) =>
1322
+ renderStepEditor(step, index + 1, {
1323
+ title: `Worker ${index + 1}`,
1324
+ icon: Bot,
1325
+ removable:
1326
+ swarmWorkerSteps.length > MIN_SWARM_WORKERS,
1327
+ badgeLabel: `W${index + 1}`,
1328
+ })
1329
+ )}
1330
+ </div>
1331
+ </FormSectionCard>
1332
+
1333
+ {refineryStep &&
1334
+ renderStepEditor(
1335
+ refineryStep,
1336
+ swarmWorkerSteps.length + 1,
1337
+ {
1338
+ title: "Refinery",
1339
+ icon: MessageSquare,
1340
+ hint: "Merges the mayor plan and completed worker outputs into one final result.",
1341
+ badgeLabel: "REFINERY",
1342
+ }
1343
+ )}
1344
+ </>
1345
+ ) : (
1346
+ steps.map((step, index) =>
1347
+ renderStepEditor(step, index, {
1348
+ title: `Step ${index + 1}`,
1349
+ removable: steps.length > 1,
1350
+ })
1351
+ )
1352
+ )}
1353
+
1354
+ {error && <p className="text-sm text-destructive">{error}</p>}
1355
+
1356
+ <div className="flex items-center gap-3 pt-2">
1357
+ <Button
1358
+ type="submit"
1359
+ disabled={loading || !name.trim()}
1360
+ >
1361
+ {loading ? submitLabels[mode][0] : submitLabels[mode][1]}
1362
+ </Button>
1363
+ <Button
1364
+ type="button"
1365
+ variant="outline"
1366
+ onClick={() => router.back()}
1367
+ >
1368
+ Cancel
1369
+ </Button>
1370
+ </div>
1371
+ </div>
1372
+ </div>
1373
+ </form>
1374
+ </div>
1375
+ );
1376
+ }