stagent 0.9.2 → 0.9.5

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 (50) hide show
  1. package/dist/cli.js +36 -1
  2. package/docs/superpowers/specs/2026-04-06-workflow-intelligence-stack-design.md +388 -0
  3. package/package.json +1 -1
  4. package/src/app/api/license/route.ts +3 -2
  5. package/src/app/api/workflows/[id]/debug/route.ts +18 -0
  6. package/src/app/api/workflows/[id]/execute/route.ts +39 -8
  7. package/src/app/api/workflows/optimize/route.ts +30 -0
  8. package/src/app/layout.tsx +4 -2
  9. package/src/components/chat/chat-message-markdown.tsx +78 -3
  10. package/src/components/chat/chat-message.tsx +12 -4
  11. package/src/components/settings/cloud-account-section.tsx +14 -12
  12. package/src/components/workflows/error-timeline.tsx +83 -0
  13. package/src/components/workflows/step-live-metrics.tsx +182 -0
  14. package/src/components/workflows/step-progress-bar.tsx +77 -0
  15. package/src/components/workflows/workflow-debug-panel.tsx +192 -0
  16. package/src/components/workflows/workflow-optimizer-panel.tsx +227 -0
  17. package/src/lib/agents/claude-agent.ts +4 -4
  18. package/src/lib/agents/runtime/anthropic-direct.ts +3 -3
  19. package/src/lib/agents/runtime/catalog.ts +30 -1
  20. package/src/lib/agents/runtime/openai-direct.ts +3 -3
  21. package/src/lib/billing/products.ts +6 -6
  22. package/src/lib/book/chapter-mapping.ts +6 -0
  23. package/src/lib/book/content.ts +10 -0
  24. package/src/lib/book/reading-paths.ts +1 -1
  25. package/src/lib/chat/__tests__/engine-stream-helpers.test.ts +57 -0
  26. package/src/lib/chat/engine.ts +68 -7
  27. package/src/lib/chat/stagent-tools.ts +2 -0
  28. package/src/lib/chat/tools/runtime-tools.ts +28 -0
  29. package/src/lib/chat/tools/schedule-tools.ts +44 -1
  30. package/src/lib/chat/tools/settings-tools.ts +40 -10
  31. package/src/lib/chat/tools/workflow-tools.ts +93 -4
  32. package/src/lib/chat/types.ts +21 -0
  33. package/src/lib/data/clear.ts +3 -0
  34. package/src/lib/db/bootstrap.ts +38 -0
  35. package/src/lib/db/migrations/0022_workflow_intelligence_phase1.sql +5 -0
  36. package/src/lib/db/migrations/0023_add_execution_stats.sql +15 -0
  37. package/src/lib/db/schema.ts +41 -1
  38. package/src/lib/license/__tests__/manager.test.ts +64 -0
  39. package/src/lib/license/manager.ts +80 -25
  40. package/src/lib/schedules/__tests__/interval-parser.test.ts +87 -0
  41. package/src/lib/schedules/__tests__/prompt-analyzer.test.ts +51 -0
  42. package/src/lib/schedules/interval-parser.ts +187 -0
  43. package/src/lib/schedules/prompt-analyzer.ts +87 -0
  44. package/src/lib/schedules/scheduler.ts +179 -9
  45. package/src/lib/workflows/cost-estimator.ts +141 -0
  46. package/src/lib/workflows/engine.ts +245 -45
  47. package/src/lib/workflows/error-analysis.ts +249 -0
  48. package/src/lib/workflows/execution-stats.ts +252 -0
  49. package/src/lib/workflows/optimizer.ts +193 -0
  50. package/src/lib/workflows/types.ts +6 -0
@@ -24,6 +24,10 @@ import {
24
24
  buildWorkflowDocumentContext,
25
25
  buildPoolDocumentContext,
26
26
  } from "@/lib/documents/context-builder";
27
+ import { resolveStepBudget, estimateWorkflowCost } from "./cost-estimator";
28
+ import { resolveAgentRuntime } from "@/lib/agents/runtime/catalog";
29
+ import { getSetting } from "@/lib/settings/helpers";
30
+ import { updateExecutionStats } from "./execution-stats";
27
31
 
