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
@@ -1,9 +1,22 @@
1
1
  import { db } from "@/lib/db";
2
2
  import { workflows, agentLogs } from "@/lib/db/schema";
3
3
  import { eq } from "drizzle-orm";
4
- import { executeChildTask, updateWorkflowState } from "./engine";
5
- import type { WorkflowDefinition, LoopState, IterationState, LoopStopReason } from "./types";
4
+ import { executeChildTask } from "./engine";
5
+ import type {
6
+ WorkflowDefinition,
7
+ LoopState,
8
+ IterationState,
9
+ LoopStopReason,
10
+ WorkflowEnrichmentTargetContract,
11
+ } from "./types";
6
12
  import { createInitialLoopState } from "./types";
13
+ import {
14
+ resolvePostAction,
15
+ shouldSkipPostActionValue,
16
+ extractPostActionValue,
17
+ } from "./post-action";
18
+ import { updateRow } from "@/lib/data/tables";
19
+ import { normalizeEnrichmentOutput } from "@/lib/tables/enrichment-planner";
7
20
 
8
21
  /**
9
22
  * Execute the loop pattern — autonomous iteration with stop conditions.
@@ -27,8 +40,18 @@ export async function executeLoop(
27
40
  assignedAgent,
28
41
  agentProfile,
29
42
  completionSignals,
43
+ items,
44
+ itemVariable,
30
45
  } = definition.loopConfig;
31
- const loopPrompt = definition.steps[0].prompt;
46
+
47
+ // Row-driven loop: iterate exactly once per item (capped at maxIterations).
48
+ // Items array presence flips the loop into a finite fan-out pattern.
49
+ const isRowDriven = Array.isArray(items);
50
+ const rowItems = isRowDriven ? (items as unknown[]) : [];
51
+ const boundVarName = itemVariable && itemVariable.length > 0 ? itemVariable : "item";
52
+ const effectiveMax = isRowDriven
53
+ ? Math.min(rowItems.length, maxIterations)
54
+ : maxIterations;
32
55
 
33
56
  // Restore existing state (resume) or create fresh
34
57
  const loopState = await restoreOrCreateLoopState(workflowId);
@@ -49,7 +72,7 @@ export async function executeLoop(
49
72
  }
50
73
 
51
74
  try {
52
- while (loopState.currentIteration < maxIterations) {
75
+ while (loopState.currentIteration < effectiveMax) {
53
76
  // Check pause: re-fetch workflow status from DB
54
77
  const [workflow] = await db
55
78
  .select()
@@ -81,14 +104,6 @@ export async function executeLoop(
81
104
 
82
105
  const iterationNum = loopState.currentIteration + 1;
83
106
 
84
- // Build iteration prompt
85
- const prompt = buildIterationPrompt(
86
- loopPrompt,
87
- previousOutput,
88
- iterationNum,
89
- maxIterations
90
- );
91
-
92
107
  // Create iteration state
93
108
  const iterationState: IterationState = {
94
109
  iteration: iterationNum,
@@ -112,14 +127,29 @@ export async function executeLoop(
112
127
  timestamp: new Date(),
113
128
  });
114
129
 
115
- // Execute child task
116
- const result = await executeChildTask(
117
- workflowId,
118
- `Loop Iteration ${iterationNum}`,
119
- prompt,
120
- assignedAgent ?? definition.steps[0].assignedAgent,
121
- agentProfile ?? definition.steps[0].agentProfile
122
- );
130
+ const result = isRowDriven
131
+ ? await executeRowDrivenIteration({
132
+ workflowId,
133
+ definition,
134
+ row: rowItems[loopState.currentIteration],
135
+ itemVariable: boundVarName,
136
+ iteration: iterationNum,
137
+ totalRows: effectiveMax,
138
+ loopAssignedAgent: assignedAgent,
139
+ loopAgentProfile: agentProfile,
140
+ })
141
+ : await executeChildTask(
142
+ workflowId,
143
+ `Loop Iteration ${iterationNum}`,
144
+ buildIterationPrompt(
145
+ definition.steps[0].prompt,
146
+ previousOutput,
147
+ iterationNum,
148
+ maxIterations
149
+ ),
150
+ assignedAgent ?? definition.steps[0].assignedAgent,
151
+ agentProfile ?? definition.steps[0].agentProfile
152
+ );
123
153
 
124
154
  // Update iteration state
125
155
  const iterStartTime = new Date(iterationState.startedAt!).getTime();
@@ -130,19 +160,29 @@ export async function executeLoop(
130
160
  if (result.status === "completed") {
131
161
  iterationState.status = "completed";
132
162
  iterationState.result = result.result;
133
- previousOutput = result.result ?? "";
163
+ if (!isRowDriven) {
164
+ previousOutput = result.result ?? "";
165
+ }
134
166
  } else {
135
167
  iterationState.status = "failed";
136
168
  iterationState.error = result.error;
137
- await finalizeLoop(workflowId, loopState, "error");
138
- return;
169
+ loopState.currentIteration = iterationNum;
170
+ await updateLoopState(workflowId, loopState, "active");
171
+ if (!isRowDriven) {
172
+ await finalizeLoop(workflowId, loopState, "error");
173
+ return;
174
+ }
175
+ continue;
139
176
  }
140
177
 
141
178
  loopState.currentIteration = iterationNum;
142
179
  await updateLoopState(workflowId, loopState, "active");
143
180
 
144
- // Check completion signal
181
+ // Check completion signal — only for autonomous loops. Row-driven
182
+ // loops always run through every item; per-row completion text like
183
+ // "NOT_FOUND" must not abort the fan-out.
145
184
  if (
185
+ !isRowDriven &&
146
186
  result.result &&
147
187
  detectCompletionSignal(result.result, completionSignals)
148
188
  ) {
@@ -164,6 +204,62 @@ export async function executeLoop(
164
204
  }
165
205
  }
166
206
 
207
+ /**
208
+ * Build the prompt for a single row-driven iteration.
209
+ *
210
+ * Row-driven loops fan out one iteration per item in `loopConfig.items` and
211
+ * stop when items are exhausted (no LOOP_COMPLETE signal needed). The row
212
+ * payload is serialized as JSON under the bound variable name so the agent
213
+ * can read every field without us pre-committing to a templating syntax.
214
+ */
215
+ export function buildRowIterationPrompt(
216
+ template: string,
217
+ row: unknown,
218
+ itemVariable: string,
219
+ iteration: number,
220
+ totalRows: number,
221
+ previousStepOutput: string,
222
+ stepOutputs: Record<string, string>
223
+ ): string {
224
+ const resolvedTemplate = resolveRowTemplate(template, {
225
+ [itemVariable]: row,
226
+ previous: previousStepOutput,
227
+ stepOutputs,
228
+ });
229
+ const parts: string[] = [];
230
+ parts.push(`Row ${iteration} of ${totalRows}.`);
231
+ parts.push(
232
+ `\nCurrent ${itemVariable}:\n\`\`\`json\n${JSON.stringify(row, null, 2)}\n\`\`\``
233
+ );
234
+ if (previousStepOutput) {
235
+ parts.push(`\nPrevious step output:\n${previousStepOutput}`);
236
+ }
237
+ parts.push(`\n---\n\n${resolvedTemplate}`);
238
+ return parts.join("");
239
+ }
240
+
241
+ function resolveRowTemplate(
242
+ template: string,
243
+ context: Record<string, unknown>
244
+ ): string {
245
+ return template.replace(/\{\{\s*([^}]+)\s*\}\}/g, (_match, path: string) => {
246
+ const value = readContextPath(context, path.trim());
247
+ if (value === undefined || value === null) return "";
248
+ return typeof value === "string" ? value : JSON.stringify(value);
249
+ });
250
+ }
251
+
252
+ function readContextPath(value: unknown, path: string): unknown {
253
+ const parts = path.split(".");
254
+ let current = value;
255
+ for (const part of parts) {
256
+ if (current === null || current === undefined) return undefined;
257
+ if (typeof current !== "object") return undefined;
258
+ current = (current as Record<string, unknown>)[part];
259
+ }
260
+ return current;
261
+ }
262
+
167
263
  /**
168
264
  * Build the prompt for a single iteration, including previous output context.
169
265
  */
