stagent 0.9.5 → 0.10.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 (277) hide show
  1. package/README.md +5 -42
  2. package/dist/cli.js +42 -18
  3. package/docs/.coverage-gaps.json +13 -55
  4. package/docs/.last-generated +1 -1
  5. package/docs/features/provider-runtimes.md +4 -0
  6. package/docs/features/schedules.md +32 -4
  7. package/docs/features/settings.md +28 -5
  8. package/docs/features/tables.md +9 -2
  9. package/docs/features/workflows.md +10 -4
  10. package/docs/journeys/developer.md +15 -1
  11. package/docs/journeys/personal-use.md +21 -4
  12. package/docs/superpowers/plans/2026-04-07-instance-bootstrap.md +1691 -0
  13. package/docs/superpowers/plans/2026-04-08-schedule-orchestration.md +2983 -0
  14. package/docs/superpowers/plans/2026-04-11-schedule-maxturns-api-control.md +551 -0
  15. package/docs/superpowers/plans/2026-04-11-task-create-profile-validation.md +864 -0
  16. package/docs/superpowers/plans/2026-04-11-task-runtime-stagent-mcp-injection.md +739 -0
  17. package/docs/superpowers/specs/2026-04-08-chat-sse-resilience-hotfix-design.md +201 -0
  18. package/docs/superpowers/specs/2026-04-08-schedule-orchestration-design.md +371 -0
  19. package/docs/superpowers/specs/2026-04-08-swarm-visibility-design.md +213 -0
  20. package/package.json +3 -2
  21. package/src/__tests__/instrumentation-smoke.test.ts +15 -0
  22. package/src/app/analytics/page.tsx +1 -21
  23. package/src/app/api/chat/conversations/[id]/messages/route.ts +22 -1
  24. package/src/app/api/diagnostics/chat-streams/route.ts +65 -0
  25. package/src/app/api/instance/config/route.ts +41 -0
  26. package/src/app/api/instance/init/route.ts +34 -0
  27. package/src/app/api/instance/upgrade/check/route.ts +26 -0
  28. package/src/app/api/instance/upgrade/route.ts +96 -0
  29. package/src/app/api/instance/upgrade/status/route.ts +35 -0
  30. package/src/app/api/memory/route.ts +0 -11
  31. package/src/app/api/notifications/route.ts +4 -2
  32. package/src/app/api/projects/[id]/route.ts +5 -155
  33. package/src/app/api/projects/__tests__/delete-project.test.ts +10 -19
  34. package/src/app/api/schedules/[id]/execute/route.ts +111 -0
  35. package/src/app/api/schedules/[id]/route.ts +9 -1
  36. package/src/app/api/schedules/__tests__/execute-route.test.ts +118 -0
  37. package/src/app/api/schedules/route.ts +3 -12
  38. package/src/app/api/settings/openai/login/route.ts +22 -0
  39. package/src/app/api/settings/openai/logout/route.ts +7 -0
  40. package/src/app/api/settings/openai/route.ts +21 -1
  41. package/src/app/api/settings/providers/route.ts +35 -8
  42. package/src/app/api/tables/[id]/enrich/__tests__/route.test.ts +153 -0
  43. package/src/app/api/tables/[id]/enrich/plan/route.ts +98 -0
  44. package/src/app/api/tables/[id]/enrich/route.ts +147 -0
  45. package/src/app/api/tables/[id]/enrich/runs/route.ts +25 -0
  46. package/src/app/api/tasks/[id]/execute/route.ts +0 -21
  47. package/src/app/api/workflows/[id]/resume/route.ts +59 -0
  48. package/src/app/api/workflows/[id]/status/route.ts +22 -8
  49. package/src/app/api/workspace/context/route.ts +2 -0
  50. package/src/app/api/workspace/fix-data-dir/route.ts +81 -0
  51. package/src/app/chat/page.tsx +11 -0
  52. package/src/app/inbox/page.tsx +12 -5
  53. package/src/app/layout.tsx +42 -21
  54. package/src/app/page.tsx +0 -2
  55. package/src/app/settings/page.tsx +6 -9
  56. package/src/components/chat/__tests__/chat-session-provider.test.tsx +408 -0
  57. package/src/components/chat/chat-command-popover.tsx +2 -2
  58. package/src/components/chat/chat-input.tsx +2 -3
  59. package/src/components/chat/chat-session-provider.tsx +720 -0
  60. package/src/components/chat/chat-shell.tsx +92 -401
  61. package/src/components/instance/__tests__/instance-section.test.tsx +125 -0
  62. package/src/components/instance/instance-section.tsx +382 -0
  63. package/src/components/instance/upgrade-badge.tsx +219 -0
  64. package/src/components/notifications/__tests__/batch-proposal-review.test.tsx +95 -0
  65. package/src/components/notifications/__tests__/notification-item.test.tsx +106 -0
  66. package/src/components/notifications/batch-proposal-review.tsx +20 -5
  67. package/src/components/notifications/inbox-list.tsx +11 -2
  68. package/src/components/notifications/notification-item.tsx +56 -2
  69. package/src/components/notifications/pending-approval-host.tsx +56 -37
  70. package/src/components/schedules/schedule-create-sheet.tsx +19 -1
  71. package/src/components/schedules/schedule-edit-sheet.tsx +20 -1
  72. package/src/components/schedules/schedule-form.tsx +31 -0
  73. package/src/components/settings/__tests__/providers-runtimes-section.test.tsx +149 -0
  74. package/src/components/settings/auth-method-selector.tsx +19 -4
  75. package/src/components/settings/auth-status-badge.tsx +28 -3
  76. package/src/components/settings/openai-chatgpt-auth-control.tsx +278 -0
  77. package/src/components/settings/openai-runtime-section.tsx +7 -1
  78. package/src/components/settings/providers-runtimes-section.tsx +138 -19
  79. package/src/components/shared/app-sidebar.tsx +4 -3
  80. package/src/components/shared/command-palette.tsx +4 -5
  81. package/src/components/shared/theme-toggle.tsx +5 -24
  82. package/src/components/shared/workspace-indicator.tsx +61 -2
  83. package/src/components/tables/__tests__/table-enrichment-sheet.test.tsx +130 -0
  84. package/src/components/tables/table-create-sheet.tsx +4 -0
  85. package/src/components/tables/table-enrichment-runs.tsx +103 -0
  86. package/src/components/tables/table-enrichment-sheet.tsx +538 -0
  87. package/src/components/tables/table-spreadsheet.tsx +29 -5
  88. package/src/components/tables/table-toolbar.tsx +10 -1
  89. package/src/components/tasks/kanban-board.tsx +1 -0
  90. package/src/components/tasks/kanban-column.tsx +53 -14
  91. package/src/components/tasks/task-bento-grid.tsx +19 -0
  92. package/src/components/tasks/task-card.tsx +26 -3
  93. package/src/components/tasks/task-chip-bar.tsx +24 -0
  94. package/src/components/tasks/task-result-renderer.tsx +1 -1
  95. package/src/components/workflows/delay-step-body.tsx +109 -0
  96. package/src/components/workflows/hooks/use-workflow-status.ts +50 -0
  97. package/src/components/workflows/loop-status-view.tsx +1 -1
  98. package/src/components/workflows/shared/step-result.tsx +78 -0
  99. package/src/components/workflows/shared/workflow-header.tsx +141 -0
  100. package/src/components/workflows/shared/workflow-loading-skeleton.tsx +36 -0
  101. package/src/components/workflows/swarm-dashboard.tsx +2 -15
  102. package/src/components/workflows/views/loop-pattern-view.tsx +137 -0
  103. package/src/components/workflows/views/sequence-pattern-view.tsx +511 -0
  104. package/src/components/workflows/workflow-form-view.tsx +133 -16
  105. package/src/components/workflows/workflow-status-view.tsx +30 -740
  106. package/src/instrumentation-node.ts +94 -0
  107. package/src/instrumentation.ts +4 -48
  108. package/src/lib/agents/__tests__/claude-agent.test.ts +199 -0
  109. package/src/lib/agents/__tests__/execution-manager.test.ts +1 -27
  110. package/src/lib/agents/__tests__/failure-reason.test.ts +68 -0
  111. package/src/lib/agents/__tests__/learned-context.test.ts +0 -11
  112. package/src/lib/agents/__tests__/learning-session.test.ts +158 -0
  113. package/src/lib/agents/__tests__/pattern-extractor.test.ts +48 -0
  114. package/src/lib/agents/claude-agent.ts +155 -18
  115. package/src/lib/agents/execution-manager.ts +0 -35
  116. package/src/lib/agents/learned-context.ts +0 -12
  117. package/src/lib/agents/learning-session.ts +18 -5
  118. package/src/lib/agents/profiles/__tests__/registry.test.ts +6 -4
  119. package/src/lib/agents/profiles/builtins/upgrade-assistant/SKILL.md +70 -0
  120. package/src/lib/agents/profiles/builtins/upgrade-assistant/profile.yaml +32 -0
  121. package/src/lib/agents/runtime/__tests__/openai-codex-auth.test.ts +118 -0
  122. package/src/lib/agents/runtime/codex-app-server-client.ts +11 -5
  123. package/src/lib/agents/runtime/openai-codex-auth.ts +389 -0
  124. package/src/lib/agents/runtime/openai-codex.ts +29 -60
  125. package/src/lib/agents/runtime/types.ts +8 -0
  126. package/src/lib/book/chapter-mapping.ts +11 -0
  127. package/src/lib/book/content.ts +10 -0
  128. package/src/lib/chat/__tests__/active-streams.test.ts +49 -0
  129. package/src/lib/chat/__tests__/finalize-safety-net.test.ts +139 -0
  130. package/src/lib/chat/__tests__/reconcile.test.ts +137 -0
  131. package/src/lib/chat/__tests__/stream-telemetry.test.ts +151 -0
  132. package/src/lib/chat/active-streams.ts +27 -0
  133. package/src/lib/chat/codex-engine.ts +16 -17
  134. package/src/lib/chat/context-builder.ts +5 -3
  135. package/src/lib/chat/engine.ts +50 -3
  136. package/src/lib/chat/reconcile.ts +117 -0
  137. package/src/lib/chat/stagent-tools.ts +1 -0
  138. package/src/lib/chat/stream-telemetry.ts +132 -0
  139. package/src/lib/chat/suggested-prompts.ts +28 -1
  140. package/src/lib/chat/system-prompt.ts +26 -1
  141. package/src/lib/chat/tool-catalog.ts +2 -1
  142. package/src/lib/chat/tools/__tests__/enrich-table-tool.test.ts +127 -0
  143. package/src/lib/chat/tools/__tests__/schedule-tools.test.ts +261 -0
  144. package/src/lib/chat/tools/__tests__/task-tools.test.ts +352 -0
  145. package/src/lib/chat/tools/__tests__/workflow-tools-dedup.test.ts +217 -0
  146. package/src/lib/chat/tools/document-tools.ts +29 -13
  147. package/src/lib/chat/tools/helpers.ts +39 -0
  148. package/src/lib/chat/tools/notification-tools.ts +9 -5
  149. package/src/lib/chat/tools/project-tools.ts +33 -0
  150. package/src/lib/chat/tools/schedule-tools.ts +44 -11
  151. package/src/lib/chat/tools/table-tools.ts +71 -0
  152. package/src/lib/chat/tools/task-tools.ts +84 -20
  153. package/src/lib/chat/tools/workflow-tools.ts +234 -32
  154. package/src/lib/constants/settings.ts +8 -18
  155. package/src/lib/data/__tests__/clear.test.ts +56 -2
  156. package/src/lib/data/clear.ts +20 -15
  157. package/src/lib/data/delete-project.ts +171 -0
  158. package/src/lib/db/__tests__/bootstrap.test.ts +1 -1
  159. package/src/lib/db/bootstrap.ts +45 -16
  160. package/src/lib/db/index.ts +5 -0
  161. package/src/lib/db/migrations/0009_add_app_instances.sql +25 -0
  162. package/src/lib/db/migrations/0024_add_workflow_resume_at.sql +10 -0
  163. package/src/lib/db/migrations/0025_drop_app_instances.sql +3 -0
  164. package/src/lib/db/migrations/0026_drop_license.sql +3 -0
  165. package/src/lib/db/migrations/meta/_journal.json +21 -0
  166. package/src/lib/db/schema.ts +68 -23
  167. package/src/lib/environment/workspace-context.ts +13 -1
  168. package/src/lib/import/dedup.ts +4 -54
  169. package/src/lib/instance/__tests__/bootstrap.test.ts +362 -0
  170. package/src/lib/instance/__tests__/detect.test.ts +115 -0
  171. package/src/lib/instance/__tests__/fingerprint.test.ts +48 -0
  172. package/src/lib/instance/__tests__/git-ops.test.ts +95 -0
  173. package/src/lib/instance/__tests__/settings.test.ts +83 -0
  174. package/src/lib/instance/__tests__/upgrade-poller.test.ts +131 -0
  175. package/src/lib/instance/bootstrap.ts +270 -0
  176. package/src/lib/instance/detect.ts +49 -0
  177. package/src/lib/instance/fingerprint.ts +78 -0
  178. package/src/lib/instance/git-ops.ts +95 -0
  179. package/src/lib/instance/settings.ts +61 -0
  180. package/src/lib/instance/types.ts +77 -0
  181. package/src/lib/instance/upgrade-poller.ts +153 -0
  182. package/src/lib/notifications/__tests__/visibility.test.ts +51 -0
  183. package/src/lib/notifications/visibility.ts +33 -0
  184. package/src/lib/schedules/__tests__/collision-check.test.ts +93 -0
  185. package/src/lib/schedules/__tests__/config.test.ts +62 -0
  186. package/src/lib/schedules/__tests__/firing-metrics.test.ts +99 -0
  187. package/src/lib/schedules/__tests__/integration.test.ts +82 -0
  188. package/src/lib/schedules/__tests__/slot-claim.test.ts +242 -0
  189. package/src/lib/schedules/__tests__/tick-scheduler.test.ts +102 -0
  190. package/src/lib/schedules/__tests__/turn-budget.test.ts +228 -0
  191. package/src/lib/schedules/collision-check.ts +105 -0
  192. package/src/lib/schedules/config.ts +53 -0
  193. package/src/lib/schedules/scheduler.ts +232 -13
  194. package/src/lib/schedules/slot-claim.ts +105 -0
  195. package/src/lib/settings/__tests__/openai-auth.test.ts +101 -0
  196. package/src/lib/settings/__tests__/openai-login-manager.test.ts +64 -0
  197. package/src/lib/settings/__tests__/runtime-setup.test.ts +33 -0
  198. package/src/lib/settings/openai-auth.ts +105 -10
  199. package/src/lib/settings/openai-login-manager.ts +260 -0
  200. package/src/lib/settings/runtime-setup.ts +14 -4
  201. package/src/lib/tables/__tests__/enrichment-planner.test.ts +124 -0
  202. package/src/lib/tables/__tests__/enrichment.test.ts +147 -0
  203. package/src/lib/tables/enrichment-planner.ts +454 -0
  204. package/src/lib/tables/enrichment.ts +328 -0
  205. package/src/lib/tables/query-builder.ts +5 -2
  206. package/src/lib/tables/trigger-evaluator.ts +3 -2
  207. package/src/lib/theme.ts +71 -0
  208. package/src/lib/usage/ledger.ts +2 -18
  209. package/src/lib/util/__tests__/similarity.test.ts +106 -0
  210. package/src/lib/util/similarity.ts +77 -0
  211. package/src/lib/utils/format-timestamp.ts +24 -0
  212. package/src/lib/utils/stagent-paths.ts +12 -0
  213. package/src/lib/validators/__tests__/blueprint.test.ts +172 -0
  214. package/src/lib/validators/__tests__/settings.test.ts +10 -0
  215. package/src/lib/validators/blueprint.ts +70 -9
  216. package/src/lib/validators/profile.ts +2 -2
  217. package/src/lib/validators/settings.ts +3 -1
  218. package/src/lib/workflows/__tests__/delay.test.ts +196 -0
  219. package/src/lib/workflows/__tests__/engine.test.ts +8 -0
  220. package/src/lib/workflows/__tests__/loop-executor.test.ts +54 -0
  221. package/src/lib/workflows/__tests__/post-action.test.ts +108 -0
  222. package/src/lib/workflows/blueprints/instantiator.ts +22 -1
  223. package/src/lib/workflows/blueprints/types.ts +10 -2
  224. package/src/lib/workflows/delay.ts +106 -0
  225. package/src/lib/workflows/engine.ts +207 -4
  226. package/src/lib/workflows/loop-executor.ts +349 -24
  227. package/src/lib/workflows/post-action.ts +91 -0
  228. package/src/lib/workflows/types.ts +166 -1
  229. package/src/app/api/license/checkout/route.ts +0 -28
  230. package/src/app/api/license/portal/route.ts +0 -26
  231. package/src/app/api/license/route.ts +0 -89
  232. package/src/app/api/license/usage/route.ts +0 -63
  233. package/src/app/api/marketplace/browse/route.ts +0 -15
  234. package/src/app/api/marketplace/import/route.ts +0 -28
  235. package/src/app/api/marketplace/publish/route.ts +0 -40
  236. package/src/app/api/onboarding/email/route.ts +0 -53
  237. package/src/app/api/settings/telemetry/route.ts +0 -14
  238. package/src/app/api/sync/export/route.ts +0 -54
  239. package/src/app/api/sync/restore/route.ts +0 -37
  240. package/src/app/api/sync/sessions/route.ts +0 -24
  241. package/src/app/auth/callback/route.ts +0 -73
  242. package/src/app/marketplace/page.tsx +0 -19
  243. package/src/components/analytics/analytics-gate-card.tsx +0 -101
  244. package/src/components/marketplace/blueprint-card.tsx +0 -61
  245. package/src/components/marketplace/marketplace-browser.tsx +0 -131
  246. package/src/components/onboarding/email-capture-card.tsx +0 -104
  247. package/src/components/settings/activation-form.tsx +0 -95
  248. package/src/components/settings/cloud-account-section.tsx +0 -147
  249. package/src/components/settings/cloud-sync-section.tsx +0 -155
  250. package/src/components/settings/subscription-section.tsx +0 -410
  251. package/src/components/settings/telemetry-section.tsx +0 -80
  252. package/src/components/shared/premium-gate-overlay.tsx +0 -50
  253. package/src/components/shared/schedule-gate-dialog.tsx +0 -64
  254. package/src/components/shared/upgrade-banner.tsx +0 -112
  255. package/src/hooks/use-supabase-auth.ts +0 -79
  256. package/src/lib/billing/email.ts +0 -54
  257. package/src/lib/billing/products.ts +0 -80
  258. package/src/lib/billing/stripe.ts +0 -101
  259. package/src/lib/cloud/supabase-browser.ts +0 -32
  260. package/src/lib/cloud/supabase-client.ts +0 -56
  261. package/src/lib/license/__tests__/features.test.ts +0 -56
  262. package/src/lib/license/__tests__/key-format.test.ts +0 -88
  263. package/src/lib/license/__tests__/manager.test.ts +0 -64
  264. package/src/lib/license/__tests__/tier-limits.test.ts +0 -79
  265. package/src/lib/license/cloud-validation.ts +0 -60
  266. package/src/lib/license/features.ts +0 -44
  267. package/src/lib/license/key-format.ts +0 -101
  268. package/src/lib/license/limit-check.ts +0 -111
  269. package/src/lib/license/limit-queries.ts +0 -51
  270. package/src/lib/license/manager.ts +0 -345
  271. package/src/lib/license/notifications.ts +0 -59
  272. package/src/lib/license/tier-limits.ts +0 -71
  273. package/src/lib/marketplace/marketplace-client.ts +0 -107
  274. package/src/lib/sync/cloud-sync.ts +0 -235
  275. package/src/lib/telemetry/conversion-events.ts +0 -71
  276. package/src/lib/telemetry/queue.ts +0 -122
  277. package/src/lib/validators/license.ts +0 -33
