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
@@ -0,0 +1,249 @@
1
+ import { db } from "@/lib/db";
2
+ import { workflows, agentLogs } from "@/lib/db/schema";
3
+ import { eq, sql } from "drizzle-orm";
4
+ import type { WorkflowDefinition, WorkflowState, StepState } from "./types";
5
+
6
+ export interface TimelineEvent {
7
+ timestamp: string;
8
+ event: string;
9
+ severity: "success" | "warning" | "error";
10
+ details: string;
11
+ stepId?: string;
12
+ }
13
+
14
+ export interface FixSuggestion {
15
+ tier: "quick" | "better" | "best";
16
+ title: string;
17
+ description: string;
18
+ action?: string;
19
+ }
20
+
21
+ export interface DebugAnalysis {
22
+ rootCause: {
23
+ type: "budget_exceeded" | "timeout" | "transient" | "unknown";
24
+ summary: string;
25
+ };
26
+ timeline: TimelineEvent[];
27
+ suggestions: FixSuggestion[];
28
+ stepErrors: Array<{
29
+ stepId: string;
30
+ stepName: string;
31
+ error: string;
32
+ }>;
33
+ }
34
+
35
+ function classifyRootCause(errors: string[]): DebugAnalysis["rootCause"] {
36
+ const combined = errors.join(" ");
37
+
38
+ if (/budget|Budget|maximum budget/i.test(combined)) {
39
+ return {
40
+ type: "budget_exceeded",
41
+ summary: "The workflow exceeded its allocated budget before completing all steps.",
42
+ };
43
+ }
44
+
45
+ if (/timeout|max turns|Turn limit/i.test(combined)) {
46
+ return {
47
+ type: "timeout",
48
+ summary: "The workflow ran out of turns or hit a timeout before completing.",
49
+ };
50
+ }
51
+
52
+ if (/connection|rate limit|ECONNREFUSED/i.test(combined)) {
53
+ return {
54
+ type: "transient",
55
+ summary: "A transient network or rate-limit error interrupted the workflow.",
56
+ };
57
+ }
58
+
59
+ return {
60
+ type: "unknown",
61
+ summary: errors.length > 0
62
+ ? `Workflow failed: ${errors[0].slice(0, 200)}`
63
+ : "The workflow failed for an unknown reason.",
64
+ };
65
+ }
66
+
67
+ function buildSuggestions(type: DebugAnalysis["rootCause"]["type"]): FixSuggestion[] {
68
+ switch (type) {
69
+ case "budget_exceeded":
70
+ return [
71
+ {
72
+ tier: "quick",
73
+ title: "Raise budget to $10",
74
+ description: "Increase the per-step or workflow budget cap to allow the agent more room to finish.",
75
+ action: "raise_budget",
76
+ },
77
+ {
78
+ tier: "better",
79
+ title: "Reduce document context per step",
80
+ description: "Attach fewer or smaller documents to each step so the agent consumes less budget on context.",
81
+ action: "reduce_docs",
82
+ },
83
+ {
84
+ tier: "best",
85
+ title: "Split into smaller workflows",
86
+ description: "Break the workflow into focused sub-workflows that each stay within budget.",
87
+ action: "restructure",
88
+ },
89
+ ];
90
+ case "timeout":
91
+ return [
92
+ {
93
+ tier: "quick",
94
+ title: "Increase max turns to 100",
95
+ description: "Give the agent more turns to complete complex reasoning chains.",
96
+ },
97
+ {
98
+ tier: "better",
99
+ title: "Simplify step prompts",
100
+ description: "Reduce prompt complexity so the agent can finish in fewer turns.",
101
+ },
102
+ {
103
+ tier: "best",
104
+ title: "Break complex steps into sub-steps",
105
+ description: "Decompose multi-part steps so each one is tractable within the turn limit.",
106
+ },
107
+ ];
108
+ case "transient":
109
+ return [
110
+ {
111
+ tier: "quick",
112
+ title: "Retry the workflow",
113
+ description: "The error was likely temporary. Re-running may succeed without changes.",
114
+ },
115
+ {
116
+ tier: "better",
117
+ title: "Switch to a different runtime",
118
+ description: "Use an alternative agent runtime that may have better availability.",
119
+ },
120
+ {
121
+ tier: "best",
122
+ title: "Add retry logic to step definitions",
123
+ description: "Configure automatic retries on transient failures for resilient execution.",
124
+ },
125
+ ];
126
+ default:
127
+ return [
128
+ {
129
+ tier: "quick",
130
+ title: "Check agent logs for details",
131
+ description: "Review the full agent log output for the failed step to understand the error.",
132
+ },
133
+ {
134
+ tier: "better",
135
+ title: "Simplify workflow",
136
+ description: "Reduce the number of steps or prompt complexity to isolate the failure.",
137
+ },
138
+ {
139
+ tier: "best",
140
+ title: "Contact support",
141
+ description: "If the issue persists, reach out with the workflow ID and debug timeline.",
142
+ },
143
+ ];
144
+ }
145
+ }
146
+
147
+ function mapEventSeverity(event: string): TimelineEvent["severity"] {
148
+ if (event.includes("failed") || event.includes("error")) return "error";
149
+ if (event.includes("completed") || event.includes("success")) return "success";
150
+ return "warning";
151
+ }
152
+
153
+ export async function analyzeWorkflowFailure(workflowId: string): Promise<DebugAnalysis> {
154
+ // 1. Fetch workflow and parse state
155
+ const [workflow] = await db
156
+ .select()
157
+ .from(workflows)
158
+ .where(eq(workflows.id, workflowId));
159
+
160
+ if (!workflow) {
161
+ throw new Error(`Workflow ${workflowId} not found`);
162
+ }
163
+
164
+ const definition: WorkflowDefinition & { _state?: WorkflowState } = JSON.parse(
165
+ workflow.definition
166
+ );
167
+ const state = definition._state;
168
+
169
+ // 2. Query agent_logs for this workflow
170
+ const logs = await db
171
+ .select()
172
+ .from(agentLogs)
173
+ .where(sql`${agentLogs.payload} LIKE ${"%" + workflowId + "%"}`)
174
+ .orderBy(agentLogs.timestamp);
175
+
176
+ // 3. Build timeline from agent_logs
177
+ const timeline: TimelineEvent[] = logs.map((log) => {
178
+ let details = "";
179
+ try {
180
+ const payload = JSON.parse(log.payload ?? "{}");
181
+ details = payload.error || payload.result?.slice(0, 200) || payload.stepName || log.event;
182
+ } catch {
183
+ details = log.event;
184
+ }
185
+
186
+ let stepId: string | undefined;
187
+ try {
188
+ const payload = JSON.parse(log.payload ?? "{}");
189
+ stepId = payload.stepId;
190
+ } catch {
191
+ // ignore
192
+ }
193
+
194
+ return {
195
+ timestamp: log.timestamp instanceof Date
196
+ ? log.timestamp.toISOString()
197
+ : new Date(log.timestamp).toISOString(),
198
+ event: log.event,
199
+ severity: mapEventSeverity(log.event),
200
+ details,
201
+ stepId,
202
+ };
203
+ });
204
+
205
+ // 4. Extract step errors from _state
206
+ const stepErrors: DebugAnalysis["stepErrors"] = [];
207
+ const errorMessages: string[] = [];
208
+
209
+ if (state?.stepStates) {
210
+ for (const ss of state.stepStates) {
211
+ if (ss.status === "failed" && ss.error) {
212
+ const stepDef = definition.steps.find((s) => s.id === ss.stepId);
213
+ stepErrors.push({
214
+ stepId: ss.stepId,
215
+ stepName: stepDef?.name ?? ss.stepId,
216
+ error: ss.error,
217
+ });
218
+ errorMessages.push(ss.error);
219
+ }
220
+ }
221
+ }
222
+
223
+ // Also gather errors from logs if no step errors found
224
+ if (errorMessages.length === 0) {
225
+ for (const log of logs) {
226
+ try {
227
+ const payload = JSON.parse(log.payload ?? "{}");
228
+ if (payload.error) {
229
+ errorMessages.push(payload.error);
230
+ }
231
+ } catch {
232
+ // ignore
233
+ }
234
+ }
235
+ }
236
+
237
+ // 5. Classify root cause
238
+ const rootCause = classifyRootCause(errorMessages);
239
+
240
+ // 6. Generate suggestions
241
+ const suggestions = buildSuggestions(rootCause.type);
242
+
243
+ return {
244
+ rootCause,
245
+ timeline,
246
+ suggestions,
247
+ stepErrors,
248
+ };
249
+ }
@@ -0,0 +1,252 @@
1
+ import { db } from "@/lib/db";
2
+ import {
3
+ workflows,
4
+ usageLedger,
5
+ agentLogs,
6
+ workflowExecutionStats,
7
+ workflowDocumentInputs,
8
+ } from "@/lib/db/schema";
9
+ import { eq, and, sql } from "drizzle-orm";
10
+ import type { WorkflowDefinition } from "./types";
11
+
12
+ // ── Stats update ────────────────────────────────────────────────────
13
+
14
+ /**
15
+ * Update aggregated execution stats after a workflow run completes or fails.
16
+ * Keyed by (pattern, stepCount) bucket — running averages across multiple runs.
17
+ */
18
+ export async function updateExecutionStats(workflowId: string): Promise<void> {
19
+ const [workflow] = await db
20
+ .select()
21
+ .from(workflows)
22
+ .where(eq(workflows.id, workflowId));
23
+
24
+ if (!workflow) return;
25
+
26
+ const definition: WorkflowDefinition = JSON.parse(workflow.definition);
27
+ const pattern = definition.pattern;
28
+ const stepCount = definition.steps.length;
29
+ const succeeded = workflow.status === "completed";
30
+
31
+ // Query usage ledger for this workflow's run
32
+ const usageEntries = await db
33
+ .select()
34
+ .from(usageLedger)
35
+ .where(eq(usageLedger.workflowId, workflowId));
36
+
37
+ const totalCostMicros = usageEntries.reduce(
38
+ (sum, entry) => sum + (entry.costMicros ?? 0),
39
+ 0
40
+ );
41
+ const totalDurationMs = usageEntries.reduce((sum, entry) => {
42
+ if (entry.startedAt && entry.finishedAt) {
43
+ return sum + (entry.finishedAt.getTime() - entry.startedAt.getTime());
44
+ }
45
+ return sum;
46
+ }, 0);
47
+
48
+ const avgCostPerStep = stepCount > 0 ? Math.round(totalCostMicros / stepCount) : 0;
49
+ const avgDurationPerStep = stepCount > 0 ? Math.round(totalDurationMs / stepCount) : 0;
50
+
51
+ // Count documents per step
52
+ const docBindings = await db
53
+ .select()
54
+ .from(workflowDocumentInputs)
55
+ .where(eq(workflowDocumentInputs.workflowId, workflowId));
56
+ const avgDocsPerStep = stepCount > 0 ? docBindings.length / stepCount : 0;
57
+
58
+ // Collect runtime distribution
59
+ const runtimeCounts: Record<string, number> = {};
60
+ for (const entry of usageEntries) {
61
+ runtimeCounts[entry.runtimeId] = (runtimeCounts[entry.runtimeId] ?? 0) + 1;
62
+ }
63
+
64
+ // Classify failure type from agent logs
65
+ let failureType: string | null = null;
66
+ if (!succeeded) {
67
+ const failLogs = await db
68
+ .select()
69
+ .from(agentLogs)
70
+ .where(
71
+ and(
72
+ eq(agentLogs.event, "workflow_failed"),
73
+ sql`${agentLogs.payload} LIKE ${"%" + workflowId + "%"}`
74
+ )
75
+ );
76
+
77
+ for (const log of failLogs) {
78
+ const payload = log.payload ?? "";
79
+ if (payload.includes("budget") || payload.includes("Budget")) {
80
+ failureType = "budget_exceeded";
81
+ } else if (payload.includes("timeout") || payload.includes("max turns")) {
82
+ failureType = "timeout";
83
+ } else if (payload.includes("connection") || payload.includes("rate limit")) {
84
+ failureType = "transient";
85
+ } else {
86
+ failureType = "other";
87
+ }
88
+ }
89
+ }
90
+
91
+ // Upsert into stats table keyed by (pattern, stepCount)
92
+ const bucketId = `${pattern}:${stepCount}`;
93
+ const now = new Date().toISOString();
94
+
95
+ const [existing] = await db
96
+ .select()
97
+ .from(workflowExecutionStats)
98
+ .where(eq(workflowExecutionStats.id, bucketId));
99
+
100
+ if (existing) {
101
+ const n = existing.sampleCount;
102
+ const newN = n + 1;
103
+
104
+ // Running average: newAvg = (oldAvg * n + newValue) / (n + 1)
105
+ const newAvgCost = Math.round(
106
+ ((existing.avgCostPerStepMicros ?? 0) * n + avgCostPerStep) / newN
107
+ );
108
+ const newAvgDuration = Math.round(
109
+ ((existing.avgDurationPerStepMs ?? 0) * n + avgDurationPerStep) / newN
110
+ );
111
+ const newAvgDocs = ((existing.avgDocsPerStep ?? 0) * n + avgDocsPerStep) / newN;
112
+ const newSuccessRate =
113
+ ((existing.successRate ?? 0) * n + (succeeded ? 1 : 0)) / newN;
114
+
115
+ // Merge common failures
116
+ const existingFailures: Record<string, number> = existing.commonFailures
117
+ ? JSON.parse(existing.commonFailures)
118
+ : {};
119
+ if (failureType) {
120
+ existingFailures[failureType] = (existingFailures[failureType] ?? 0) + 1;
121
+ }
122
+
123
+ // Merge runtime breakdown (convert counts to rates)
124
+ const existingRuntimes: Record<string, number> = existing.runtimeBreakdown
125
+ ? JSON.parse(existing.runtimeBreakdown)
126
+ : {};
127
+ for (const [rtId, count] of Object.entries(runtimeCounts)) {
128
+ existingRuntimes[rtId] = (existingRuntimes[rtId] ?? 0) + count;
129
+ }
130
+
131
+ await db
132
+ .update(workflowExecutionStats)
133
+ .set({
134
+ avgCostPerStepMicros: newAvgCost,
135
+ avgDurationPerStepMs: newAvgDuration,
136
+ avgDocsPerStep: Math.round(newAvgDocs * 100) / 100,
137
+ successRate: Math.round(newSuccessRate * 1000) / 1000,
138
+ commonFailures: JSON.stringify(existingFailures),
139
+ runtimeBreakdown: JSON.stringify(existingRuntimes),
140
+ sampleCount: newN,
141
+ lastUpdated: now,
142
+ })
143
+ .where(eq(workflowExecutionStats.id, bucketId));
144
+ } else {
145
+ await db.insert(workflowExecutionStats).values({
146
+ id: bucketId,
147
+ pattern,
148
+ stepCount,
149
+ avgDocsPerStep: Math.round(avgDocsPerStep * 100) / 100,
150
+ avgCostPerStepMicros: avgCostPerStep,
151
+ avgDurationPerStepMs: avgDurationPerStep,
152
+ successRate: succeeded ? 1.0 : 0.0,
153
+ commonFailures: failureType ? JSON.stringify({ [failureType]: 1 }) : "{}",
154
+ runtimeBreakdown: JSON.stringify(runtimeCounts),
155
+ sampleCount: 1,
156
+ lastUpdated: now,
157
+ createdAt: now,
158
+ });
159
+ }
160
+ }
161
+
162
+ // ── Optimization hints ──────────────────────────────────────────────
163
+
164
+ export interface OptimizationHints {
165
+ budgetRecommendation: number | null;
166
+ docBindingStrategy: "global" | "per-step";
167
+ runtimeRecommendation: string | null;
168
+ patternComparison: { pattern: string; successRate: number } | null;
169
+ similarWorkflowStats: {
170
+ avgCostPerStepMicros: number;
171
+ avgDurationPerStepMs: number;
172
+ successRate: number;
173
+ sampleCount: number;
174
+ } | null;
175
+ }
176
+
177
+ /**
178
+ * Get optimization hints based on historical execution data.
179
+ * Returns sensible defaults when no history exists (cold start).
180
+ */
181
+ export async function getWorkflowOptimizationHints(
182
+ pattern: string,
183
+ stepCount: number,
184
+ _docCount: number
185
+ ): Promise<OptimizationHints> {
186
+ const bucketId = `${pattern}:${stepCount}`;
187
+ const [stats] = await db
188
+ .select()
189
+ .from(workflowExecutionStats)
190
+ .where(eq(workflowExecutionStats.id, bucketId));
191
+
192
+ if (!stats || stats.sampleCount === 0) {
193
+ return {
194
+ budgetRecommendation: null,
195
+ docBindingStrategy: "global",
196
+ runtimeRecommendation: null,
197
+ patternComparison: null,
198
+ similarWorkflowStats: null,
199
+ };
200
+ }
201
+
202
+ // Budget recommendation: avg + 50% buffer
203
+ const avgCostUsd = (stats.avgCostPerStepMicros ?? 0) / 1_000_000;
204
+ const budgetRecommendation =
205
+ Math.round(avgCostUsd * 1.5 * stepCount * 100) / 100;
206
+
207
+ // Doc binding strategy: per-step if avg docs > 3
208
+ const docBindingStrategy =
209
+ (stats.avgDocsPerStep ?? 0) > 3 ? "per-step" : "global";
210
+
211
+ // Runtime recommendation: pick highest success rate from breakdown
212
+ let runtimeRecommendation: string | null = null;
213
+ if (stats.runtimeBreakdown) {
214
+ const breakdown: Record<string, number> = JSON.parse(stats.runtimeBreakdown);
215
+ const totalRuns = Object.values(breakdown).reduce((a, b) => a + b, 0);
216
+ if (totalRuns > 0) {
217
+ runtimeRecommendation = Object.entries(breakdown).sort(
218
+ ([, a], [, b]) => b - a
219
+ )[0]?.[0] ?? null;
220
+ }
221
+ }
222
+
223
+ // Pattern comparison: check if alternative pattern has better success rate
224
+ let patternComparison: { pattern: string; successRate: number } | null = null;
225
+ const allStats = await db.select().from(workflowExecutionStats);
226
+ for (const altStats of allStats) {
227
+ if (
228
+ altStats.pattern !== pattern &&
229
+ altStats.sampleCount >= 3 &&
230
+ (altStats.successRate ?? 0) > (stats.successRate ?? 0) + 0.2
231
+ ) {
232
+ patternComparison = {
233
+ pattern: altStats.pattern,
234
+ successRate: altStats.successRate ?? 0,
235
+ };
236
+ break;
237
+ }
238
+ }
239
+
240
+ return {
241
+ budgetRecommendation: budgetRecommendation > 0 ? budgetRecommendation : null,
242
+ docBindingStrategy,
243
+ runtimeRecommendation,
244
+ patternComparison,
245
+ similarWorkflowStats: {
246
+ avgCostPerStepMicros: stats.avgCostPerStepMicros ?? 0,
247
+ avgDurationPerStepMs: stats.avgDurationPerStepMs ?? 0,
248
+ successRate: stats.successRate ?? 0,
249
+ sampleCount: stats.sampleCount,
250
+ },
251
+ };
252
+ }
@@ -0,0 +1,193 @@
1
+ import type { WorkflowDefinition } from "./types";
2
+ import { getWorkflowOptimizationHints } from "./execution-stats";
3
+ import { estimateWorkflowCost } from "./cost-estimator";
4
+
5
+ export type SuggestionType =
6
+ | "document_binding"
7
+ | "budget_estimate"
8
+ | "runtime_recommendation"
9
+ | "pattern_insight";
10
+
11
+ export interface OptimizationSuggestion {
12
+ type: SuggestionType;
13
+ title: string;
14
+ description: string;
15
+ data: Record<string, unknown>;
16
+ action?: {
17
+ label: string;
18
+ type: string;
19
+ payload: Record<string, unknown>;
20
+ };
21
+ }
22
+
23
+ /**
24
+ * Generate optimization suggestions for a workflow definition.
25
+ * Analyzes document bindings, budget, runtime, and pattern choices
26
+ * against historical execution data.
27
+ *
28
+ * Graceful on errors — each sub-analysis is independent and failures
29
+ * are caught so the remaining suggestions still return.
30
+ */
31
+ export async function generateOptimizationSuggestions(
32
+ definition: Partial<WorkflowDefinition>,
33
+ workflowId?: string
34
+ ): Promise<OptimizationSuggestion[]> {
35
+ const suggestions: OptimizationSuggestion[] = [];
36
+
37
+ // ── Document Binding Analysis ──────────────────────────────────────
38
+ try {
39
+ const steps = definition.steps ?? [];
40
+ if (steps.length > 0) {
41
+ // Collect all document IDs referenced at the workflow level (global docs)
42
+ const globalDocIds: string[] = [];
43
+ for (const step of steps) {
44
+ if (step.documentIds) {
45
+ for (const docId of step.documentIds) {
46
+ if (!globalDocIds.includes(docId)) {
47
+ globalDocIds.push(docId);
48
+ }
49
+ }
50
+ }
51
+ }
52
+
53
+ const stepCount = steps.length;
54
+ const docCount = globalDocIds.length;
55
+
56
+ // If many docs across many steps, per-step binding reduces redundant injections
57
+ if (stepCount > 2 && docCount > 3) {
58
+ const totalInjections = docCount * stepCount;
59
+ // Estimate that per-step binding typically needs ~40% of total injections
60
+ const perStepInjections = Math.max(
61
+ stepCount,
62
+ Math.round(totalInjections * 0.4)
63
+ );
64
+
65
+ suggestions.push({
66
+ type: "document_binding",
67
+ title: "Reduce document injections with per-step binding",
68
+ description: `${docCount} docs × ${stepCount} steps = ${totalInjections} injections → only ${perStepInjections} needed with per-step binding`,
69
+ data: {
70
+ globalDocCount: docCount,
71
+ stepCount,
72
+ totalInjections,
73
+ perStepInjections,
74
+ },
75
+ action: {
76
+ label: "Enable per-step docs",
77
+ type: "set_per_step_docs",
78
+ payload: { strategy: "per-step" },
79
+ },
80
+ });
81
+ }
82
+ }
83
+ } catch {
84
+ // Silently skip document binding analysis on error
85
+ }
86
+
87
+ // ── Budget Estimate ────────────────────────────────────────────────
88
+ try {
89
+ if (workflowId) {
90
+ const costEstimate = await estimateWorkflowCost(workflowId);
91
+
92
+ if (costEstimate.steps.length > 0) {
93
+ const perStepBreakdown = costEstimate.steps.map((s) => ({
94
+ name: s.name,
95
+ estimatedCostUsd: s.estimatedCostUsd,
96
+ budgetCapUsd: s.budgetCapUsd,
97
+ }));
98
+
99
+ suggestions.push({
100
+ type: "budget_estimate",
101
+ title: `Estimated cost: $${costEstimate.totalEstimatedCostUsd.toFixed(4)}`,
102
+ description: costEstimate.overBudget
103
+ ? `Over budget — total cap is $${costEstimate.totalBudgetCapUsd.toFixed(2)}. ${costEstimate.warnings[0] ?? ""}`
104
+ : `Within budget cap of $${costEstimate.totalBudgetCapUsd.toFixed(2)}`,
105
+ data: {
106
+ totalEstimatedCostUsd: costEstimate.totalEstimatedCostUsd,
107
+ totalBudgetCapUsd: costEstimate.totalBudgetCapUsd,
108
+ overBudget: costEstimate.overBudget,
109
+ perStepBreakdown,
110
+ warnings: costEstimate.warnings,
111
+ },
112
+ action: costEstimate.overBudget
113
+ ? {
114
+ label: "Adjust budget",
115
+ type: "set_budget",
116
+ payload: {
117
+ suggestedBudgetUsd:
118
+ Math.round(costEstimate.totalEstimatedCostUsd * 1.3 * 100) / 100,
119
+ },
120
+ }
121
+ : undefined,
122
+ });
123
+ }
124
+ }
125
+ } catch {
126
+ // Silently skip budget analysis on error
127
+ }
128
+
129
+ // ── Runtime + Pattern Recommendation ───────────────────────────────
130
+ try {
131
+ const pattern = definition.pattern ?? "sequence";
132
+ const stepCount = definition.steps?.length ?? 0;
133
+
134
+ // Count docs across all steps
135
+ let docCount = 0;
136
+ for (const step of definition.steps ?? []) {
137
+ docCount += step.documentIds?.length ?? 0;
138
+ }
139
+
140
+ const hints = await getWorkflowOptimizationHints(pattern, stepCount, docCount);
141
+
142
+ // Runtime recommendation
143
+ if (hints.runtimeRecommendation) {
144
+ const successPct = hints.similarWorkflowStats
145
+ ? `${Math.round(hints.similarWorkflowStats.successRate * 100)}%`
146
+ : "N/A";
147
+
148
+ suggestions.push({
149
+ type: "runtime_recommendation",
150
+ title: `Recommended runtime: ${hints.runtimeRecommendation}`,
151
+ description: `Based on ${hints.similarWorkflowStats?.sampleCount ?? 0} similar runs with ${successPct} success rate`,
152
+ data: {
153
+ runtimeId: hints.runtimeRecommendation,
154
+ similarWorkflowStats: hints.similarWorkflowStats,
155
+ },
156
+ action: {
157
+ label: "Use this runtime",
158
+ type: "set_runtime",
159
+ payload: { runtimeId: hints.runtimeRecommendation },
160
+ },
161
+ });
162
+ }
163
+
164
+ // Pattern comparison — suggest alternative if >20% better success rate
165
+ if (hints.patternComparison) {
166
+ const altSuccessPct = Math.round(hints.patternComparison.successRate * 100);
167
+ const currentSuccessPct = hints.similarWorkflowStats
168
+ ? Math.round(hints.similarWorkflowStats.successRate * 100)
169
+ : 0;
170
+
171
+ suggestions.push({
172
+ type: "pattern_insight",
173
+ title: `Consider "${hints.patternComparison.pattern}" pattern`,
174
+ description: `${altSuccessPct}% success rate vs ${currentSuccessPct}% for "${pattern}" — a ${altSuccessPct - currentSuccessPct}% improvement`,
175
+ data: {
176
+ currentPattern: pattern,
177
+ suggestedPattern: hints.patternComparison.pattern,
178
+ currentSuccessRate: currentSuccessPct,
179
+ suggestedSuccessRate: altSuccessPct,
180
+ },
181
+ action: {
182
+ label: "Switch pattern",
183
+ type: "change_pattern",
184
+ payload: { pattern: hints.patternComparison.pattern },
185
+ },
186
+ });
187
+ }
188
+ } catch {
189
+ // Silently skip runtime/pattern analysis on error
190
+ }
191
+
192
+ return suggestions;
193
+ }