@@ -253,6 +349,235 @@ async function restoreOrCreateLoopState(
253
349
  return createInitialLoopState();
254
350
  }
255
351
 
352
+ /**
353
+ * Apply a `postAction` for a single row-driven iteration. Resolves any
354
+ * `{{row.field}}` placeholders, runs the skip rules, and writes the value
355
+ * via `updateRow`. Every outcome is logged to `agent_logs` so enrichment
356
+ * runs are auditable end-to-end. Errors are caught and logged — never
357
+ * thrown — so a single bad row can't abort the fan-out.
358
+ */
359
+ async function applyRowPostAction(params: {
360
+ workflowId: string;
361
+ taskId: string;
362
+ postAction: NonNullable<import("./types").WorkflowStep["postAction"]>;
363
+ row: unknown;
364
+ itemVariable: string;
365
+ taskResult: string;
366
+ targetContract?: WorkflowEnrichmentTargetContract;
367
+ }): Promise<void> {
368
+ const {
369
+ workflowId,
370
+ taskId,
371
+ postAction,
372
+ row,
373
+ itemVariable,
374
+ taskResult,
375
+ targetContract,
376
+ } = params;
377
+
378
+ try {
379
+ if (postAction.type !== "update_row") {
380
+ // Future-proofing: unknown variants log + return rather than throwing.
381
+ await db.insert(agentLogs).values({
382
+ id: crypto.randomUUID(),
383
+ taskId,
384
+ agentType: "loop-executor",
385
+ event: "post_action_unknown_type",
386
+ payload: JSON.stringify({ workflowId, postAction }),
387
+ timestamp: new Date(),
388
+ });
389
+ return;
390
+ }
391
+
392
+ const resolved = resolvePostAction(postAction, row, itemVariable);
393
+ const rawValue = extractPostActionValue(taskResult);
394
+
395
+ if (!targetContract && shouldSkipPostActionValue(rawValue)) {
396
+ await db.insert(agentLogs).values({
397
+ id: crypto.randomUUID(),
398
+ taskId,
399
+ agentType: "loop-executor",
400
+ event: "post_action_skipped",
401
+ payload: JSON.stringify({
402
+ workflowId,
403
+ rowId: resolved.rowId,
404
+ column: resolved.column,
405
+ reason: rawValue.trim() === "" ? "empty" : "not_found",
406
+ }),
407
+ timestamp: new Date(),
408
+ });
409
+ return;
410
+ }
411
+
412
+ const normalized = targetContract
413
+ ? normalizeEnrichmentOutput(rawValue, targetContract)
414
+ : { kind: "valid" as const, value: rawValue };
415
+
416
+ if (normalized.kind === "skip") {
417
+ await db.insert(agentLogs).values({
418
+ id: crypto.randomUUID(),
419
+ taskId,
420
+ agentType: "loop-executor",
421
+ event: "post_action_skipped",
422
+ payload: JSON.stringify({
423
+ workflowId,
424
+ rowId: resolved.rowId,
425
+ column: resolved.column,
426
+ reason: normalized.reason,
427
+ }),
428
+ timestamp: new Date(),
429
+ });
430
+ return;
431
+ }
432
+
433
+ if (normalized.kind === "invalid") {
434
+ await db.insert(agentLogs).values({
435
+ id: crypto.randomUUID(),
436
+ taskId,
437
+ agentType: "loop-executor",
438
+ event: "post_action_contract_invalid",
439
+ payload: JSON.stringify({
440
+ workflowId,
441
+ rowId: resolved.rowId,
442
+ column: resolved.column,
443
+ error: normalized.reason,
444
+ rawValue,
445
+ }),
446
+ timestamp: new Date(),
447
+ });
448
+ return;
449
+ }
450
+
451
+ if (!resolved.rowId) {
452
+ await db.insert(agentLogs).values({
453
+ id: crypto.randomUUID(),
454
+ taskId,
455
+ agentType: "loop-executor",
456
+ event: "post_action_failed",
457
+ payload: JSON.stringify({
458
+ workflowId,
459
+ error: "rowId resolved to empty string — check postAction template",
460
+ postAction,
461
+ }),
462
+ timestamp: new Date(),
463
+ });
464
+ return;
465
+ }
466
+
467
+ const updated = await updateRow(resolved.rowId, {
468
+ data: { [resolved.column]: normalized.value },
469
+ });
470
+
471
+ await db.insert(agentLogs).values({
472
+ id: crypto.randomUUID(),
473
+ taskId,
474
+ agentType: "loop-executor",
475
+ event: updated ? "post_action_applied" : "post_action_failed",
476
+ payload: JSON.stringify({
477
+ workflowId,
478
+ rowId: resolved.rowId,
479
+ column: resolved.column,
480
+ tableId: resolved.tableId,
481
+ ...(updated ? {} : { error: "row not found" }),
482
+ }),
483
+ timestamp: new Date(),
484
+ });
485
+ } catch (err) {
486
+ // Never let postAction failures abort the loop iteration.
487
+ await db.insert(agentLogs).values({
488
+ id: crypto.randomUUID(),
489
+ taskId,
490
+ agentType: "loop-executor",
491
+ event: "post_action_failed",
492
+ payload: JSON.stringify({
493
+ workflowId,
494
+ error: err instanceof Error ? err.message : String(err),
495
+ }),
496
+ timestamp: new Date(),
497
+ }).catch(() => {});
498
+ }
499
+ }
500
+
501
+ async function executeRowDrivenIteration(params: {
502
+ workflowId: string;
503
+ definition: WorkflowDefinition;
504
+ row: unknown;
505
+ itemVariable: string;
506
+ iteration: number;
507
+ totalRows: number;
508
+ loopAssignedAgent?: string;
509
+ loopAgentProfile?: string;
510
+ }): Promise<{ taskId: string; status: string; result?: string; error?: string }> {
511
+ const {
512
+ workflowId,
513
+ definition,
514
+ row,
515
+ itemVariable,
516
+ iteration,
517
+ totalRows,
518
+ loopAssignedAgent,
519
+ loopAgentProfile,
520
+ } = params;
521
+
522
+ let previousStepOutput = "";
523
+ let lastTaskId = "";
524
+ const stepOutputs: Record<string, string> = {};
525
+
526
+ for (const step of definition.steps) {
527
+ const prompt = buildRowIterationPrompt(
528
+ step.prompt,
529
+ row,
530
+ itemVariable,
531
+ iteration,
532
+ totalRows,
533
+ previousStepOutput,
534
+ stepOutputs
535
+ );
536
+
537
+ const result = await executeChildTask(
538
+ workflowId,
539
+ `${step.name} · Row ${iteration}`,
540
+ prompt,
541
+ loopAssignedAgent ?? step.assignedAgent,
542
+ loopAgentProfile ?? step.agentProfile,
543
+ undefined,
544
+ step.id,
545
+ step.budgetUsd,
546
+ step.runtimeId
547
+ );
548
+ lastTaskId = result.taskId;
549
+
550
+ if (result.status !== "completed") {
551
+ return {
552
+ taskId: lastTaskId,
553
+ status: "failed",
554
+ error: `${step.name}: ${result.error ?? "Task did not complete successfully"}`,
555
+ };
556
+ }
557
+
558
+ previousStepOutput = result.result ?? "";
559
+ stepOutputs[step.id] = previousStepOutput;
560
+
561
+ if (step.postAction) {
562
+ await applyRowPostAction({
563
+ workflowId,
564
+ taskId: result.taskId,
565
+ postAction: step.postAction,
566
+ row,
567
+ itemVariable,
568
+ taskResult: previousStepOutput,
569
+ targetContract: definition.metadata?.enrichment?.targetContract,
570
+ });
571
+ }
572
+ }
573
+
574
+ return {
575
+ taskId: lastTaskId,
576
+ status: "completed",
577
+ result: previousStepOutput,
578
+ };
579
+ }
580
+
256
581
  /**
257
582
  * Finalize a loop with a stop reason and mark the workflow as completed.
258
583
  */