28
32
  /**
29
33
  * Execute a workflow by advancing through its steps according to the pattern.
@@ -43,6 +47,20 @@ export async function executeWorkflow(workflowId: string): Promise<void> {
43
47
  // Extract parent task ID for document context propagation to child steps
44
48
  const parentTaskId: string | undefined = definition.sourceTaskId ?? undefined;
45
49
 
50
+ // Pre-flight cost estimation — advisory, never blocks execution
51
+ try {
52
+ const costEstimate = await estimateWorkflowCost(workflowId);
53
+ state.costEstimate = costEstimate;
54
+ if (costEstimate.warnings.length > 0) {
55
+ console.warn(`[workflow-engine] Cost warnings for ${workflowId}:`, costEstimate.warnings);
56
+ }
57
+ } catch (err) {
58
+ console.error(`[workflow-engine] Cost estimation failed (non-blocking):`, err);
59
+ }
60
+
61
+ // Workflow-level runtime (stored on workflow row or system setting)
62
+ const workflowRuntimeId = workflow.runtimeId ?? undefined;
63
+
46
64
  await updateWorkflowState(workflowId, state, "active");
47
65
 
48
66
  await db.insert(agentLogs).values({
@@ -50,7 +68,12 @@ export async function executeWorkflow(workflowId: string): Promise<void> {
50
68
  taskId: null,
51
69
  agentType: "workflow-engine",
52
70
  event: "workflow_started",
53
- payload: JSON.stringify({ workflowId, pattern: definition.pattern }),
71
+ payload: JSON.stringify({
72
+ workflowId,
73
+ pattern: definition.pattern,
74
+ runtimeId: workflowRuntimeId ?? "default",
75
+ costEstimate: state.costEstimate,
76
+ }),
54
77
  timestamp: new Date(),
55
78
  });
56
79
 
@@ -84,6 +107,10 @@ export async function executeWorkflow(workflowId: string): Promise<void> {
84
107
  timestamp: new Date(),
85
108
  });
86
109
  } finally {
110
+ // Update execution stats — fire-and-forget, never breaks execution
111
+ updateExecutionStats(workflowId).catch((err) => {
112
+ console.error("[workflow-engine] Stats update failed:", err);
113
+ });
87
114
  // Close learning session — flush buffered proposals as batch notification
88
115
  await closeLearningSession(workflowId).catch((err) => {
89
116
  console.error("[workflow-engine] Failed to close learning session:", err);
@@ -95,19 +122,19 @@ export async function executeWorkflow(workflowId: string): Promise<void> {
95
122
  try {
96
123
  switch (definition.pattern) {
97
124
  case "sequence":
98
- await executeSequence(workflowId, definition, state, parentTaskId);
125
+ await executeSequence(workflowId, definition, state, parentTaskId, workflowRuntimeId);
99
126
  break;
100
127
  case "planner-executor":
101
- await executePlannerExecutor(workflowId, definition, state, parentTaskId);
128
+ await executePlannerExecutor(workflowId, definition, state, parentTaskId, workflowRuntimeId);
102
129
  break;
103
130
  case "checkpoint":
104
- await executeCheckpoint(workflowId, definition, state, parentTaskId);
131
+ await executeCheckpoint(workflowId, definition, state, parentTaskId, workflowRuntimeId);
105
132
  break;
106
133
  case "parallel":
107
- await executeParallel(workflowId, definition, state, parentTaskId);
134
+ await executeParallel(workflowId, definition, state, parentTaskId, workflowRuntimeId);
108
135
  break;
109
136
  case "swarm":
110
- await executeSwarm(workflowId, definition, state, parentTaskId);
137
+ await executeSwarm(workflowId, definition, state, parentTaskId, workflowRuntimeId);
111
138
  break;
112
139
  }
113
140
 
@@ -139,6 +166,10 @@ export async function executeWorkflow(workflowId: string): Promise<void> {
139
166
  timestamp: new Date(),
140
167
  });
141
168
  } finally {
169
+ // Update execution stats — fire-and-forget, never breaks execution
170
+ updateExecutionStats(workflowId).catch((err) => {
171
+ console.error("[workflow-engine] Stats update failed:", err);
172
+ });
142
173
  // Close learning session — flush buffered proposals as batch notification
143
174
  await closeLearningSession(workflowId).catch((err) => {
144
175
  console.error("[workflow-engine] Failed to close learning session:", err);
@@ -153,7 +184,8 @@ async function executeSequence(
153
184
  workflowId: string,
154
185
  definition: WorkflowDefinition,
155
186
  state: WorkflowState,
156
- parentTaskId?: string
187
+ parentTaskId?: string,
188
+ workflowRuntimeId?: string
157
189
  ): Promise<void> {
158
190
  let previousOutput = "";
159
191
 
@@ -174,7 +206,10 @@ async function executeSequence(
174
206
  state,
175
207
  step.assignedAgent,
176
208
  step.agentProfile,
177
- parentTaskId
209
+ parentTaskId,
210
+ step.budgetUsd,
211
+ step.runtimeId,
212
+ workflowRuntimeId
178
213
  );
179
214
 
180
215
  if (result.status === "failed") {
@@ -192,7 +227,8 @@ async function executePlannerExecutor(
192
227
  workflowId: string,
193
228
  definition: WorkflowDefinition,
194
229
  state: WorkflowState,
195
- parentTaskId?: string
230
+ parentTaskId?: string,
231
+ workflowRuntimeId?: string
196
232
  ): Promise<void> {
197
233
  if (definition.steps.length < 2) {
198
234
  throw new Error("Planner-Executor requires at least 2 steps (planner + executor)");
@@ -209,7 +245,10 @@ async function executePlannerExecutor(
209
245
  state,
210
246
  plannerStep.assignedAgent,
211
247
  plannerStep.agentProfile,
212
- parentTaskId
248
+ parentTaskId,
249
+ plannerStep.budgetUsd,
250
+ plannerStep.runtimeId,
251
+ workflowRuntimeId
213
252
  );
214
253
 
215
254
  if (planResult.status === "failed") {
@@ -230,7 +269,10 @@ async function executePlannerExecutor(
230
269
  state,
231
270
  step.assignedAgent,
232
271
  step.agentProfile,
233
- parentTaskId
272
+ parentTaskId,
273
+ step.budgetUsd,
274
+ step.runtimeId,
275
+ workflowRuntimeId
234
276
  );
235
277
 
236
278
  if (result.status === "failed") {
@@ -246,7 +288,8 @@ async function executeCheckpoint(
246
288
  workflowId: string,
247
289
  definition: WorkflowDefinition,
248
290
  state: WorkflowState,
249
- parentTaskId?: string
291
+ parentTaskId?: string,
292
+ workflowRuntimeId?: string
250
293
  ): Promise<void> {
251
294
  let previousOutput = "";
252
295
 
@@ -279,7 +322,10 @@ async function executeCheckpoint(
279
322
  state,
280
323
  step.assignedAgent,
281
324
  step.agentProfile,
282
- parentTaskId
325
+ parentTaskId,
326
+ step.budgetUsd,
327
+ step.runtimeId,
328
+ workflowRuntimeId
283
329
  );
284
330
 
285
331
  if (result.status === "failed") {
@@ -297,7 +343,8 @@ async function executeParallel(
297
343
  workflowId: string,
298
344
  definition: WorkflowDefinition,
299
345
  state: WorkflowState,
300
- parentTaskId?: string
346
+ parentTaskId?: string,
347
+ workflowRuntimeId?: string
301
348
  ): Promise<void> {
302
349
  const structure = getParallelWorkflowStructure(definition);
303
350
  if (!structure) {
@@ -356,6 +403,9 @@ async function executeParallel(
356
403
  stepState.result = undefined;
357
404
  });
358
405
 
406
+ const stepBudget = await resolveStepBudget(step);
407
+ const stepRuntime = await resolveStepRuntime(step.runtimeId, workflowRuntimeId);
408
+
359
409
  const result = await executeChildTask(
360
410
  workflowId,
361
411
  step.name,
@@ -363,7 +413,9 @@ async function executeParallel(
363
413
  step.assignedAgent,
364
414
  step.agentProfile,
365
415
  parentTaskId,
366
- step.id
416
+ step.id,
417
+ stepBudget,
418
+ stepRuntime
367
419
  );
368
420
 
369
421
  const completedAt = new Date().toISOString();
@@ -431,6 +483,9 @@ async function executeParallel(
431
483
  synthesisPrompt: synthesisStep.prompt,
432
484
  });
433
485
 
486
+ const synthesisBudget = await resolveStepBudget(synthesisStep);
487
+ const synthesisRuntime = await resolveStepRuntime(synthesisStep.runtimeId, workflowRuntimeId);
488
+
434
489
  const synthesisResult = await executeChildTask(
435
490
  workflowId,
436
491
  synthesisStep.name,
@@ -438,7 +493,9 @@ async function executeParallel(
438
493
  synthesisStep.assignedAgent,
439
494
  synthesisStep.agentProfile,
440
495
  parentTaskId,
441
- synthesisStep.id
496
+ synthesisStep.id,
497
+ synthesisBudget,
498
+ synthesisRuntime
442
499
  );
443
500
 
444
501
  await commitState((draft) => {
@@ -474,7 +531,8 @@ async function executeSwarm(
474
531
  workflowId: string,
475
532
  definition: WorkflowDefinition,
476
533
  state: WorkflowState,
477
- parentTaskId?: string
534
+ parentTaskId?: string,
535
+ workflowRuntimeId?: string
478
536
  ): Promise<void> {
479
537
  const structure = getSwarmWorkflowStructure(definition);
480
538
  if (!structure) {
@@ -501,7 +559,10 @@ async function executeSwarm(
501
559
  state,
502
560
  mayorStep.assignedAgent,
503
561
  mayorStep.agentProfile,
504
- parentTaskId
562
+ parentTaskId,
563
+ mayorStep.budgetUsd,
564
+ mayorStep.runtimeId,
565
+ workflowRuntimeId
505
566
  );
506
567
 
507
568
  if (mayorResult.status === "failed") {
@@ -558,6 +619,9 @@ async function executeSwarm(
558
619
  stepState.result = undefined;
559
620
  });
560
621
 
622
+ const workerBudget = await resolveStepBudget(step);
623
+ const workerRuntime = await resolveStepRuntime(step.runtimeId, workflowRuntimeId);
624
+
561
625
  const result = await executeChildTask(
562
626
  workflowId,
563
627
  step.name,
@@ -565,7 +629,9 @@ async function executeSwarm(
565
629
  step.assignedAgent,
566
630
  step.agentProfile,
567
631
  parentTaskId,
568
- step.id
632
+ step.id,
633
+ workerBudget,
634
+ workerRuntime
569
635
  );
570
636
 
571
637
  const completedAt = new Date().toISOString();
@@ -618,6 +684,7 @@ async function executeSwarm(
618
684
  result: worker.result.result ?? "",
619
685
  })),
620
686
  parentTaskId,
687
+ workflowRuntimeId,
621
688
  });
622
689
  }
623
690
 
@@ -648,10 +715,13 @@ async function runSwarmRefinery(input: {
648
715
  prompt: string;
649
716
  assignedAgent?: string;
650
717
  agentProfile?: string;
718
+ budgetUsd?: number;
719
+ runtimeId?: string;
651
720
  };
652
721
  refineryIndex: number;
653
722
  workerOutputs: Array<{ stepName: string; result: string }>;
654
723
  parentTaskId?: string;
724
+ workflowRuntimeId?: string;
655
725
  }): Promise<void> {
656
726
  const {
657
727
  workflowId,
@@ -662,6 +732,7 @@ async function runSwarmRefinery(input: {
662
732
  refineryIndex,
663
733
  workerOutputs,
664
734
  parentTaskId,
735
+ workflowRuntimeId,
665
736
  } = input;
666
737
 
667
738
  state.currentStepIndex = refineryIndex;
@@ -680,6 +751,9 @@ async function runSwarmRefinery(input: {
680
751
  refineryPrompt: refineryStep.prompt,
681
752
  });
682
753
 
754
+ const refineryBudget = await resolveStepBudget(refineryStep as import("./types").WorkflowStep);
755
+ const refineryRuntime = await resolveStepRuntime(refineryStep.runtimeId, workflowRuntimeId);
756
+
683
757
  const refineryResult = await executeChildTask(
684
758
  workflowId,
685
759
  refineryStep.name,
@@ -687,7 +761,9 @@ async function runSwarmRefinery(input: {
687
761
  refineryStep.assignedAgent,
688
762
  refineryStep.agentProfile,
689
763
  parentTaskId,
690
- refineryStep.id
764
+ refineryStep.id,
765
+ refineryBudget,
766
+ refineryRuntime
691
767
  );
692
768
 
693
769
  refineryState.taskId = refineryResult.taskId;
@@ -713,6 +789,26 @@ async function runSwarmRefinery(input: {
713
789
  }
714
790
  }
715
791
 
792
+ /**
793
+ * Resolve the runtime for a workflow step.
794
+ *
795
+ * Precedence (highest wins):
796
+ * 1. step.runtimeId (per-step override)
797
+ * 2. workflow.runtimeId (per-workflow)
798
+ * 3. routing.preference setting
799
+ * 4. DEFAULT_AGENT_RUNTIME
800
+ */
801
+ async function resolveStepRuntime(
802
+ stepRuntimeId?: string,
803
+ workflowRuntimeId?: string
804
+ ): Promise<string | undefined> {
805
+ if (stepRuntimeId) return resolveAgentRuntime(stepRuntimeId);
806
+ if (workflowRuntimeId) return resolveAgentRuntime(workflowRuntimeId);
807
+ const routingPref = await getSetting("default_runtime");
808
+ if (routingPref) return resolveAgentRuntime(routingPref);
809
+ return undefined; // Let executeTaskWithRuntime use its own default
810
+ }
811
+
716
812
  /**
717
813
  * Create and execute a child task, returning its result.
718
814
  * Shared by step-based patterns and the loop executor.
@@ -724,7 +820,9 @@ export async function executeChildTask(
724
820
  assignedAgent?: string,
725
821
  agentProfile?: string,
726
822
  parentTaskId?: string,
727
- stepId?: string
823
+ stepId?: string,
824
+ maxBudgetUsd?: number,
825
+ runtimeId?: string
728
826
  ): Promise<{ taskId: string; status: string; result?: string; error?: string }> {
729
827
  const [workflow] = await db
730
828
  .select()
@@ -766,6 +864,7 @@ export async function executeChildTask(
766
864
  assignedAgent: assignedAgent ?? null,
767
865
  agentProfile: resolvedProfile ?? null,
768
866
  workflowRunNumber: workflow?.runNumber ?? null,
867
+ maxBudgetUsd: maxBudgetUsd ?? null,
769
868
  createdAt: new Date(),
770
869
  updatedAt: new Date(),
771
870
  });
@@ -776,9 +875,18 @@ export async function executeChildTask(
776
875
  .where(eq(tasks.id, taskId));
777
876
 
778
877
  try {
779
- await executeTaskWithRuntime(taskId);
878
+ await executeTaskWithRuntime(taskId, runtimeId);
780
879
  } catch (err) {
781
880
  console.error(`[workflow-engine] Runtime execution failed for task ${taskId}:`, err);
881
+ // Mark task as failed in DB so the status check below correctly detects failure
882
+ await db
883
+ .update(tasks)
884
+ .set({
885
+ status: "failed",
886
+ result: err instanceof Error ? err.message : String(err),
887
+ updatedAt: new Date(),
888
+ })
889
+ .where(eq(tasks.id, taskId));
782
890
  }
783
891
 
784
892
  const [completedTask] = await db
@@ -798,6 +906,11 @@ export async function executeChildTask(
798
906
 
799
907
  /**
800
908
  * Execute a single workflow step by creating a task and waiting for completion.
909
+ *
910
+ * State write is deferred until after task creation succeeds (Feature 3 fix).
911
+ * On failure, step state is explicitly rolled back to "failed".
912
+ *
913
+ * @param promptOverride — optional modified prompt (e.g., with context from previous step)
801
914
  */
