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.
- package/dist/cli.js +36 -1
- package/docs/superpowers/specs/2026-04-06-workflow-intelligence-stack-design.md +388 -0
- package/package.json +1 -1
- package/src/app/api/license/route.ts +3 -2
- package/src/app/api/workflows/[id]/debug/route.ts +18 -0
- package/src/app/api/workflows/[id]/execute/route.ts +39 -8
- package/src/app/api/workflows/optimize/route.ts +30 -0
- package/src/app/layout.tsx +4 -2
- package/src/components/chat/chat-message-markdown.tsx +78 -3
- package/src/components/chat/chat-message.tsx +12 -4
- package/src/components/settings/cloud-account-section.tsx +14 -12
- package/src/components/workflows/error-timeline.tsx +83 -0
- package/src/components/workflows/step-live-metrics.tsx +182 -0
- package/src/components/workflows/step-progress-bar.tsx +77 -0
- package/src/components/workflows/workflow-debug-panel.tsx +192 -0
- package/src/components/workflows/workflow-optimizer-panel.tsx +227 -0
- package/src/lib/agents/claude-agent.ts +4 -4
- package/src/lib/agents/runtime/anthropic-direct.ts +3 -3
- package/src/lib/agents/runtime/catalog.ts +30 -1
- package/src/lib/agents/runtime/openai-direct.ts +3 -3
- package/src/lib/billing/products.ts +6 -6
- package/src/lib/book/chapter-mapping.ts +6 -0
- package/src/lib/book/content.ts +10 -0
- package/src/lib/book/reading-paths.ts +1 -1
- package/src/lib/chat/__tests__/engine-stream-helpers.test.ts +57 -0
- package/src/lib/chat/engine.ts +68 -7
- package/src/lib/chat/stagent-tools.ts +2 -0
- package/src/lib/chat/tools/runtime-tools.ts +28 -0
- package/src/lib/chat/tools/schedule-tools.ts +44 -1
- package/src/lib/chat/tools/settings-tools.ts +40 -10
- package/src/lib/chat/tools/workflow-tools.ts +93 -4
- package/src/lib/chat/types.ts +21 -0
- package/src/lib/data/clear.ts +3 -0
- package/src/lib/db/bootstrap.ts +38 -0
- package/src/lib/db/migrations/0022_workflow_intelligence_phase1.sql +5 -0
- package/src/lib/db/migrations/0023_add_execution_stats.sql +15 -0
- package/src/lib/db/schema.ts +41 -1
- package/src/lib/license/__tests__/manager.test.ts +64 -0
- package/src/lib/license/manager.ts +80 -25
- package/src/lib/schedules/__tests__/interval-parser.test.ts +87 -0
- package/src/lib/schedules/__tests__/prompt-analyzer.test.ts +51 -0
- package/src/lib/schedules/interval-parser.ts +187 -0
- package/src/lib/schedules/prompt-analyzer.ts +87 -0
- package/src/lib/schedules/scheduler.ts +179 -9
- package/src/lib/workflows/cost-estimator.ts +141 -0
- package/src/lib/workflows/engine.ts +245 -45
- package/src/lib/workflows/error-analysis.ts +249 -0
- package/src/lib/workflows/execution-stats.ts +252 -0
- package/src/lib/workflows/optimizer.ts +193 -0
- 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({
|
|
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
|
-
|
|
820
|
-
|
|
821
|
-
|
|
822
|
-
|
|
823
|
-
|
|
824
|
-
|
|
825
|
-
|
|
826
|
-
|
|
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
|
-
|
|
830
|
-
|
|
831
|
-
|
|
832
|
-
|
|
833
|
-
|
|
834
|
-
|
|
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 =
|
|
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)
|
|
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") {
|