stagent 0.9.6 → 0.11.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 (396) hide show
  1. package/README.md +20 -44
  2. package/dist/cli.js +66 -18
  3. package/docs/.coverage-gaps.json +144 -56
  4. package/docs/.last-generated +1 -1
  5. package/docs/features/agent-intelligence.md +12 -2
  6. package/docs/features/chat.md +40 -5
  7. package/docs/features/cost-usage.md +1 -1
  8. package/docs/features/documents.md +5 -2
  9. package/docs/features/inbox-notifications.md +10 -2
  10. package/docs/features/keyboard-navigation.md +12 -3
  11. package/docs/features/provider-runtimes.md +20 -2
  12. package/docs/features/schedules.md +32 -4
  13. package/docs/features/settings.md +28 -5
  14. package/docs/features/shared-components.md +7 -3
  15. package/docs/features/tables.md +11 -2
  16. package/docs/features/tool-permissions.md +6 -2
  17. package/docs/features/workflows.md +14 -4
  18. package/docs/index.md +1 -1
  19. package/docs/journeys/developer.md +39 -2
  20. package/docs/journeys/personal-use.md +32 -8
  21. package/docs/journeys/power-user.md +45 -14
  22. package/docs/journeys/work-use.md +17 -8
  23. package/docs/manifest.json +15 -15
  24. package/docs/superpowers/plans/2026-04-07-instance-bootstrap.md +1691 -0
  25. package/docs/superpowers/plans/2026-04-08-schedule-orchestration.md +2983 -0
  26. package/docs/superpowers/plans/2026-04-11-schedule-maxturns-api-control.md +551 -0
  27. package/docs/superpowers/plans/2026-04-11-task-create-profile-validation.md +864 -0
  28. package/docs/superpowers/plans/2026-04-11-task-runtime-stagent-mcp-injection.md +739 -0
  29. package/docs/superpowers/plans/2026-04-14-chat-command-namespace-refactor.md +1390 -0
  30. package/docs/superpowers/plans/2026-04-14-chat-environment-integration.md +1561 -0
  31. package/docs/superpowers/plans/2026-04-14-chat-polish-bundle-v1.md +1219 -0
  32. package/docs/superpowers/plans/2026-04-14-chat-session-persistence-provider-closeout.md +399 -0
  33. package/docs/superpowers/specs/2026-04-08-chat-sse-resilience-hotfix-design.md +201 -0
  34. package/docs/superpowers/specs/2026-04-08-schedule-orchestration-design.md +371 -0
  35. package/docs/superpowers/specs/2026-04-08-swarm-visibility-design.md +213 -0
  36. package/next.config.mjs +1 -0
  37. package/package.json +3 -2
  38. package/src/__tests__/instrumentation-smoke.test.ts +15 -0
  39. package/src/app/analytics/page.tsx +1 -21
  40. package/src/app/api/chat/conversations/[id]/messages/route.ts +22 -1
  41. package/src/app/api/chat/conversations/[id]/skills/__tests__/activate.test.ts +141 -0
  42. package/src/app/api/chat/conversations/[id]/skills/activate/route.ts +74 -0
  43. package/src/app/api/chat/conversations/[id]/skills/deactivate/route.ts +33 -0
  44. package/src/app/api/chat/export/route.ts +52 -0
  45. package/src/app/api/chat/files/search/route.ts +50 -0
  46. package/src/app/api/diagnostics/chat-streams/route.ts +65 -0
  47. package/src/app/api/environment/rescan-if-stale/__tests__/route.test.ts +45 -0
  48. package/src/app/api/environment/rescan-if-stale/route.ts +23 -0
  49. package/src/app/api/environment/skills/route.ts +13 -0
  50. package/src/app/api/instance/config/route.ts +41 -0
  51. package/src/app/api/instance/init/route.ts +34 -0
  52. package/src/app/api/instance/upgrade/check/route.ts +26 -0
  53. package/src/app/api/instance/upgrade/route.ts +96 -0
  54. package/src/app/api/instance/upgrade/status/route.ts +35 -0
  55. package/src/app/api/memory/route.ts +0 -11
  56. package/src/app/api/notifications/route.ts +4 -2
  57. package/src/app/api/projects/[id]/route.ts +5 -155
  58. package/src/app/api/projects/__tests__/delete-project.test.ts +10 -19
  59. package/src/app/api/schedules/[id]/execute/route.ts +111 -0
  60. package/src/app/api/schedules/[id]/route.ts +9 -1
  61. package/src/app/api/schedules/__tests__/execute-route.test.ts +118 -0
  62. package/src/app/api/schedules/route.ts +3 -12
  63. package/src/app/api/settings/chat/pins/route.ts +94 -0
  64. package/src/app/api/settings/chat/saved-searches/__tests__/route.test.ts +119 -0
  65. package/src/app/api/settings/chat/saved-searches/route.ts +79 -0
  66. package/src/app/api/settings/environment/route.ts +26 -0
  67. package/src/app/api/settings/openai/login/route.ts +22 -0
  68. package/src/app/api/settings/openai/logout/route.ts +7 -0
  69. package/src/app/api/settings/openai/route.ts +21 -1
  70. package/src/app/api/settings/providers/route.ts +35 -8
  71. package/src/app/api/tables/[id]/enrich/__tests__/route.test.ts +153 -0
  72. package/src/app/api/tables/[id]/enrich/plan/route.ts +98 -0
  73. package/src/app/api/tables/[id]/enrich/route.ts +147 -0
  74. package/src/app/api/tables/[id]/enrich/runs/route.ts +25 -0
  75. package/src/app/api/tasks/[id]/execute/route.ts +52 -33
  76. package/src/app/api/tasks/[id]/respond/route.ts +31 -15
  77. package/src/app/api/tasks/[id]/resume/route.ts +24 -3
  78. package/src/app/api/workflows/[id]/resume/route.ts +59 -0
  79. package/src/app/api/workflows/[id]/status/route.ts +22 -8
  80. package/src/app/api/workspace/context/route.ts +2 -0
  81. package/src/app/api/workspace/fix-data-dir/route.ts +81 -0
  82. package/src/app/chat/page.tsx +11 -0
  83. package/src/app/documents/page.tsx +4 -1
  84. package/src/app/inbox/page.tsx +12 -5
  85. package/src/app/layout.tsx +42 -21
  86. package/src/app/page.tsx +0 -2
  87. package/src/app/settings/page.tsx +8 -9
  88. package/src/components/chat/__tests__/capability-banner.test.tsx +38 -0
  89. package/src/components/chat/__tests__/chat-session-provider.test.tsx +573 -0
  90. package/src/components/chat/__tests__/skill-row.test.tsx +91 -0
  91. package/src/components/chat/capability-banner.tsx +68 -0
  92. package/src/components/chat/chat-command-popover.tsx +670 -49
  93. package/src/components/chat/chat-input.tsx +104 -10
  94. package/src/components/chat/chat-message.tsx +12 -3
  95. package/src/components/chat/chat-session-provider.tsx +790 -0
  96. package/src/components/chat/chat-shell.tsx +151 -401
  97. package/src/components/chat/command-tab-bar.tsx +68 -0
  98. package/src/components/chat/conversation-template-picker.tsx +421 -0
  99. package/src/components/chat/help-dialog.tsx +39 -0
  100. package/src/components/chat/skill-composition-conflict-dialog.tsx +96 -0
  101. package/src/components/chat/skill-row.tsx +147 -0
  102. package/src/components/documents/document-browser.tsx +37 -19
  103. package/src/components/instance/__tests__/instance-section.test.tsx +125 -0
  104. package/src/components/instance/instance-section.tsx +382 -0
  105. package/src/components/instance/upgrade-badge.tsx +219 -0
  106. package/src/components/notifications/__tests__/batch-proposal-review.test.tsx +95 -0
  107. package/src/components/notifications/__tests__/notification-item.test.tsx +106 -0
  108. package/src/components/notifications/__tests__/permission-response-actions.test.tsx +70 -0
  109. package/src/components/notifications/batch-proposal-review.tsx +20 -5
  110. package/src/components/notifications/inbox-list.tsx +11 -2
  111. package/src/components/notifications/notification-item.tsx +56 -2
  112. package/src/components/notifications/pending-approval-host.tsx +56 -37
  113. package/src/components/notifications/permission-response-actions.tsx +155 -1
  114. package/src/components/schedules/schedule-create-sheet.tsx +19 -1
  115. package/src/components/schedules/schedule-edit-sheet.tsx +20 -1
  116. package/src/components/schedules/schedule-form.tsx +31 -0
  117. package/src/components/settings/__tests__/providers-runtimes-section.test.tsx +149 -0
  118. package/src/components/settings/auth-method-selector.tsx +19 -4
  119. package/src/components/settings/auth-status-badge.tsx +28 -3
  120. package/src/components/settings/environment-section.tsx +102 -0
  121. package/src/components/settings/openai-chatgpt-auth-control.tsx +278 -0
  122. package/src/components/settings/openai-runtime-section.tsx +7 -1
  123. package/src/components/settings/providers-runtimes-section.tsx +138 -19
  124. package/src/components/shared/__tests__/filter-hint.test.tsx +40 -0
  125. package/src/components/shared/__tests__/saved-searches-manager.test.tsx +147 -0
  126. package/src/components/shared/app-sidebar.tsx +4 -3
  127. package/src/components/shared/command-palette.tsx +266 -7
  128. package/src/components/shared/filter-hint.tsx +70 -0
  129. package/src/components/shared/filter-input.tsx +59 -0
  130. package/src/components/shared/saved-searches-manager.tsx +199 -0
  131. package/src/components/shared/theme-toggle.tsx +5 -24
  132. package/src/components/shared/workspace-indicator.tsx +61 -2
  133. package/src/components/tables/__tests__/table-enrichment-sheet.test.tsx +130 -0
  134. package/src/components/tables/table-create-sheet.tsx +4 -0
  135. package/src/components/tables/table-enrichment-runs.tsx +103 -0
  136. package/src/components/tables/table-enrichment-sheet.tsx +538 -0
  137. package/src/components/tables/table-spreadsheet.tsx +29 -5
  138. package/src/components/tables/table-toolbar.tsx +10 -1
  139. package/src/components/tasks/kanban-board.tsx +1 -0
  140. package/src/components/tasks/kanban-column.tsx +53 -14
  141. package/src/components/tasks/task-bento-grid.tsx +31 -2
  142. package/src/components/tasks/task-card.tsx +29 -3
  143. package/src/components/tasks/task-chip-bar.tsx +54 -1
  144. package/src/components/tasks/task-result-renderer.tsx +1 -1
  145. package/src/components/workflows/delay-step-body.tsx +109 -0
  146. package/src/components/workflows/hooks/use-workflow-status.ts +50 -0
  147. package/src/components/workflows/loop-status-view.tsx +1 -1
  148. package/src/components/workflows/shared/step-result.tsx +78 -0
  149. package/src/components/workflows/shared/workflow-header.tsx +141 -0
  150. package/src/components/workflows/shared/workflow-loading-skeleton.tsx +36 -0
  151. package/src/components/workflows/swarm-dashboard.tsx +2 -15
  152. package/src/components/workflows/views/loop-pattern-view.tsx +137 -0
  153. package/src/components/workflows/views/sequence-pattern-view.tsx +511 -0
  154. package/src/components/workflows/workflow-form-view.tsx +133 -16
  155. package/src/components/workflows/workflow-status-view.tsx +30 -740
  156. package/src/hooks/__tests__/use-chat-autocomplete-tabs.test.ts +47 -0
  157. package/src/hooks/__tests__/use-saved-searches.test.ts +70 -0
  158. package/src/hooks/use-active-skills.ts +110 -0
  159. package/src/hooks/use-chat-autocomplete.ts +120 -7
  160. package/src/hooks/use-enriched-skills.ts +19 -0
  161. package/src/hooks/use-pinned-entries.ts +104 -0
  162. package/src/hooks/use-recent-user-messages.ts +19 -0
  163. package/src/hooks/use-saved-searches.ts +142 -0
  164. package/src/instrumentation-node.ts +94 -0
  165. package/src/instrumentation.ts +4 -48
  166. package/src/lib/agents/__tests__/claude-agent-sdk-options.test.ts +56 -0
  167. package/src/lib/agents/__tests__/claude-agent.test.ts +212 -0
  168. package/src/lib/agents/__tests__/execution-manager.test.ts +1 -27
  169. package/src/lib/agents/__tests__/failure-reason.test.ts +68 -0
  170. package/src/lib/agents/__tests__/learned-context.test.ts +0 -11
  171. package/src/lib/agents/__tests__/learning-session.test.ts +158 -0
  172. package/src/lib/agents/__tests__/pattern-extractor.test.ts +48 -0
  173. package/src/lib/agents/__tests__/task-dispatch.test.ts +166 -0
  174. package/src/lib/agents/__tests__/tool-permissions.test.ts +60 -0
  175. package/src/lib/agents/claude-agent.ts +217 -21
  176. package/src/lib/agents/execution-manager.ts +0 -35
  177. package/src/lib/agents/handoff/bus.ts +2 -2
  178. package/src/lib/agents/learned-context.ts +0 -12
  179. package/src/lib/agents/learning-session.ts +18 -5
  180. package/src/lib/agents/profiles/__tests__/list-fused-profiles.test.ts +110 -0
  181. package/src/lib/agents/profiles/__tests__/registry.test.ts +53 -4
  182. package/src/lib/agents/profiles/builtins/upgrade-assistant/SKILL.md +97 -0
  183. package/src/lib/agents/profiles/builtins/upgrade-assistant/profile.yaml +36 -0
  184. package/src/lib/agents/profiles/list-fused-profiles.ts +104 -0
  185. package/src/lib/agents/profiles/registry.ts +18 -0
  186. package/src/lib/agents/profiles/types.ts +7 -1
  187. package/src/lib/agents/router.ts +3 -6
  188. package/src/lib/agents/runtime/__tests__/catalog.test.ts +130 -0
  189. package/src/lib/agents/runtime/__tests__/execution-target.test.ts +183 -0
  190. package/src/lib/agents/runtime/__tests__/openai-codex-auth.test.ts +118 -0
  191. package/src/lib/agents/runtime/anthropic-direct.ts +8 -0
  192. package/src/lib/agents/runtime/catalog.ts +121 -0
  193. package/src/lib/agents/runtime/claude-sdk.ts +32 -0
  194. package/src/lib/agents/runtime/codex-app-server-client.ts +11 -5
  195. package/src/lib/agents/runtime/execution-target.ts +456 -0
  196. package/src/lib/agents/runtime/index.ts +4 -0
  197. package/src/lib/agents/runtime/launch-failure.ts +101 -0
  198. package/src/lib/agents/runtime/openai-codex-auth.ts +389 -0
  199. package/src/lib/agents/runtime/openai-codex.ts +64 -60
  200. package/src/lib/agents/runtime/openai-direct.ts +8 -0
  201. package/src/lib/agents/runtime/types.ts +8 -0
  202. package/src/lib/agents/task-dispatch.ts +220 -0
  203. package/src/lib/agents/tool-permissions.ts +16 -1
  204. package/src/lib/book/chapter-mapping.ts +11 -0
  205. package/src/lib/book/content.ts +10 -0
  206. package/src/lib/chat/__tests__/active-skill-injection.test.ts +261 -0
  207. package/src/lib/chat/__tests__/active-streams.test.ts +49 -0
  208. package/src/lib/chat/__tests__/clean-filter-input.test.ts +68 -0
  209. package/src/lib/chat/__tests__/command-tabs.test.ts +68 -0
  210. package/src/lib/chat/__tests__/context-builder-files.test.ts +112 -0
  211. package/src/lib/chat/__tests__/dismissals.test.ts +65 -0
  212. package/src/lib/chat/__tests__/engine-sdk-options.test.ts +117 -0
  213. package/src/lib/chat/__tests__/finalize-safety-net.test.ts +139 -0
  214. package/src/lib/chat/__tests__/reconcile.test.ts +137 -0
  215. package/src/lib/chat/__tests__/skill-conflict.test.ts +35 -0
  216. package/src/lib/chat/__tests__/stream-telemetry.test.ts +151 -0
  217. package/src/lib/chat/__tests__/types.test.ts +28 -0
  218. package/src/lib/chat/active-skills.ts +31 -0
  219. package/src/lib/chat/active-streams.ts +27 -0
  220. package/src/lib/chat/clean-filter-input.ts +30 -0
  221. package/src/lib/chat/codex-engine.ts +46 -24
  222. package/src/lib/chat/command-tabs.ts +61 -0
  223. package/src/lib/chat/context-builder.ts +146 -4
  224. package/src/lib/chat/dismissals.ts +73 -0
  225. package/src/lib/chat/engine.ts +159 -18
  226. package/src/lib/chat/files/__tests__/search.test.ts +135 -0
  227. package/src/lib/chat/files/expand-mention.ts +76 -0
  228. package/src/lib/chat/files/search.ts +99 -0
  229. package/src/lib/chat/reconcile.ts +117 -0
  230. package/src/lib/chat/skill-composition.ts +210 -0
  231. package/src/lib/chat/skill-conflict.ts +105 -0
  232. package/src/lib/chat/stagent-tools.ts +7 -19
  233. package/src/lib/chat/stream-telemetry.ts +137 -0
  234. package/src/lib/chat/suggested-prompts.ts +28 -1
  235. package/src/lib/chat/system-prompt.ts +48 -1
  236. package/src/lib/chat/tool-catalog.ts +35 -4
  237. package/src/lib/chat/tools/__tests__/enrich-table-tool.test.ts +127 -0
  238. package/src/lib/chat/tools/__tests__/profile-tools.test.ts +51 -0
  239. package/src/lib/chat/tools/__tests__/schedule-tools.test.ts +261 -0
  240. package/src/lib/chat/tools/__tests__/settings-tools.test.ts +294 -0
  241. package/src/lib/chat/tools/__tests__/skill-tools.test.ts +474 -0
  242. package/src/lib/chat/tools/__tests__/task-tools.test.ts +399 -0
  243. package/src/lib/chat/tools/__tests__/workflow-tools-dedup.test.ts +351 -0
  244. package/src/lib/chat/tools/blueprint-tools.ts +190 -0
  245. package/src/lib/chat/tools/document-tools.ts +29 -13
  246. package/src/lib/chat/tools/helpers.ts +41 -0
  247. package/src/lib/chat/tools/notification-tools.ts +9 -5
  248. package/src/lib/chat/tools/profile-tools.ts +120 -23
  249. package/src/lib/chat/tools/project-tools.ts +33 -0
  250. package/src/lib/chat/tools/schedule-tools.ts +44 -11
  251. package/src/lib/chat/tools/skill-tools.ts +183 -0
  252. package/src/lib/chat/tools/table-tools.ts +71 -0
  253. package/src/lib/chat/tools/task-tools.ts +89 -21
  254. package/src/lib/chat/tools/workflow-tools.ts +275 -32
  255. package/src/lib/chat/types.ts +15 -0
  256. package/src/lib/constants/settings.ts +10 -18
  257. package/src/lib/data/__tests__/clear.test.ts +56 -2
  258. package/src/lib/data/clear.ts +17 -16
  259. package/src/lib/data/delete-project.ts +171 -0
  260. package/src/lib/db/__tests__/bootstrap.test.ts +1 -1
  261. package/src/lib/db/bootstrap.ts +62 -16
  262. package/src/lib/db/index.ts +5 -0
  263. package/src/lib/db/migrations/0009_add_app_instances.sql +25 -0
  264. package/src/lib/db/migrations/0024_add_workflow_resume_at.sql +10 -0
  265. package/src/lib/db/migrations/0025_drop_app_instances.sql +3 -0
  266. package/src/lib/db/migrations/0026_drop_license.sql +3 -0
  267. package/src/lib/db/migrations/meta/_journal.json +21 -0
  268. package/src/lib/db/schema.ts +94 -23
  269. package/src/lib/environment/__tests__/auto-promote.test.ts +132 -0
  270. package/src/lib/environment/__tests__/list-skills-enriched.test.ts +55 -0
  271. package/src/lib/environment/__tests__/skill-enrichment.test.ts +129 -0
  272. package/src/lib/environment/__tests__/skill-recommendations.test.ts +87 -0
  273. package/src/lib/environment/data.ts +9 -0
  274. package/src/lib/environment/list-skills.ts +176 -0
  275. package/src/lib/environment/parsers/__tests__/skill.test.ts +54 -0
  276. package/src/lib/environment/parsers/skill.ts +26 -5
  277. package/src/lib/environment/profile-generator.ts +54 -0
  278. package/src/lib/environment/skill-enrichment.ts +106 -0
  279. package/src/lib/environment/skill-recommendations.ts +66 -0
  280. package/src/lib/environment/workspace-context.ts +13 -1
  281. package/src/lib/filters/__tests__/parse.quoted.test.ts +40 -0
  282. package/src/lib/filters/__tests__/parse.test.ts +135 -0
  283. package/src/lib/filters/parse.ts +86 -0
  284. package/src/lib/import/dedup.ts +4 -54
  285. package/src/lib/instance/__tests__/bootstrap.test.ts +362 -0
  286. package/src/lib/instance/__tests__/detect.test.ts +115 -0
  287. package/src/lib/instance/__tests__/fingerprint.test.ts +48 -0
  288. package/src/lib/instance/__tests__/git-ops.test.ts +95 -0
  289. package/src/lib/instance/__tests__/settings.test.ts +83 -0
  290. package/src/lib/instance/__tests__/upgrade-poller.test.ts +181 -0
  291. package/src/lib/instance/bootstrap.ts +270 -0
  292. package/src/lib/instance/detect.ts +49 -0
  293. package/src/lib/instance/fingerprint.ts +76 -0
  294. package/src/lib/instance/git-ops.ts +95 -0
  295. package/src/lib/instance/settings.ts +61 -0
  296. package/src/lib/instance/types.ts +77 -0
  297. package/src/lib/instance/upgrade-poller.ts +205 -0
  298. package/src/lib/notifications/__tests__/visibility.test.ts +51 -0
  299. package/src/lib/notifications/visibility.ts +33 -0
  300. package/src/lib/schedules/__tests__/collision-check.test.ts +93 -0
  301. package/src/lib/schedules/__tests__/config.test.ts +62 -0
  302. package/src/lib/schedules/__tests__/firing-metrics.test.ts +99 -0
  303. package/src/lib/schedules/__tests__/integration.test.ts +82 -0
  304. package/src/lib/schedules/__tests__/slot-claim.test.ts +242 -0
  305. package/src/lib/schedules/__tests__/tick-scheduler.test.ts +102 -0
  306. package/src/lib/schedules/__tests__/turn-budget.test.ts +228 -0
  307. package/src/lib/schedules/collision-check.ts +105 -0
  308. package/src/lib/schedules/config.ts +53 -0
  309. package/src/lib/schedules/scheduler.ts +236 -17
  310. package/src/lib/schedules/slot-claim.ts +105 -0
  311. package/src/lib/settings/__tests__/openai-auth.test.ts +101 -0
  312. package/src/lib/settings/__tests__/openai-login-manager.test.ts +64 -0
  313. package/src/lib/settings/__tests__/runtime-setup.test.ts +33 -0
  314. package/src/lib/settings/openai-auth.ts +105 -10
  315. package/src/lib/settings/openai-login-manager.ts +260 -0
  316. package/src/lib/settings/runtime-setup.ts +14 -4
  317. package/src/lib/tables/__tests__/enrichment-planner.test.ts +124 -0
  318. package/src/lib/tables/__tests__/enrichment.test.ts +147 -0
  319. package/src/lib/tables/enrichment-planner.ts +454 -0
  320. package/src/lib/tables/enrichment.ts +328 -0
  321. package/src/lib/tables/query-builder.ts +5 -2
  322. package/src/lib/tables/trigger-evaluator.ts +3 -2
  323. package/src/lib/theme.ts +71 -0
  324. package/src/lib/usage/ledger.ts +2 -18
  325. package/src/lib/util/__tests__/similarity.test.ts +106 -0
  326. package/src/lib/util/similarity.ts +77 -0
  327. package/src/lib/utils/format-timestamp.ts +24 -0
  328. package/src/lib/utils/stagent-paths.ts +12 -0
  329. package/src/lib/validators/__tests__/blueprint.test.ts +172 -0
  330. package/src/lib/validators/__tests__/settings.test.ts +10 -0
  331. package/src/lib/validators/blueprint.ts +70 -9
  332. package/src/lib/validators/profile.ts +2 -2
  333. package/src/lib/validators/settings.ts +3 -1
  334. package/src/lib/workflows/__tests__/delay.test.ts +196 -0
  335. package/src/lib/workflows/__tests__/engine.test.ts +8 -0
  336. package/src/lib/workflows/__tests__/loop-executor.test.ts +54 -0
  337. package/src/lib/workflows/__tests__/post-action.test.ts +108 -0
  338. package/src/lib/workflows/blueprints/__tests__/render-prompt.test.ts +124 -0
  339. package/src/lib/workflows/blueprints/instantiator.ts +22 -1
  340. package/src/lib/workflows/blueprints/render-prompt.ts +71 -0
  341. package/src/lib/workflows/blueprints/types.ts +16 -2
  342. package/src/lib/workflows/delay.ts +106 -0
  343. package/src/lib/workflows/engine.ts +212 -7
  344. package/src/lib/workflows/loop-executor.ts +349 -24
  345. package/src/lib/workflows/post-action.ts +91 -0
  346. package/src/lib/workflows/types.ts +166 -1
  347. package/src/test/setup.ts +10 -0
  348. package/src/app/api/license/checkout/route.ts +0 -28
  349. package/src/app/api/license/portal/route.ts +0 -26
  350. package/src/app/api/license/route.ts +0 -89
  351. package/src/app/api/license/usage/route.ts +0 -63
  352. package/src/app/api/marketplace/browse/route.ts +0 -15
  353. package/src/app/api/marketplace/import/route.ts +0 -28
  354. package/src/app/api/marketplace/publish/route.ts +0 -40
  355. package/src/app/api/onboarding/email/route.ts +0 -53
  356. package/src/app/api/settings/telemetry/route.ts +0 -14
  357. package/src/app/api/sync/export/route.ts +0 -54
  358. package/src/app/api/sync/restore/route.ts +0 -37
  359. package/src/app/api/sync/sessions/route.ts +0 -24
  360. package/src/app/auth/callback/route.ts +0 -73
  361. package/src/app/marketplace/page.tsx +0 -19
  362. package/src/components/analytics/analytics-gate-card.tsx +0 -101
  363. package/src/components/marketplace/blueprint-card.tsx +0 -61
  364. package/src/components/marketplace/marketplace-browser.tsx +0 -131
  365. package/src/components/onboarding/email-capture-card.tsx +0 -104
  366. package/src/components/settings/activation-form.tsx +0 -95
  367. package/src/components/settings/cloud-account-section.tsx +0 -147
  368. package/src/components/settings/cloud-sync-section.tsx +0 -155
  369. package/src/components/settings/subscription-section.tsx +0 -410
  370. package/src/components/settings/telemetry-section.tsx +0 -80
  371. package/src/components/shared/premium-gate-overlay.tsx +0 -50
  372. package/src/components/shared/schedule-gate-dialog.tsx +0 -64
  373. package/src/components/shared/upgrade-banner.tsx +0 -112
  374. package/src/hooks/use-supabase-auth.ts +0 -79
  375. package/src/lib/billing/email.ts +0 -54
  376. package/src/lib/billing/products.ts +0 -80
  377. package/src/lib/billing/stripe.ts +0 -101
  378. package/src/lib/cloud/supabase-browser.ts +0 -32
  379. package/src/lib/cloud/supabase-client.ts +0 -56
  380. package/src/lib/license/__tests__/features.test.ts +0 -56
  381. package/src/lib/license/__tests__/key-format.test.ts +0 -88
  382. package/src/lib/license/__tests__/manager.test.ts +0 -64
  383. package/src/lib/license/__tests__/tier-limits.test.ts +0 -79
  384. package/src/lib/license/cloud-validation.ts +0 -60
  385. package/src/lib/license/features.ts +0 -44
  386. package/src/lib/license/key-format.ts +0 -101
  387. package/src/lib/license/limit-check.ts +0 -111
  388. package/src/lib/license/limit-queries.ts +0 -51
  389. package/src/lib/license/manager.ts +0 -345
  390. package/src/lib/license/notifications.ts +0 -59
  391. package/src/lib/license/tier-limits.ts +0 -71
  392. package/src/lib/marketplace/marketplace-client.ts +0 -107
  393. package/src/lib/sync/cloud-sync.ts +0 -235
  394. package/src/lib/telemetry/conversion-events.ts +0 -71
  395. package/src/lib/telemetry/queue.ts +0 -122
  396. package/src/lib/validators/license.ts +0 -33