@@ -0,0 +1,91 @@
1
+ /**
2
+ * Post-step action helpers — pure logic for the declarative side-effect
3
+ * framework that bulk row enrichment uses to write agent results back into
4
+ * user table cells.
5
+ *
6
+ * Dispatch (the actual `updateRow` call) lives in the loop-executor where
7
+ * it has DB access; this module stays pure so the resolution + skip rules
8
+ * can be unit-tested without mocking DB.
9
+ *
10
+ * See features/bulk-row-enrichment.md.
11
+ */
12
+
13
+ import type { StepPostAction } from "./types";
14
+
15
+ /**
16
+ * Substitute `{{itemVariable.field}}` placeholders in a postAction definition
17
+ * against the current loop iteration's row. Supports nested paths via dotted
18
+ * field names (e.g. `{{row.meta.id}}`). Only `rowId` is templated today —
19
+ * `tableId` and `column` are static, and templating them would invite SQL
20
+ * surprises.
21
+ */
22
+ export function resolvePostAction(
23
+ action: StepPostAction,
24
+ row: unknown,
25
+ itemVariable: string
26
+ ): StepPostAction {
27
+ return {
28
+ ...action,
29
+ rowId: substituteRowPath(action.rowId, row, itemVariable),
30
+ };
31
+ }
32
+
33
+ /**
34
+ * Replace `{{itemVariable.path.to.field}}` with the value at that path on row.
35
+ * Multiple placeholders in the same string are all replaced. Missing paths
36
+ * resolve to an empty string (caller should validate the result).
37
+ */
38
+ function substituteRowPath(
39
+ template: string,
40
+ row: unknown,
41
+ itemVariable: string
42
+ ): string {
43
+ // Match {{<itemVariable>.<dotted.path>}} — escape itemVariable for safety
44
+ const escaped = itemVariable.replace(/[.*+?^${}()|[\]\\]/g, "\\$&");
45
+ const pattern = new RegExp(`\\{\\{\\s*${escaped}\\.([\\w.]+)\\s*\\}\\}`, "g");
46
+
47
+ return template.replace(pattern, (_match, path: string) => {
48
+ const value = readPath(row, path);
49
+ if (value === undefined || value === null) return "";
50
+ return String(value);
51
+ });
52
+ }
53
+
54
+ function readPath(obj: unknown, path: string): unknown {
55
+ const parts = path.split(".");
56
+ let current: unknown = obj;
57
+ for (const part of parts) {
58
+ if (current === null || current === undefined) return undefined;
59
+ if (typeof current !== "object") return undefined;
60
+ current = (current as Record<string, unknown>)[part];
61
+ }
62
+ return current;
63
+ }
64
+
65
+ /**
66
+ * Decide whether the agent's result should be written back to the cell, or
67
+ * skipped silently. We skip empty/whitespace-only results and the literal
68
+ * `NOT_FOUND` sentinel (case-insensitive) so an enrichment workflow can
69
+ * gracefully say "no value for this row" without overwriting a real value
70
+ * with garbage.
71
+ *
72
+ * Substring matches are intentionally NOT skipped — only the trimmed value
73
+ * being exactly `NOT_FOUND` triggers the skip. This avoids dropping a long
74
+ * answer that happens to mention the sentinel.
75
+ */
76
+ export function shouldSkipPostActionValue(value: string): boolean {
77
+ const trimmed = value.trim();
78
+ if (trimmed === "") return true;
79
+ if (trimmed.toUpperCase() === "NOT_FOUND") return true;
80
+ return false;
81
+ }
82
+
83
+ /**
84
+ * Pull the writable value out of a task result. Trims whitespace and tolerates
85
+ * undefined/null without throwing. The caller should run the result through
86
+ * `shouldSkipPostActionValue` before writing.
87
+ */
88
+ export function extractPostActionValue(result: string | undefined | null): string {
89
+ if (result === undefined || result === null) return "";
90
+ return result.trim();
91
+ }