@@ -3,12 +3,13 @@ import { z } from "zod";
3
3
  import { db } from "@/lib/db";
4
4
  import { tasks } from "@/lib/db/schema";
5
5
  import { eq, and, desc } from "drizzle-orm";
6
- import { ok, err, type ToolContext } from "./helpers";
6
+ import { ok, err, resolveEntityId, type ToolContext } from "./helpers";
7
7
  import {
8
8
  DEFAULT_AGENT_RUNTIME,
9
9
  isAgentRuntimeId,
10
10
  SUPPORTED_AGENT_RUNTIMES,
11
11
  } from "@/lib/agents/runtime/catalog";
12
+ import { getProfile, listProfiles } from "@/lib/agents/profiles/registry";
12
13
 
13
14
  const VALID_TASK_STATUSES = [
14
15
  "planned",
@@ -19,6 +20,25 @@ const VALID_TASK_STATUSES = [
19
20
  "cancelled",
20
21
  ] as const;
21
22
 
23
+ /**
24
+ * Zod refinement shared by create_task and update_task for the agentProfile
25
+ * field. Returns true for valid registered profile IDs. The error message
26
+ * lists a truncated sample of valid IDs from the registry so operators can
27
+ * self-correct without cross-referencing docs.
28
+ */
29
+ function isValidAgentProfile(id: string): boolean {
30
+ return getProfile(id) !== undefined;
31
+ }
32
+
33
+ function agentProfileErrorMessage(invalid: string): string {
34
+ const valid = listProfiles()
35
+ .map((p) => p.id)
36
+ .sort();
37
+ const sample = valid.slice(0, 8).join(", ");
38
+ const more = valid.length > 8 ? `, and ${valid.length - 8} more` : "";
39
+ return `Invalid agentProfile "${invalid}". Valid profiles: ${sample}${more}. Run list_profiles (or inspect ~/.claude/skills/) to see the full set.`;
40
+ }
41
+
22
42
  export function taskTools(ctx: ToolContext) {
23
43
  return [
24
44
  defineTool(
@@ -51,6 +71,14 @@ export function taskTools(ctx: ToolContext) {
51
71
  .orderBy(tasks.priority, desc(tasks.createdAt))
52
72
  .limit(50);
53
73
 
74
+ if (result.length === 0 && effectiveProjectId) {
75
+ return ok({
76
+ tasks: [],
77
+ note: `No tasks found in project ${effectiveProjectId}. ` +
78
+ `Use projectId: null to list tasks from any project, ` +
79
+ `or get_task <id> to look up a specific task directly.`,
80
+ });
81
+ }
54
82
  return ok(result);
55
83
  } catch (e) {
56
84
  return err(e instanceof Error ? e.message : "Failed to list tasks");
@@ -90,13 +118,19 @@ export function taskTools(ctx: ToolContext) {
90
118
  ),
91
119
  agentProfile: z
92
120
  .string()
121
+ .refine(isValidAgentProfile, {
122
+ message: "Invalid agentProfile (not in profile registry). See list_profiles.",
123
+ })
93
124
  .optional()
94
125
  .describe(
95
- "Agent profile ID (e.g. general, code-reviewer, researcher, reddit-researcher)"
126
+ "Agent profile ID (e.g. general, code-reviewer, researcher). Validated against the profile registry."
96
127
  ),
97
128
  },
98
129
  async (args) => {
99
130
  try {
131
+ if (args.agentProfile !== undefined && !isValidAgentProfile(args.agentProfile)) {
132
+ return err(agentProfileErrorMessage(args.agentProfile));
133
+ }
100
134
  if (args.assignedAgent && !isAgentRuntimeId(args.assignedAgent)) {
101
135
  return err(
102
136
  `Invalid runtime "${args.assignedAgent}". Valid: ${SUPPORTED_AGENT_RUNTIMES.join(", ")}`
@@ -162,13 +196,23 @@ export function taskTools(ctx: ToolContext) {
162
196
  ),
163
197
  agentProfile: z
164
198
  .string()
199
+ .refine(isValidAgentProfile, {
200
+ message: "Invalid agentProfile (not in profile registry). See list_profiles.",
201
+ })
165
202
  .optional()
166
203
  .describe(
167
- "Agent profile ID (e.g. general, code-reviewer, researcher, reddit-researcher)"
204
+ "Agent profile ID (e.g. general, code-reviewer, researcher). Validated against the profile registry."
168
205
  ),
169
206
  },
170
207
  async (args) => {
171
208
  try {
209
+ const resolved = await resolveEntityId(tasks, tasks.id, args.taskId);
210
+ if ("error" in resolved) return err(resolved.error);
211
+ const taskId = resolved.id;
212
+
213
+ if (args.agentProfile !== undefined && !isValidAgentProfile(args.agentProfile)) {
214
+ return err(agentProfileErrorMessage(args.agentProfile));
215
+ }
172
216
  if (args.assignedAgent && !isAgentRuntimeId(args.assignedAgent)) {
173
217
  return err(
174
218
  `Invalid runtime "${args.assignedAgent}". Valid: ${SUPPORTED_AGENT_RUNTIMES.join(", ")}`
@@ -178,10 +222,10 @@ export function taskTools(ctx: ToolContext) {
178
222
  const existing = await db
179
223
  .select()
180
224
  .from(tasks)
181
- .where(eq(tasks.id, args.taskId))
225
+ .where(eq(tasks.id, taskId))
182
226
  .get();
183
227
 
184
- if (!existing) return err(`Task not found: ${args.taskId}`);
228
+ if (!existing) return err(`Task not found: ${taskId}`);
185
229
 
186
230
  const updates: Record<string, unknown> = { updatedAt: new Date() };
187
231
  if (args.title !== undefined) updates.title = args.title;
@@ -197,12 +241,12 @@ export function taskTools(ctx: ToolContext) {
197
241
  await db
198
242
  .update(tasks)
199
243
  .set(updates)
200
- .where(eq(tasks.id, args.taskId));
244
+ .where(eq(tasks.id, taskId));
201
245
 
202
246
  const [task] = await db
203
247
  .select()
204
248
  .from(tasks)
205
- .where(eq(tasks.id, args.taskId));
249
+ .where(eq(tasks.id, taskId));
206
250
 
207
251
  ctx.onToolResult?.("update_task", task);
208
252
  return ok(task);
@@ -220,13 +264,17 @@ export function taskTools(ctx: ToolContext) {
220
264
  },
221
265
  async (args) => {
222
266
  try {
267
+ const resolved = await resolveEntityId(tasks, tasks.id, args.taskId);
268
+ if ("error" in resolved) return err(resolved.error);
269
+ const taskId = resolved.id;
270
+
223
271
  const task = await db
224
272
  .select()
225
273
  .from(tasks)
226
- .where(eq(tasks.id, args.taskId))
274
+ .where(eq(tasks.id, taskId))
227
275
  .get();
228
276
 
229
- if (!task) return err(`Task not found: ${args.taskId}`);
277
+ if (!task) return err(`Task not found: ${taskId}`);
230
278
  ctx.onToolResult?.("get_task", task);
231
279
  return ok(task);
232
280
  } catch (e) {
@@ -249,6 +297,10 @@ export function taskTools(ctx: ToolContext) {
249
297
  },
250
298
  async (args) => {
251
299
  try {
300
+ const resolved = await resolveEntityId(tasks, tasks.id, args.taskId);
301
+ if ("error" in resolved) return err(resolved.error);
302
+ const taskId = resolved.id;
303
+
252
304
  if (args.assignedAgent && !isAgentRuntimeId(args.assignedAgent)) {
253
305
  return err(
254
306
  `Invalid runtime "${args.assignedAgent}". Valid: ${SUPPORTED_AGENT_RUNTIMES.join(", ")}`
@@ -258,26 +310,34 @@ export function taskTools(ctx: ToolContext) {
258
310
  const task = await db
259
311
  .select()
260
312
  .from(tasks)
261
- .where(eq(tasks.id, args.taskId))
313
+ .where(eq(tasks.id, taskId))
262
314
  .get();
263
315
 
264
- if (!task) return err(`Task not found: ${args.taskId}`);
316
+ if (!task) return err(`Task not found: ${taskId}`);
265
317
  if (task.status === "running") return err("Task is already running");
266
318
 
319
+ if (task.agentProfile && !isValidAgentProfile(task.agentProfile)) {
320
+ return err(
321
+ `Task ${taskId} has an invalid agentProfile "${task.agentProfile}" (not in profile registry). ` +
322
+ `Fix with update_task { taskId, agentProfile: "<valid-id>" } before retrying. ` +
323
+ agentProfileErrorMessage(task.agentProfile).split(". ").slice(1).join(". ")
324
+ );
325
+ }
326
+
267
327
  const runtimeId = args.assignedAgent ?? task.assignedAgent ?? DEFAULT_AGENT_RUNTIME;
268
328
 
269
329
  // Set status to queued
270
330
  await db
271
331
  .update(tasks)
272
332
  .set({ status: "queued", assignedAgent: runtimeId, updatedAt: new Date() })
273
- .where(eq(tasks.id, args.taskId));
333
+ .where(eq(tasks.id, taskId));
274
334
 
275
335
  // Fire-and-forget execution
276
336
  const { executeTaskWithAgent } = await import("@/lib/agents/router");
277
- executeTaskWithAgent(args.taskId, runtimeId).catch(() => {});
337
+ executeTaskWithAgent(taskId, runtimeId).catch(() => {});
278
338
 
279
- ctx.onToolResult?.("execute_task", { id: args.taskId, title: task.title });
280
- return ok({ message: "Execution started", taskId: args.taskId, runtime: runtimeId });
339
+ ctx.onToolResult?.("execute_task", { id: taskId, title: task.title });
340
+ return ok({ message: "Execution started", taskId, runtime: runtimeId });
281
341
  } catch (e) {
282
342
  return err(e instanceof Error ? e.message : "Failed to execute task");
283
343
  }
@@ -292,17 +352,21 @@ export function taskTools(ctx: ToolContext) {
292
352
  },
293
353
  async (args) => {
294
354
  try {
355
+ const resolved = await resolveEntityId(tasks, tasks.id, args.taskId);
356
+ if ("error" in resolved) return err(resolved.error);
357
+ const taskId = resolved.id;
358
+
295
359
  const task = await db
296
360
  .select()
297
361
  .from(tasks)
298
- .where(eq(tasks.id, args.taskId))
362
+ .where(eq(tasks.id, taskId))
299
363
  .get();
300
364
 
301
- if (!task) return err(`Task not found: ${args.taskId}`);
365
+ if (!task) return err(`Task not found: ${taskId}`);
302
366
  if (task.status !== "running") return err(`Task is not running (status: ${task.status})`);
303
367
 
304
368
  const { getExecution } = await import("@/lib/agents/execution-manager");
305
- const execution = getExecution(args.taskId);
369
+ const execution = getExecution(taskId);
306
370
  if (execution?.abortController) {
307
371
  execution.abortController.abort();
308
372
  }
@@ -310,9 +374,9 @@ export function taskTools(ctx: ToolContext) {
310
374
  await db
311
375
  .update(tasks)
312
376
  .set({ status: "cancelled", updatedAt: new Date() })
313
- .where(eq(tasks.id, args.taskId));
377
+ .where(eq(tasks.id, taskId));
314
378
 
315
- return ok({ message: "Task cancelled", taskId: args.taskId });
379
+ return ok({ message: "Task cancelled", taskId });
316
380
  } catch (e) {
317
381
  return err(e instanceof Error ? e.message : "Failed to cancel task");
318
382
  }
@@ -10,7 +10,8 @@ import {
10
10
  workflowDocumentInputs,
11
11
  } from "@/lib/db/schema";
12
12
  import { eq, and, desc, inArray, like } from "drizzle-orm";
13
- import { ok, err, type ToolContext } from "./helpers";
13
+ import { ok, err, resolveEntityId, type ToolContext } from "./helpers";
14
+ import { extractKeywords, jaccard } from "@/lib/util/similarity";
14
15
 
15
16
  const VALID_WORKFLOW_STATUSES = [
16
17
  "draft",
@@ -20,6 +21,107 @@ const VALID_WORKFLOW_STATUSES = [
20
21
  "failed",
21
22
  ] as const;
22
23
 
24
+ /** Minimum Jaccard score for two workflows to count as "near duplicates". */
25
+ const WORKFLOW_DEDUP_THRESHOLD = 0.7;
26
+
27
+ /**
28
+ * Pull the comparable text out of a workflow definition JSON string:
29
+ * name + each step's name + each step's prompt. Invalid JSON returns "".
30
+ *
31
+ * Shared by findSimilarWorkflows for the candidate and each existing row.
32
+ */
33
+ function workflowComparableText(name: string, definitionJson: string | null): string {
34
+ const parts: string[] = [name];
35
+ if (!definitionJson) return parts.join(" ");
36
+ try {
37
+ const def = JSON.parse(definitionJson);
38
+ if (Array.isArray(def?.steps)) {
39
+ for (const step of def.steps) {
40
+ if (typeof step?.name === "string") parts.push(step.name);
41
+ if (typeof step?.prompt === "string") parts.push(step.prompt);
42
+ }
43
+ }
44
+ } catch {
45
+ // Malformed JSON — fall back to just the name.
46
+ }
47
+ return parts.join(" ");
48
+ }
49
+
50
+ export interface SimilarWorkflowMatch {
51
+ id: string;
52
+ name: string;
53
+ similarity: number;
54
+ reason: string;
55
+ }
56
+
57
+ /**
58
+ * Find workflows in the same project that look similar to a candidate.
59
+ *
60
+ * Two-tier check:
61
+ * 1. Exact name match (case-insensitive) → similarity 1.0
62
+ * 2. Jaccard similarity over extracted keywords from name+step titles+prompts
63
+ *
64
+ * Returns up to 3 matches with similarity >= WORKFLOW_DEDUP_THRESHOLD,
65
+ * sorted by similarity descending. Used by `create_workflow` to warn the
66
+ * LLM before blindly inserting another row in long conversations where
67
+ * the sliding-window context builder evicts earlier creations.
68
+ *
69
+ * When projectId is null (no active project), returns [] — cross-project
70
+ * dedup would be misleading, and the handful of null-project rows that
71
+ * exist aren't worth de-duplicating against each other.
72
+ */
73
+ export async function findSimilarWorkflows(
74
+ projectId: string | null,
75
+ candidateName: string,
76
+ candidateDefinitionJson: string
77
+ ): Promise<SimilarWorkflowMatch[]> {
78
+ if (!projectId) return [];
79
+
80
+ const existing = await db
81
+ .select({
82
+ id: workflows.id,
83
+ name: workflows.name,
84
+ definition: workflows.definition,
85
+ })
86
+ .from(workflows)
87
+ .where(eq(workflows.projectId, projectId));
88
+
89
+ const matches: SimilarWorkflowMatch[] = [];
90
+ const candidateKeywords = extractKeywords(
91
+ workflowComparableText(candidateName, candidateDefinitionJson)
92
+ );
93
+ const candidateNameLower = candidateName.trim().toLowerCase();
94
+
95
+ for (const row of existing) {
96
+ // Tier 1: exact name match (case-insensitive)
97
+ if (row.name.trim().toLowerCase() === candidateNameLower) {
98
+ matches.push({
99
+ id: row.id,
100
+ name: row.name,
101
+ similarity: 1,
102
+ reason: `Same name: "${row.name}"`,
103
+ });
104
+ continue;
105
+ }
106
+
107
+ // Tier 2: Jaccard similarity on keywords
108
+ const existingKeywords = extractKeywords(
109
+ workflowComparableText(row.name, row.definition)
110
+ );
111
+ const similarity = jaccard(candidateKeywords, existingKeywords);
112
+ if (similarity >= WORKFLOW_DEDUP_THRESHOLD) {
113
+ matches.push({
114
+ id: row.id,
115
+ name: row.name,
116
+ similarity,
117
+ reason: `Similar content to "${row.name}" (${Math.round(similarity * 100)}%)`,
118
+ });
119
+ }
120
+ }
121
+
122
+ return matches.sort((a, b) => b.similarity - a.similarity).slice(0, 3);
123
+ }
124
+
23
125
  export function workflowTools(ctx: ToolContext) {
24
126
  return [
25
127
  defineTool(
@@ -69,7 +171,7 @@ export function workflowTools(ctx: ToolContext) {
69
171
 
70
172
  defineTool(
71
173
  "create_workflow",
72
- "Create a new workflow with a definition. The definition must include a pattern (sequence, parallel, checkpoint, planner-executor, swarm, loop) and steps array.",
174
+ "Create a new workflow with a definition. The definition must include a pattern (sequence, parallel, checkpoint, planner-executor, swarm, loop) and steps array. Sequence-pattern steps can be either task steps (with prompt + assignedAgent/agentProfile) or delay steps (with delayDuration like '3d', '2h', '30m', '1w') that pause the workflow between tasks — use delay steps for time-distributed sequences (outreach cadences, drip campaigns, cooling periods) rather than creating separate workflows or schedules. IMPORTANT: for the 'run agent on every row of a table' pattern, prefer enrich_table over create_workflow — enrich_table generates the optimal loop configuration, binds each row as {{row.field}} context, wires up the postAction row writeback, and handles idempotent skip of already-populated rows. Hand-rolled equivalents miss these safeguards.",
73
175
  {
74
176
  name: z.string().min(1).max(200).describe("Workflow name"),
75
177
  projectId: z
@@ -79,7 +181,11 @@ export function workflowTools(ctx: ToolContext) {
79
181
  definition: z
80
182
  .string()
81
183
  .describe(
82
- 'Workflow definition as JSON string. Must include "pattern" and "steps" array. Example: {"pattern":"sequence","steps":[{"id":"step-1","name":"step1","prompt":"Do X","assignedAgent":"claude"}]}'
184
+ 'Workflow definition as JSON string. Must include "pattern" and "steps" array. ' +
185
+ 'Task step example: {"id":"s1","name":"Research","prompt":"Do X","assignedAgent":"claude"}. ' +
186
+ 'Delay step example (sequence pattern only): {"id":"s2","name":"Wait 3 days","delayDuration":"3d"}. ' +
187
+ 'A complete drip sequence: {"pattern":"sequence","steps":[{"id":"s1","name":"Initial","prompt":"Send first email","assignedAgent":"claude"},{"id":"s2","name":"Wait","delayDuration":"3d"},{"id":"s3","name":"Follow-up","prompt":"Send follow-up","assignedAgent":"claude"}]}. ' +
188
+ 'Delay bounds: 1m to 30d. Delay steps must NOT have prompt/profile fields.'
83
189
  ),
84
190
  documentIds: z
85
191
  .array(z.string())
@@ -93,6 +199,12 @@ export function workflowTools(ctx: ToolContext) {
93
199
  .describe(
94
200
  "Runtime to use for workflow execution (e.g., 'openai-direct', 'anthropic-direct'). Use list_runtimes to see available options. Omit to use the system default."
95
201
  ),
202
+ force: z
203
+ .boolean()
204
+ .optional()
205
+ .describe(
206
+ "Set to true to bypass the near-duplicate check and always create a new workflow. Only use this when the user has explicitly confirmed they want a second workflow alongside a similar existing one (e.g., 'v2', 'alternate approach'). Default false."
207
+ ),
96
208
  },
97
209
  async (args) => {
98
210
  try {
@@ -127,6 +239,28 @@ export function workflowTools(ctx: ToolContext) {
127
239
  }
128
240
 
129
241
  const effectiveProjectId = args.projectId ?? ctx.projectId ?? null;
242
+
243
+ // Dedup guard: long chat conversations can truncate the earlier
244
+ // create_workflow tool call out of the sliding-window context, so
245
+ // the LLM loses its own history and re-creates on "redesign"
246
+ // requests. Check for near-duplicates in the same project before
247
+ // inserting. Pass force=true to bypass.
248
+ if (!args.force) {
249
+ const similar = await findSimilarWorkflows(
250
+ effectiveProjectId,
251
+ args.name,
252
+ args.definition
253
+ );
254
+ if (similar.length > 0) {
255
+ return ok({
256
+ status: "similar-found",
257
+ message:
258
+ "Found similar workflow(s) in this project. Use update_workflow to modify an existing one, or pass force=true to create a new workflow alongside them.",
259
+ matches: similar,
260
+ });
261
+ }
262
+ }
263
+
130
264
  const now = new Date();
131
265
  const id = crypto.randomUUID();
132
266
 
@@ -211,13 +345,17 @@ export function workflowTools(ctx: ToolContext) {
211
345
  },
212
346
  async (args) => {
213
347
  try {
214
- const workflow = await db
348
+ const resolved = await resolveEntityId(workflows, workflows.id, args.workflowId);
349
+ if ("error" in resolved) return err(resolved.error);
350
+ const workflowId = resolved.id;
351
+
352
+ const workflow = db
215
353
  .select()
216
354
  .from(workflows)
217
- .where(eq(workflows.id, args.workflowId))
355
+ .where(eq(workflows.id, workflowId))
218
356
  .get();
219
357
 
220
- if (!workflow) return err(`Workflow not found: ${args.workflowId}`);
358
+ if (!workflow) return err(`Workflow not found: ${workflowId}`);
221
359
 
222
360
  const { parseWorkflowState } = await import("@/lib/workflows/engine");
223
361
  const { definition, state } = parseWorkflowState(workflow.definition);
@@ -266,13 +404,17 @@ export function workflowTools(ctx: ToolContext) {
266
404
  },
267
405
  async (args) => {
268
406
  try {
269
- const existing = await db
407
+ const resolved = await resolveEntityId(workflows, workflows.id, args.workflowId);
408
+ if ("error" in resolved) return err(resolved.error);
409
+ const workflowId = resolved.id;
410
+
411
+ const existing = db
270
412
  .select()
271
413
  .from(workflows)
272
- .where(eq(workflows.id, args.workflowId))
414
+ .where(eq(workflows.id, workflowId))
273
415
  .get();
274
416
 
275
- if (!existing) return err(`Workflow not found: ${args.workflowId}`);
417
+ if (!existing) return err(`Workflow not found: ${workflowId}`);
276
418
  if (existing.status !== "draft")
277
419
  return err(`Cannot edit a workflow in '${existing.status}' status. Only draft workflows can be edited.`);
278
420
 
@@ -293,12 +435,12 @@ export function workflowTools(ctx: ToolContext) {
293
435
  await db
294
436
  .update(workflows)
295
437
  .set(updates)
296
- .where(eq(workflows.id, args.workflowId));
438
+ .where(eq(workflows.id, workflowId));
297
439
 
298
440
  const [workflow] = await db
299
441
  .select()
300
442
  .from(workflows)
301
- .where(eq(workflows.id, args.workflowId));
443
+ .where(eq(workflows.id, workflowId));
302
444
 
303
445
  ctx.onToolResult?.("update_workflow", workflow);
304
446
  return ok({
@@ -321,13 +463,17 @@ export function workflowTools(ctx: ToolContext) {
321
463
  },
322
464
  async (args) => {
323
465
  try {
324
- const existing = await db
466
+ const resolved = await resolveEntityId(workflows, workflows.id, args.workflowId);
467
+ if ("error" in resolved) return err(resolved.error);
468
+ const workflowId = resolved.id;
469
+
470
+ const existing = db
325
471
  .select()
326
472
  .from(workflows)
327
- .where(eq(workflows.id, args.workflowId))
473
+ .where(eq(workflows.id, workflowId))
328
474
  .get();
329
475
 
330
- if (!existing) return err(`Workflow not found: ${args.workflowId}`);
476
+ if (!existing) return err(`Workflow not found: ${workflowId}`);
331
477
  if (existing.status === "active")
332
478
  return err("Cannot delete an active workflow. Pause or stop it first.");
333
479
 
@@ -335,7 +481,7 @@ export function workflowTools(ctx: ToolContext) {
335
481
  const childTasks = await db
336
482
  .select({ id: tasks.id })
337
483
  .from(tasks)
338
- .where(eq(tasks.workflowId, args.workflowId));
484
+ .where(eq(tasks.workflowId, workflowId));
339
485
 
340
486
  const taskIds = childTasks.map((t) => t.id);
341
487
  for (const taskId of taskIds) {
@@ -343,10 +489,10 @@ export function workflowTools(ctx: ToolContext) {
343
489
  await db.delete(agentLogs).where(eq(agentLogs.taskId, taskId));
344
490
  await db.delete(documents).where(eq(documents.taskId, taskId));
345
491
  }
346
- await db.delete(tasks).where(eq(tasks.workflowId, args.workflowId));
347
- await db.delete(workflows).where(eq(workflows.id, args.workflowId));
492
+ await db.delete(tasks).where(eq(tasks.workflowId, workflowId));
493
+ await db.delete(workflows).where(eq(workflows.id, workflowId));
348
494
 
349
- return ok({ message: "Workflow deleted", workflowId: args.workflowId, name: existing.name });
495
+ return ok({ message: "Workflow deleted", workflowId, name: existing.name });
350
496
  } catch (e) {
351
497
  return err(e instanceof Error ? e.message : "Failed to delete workflow");
352
498
  }
@@ -361,13 +507,17 @@ export function workflowTools(ctx: ToolContext) {
361
507
  },
362
508
  async (args) => {
363
509
  try {
364
- const workflow = await db
510
+ const resolved = await resolveEntityId(workflows, workflows.id, args.workflowId);
511
+ if ("error" in resolved) return err(resolved.error);
512
+ const workflowId = resolved.id;
513
+
514
+ const workflow = db
365
515
  .select()
366
516
  .from(workflows)
367
- .where(eq(workflows.id, args.workflowId))
517
+ .where(eq(workflows.id, workflowId))
368
518
  .get();
369
519
 
370
- if (!workflow) return err(`Workflow not found: ${args.workflowId}`);
520
+ if (!workflow) return err(`Workflow not found: ${workflowId}`);
371
521
 
372
522
  // Allow re-execution from crashed "active" if no live tasks
373
523
  if (workflow.status === "active") {
@@ -376,7 +526,7 @@ export function workflowTools(ctx: ToolContext) {
376
526
  .from(tasks)
377
527
  .where(
378
528
  and(
379
- eq(tasks.workflowId, args.workflowId),
529
+ eq(tasks.workflowId, workflowId),
380
530
  inArray(tasks.status, ["running", "queued"])
381
531
  )
382
532
  );
@@ -404,7 +554,7 @@ export function workflowTools(ctx: ToolContext) {
404
554
  .set({ status: "cancelled", updatedAt: new Date() })
405
555
  .where(
406
556
  and(
407
- eq(tasks.workflowId, args.workflowId),
557
+ eq(tasks.workflowId, workflowId),
408
558
  inArray(tasks.status, ["running", "queued"])
409
559
  )
410
560
  );
@@ -419,27 +569,71 @@ export function workflowTools(ctx: ToolContext) {
419
569
  status: "draft",
420
570
  updatedAt: new Date(),
421
571
  })
422
- .where(eq(workflows.id, args.workflowId));
572
+ .where(eq(workflows.id, workflowId));
423
573
  }
424
574
 
425
575
  // Atomic claim: set to active
426
576
  await db
427
577
  .update(workflows)
428
578
  .set({ status: "active", updatedAt: new Date() })
429
- .where(eq(workflows.id, args.workflowId));
579
+ .where(eq(workflows.id, workflowId));
430
580
 
431
581
  // Fire-and-forget
432
582
  const { executeWorkflow } = await import("@/lib/workflows/engine");
433
- executeWorkflow(args.workflowId).catch(() => {});
583
+ executeWorkflow(workflowId).catch(() => {});
434
584
 
435
- ctx.onToolResult?.("execute_workflow", { id: args.workflowId, name: workflow.name });
436
- return ok({ message: "Workflow execution started", workflowId: args.workflowId, name: workflow.name });
585
+ ctx.onToolResult?.("execute_workflow", { id: workflowId, name: workflow.name });
586
+ return ok({ message: "Workflow execution started", workflowId, name: workflow.name });
437
587
  } catch (e) {
438
588
  return err(e instanceof Error ? e.message : "Failed to execute workflow");
439
589
  }
440
590
  }
441
591
  ),
442
592
 
593
+ defineTool(
594
+ "resume_workflow",
595
+ "Resume a workflow that is paused at a delay step, immediately skipping the remaining delay. Use when the user says 'resume now' or 'skip the wait' for a paused workflow. Only works if the workflow status is 'paused' — a 409 response means the scheduler already resumed it. Requires approval.",
596
+ {
597
+ workflowId: z.string().describe("The workflow ID to resume"),
598
+ },
599
+ async (args) => {
600
+ try {
601
+ const resolved = await resolveEntityId(workflows, workflows.id, args.workflowId);
602
+ if ("error" in resolved) return err(resolved.error);
603
+ const workflowId = resolved.id;
604
+
605
+ const workflow = db
606
+ .select()
607
+ .from(workflows)
608
+ .where(eq(workflows.id, workflowId))
609
+ .get();
610
+
611
+ if (!workflow) return err(`Workflow not found: ${workflowId}`);
612
+
613
+ if (workflow.status !== "paused") {
614
+ return err(
615
+ `Workflow is not paused (current status: ${workflow.status}). Only paused workflows can be resumed.`,
616
+ );
617
+ }
618
+
619
+ const { resumeWorkflow } = await import("@/lib/workflows/engine");
620
+ // Fire-and-forget: resumeWorkflow performs atomic status transition internally.
621
+ resumeWorkflow(workflowId).catch((error) => {
622
+ console.error(`Workflow ${workflowId} resume failed:`, error);
623
+ });
624
+
625
+ ctx.onToolResult?.("resume_workflow", { id: workflowId, name: workflow.name });
626
+ return ok({
627
+ message: "Workflow resume dispatched",
628
+ workflowId,
629
+ name: workflow.name,
630
+ });
631
+ } catch (e) {
632
+ return err(e instanceof Error ? e.message : "Failed to resume workflow");
633
+ }
634
+ },
635
+ ),
636
+
443
637
  defineTool(
444
638
  "get_workflow_status",
445
639
  "Get the current execution status of a workflow, including step-by-step progress.",
@@ -448,13 +642,17 @@ export function workflowTools(ctx: ToolContext) {
448
642
  },
449
643
  async (args) => {
450
644
  try {
451
- const workflow = await db
645
+ const resolved = await resolveEntityId(workflows, workflows.id, args.workflowId);
646
+ if ("error" in resolved) return err(resolved.error);
647
+ const workflowId = resolved.id;
648
+
649
+ const workflow = db
452
650
  .select()
453
651
  .from(workflows)
454
- .where(eq(workflows.id, args.workflowId))
652
+ .where(eq(workflows.id, workflowId))
455
653
  .get();
456
654
 
457
- if (!workflow) return err(`Workflow not found: ${args.workflowId}`);
655
+ if (!workflow) return err(`Workflow not found: ${workflowId}`);
458
656
 
459
657
  const { parseWorkflowState } = await import("@/lib/workflows/engine");
460
658
  const { definition, state } = parseWorkflowState(workflow.definition);
@@ -531,11 +729,15 @@ export function workflowTools(ctx: ToolContext) {
531
729
  }
532
730
 
533
731
  if (args.sourceWorkflowId) {
732
+ const resolvedSrc = await resolveEntityId(workflows, workflows.id, args.sourceWorkflowId);
733
+ if ("error" in resolvedSrc) return err(resolvedSrc.error);
734
+ const srcWorkflowId = resolvedSrc.id;
735
+
534
736
  // Find task IDs belonging to the source workflow
535
737
  const workflowTasks = await db
536
738
  .select({ id: tasks.id })
537
739
  .from(tasks)
538
- .where(eq(tasks.workflowId, args.sourceWorkflowId));
740
+ .where(eq(tasks.workflowId, srcWorkflowId));
539
741
 
540
742
  const taskIds = workflowTasks.map((t) => t.id);
541
743
  if (taskIds.length > 0) {