@@ -0,0 +1,153 @@
1
+ import { describe, it, expect, vi, beforeEach } from "vitest";
2
+
3
+ const { mockCreateEnrichmentWorkflow } = vi.hoisted(() => ({
4
+ mockCreateEnrichmentWorkflow: vi.fn(),
5
+ }));
6
+
7
+ vi.mock("@/lib/tables/enrichment", () => ({
8
+ createEnrichmentWorkflow: mockCreateEnrichmentWorkflow,
9
+ }));
10
+
11
+ import { POST } from "../route";
12
+
13
+ function makeRequest(body: unknown): Request {
14
+ return new Request("http://test/api/tables/tbl_x/enrich", {
15
+ method: "POST",
16
+ headers: { "content-type": "application/json" },
17
+ body: JSON.stringify(body),
18
+ });
19
+ }
20
+
21
+ const params = Promise.resolve({ id: "tbl_x" });
22
+
23
+ describe("POST /api/tables/[id]/enrich", () => {
24
+ beforeEach(() => {
25
+ mockCreateEnrichmentWorkflow.mockReset();
26
+ });
27
+
28
+ it("rejects requests with missing required fields (400)", async () => {
29
+ const res = await POST(makeRequest({}) as never, {
30
+ params,
31
+ });
32
+ expect(res.status).toBe(400);
33
+ expect(mockCreateEnrichmentWorkflow).not.toHaveBeenCalled();
34
+ });
35
+
36
+ it("returns 202 with workflowId and rowCount on success", async () => {
37
+ mockCreateEnrichmentWorkflow.mockResolvedValueOnce({
38
+ workflowId: "wf_123",
39
+ rowCount: 3,
40
+ });
41
+
42
+ const res = await POST(
43
+ makeRequest({
44
+ prompt: "Find LinkedIn for {{row.name}}",
45
+ targetColumn: "linkedin",
46
+ }) as never,
47
+ { params }
48
+ );
49
+
50
+ expect(res.status).toBe(202);
51
+ const json = (await res.json()) as { workflowId: string; rowCount: number };
52
+ expect(json.workflowId).toBe("wf_123");
53
+ expect(json.rowCount).toBe(3);
54
+ expect(mockCreateEnrichmentWorkflow).toHaveBeenCalledWith(
55
+ "tbl_x",
56
+ expect.objectContaining({
57
+ prompt: "Find LinkedIn for {{row.name}}",
58
+ targetColumn: "linkedin",
59
+ })
60
+ );
61
+ });
62
+
63
+ it("caps batchSize to 200 before delegating", async () => {
64
+ mockCreateEnrichmentWorkflow.mockResolvedValueOnce({
65
+ workflowId: "wf_456",
66
+ rowCount: 200,
67
+ });
68
+
69
+ await POST(
70
+ makeRequest({
71
+ prompt: "Enrich {{row.name}}",
72
+ targetColumn: "linkedin",
73
+ batchSize: 5000,
74
+ }) as never,
75
+ { params }
76
+ );
77
+
78
+ const callArg = mockCreateEnrichmentWorkflow.mock.calls[0][1] as {
79
+ batchSize: number;
80
+ };
81
+ expect(callArg.batchSize).toBe(200);
82
+ });
83
+
84
+ it("rejects batchSize less than 1", async () => {
85
+ const res = await POST(
86
+ makeRequest({
87
+ prompt: "x",
88
+ targetColumn: "linkedin",
89
+ batchSize: 0,
90
+ }) as never,
91
+ { params }
92
+ );
93
+ expect(res.status).toBe(400);
94
+ expect(mockCreateEnrichmentWorkflow).not.toHaveBeenCalled();
95
+ });
96
+
97
+ it("returns 404 when the table is missing", async () => {
98
+ mockCreateEnrichmentWorkflow.mockRejectedValueOnce(
99
+ new Error("Table tbl_x not found")
100
+ );
101
+
102
+ const res = await POST(
103
+ makeRequest({
104
+ prompt: "x",
105
+ targetColumn: "linkedin",
106
+ }) as never,
107
+ { params }
108
+ );
109
+ expect(res.status).toBe(404);
110
+ });
111
+
112
+ it("returns 400 when the column does not exist on the table", async () => {
113
+ mockCreateEnrichmentWorkflow.mockRejectedValueOnce(
114
+ new Error('Column "ghost" does not exist on table tbl_x')
115
+ );
116
+
117
+ const res = await POST(
118
+ makeRequest({
119
+ prompt: "x",
120
+ targetColumn: "ghost",
121
+ }) as never,
122
+ { params }
123
+ );
124
+ expect(res.status).toBe(400);
125
+ });
126
+
127
+ it("forwards filter, agentProfile, and projectId to the generator", async () => {
128
+ mockCreateEnrichmentWorkflow.mockResolvedValueOnce({
129
+ workflowId: "wf_789",
130
+ rowCount: 1,
131
+ });
132
+
133
+ await POST(
134
+ makeRequest({
135
+ prompt: "x",
136
+ targetColumn: "linkedin",
137
+ filter: { column: "linkedin", operator: "is_empty" },
138
+ agentProfile: "researcher",
139
+ projectId: "proj_1",
140
+ }) as never,
141
+ { params }
142
+ );
143
+
144
+ const callArg = mockCreateEnrichmentWorkflow.mock.calls[0][1] as {
145
+ filter: unknown;
146
+ agentProfile: string;
147
+ projectId: string;
148
+ };
149
+ expect(callArg.filter).toEqual({ column: "linkedin", operator: "is_empty" });
150
+ expect(callArg.agentProfile).toBe("researcher");
151
+ expect(callArg.projectId).toBe("proj_1");
152
+ });
153
+ });
@@ -0,0 +1,98 @@
1
+ import { NextRequest, NextResponse } from "next/server";
2
+ import { z } from "zod";
3
+ import { previewEnrichmentPlan } from "@/lib/tables/enrichment";
4
+
5
+ const MAX_BATCH_SIZE = 200;
6
+
7
+ const filterSchema = z.object({
8
+ column: z.string().min(1),
9
+ operator: z.enum([
10
+ "eq",
11
+ "neq",
12
+ "gt",
13
+ "gte",
14
+ "lt",
15
+ "lte",
16
+ "contains",
17
+ "starts_with",
18
+ "in",
19
+ "is_empty",
20
+ "is_not_empty",
21
+ ]),
22
+ value: z
23
+ .union([z.string(), z.number(), z.boolean(), z.array(z.string())])
24
+ .optional(),
25
+ });
26
+
27
+ const previewRequestSchema = z.object({
28
+ targetColumn: z.string().min(1).max(128),
29
+ promptMode: z.enum(["auto", "custom"]).optional(),
30
+ prompt: z.string().min(1).max(8192).optional(),
31
+ filter: filterSchema.optional(),
32
+ agentProfile: z.string().min(1).max(128).optional(),
33
+ agentProfileOverride: z.string().min(1).max(128).optional(),
34
+ batchSize: z.number().int().min(1).optional(),
35
+ }).superRefine((value, ctx) => {
36
+ const mode = value.promptMode ?? (value.prompt ? "custom" : "auto");
37
+ if (mode === "custom" && !value.prompt?.trim()) {
38
+ ctx.addIssue({
39
+ code: z.ZodIssueCode.custom,
40
+ path: ["prompt"],
41
+ message: "Custom enrichment requires a prompt",
42
+ });
43
+ }
44
+ });
45
+
46
+ export async function POST(
47
+ req: NextRequest,
48
+ { params }: { params: Promise<{ id: string }> }
49
+ ) {
50
+ const { id } = await params;
51
+
52
+ let body: unknown;
53
+ try {
54
+ body = await req.json();
55
+ } catch {
56
+ return NextResponse.json({ error: "Invalid JSON body" }, { status: 400 });
57
+ }
58
+
59
+ const parsed = previewRequestSchema.safeParse(body);
60
+ if (!parsed.success) {
61
+ return NextResponse.json(
62
+ {
63
+ error: parsed.error.issues.map((issue) => ({
64
+ path: issue.path.join("."),
65
+ message: issue.message,
66
+ })),
67
+ },
68
+ { status: 400 }
69
+ );
70
+ }
71
+
72
+ const { batchSize, ...rest } = parsed.data;
73
+ const cappedBatchSize =
74
+ batchSize !== undefined ? Math.min(batchSize, MAX_BATCH_SIZE) : undefined;
75
+
76
+ try {
77
+ const preview = await previewEnrichmentPlan(id, {
78
+ ...rest,
79
+ batchSize: cappedBatchSize,
80
+ });
81
+ return NextResponse.json(preview);
82
+ } catch (err) {
83
+ const message = err instanceof Error ? err.message : String(err);
84
+
85
+ if (/not found/i.test(message)) {
86
+ return NextResponse.json({ error: message }, { status: 404 });
87
+ }
88
+ if (/does not exist|unsupported/i.test(message)) {
89
+ return NextResponse.json({ error: message }, { status: 400 });
90
+ }
91
+
92
+ console.error("[tables/enrich/plan] POST error:", err);
93
+ return NextResponse.json(
94
+ { error: "Failed to build enrichment plan" },
95
+ { status: 500 }
96
+ );
97
+ }
98
+ }
@@ -0,0 +1,147 @@
1
+ import { NextRequest, NextResponse } from "next/server";
2
+ import { z } from "zod";
3
+ import { createEnrichmentWorkflow } from "@/lib/tables/enrichment";
4
+
5
+ const MAX_BATCH_SIZE = 200;
6
+
7
+ const filterSchema = z.object({
8
+ column: z.string().min(1),
9
+ operator: z.enum([
10
+ "eq",
11
+ "neq",
12
+ "gt",
13
+ "gte",
14
+ "lt",
15
+ "lte",
16
+ "contains",
17
+ "starts_with",
18
+ "in",
19
+ "is_empty",
20
+ "is_not_empty",
21
+ ]),
22
+ value: z
23
+ .union([z.string(), z.number(), z.boolean(), z.array(z.string())])
24
+ .optional(),
25
+ });
26
+
27
+ const enrichRequestSchema = z.object({
28
+ targetColumn: z.string().min(1).max(128),
29
+ promptMode: z.enum(["auto", "custom"]).optional(),
30
+ prompt: z.string().min(1).max(8192).optional(),
31
+ filter: filterSchema.optional(),
32
+ agentProfile: z.string().min(1).max(128).optional(),
33
+ agentProfileOverride: z.string().min(1).max(128).optional(),
34
+ projectId: z.string().nullable().optional(),
35
+ // Reject non-positive ints; the upper bound is *clamped* in the handler so
36
+ // callers asking for too much get a working (smaller) batch instead of a 400.
37
+ batchSize: z.number().int().min(1).optional(),
38
+ itemVariable: z.string().min(1).max(64).optional(),
39
+ workflowName: z.string().min(1).max(256).optional(),
40
+ plan: z
41
+ .object({
42
+ promptMode: z.enum(["auto", "custom"]),
43
+ strategy: z.enum([
44
+ "single-pass-lookup",
45
+ "single-pass-classify",
46
+ "research-and-synthesize",
47
+ ]),
48
+ agentProfile: z.string().min(1),
49
+ reasoning: z.string(),
50
+ steps: z.array(
51
+ z.object({
52
+ id: z.string().min(1),
53
+ name: z.string().min(1),
54
+ purpose: z.string().min(1),
55
+ prompt: z.string().min(1),
56
+ agentProfile: z.string().min(1).optional(),
57
+ })
58
+ ),
59
+ targetContract: z.object({
60
+ columnName: z.string().min(1),
61
+ columnLabel: z.string().min(1),
62
+ dataType: z.enum(["text", "number", "boolean", "select", "url", "email"]),
63
+ allowedOptions: z.array(z.string()).optional(),
64
+ }),
65
+ eligibleRowCount: z.number().int().min(0),
66
+ sampleBindings: z.array(z.record(z.string(), z.unknown())),
67
+ })
68
+ .optional(),
69
+ }).superRefine((value, ctx) => {
70
+ const hasPlan = Boolean(value.plan);
71
+ const mode = value.promptMode ?? (value.prompt ? "custom" : "auto");
72
+ if (!hasPlan && mode === "custom" && !value.prompt?.trim()) {
73
+ ctx.addIssue({
74
+ code: z.ZodIssueCode.custom,
75
+ path: ["prompt"],
76
+ message: "Custom enrichment requires a prompt",
77
+ });
78
+ }
79
+ });
80
+
81
+ /**
82
+ * POST /api/tables/[id]/enrich
83
+ *
84
+ * Kicks off a row-driven enrichment workflow for a user table. The workflow
85
+ * runs fire-and-forget (TDR-001); the response includes the workflow id and
86
+ * the number of rows that will actually be processed (already-populated rows
87
+ * are filtered out for idempotency).
88
+ *
89
+ * Status codes:
90
+ * - 202: workflow created and queued
91
+ * - 400: invalid body, unknown column, or other validation failure
92
+ * - 404: table not found
93
+ * - 500: unexpected error
94
+ */
95
+ export async function POST(
96
+ req: NextRequest,
97
+ { params }: { params: Promise<{ id: string }> }
98
+ ) {
99
+ const { id } = await params;
100
+
101
+ let body: unknown;
102
+ try {
103
+ body = await req.json();
104
+ } catch {
105
+ return NextResponse.json({ error: "Invalid JSON body" }, { status: 400 });
106
+ }
107
+
108
+ const parsed = enrichRequestSchema.safeParse(body);
109
+ if (!parsed.success) {
110
+ return NextResponse.json(
111
+ {
112
+ error: parsed.error.issues.map((issue) => ({
113
+ path: issue.path.join("."),
114
+ message: issue.message,
115
+ })),
116
+ },
117
+ { status: 400 }
118
+ );
119
+ }
120
+
121
+ const { batchSize, ...rest } = parsed.data;
122
+ const cappedBatchSize =
123
+ batchSize !== undefined ? Math.min(batchSize, MAX_BATCH_SIZE) : undefined;
124
+
125
+ try {
126
+ const result = await createEnrichmentWorkflow(id, {
127
+ ...rest,
128
+ batchSize: cappedBatchSize,
129
+ });
130
+ return NextResponse.json(result, { status: 202 });
131
+ } catch (err) {
132
+ const message = err instanceof Error ? err.message : String(err);
133
+
134
+ if (/not found/i.test(message)) {
135
+ return NextResponse.json({ error: message }, { status: 404 });
136
+ }
137
+ if (/does not exist|unsupported/i.test(message)) {
138
+ return NextResponse.json({ error: message }, { status: 400 });
139
+ }
140
+
141
+ console.error("[tables/enrich] POST error:", err);
142
+ return NextResponse.json(
143
+ { error: "Failed to start enrichment workflow" },
144
+ { status: 500 }
145
+ );
146
+ }
147
+ }
@@ -0,0 +1,25 @@
1
+ import { NextRequest, NextResponse } from "next/server";
2
+ import { listRecentEnrichmentRuns } from "@/lib/tables/enrichment";
3
+
4
+ export async function GET(
5
+ req: NextRequest,
6
+ { params }: { params: Promise<{ id: string }> }
7
+ ) {
8
+ const { id } = await params;
9
+ const searchParams = req.nextUrl.searchParams;
10
+ const rawLimit = Number(searchParams.get("limit") ?? "5");
11
+ const limit = Number.isFinite(rawLimit)
12
+ ? Math.min(Math.max(Math.trunc(rawLimit), 1), 10)
13
+ : 5;
14
+
15
+ try {
16
+ const runs = await listRecentEnrichmentRuns(id, limit);
17
+ return NextResponse.json(runs);
18
+ } catch (err) {
19
+ console.error("[tables/enrich/runs] GET error:", err);
20
+ return NextResponse.json(
21
+ { error: "Failed to load recent enrichment runs" },
22
+ { status: 500 }
23
+ );
24
+ }
25
+ }
@@ -2,16 +2,15 @@ import { NextRequest, NextResponse } from "next/server";
2
2
  import { db } from "@/lib/db";