802
915
  async function executeStep(
803
916
  workflowId: string,
@@ -807,36 +920,111 @@ async function executeStep(
807
920
  state: WorkflowState,
808
921
  assignedAgent?: string,
809
922
  agentProfile?: string,
810
- parentTaskId?: string
923
+ parentTaskId?: string,
924
+ stepBudgetUsd?: number,
925
+ stepRuntimeId?: string,
926
+ workflowRuntimeId?: string
811
927
  ): Promise<StepState> {
812
928
  const stepState = state.stepStates.find((s) => s.stepId === stepId);
813
929
  if (!stepState) throw new Error(`Step ${stepId} not found in state`);
814
930
 
931
+ // Set in-memory only — do NOT persist "running" until task exists (deferred write)
815
932
  stepState.status = "running";
816
933
  stepState.startedAt = new Date().toISOString();
817
- await updateWorkflowState(workflowId, state, "active");
818
934
 
819
- const result = await executeChildTask(
820
- workflowId,
821
- stepName,
822
- prompt,
823
- assignedAgent,
824
- agentProfile,
825
- parentTaskId,
826
- stepId
827
- );
935
+ // Log step_started event for live execution dashboard
936
+ await db.insert(agentLogs).values({
937
+ id: crypto.randomUUID(),
938
+ taskId: null,
939
+ agentType: "workflow-engine",
940
+ event: "step_started",
941
+ payload: JSON.stringify({ workflowId, stepId, stepName, stepIndex: state.currentStepIndex }),
942
+ timestamp: new Date(),
943
+ });
828
944
 
829
- stepState.taskId = result.taskId;
830
- if (result.status === "completed") {
831
- stepState.status = "completed";
832
- stepState.result = result.result ?? "";
833
- stepState.completedAt = new Date().toISOString();
834
- } else {
945
+ try {
946
+ // Resolve per-step budget and runtime
947
+ const budgetUsd = await resolveStepBudget(
948
+ stepBudgetUsd ? ({ budgetUsd: stepBudgetUsd } as import("./types").WorkflowStep) : undefined
949
+ );
950
+ const resolvedRuntime = await resolveStepRuntime(stepRuntimeId, workflowRuntimeId);
951
+
952
+ const result = await executeChildTask(
953
+ workflowId,
954
+ stepName,
955
+ prompt,
956
+ assignedAgent,
957
+ agentProfile,
958
+ parentTaskId,
959
+ stepId,
960
+ budgetUsd,
961
+ resolvedRuntime
962
+ );
963
+
964
+ stepState.taskId = result.taskId;
965
+ if (result.status === "completed") {
966
+ stepState.status = "completed";
967
+ stepState.result = result.result ?? "";
968
+ stepState.completedAt = new Date().toISOString();
969
+
970
+ // Log step_completed event for live execution dashboard
971
+ await db.insert(agentLogs).values({
972
+ id: crypto.randomUUID(),
973
+ taskId: result.taskId,
974
+ agentType: "workflow-engine",
975
+ event: "step_completed",
976
+ payload: JSON.stringify({ workflowId, stepId, stepName, stepIndex: state.currentStepIndex }),
977
+ timestamp: new Date(),
978
+ });
979
+ } else {
980
+ stepState.status = "failed";
981
+ stepState.error = result.error ?? "Task did not complete successfully";
982
+
983
+ // Log step_failed event for live execution dashboard
984
+ await db.insert(agentLogs).values({
985
+ id: crypto.randomUUID(),
986
+ taskId: result.taskId,
987
+ agentType: "workflow-engine",
988
+ event: "step_failed",
989
+ payload: JSON.stringify({
990
+ workflowId,
991
+ stepId,
992
+ stepName,
993
+ stepIndex: state.currentStepIndex,
994
+ error: result.error ?? "Task did not complete successfully",
995
+ }),
996
+ timestamp: new Date(),
997
+ });
998
+ }
999
+
1000
+ // Now safe to persist — task exists and has a final status
1001
+ await updateWorkflowState(workflowId, state, "active");
1002
+ } catch (err) {
1003
+ // Explicit rollback on failure — step state reflects the error
835
1004
  stepState.status = "failed";
836
- stepState.error = result.error ?? "Task did not complete successfully";
1005
+ stepState.error = err instanceof Error ? err.message : String(err);
1006
+ stepState.completedAt = new Date().toISOString();
1007
+
1008
+ // Log step_failed event for live execution dashboard (catch path)
1009
+ await db.insert(agentLogs).values({
1010
+ id: crypto.randomUUID(),
1011
+ taskId: stepState.taskId ?? null,
1012
+ agentType: "workflow-engine",
1013
+ event: "step_failed",
1014
+ payload: JSON.stringify({
1015
+ workflowId,
1016
+ stepId,
1017
+ stepName,
1018
+ stepIndex: state.currentStepIndex,
1019
+ error: err instanceof Error ? err.message : String(err),
1020
+ }),
1021
+ timestamp: new Date(),
1022
+ });
1023
+
1024
+ await updateWorkflowState(workflowId, state, "active");
1025
+ throw err; // Propagate — don't swallow
837
1026
  }
838
1027
 
839
- await updateWorkflowState(workflowId, state, "active");
840
1028
  return stepState;
841
1029
  }
842
1030
 
@@ -911,7 +1099,7 @@ export async function updateWorkflowState(
911
1099
  .from(workflows)
912
1100
  .where(eq(workflows.id, workflowId));
913
1101
 
914
- if (!workflow) return;
1102
+ if (!workflow) throw new Error(`Workflow ${workflowId} not found — cannot update state`);
915
1103
 
916
1104
  const definition = JSON.parse(workflow.definition);
917
1105
  const combined = { ...definition, _state: state };
@@ -981,6 +1169,7 @@ export async function retryWorkflowStep(
981
1169
 
982
1170
  // Re-execute from this step
983
1171
  const step = definition.steps[stepIndex];
1172
+ const workflowRtId = workflow.runtimeId ?? undefined;
984
1173
  const result = await executeStep(
985
1174
  workflowId,
986
1175
  step.id,
@@ -988,7 +1177,11 @@ export async function retryWorkflowStep(
988
1177
  step.prompt,
989
1178
  state,
990
1179
  step.assignedAgent,
991
- step.agentProfile
1180
+ step.agentProfile,
1181
+ undefined,
1182
+ step.budgetUsd,
1183
+ step.runtimeId,
1184
+ workflowRtId
992
1185
  );
993
1186
 
994
1187
  if (result.status === "completed") {
@@ -1006,7 +1199,11 @@ export async function retryWorkflowStep(
1006
1199
  contextPrompt,
1007
1200
  state,
1008
1201
  nextStep.assignedAgent,
1009
- nextStep.agentProfile
1202
+ nextStep.agentProfile,
1203
+ undefined,
1204
+ nextStep.budgetUsd,
1205
+ nextStep.runtimeId,
1206
+ workflowRtId
1010
1207
  );
1011
1208
  if (nextResult.status === "failed") break;
1012
1209
  previousOutput = nextResult.result ?? "";
@@ -1121,7 +1318,10 @@ async function retrySwarmStep(
1121
1318
  }),
1122
1319
  state,
1123
1320
  targetStep.assignedAgent,
1124
- targetStep.agentProfile
1321
+ targetStep.agentProfile,
1322
+ undefined,
1323
+ targetStep.budgetUsd,
1324
+ targetStep.runtimeId
1125
1325
  );
1126
1326
 
1127
1327
  if (retriedWorker.status !== "completed") {