3
3
  import { tasks, projects } from "@/lib/db/schema";
4
4
  import { eq, and } from "drizzle-orm";
5
- import { executeTaskWithAgent, classifyTaskProfile } from "@/lib/agents/router";
6
- import { DEFAULT_AGENT_RUNTIME } from "@/lib/agents/runtime/catalog";
5
+ import { classifyTaskProfile } from "@/lib/agents/router";
7
6
  import { validateRuntimeProfileAssignment } from "@/lib/agents/profiles/assignment-validation";
8
7
  import {
9
8
  BudgetLimitExceededError,
10
9
  enforceTaskBudgetGuardrails,
11
10
  } from "@/lib/settings/budget-guardrails";
12
11
  import { ensureFreshScan } from "@/lib/environment/auto-scan";
13
- import { getAllExecutions } from "@/lib/agents/execution-manager";
14
- import { licenseManager } from "@/lib/license/manager";
12
+ import { resolveTaskExecutionTarget } from "@/lib/agents/runtime/execution-target";
13
+ import { startTaskExecution } from "@/lib/agents/task-dispatch";
15
14
 
16
15
  export async function POST(
17
16
  _req: NextRequest,
@@ -49,6 +48,7 @@ export async function POST(
49
48
  }
50
49
 
51
50
  const task = claimed[0];
51
+ let taskProfile = task.agentProfile;
52
52
 
53
53
  // Auto-scan environment if the task's project has a workingDirectory
54
54
  if (task.projectId) {
@@ -61,26 +61,64 @@ export async function POST(
61
61
  }
62
62
  }
63
63
 
64
- // Auto-classify profile if none was set
65
- if (!task.agentProfile) {
64
+ let executionTarget;
65
+ try {
66
+ executionTarget = await resolveTaskExecutionTarget({
67
+ title: task.title,
68
+ description: task.description,
69
+ requestedRuntimeId: task.assignedAgent,
70
+ profileId: taskProfile,
71
+ });
72
+ } catch (error) {
73
+ const message = error instanceof Error ? error.message : String(error);
74
+ db.update(tasks)
75
+ .set({
76
+ status: "failed",
77
+ result: message,
78
+ updatedAt: new Date(),
79
+ })
80
+ .where(eq(tasks.id, id))
81
+ .run();
82
+ return NextResponse.json({ error: message }, { status: 400 });
83
+ }
84
+
85
+ // Auto-classify profile if none was set. Use the resolved runtime so the
86
+ // chosen profile is compatible with the runtime we will actually launch.
87
+ if (!taskProfile) {
66
88
  const autoProfile = classifyTaskProfile(
67
89
  task.title,
68
90
  task.description,
69
- task.assignedAgent ?? DEFAULT_AGENT_RUNTIME
91
+ executionTarget.effectiveRuntimeId
70
92
  );
71
93
  db.update(tasks)
72
94
  .set({ agentProfile: autoProfile, updatedAt: new Date() })
73
95
  .where(eq(tasks.id, id))
74
96
  .run();
97
+ taskProfile = autoProfile;
98
+ try {
99
+ executionTarget = await resolveTaskExecutionTarget({
100
+ title: task.title,
101
+ description: task.description,
102
+ requestedRuntimeId: task.assignedAgent,
103
+ profileId: taskProfile,
104
+ });
105
+ } catch (error) {
106
+ const message = error instanceof Error ? error.message : String(error);
107
+ db.update(tasks)
108
+ .set({
109
+ status: "failed",
110
+ result: message,
111
+ updatedAt: new Date(),
112
+ })
113
+ .where(eq(tasks.id, id))
114
+ .run();
115
+ return NextResponse.json({ error: message }, { status: 400 });
116
+ }
75
117
  }
76
118
 
77
119
  const compatibilityError = validateRuntimeProfileAssignment({
78
- profileId: task.agentProfile ?? classifyTaskProfile(
79
- task.title,
80
- task.description,
81
- task.assignedAgent ?? DEFAULT_AGENT_RUNTIME
82
- ),
83
- runtimeId: task.assignedAgent,
120
+ profileId: taskProfile,
121
+ runtimeId: executionTarget.effectiveRuntimeId,
84
122
  context: "Task profile",
85
123
  });
86
124
  if (compatibilityError) {
@@ -95,27 +133,8 @@ export async function POST(
95
133
  return NextResponse.json({ error: compatibilityError }, { status: 400 });
96
134
  }
97
135
 
98
- // Pre-check parallel workflow limit before fire-and-forget
99
- const parallelLimit = licenseManager.getLimit("parallelWorkflows");
100
- if (Number.isFinite(parallelLimit) && getAllExecutions().size >= parallelLimit) {
101
- // Revert task to queued since we can't execute it
102
- db.update(tasks)
103
- .set({ status: "queued", updatedAt: new Date() })
104
- .where(eq(tasks.id, id))
105
- .run();
106
- return NextResponse.json(
107
- {
108
- error: `Parallel workflow limit reached (${getAllExecutions().size}/${parallelLimit}). Wait for a running task to finish or upgrade.`,
109
- limitType: "parallelWorkflows",
110
- current: getAllExecutions().size,
111
- max: parallelLimit,
112
- },
113
- { status: 429 }
114
- );
115
- }
116
-
117
136
  // Fire-and-forget — task already marked as running
118
- executeTaskWithAgent(id, task.assignedAgent ?? DEFAULT_AGENT_RUNTIME).catch(
137
+ startTaskExecution(id, { requestedRuntimeId: task.assignedAgent }).catch(
119
138
  (err) => console.error(`Task ${id} execution error:`, err)
120
139
  );
121
140
 
@@ -43,25 +43,41 @@ export async function POST(
43
43
  return NextResponse.json({ error: "Already responded" }, { status: 409 });
44
44
  }
45
45
 
46
- // Validate updatedInput keys against the original tool input to prevent injection
46
+ // Validate updatedInput keys against the original tool input to prevent injection.
47
+ // AskUserQuestion is a special case: the original toolInput describes the question
48
+ // (`question`, `options?`) but the response carries the user's `answer` — a key not
49
+ // present in the original. Allow a tightly-scoped `{answer: string}` shape.
47
50
  let sanitizedUpdatedInput = updatedInput;
51
+ const isQuestion = notification.toolName === "AskUserQuestion";
48
52
  if (updatedInput !== undefined && updatedInput !== null && typeof updatedInput === "object" && !Array.isArray(updatedInput)) {
49
- try {
50
- const originalToolInput = typeof notification.toolInput === "string" ? JSON.parse(notification.toolInput) : (notification.toolInput ?? {});
51
- if (typeof originalToolInput === "object" && originalToolInput !== null) {
52
- const allowedKeys = new Set(Object.keys(originalToolInput));
53
- const inputRecord = updatedInput as Record<string, unknown>;
54
- const extraKeys = Object.keys(inputRecord).filter((k) => !allowedKeys.has(k));
55
- if (extraKeys.length > 0) {
56
- return NextResponse.json(
57
- { error: `updatedInput contains disallowed keys: ${extraKeys.join(", ")}` },
58
- { status: 400 }
59
- );
53
+ if (isQuestion) {
54
+ const inputRecord = updatedInput as Record<string, unknown>;
55
+ const keys = Object.keys(inputRecord);
56
+ const extraKeys = keys.filter((k) => k !== "answer");
57
+ if (extraKeys.length > 0 || typeof inputRecord.answer !== "string") {
58
+ return NextResponse.json(
59
+ { error: "AskUserQuestion response must be { answer: string }" },
60
+ { status: 400 }
61
+ );
62
+ }
63
+ } else {
64
+ try {
65
+ const originalToolInput = typeof notification.toolInput === "string" ? JSON.parse(notification.toolInput) : (notification.toolInput ?? {});
66
+ if (typeof originalToolInput === "object" && originalToolInput !== null) {
67
+ const allowedKeys = new Set(Object.keys(originalToolInput));
68
+ const inputRecord = updatedInput as Record<string, unknown>;
69
+ const extraKeys = Object.keys(inputRecord).filter((k) => !allowedKeys.has(k));
70
+ if (extraKeys.length > 0) {
71
+ return NextResponse.json(
72
+ { error: `updatedInput contains disallowed keys: ${extraKeys.join(", ")}` },
73
+ { status: 400 }
74
+ );
75
+ }
60
76
  }
77
+ } catch {
78
+ // If we can't parse the original notification data, reject updatedInput entirely
79
+ sanitizedUpdatedInput = undefined;
61
80
  }
62
- } catch {
63
- // If we can't parse the original notification data, reject updatedInput entirely
64
- sanitizedUpdatedInput = undefined;
65
81
  }
66
82
  }
67
83
 
@@ -2,13 +2,13 @@ import { NextRequest, NextResponse } from "next/server";
2
2
  import { db } from "@/lib/db";
3
3
  import { tasks } from "@/lib/db/schema";
4
4
  import { eq, and, inArray } from "drizzle-orm";
5
- import { resumeTaskWithAgent } from "@/lib/agents/router";
6
5
  import { MAX_RESUME_COUNT } from "@/lib/constants/task-status";
7
- import { DEFAULT_AGENT_RUNTIME } from "@/lib/agents/runtime/catalog";
8
6
  import {
9
7
  BudgetLimitExceededError,
10
8
  enforceTaskBudgetGuardrails,
11
9
  } from "@/lib/settings/budget-guardrails";
10
+ import { resolveResumeExecutionTarget } from "@/lib/agents/runtime/execution-target";
11
+ import { resumeTaskExecution } from "@/lib/agents/task-dispatch";
12
12
 
13
13
  export async function POST(
14
14
  _req: NextRequest,
@@ -67,8 +67,29 @@ export async function POST(
67
67
  );
68
68
  }
69
69
 
70
+ try {
71
+ await resolveResumeExecutionTarget({
72
+ requestedRuntimeId: task.assignedAgent,
73
+ effectiveRuntimeId: task.effectiveRuntimeId,
74
+ });
75
+ } catch (error) {
76
+ const message = error instanceof Error ? error.message : String(error);
77
+ db.update(tasks)
78
+ .set({
79
+ status: "failed",
80
+ result: message,
81
+ updatedAt: new Date(),
82
+ })
83
+ .where(eq(tasks.id, id))
84
+ .run();
85
+ return NextResponse.json({ error: message }, { status: 400 });
86
+ }
87
+
70
88
  // Fire-and-forget
71
- resumeTaskWithAgent(id, task.assignedAgent ?? DEFAULT_AGENT_RUNTIME).catch((err) =>
89
+ resumeTaskExecution(id, {
90
+ requestedRuntimeId: task.assignedAgent,
91
+ effectiveRuntimeId: task.effectiveRuntimeId,
92
+ }).catch((err) =>
72
93
  console.error(`Task ${id} resume error:`, err)
73
94
  );
74
95