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,192 @@
1
+ "use client";
2
+
3
+ import { useState, useEffect } from "react";
4
+ import { ChevronDown, ChevronUp, AlertTriangle, RefreshCw, FileText } from "lucide-react";
5
+ import { Button } from "@/components/ui/button";
6
+ import { Badge } from "@/components/ui/badge";
7
+ import { ErrorTimeline } from "./error-timeline";
8
+
9
+ interface DebugAnalysis {
10
+ rootCause: {
11
+ type: "budget_exceeded" | "timeout" | "transient" | "unknown";
12
+ summary: string;
13
+ };
14
+ timeline: Array<{
15
+ timestamp: string;
16
+ event: string;
17
+ severity: "success" | "warning" | "error";
18
+ details: string;
19
+ }>;
20
+ suggestions: Array<{
21
+ tier: "quick" | "better" | "best";
22
+ title: string;
23
+ description: string;
24
+ }>;
25
+ stepErrors: Array<{
26
+ stepId: string;
27
+ stepName: string;
28
+ error: string;
29
+ }>;
30
+ }
31
+
32
+ const TIER_STYLES = {
33
+ quick: "bg-emerald-50 text-emerald-700 border-emerald-200",
34
+ better: "bg-amber-50 text-amber-700 border-amber-200",
35
+ best: "bg-blue-50 text-blue-700 border-blue-200",
36
+ } as const;
37
+
38
+ const CAUSE_LABELS: Record<string, string> = {
39
+ budget_exceeded: "Budget Exceeded",
40
+ timeout: "Timeout / Turn Limit",
41
+ transient: "Transient Error",
42
+ unknown: "Unknown",
43
+ };
44
+
45
+ interface WorkflowDebugPanelProps {
46
+ workflowId: string;
47
+ }
48
+
49
+ export function WorkflowDebugPanel({ workflowId }: WorkflowDebugPanelProps) {
50
+ const [open, setOpen] = useState(false);
51
+ const [analysis, setAnalysis] = useState<DebugAnalysis | null>(null);
52
+ const [loading, setLoading] = useState(false);
53
+ const [error, setError] = useState<string | null>(null);
54
+ const [retrying, setRetrying] = useState(false);
55
+
56
+ useEffect(() => {
57
+ if (!open || analysis) return;
58
+
59
+ setLoading(true);
60
+ fetch(`/api/workflows/${workflowId}/debug`)
61
+ .then((res) => res.json())
62
+ .then((data) => {
63
+ if (data.error) {
64
+ setError(data.error);
65
+ } else {
66
+ setAnalysis(data);
67
+ }
68
+ })
69
+ .catch((err) => setError(err.message))
70
+ .finally(() => setLoading(false));
71
+ }, [open, workflowId, analysis]);
72
+
73
+ const handleRerun = async () => {
74
+ setRetrying(true);
75
+ try {
76
+ await fetch(`/api/workflows/${workflowId}/execute`, { method: "POST" });
77
+ } finally {
78
+ setRetrying(false);
79
+ }
80
+ };
81
+
82
+ return (
83
+ <div className="rounded-lg border border-red-200 bg-white overflow-hidden">
84
+ <button
85
+ onClick={() => setOpen(!open)}
86
+ className="flex w-full items-center justify-between px-4 py-3 text-left hover:bg-red-50/50 transition-colors"
87
+ >
88
+ <div className="flex items-center gap-2">
89
+ <AlertTriangle className="h-4 w-4 text-red-500" />
90
+ <span className="text-sm font-medium">Debug Analysis</span>
91
+ </div>
92
+ {open ? (
93
+ <ChevronUp className="h-4 w-4 text-muted-foreground" />
94
+ ) : (
95
+ <ChevronDown className="h-4 w-4 text-muted-foreground" />
96
+ )}
97
+ </button>
98
+
99
+ {open && (
100
+ <div className="border-t px-4 py-4 space-y-4">
101
+ {loading && (
102
+ <div className="space-y-3">
103
+ <div className="h-16 rounded-lg bg-muted animate-pulse" />
104
+ <div className="h-32 rounded-lg bg-muted animate-pulse" />
105
+ </div>
106
+ )}
107
+
108
+ {error && (
109
+ <p className="text-sm text-red-600">Failed to load analysis: {error}</p>
110
+ )}
111
+
112
+ {analysis && (
113
+ <>
114
+ {/* Root cause summary */}
115
+ <div className="rounded-lg border-l-4 border-l-red-500 border border-red-100 bg-red-50/30 p-3">
116
+ <div className="flex items-center gap-2 mb-1">
117
+ <Badge variant="destructive" className="text-xs">
118
+ {CAUSE_LABELS[analysis.rootCause.type] ?? analysis.rootCause.type}
119
+ </Badge>
120
+ </div>
121
+ <p className="text-sm text-muted-foreground">
122
+ {analysis.rootCause.summary}
123
+ </p>
124
+ </div>
125
+
126
+ {/* Step errors */}
127
+ {analysis.stepErrors.length > 0 && (
128
+ <div className="space-y-2">
129
+ <h4 className="text-xs font-medium text-muted-foreground uppercase tracking-wide">
130
+ Failed Steps
131
+ </h4>
132
+ {analysis.stepErrors.map((se) => (
133
+ <div key={se.stepId} className="rounded border p-2 text-xs">
134
+ <span className="font-medium">{se.stepName}:</span>{" "}
135
+ <span className="text-muted-foreground">{se.error}</span>
136
+ </div>
137
+ ))}
138
+ </div>
139
+ )}
140
+
141
+ {/* Timeline */}
142
+ {analysis.timeline.length > 0 && (
143
+ <div>
144
+ <h4 className="text-xs font-medium text-muted-foreground uppercase tracking-wide mb-2">
145
+ Event Timeline
146
+ </h4>
147
+ <ErrorTimeline events={analysis.timeline} />
148
+ </div>
149
+ )}
150
+
151
+ {/* Suggestions */}
152
+ {analysis.suggestions.length > 0 && (
153
+ <div>
154
+ <h4 className="text-xs font-medium text-muted-foreground uppercase tracking-wide mb-2">
155
+ Fix Suggestions
156
+ </h4>
157
+ <div className="space-y-2">
158
+ {analysis.suggestions.map((s, i) => (
159
+ <div key={i} className={`rounded-lg border p-3 ${TIER_STYLES[s.tier]}`}>
160
+ <div className="flex items-center gap-2 mb-1">
161
+ <Badge variant="outline" className="text-[10px] uppercase">
162
+ {s.tier}
163
+ </Badge>
164
+ <span className="text-sm font-medium">{s.title}</span>
165
+ </div>
166
+ <p className="text-xs opacity-80">{s.description}</p>
167
+ </div>
168
+ ))}
169
+ </div>
170
+ </div>
171
+ )}
172
+
173
+ {/* Actions */}
174
+ <div className="flex gap-2 pt-2 border-t">
175
+ <Button size="sm" onClick={handleRerun} disabled={retrying}>
176
+ <RefreshCw className={`h-3.5 w-3.5 mr-1.5 ${retrying ? "animate-spin" : ""}`} />
177
+ Re-run Workflow
178
+ </Button>
179
+ <Button size="sm" variant="outline" asChild>
180
+ <a href={`/workflows?id=${workflowId}&tab=logs`}>
181
+ <FileText className="h-3.5 w-3.5 mr-1.5" />
182
+ View Full Logs
183
+ </a>
184
+ </Button>
185
+ </div>
186
+ </>
187
+ )}
188
+ </div>
189
+ )}
190
+ </div>
191
+ );
192
+ }
@@ -0,0 +1,227 @@
1
+ "use client";
2
+
3
+ import { useState, useEffect, useRef, useCallback } from "react";
4
+ import { Button } from "@/components/ui/button";
5
+ import { Badge } from "@/components/ui/badge";
6
+ import { Skeleton } from "@/components/ui/skeleton";
7
+ import {
8
+ Sparkles,
9
+ FileText,
10
+ DollarSign,
11
+ Cpu,
12
+ GitBranch,
13
+ X,
14
+ } from "lucide-react";
15
+ import type { OptimizationSuggestion } from "@/lib/workflows/optimizer";
16
+
17
+ interface WorkflowOptimizerPanelProps {
18
+ definition: Record<string, unknown> | null;
19
+ workflowId?: string;
20
+ onApplySuggestion?: (suggestion: {
21
+ type: string;
22
+ payload: Record<string, unknown>;
23
+ }) => void;
24
+ }
25
+
26
+ const SUGGESTION_ICONS: Record<string, typeof FileText> = {
27
+ document_binding: FileText,
28
+ budget_estimate: DollarSign,
29
+ runtime_recommendation: Cpu,
30
+ pattern_insight: GitBranch,
31
+ };
32
+
33
+ const SUGGESTION_LABELS: Record<string, string> = {
34
+ document_binding: "Documents",
35
+ budget_estimate: "Budget",
36
+ runtime_recommendation: "Runtime",
37
+ pattern_insight: "Pattern",
38
+ };
39
+
40
+ export function WorkflowOptimizerPanel({
41
+ definition,
42
+ workflowId,
43
+ onApplySuggestion,
44
+ }: WorkflowOptimizerPanelProps) {
45
+ const [suggestions, setSuggestions] = useState<OptimizationSuggestion[]>([]);
46
+ const [dismissed, setDismissed] = useState<Set<number>>(new Set());
47
+ const [loading, setLoading] = useState(false);
48
+ const debounceRef = useRef<ReturnType<typeof setTimeout> | null>(null);
49
+
50
+ const fetchSuggestions = useCallback(async () => {
51
+ if (!definition) {
52
+ setSuggestions([]);
53
+ return;
54
+ }
55
+
56
+ setLoading(true);
57
+ try {
58
+ const res = await fetch("/api/workflows/optimize", {
59
+ method: "POST",
60
+ headers: { "Content-Type": "application/json" },
61
+ body: JSON.stringify({ definition, workflowId }),
62
+ });
63
+
64
+ if (res.ok) {
65
+ const data = await res.json();
66
+ setSuggestions(data.suggestions ?? []);
67
+ setDismissed(new Set());
68
+ } else {
69
+ setSuggestions([]);
70
+ }
71
+ } catch {
72
+ setSuggestions([]);
73
+ } finally {
74
+ setLoading(false);
75
+ }
76
+ }, [definition, workflowId]);
77
+
78
+ // Debounced fetch on definition changes
79
+ useEffect(() => {
80
+ if (debounceRef.current) {
81
+ clearTimeout(debounceRef.current);
82
+ }
83
+
84
+ debounceRef.current = setTimeout(() => {
85
+ fetchSuggestions();
86
+ }, 500);
87
+
88
+ return () => {
89
+ if (debounceRef.current) {
90
+ clearTimeout(debounceRef.current);
91
+ }
92
+ };
93
+ }, [fetchSuggestions]);
94
+
95
+ const handleDismiss = (index: number) => {
96
+ setDismissed((prev) => new Set(prev).add(index));
97
+ };
98
+
99
+ const handleApply = (suggestion: OptimizationSuggestion) => {
100
+ if (suggestion.action && onApplySuggestion) {
101
+ onApplySuggestion({
102
+ type: suggestion.action.type,
103
+ payload: suggestion.action.payload,
104
+ });
105
+ }
106
+ };
107
+
108
+ const visibleSuggestions = suggestions.filter(
109
+ (_, i) => !dismissed.has(i)
110
+ );
111
+
112
+ return (
113
+ <div className="space-y-3">
114
+ {/* Header */}
115
+ <div className="flex items-center gap-2">
116
+ <Sparkles className="h-4 w-4 text-muted-foreground" />
117
+ <h3 className="text-sm font-medium">Optimizer</h3>
118
+ </div>
119
+
120
+ {/* Loading state */}
121
+ {loading && (
122
+ <div className="space-y-2">
123
+ <Skeleton className="h-20 w-full rounded-lg" />
124
+ <Skeleton className="h-20 w-full rounded-lg" />
125
+ </div>
126
+ )}
127
+
128
+ {/* Empty state */}
129
+ {!loading && visibleSuggestions.length === 0 && (
130
+ <p className="text-xs text-muted-foreground py-4 text-center">
131
+ Run a few workflows to get optimization suggestions
132
+ </p>
133
+ )}
134
+
135
+ {/* Suggestion cards */}
136
+ {!loading &&
137
+ visibleSuggestions.map((suggestion, visibleIndex) => {
138
+ const originalIndex = suggestions.indexOf(suggestion);
139
+ const Icon = SUGGESTION_ICONS[suggestion.type] ?? Sparkles;
140
+ const label = SUGGESTION_LABELS[suggestion.type] ?? suggestion.type;
141
+
142
+ return (
143
+ <div
144
+ key={originalIndex}
145
+ className="rounded-lg border border-border bg-background p-3 space-y-2"
146
+ >
147
+ {/* Type badge + dismiss */}
148
+ <div className="flex items-center justify-between">
149
+ <div className="flex items-center gap-1.5">
150
+ <Icon className="h-3.5 w-3.5 text-muted-foreground" />
151
+ <Badge
152
+ variant="secondary"
153
+ className="text-[10px] px-1.5 py-0"
154
+ >
155
+ {label}
156
+ </Badge>
157
+ </div>
158
+ <button
159
+ type="button"
160
+ onClick={() => handleDismiss(originalIndex)}
161
+ className="text-muted-foreground hover:text-foreground transition-colors"
162
+ >
163
+ <X className="h-3.5 w-3.5" />
164
+ </button>
165
+ </div>
166
+
167
+ {/* Title */}
168
+ <p className="text-sm font-medium leading-tight">
169
+ {suggestion.title}
170
+ </p>
171
+
172
+ {/* Description */}
173
+ <p className="text-xs text-muted-foreground leading-relaxed">
174
+ {suggestion.description}
175
+ </p>
176
+
177
+ {/* Budget progress bar (budget_estimate type only) */}
178
+ {suggestion.type === "budget_estimate" &&
179
+ typeof suggestion.data.totalEstimatedCostUsd === "number" &&
180
+ typeof suggestion.data.totalBudgetCapUsd === "number" &&
181
+ (suggestion.data.totalBudgetCapUsd as number) > 0 && (
182
+ <div className="space-y-1">
183
+ <div className="h-1.5 w-full rounded-full bg-muted overflow-hidden">
184
+ <div
185
+ className={`h-full rounded-full transition-all ${
186
+ suggestion.data.overBudget
187
+ ? "bg-destructive"
188
+ : "bg-primary"
189
+ }`}
190
+ style={{
191
+ width: `${Math.min(
192
+ 100,
193
+ ((suggestion.data.totalEstimatedCostUsd as number) /
194
+ (suggestion.data.totalBudgetCapUsd as number)) *
195
+ 100
196
+ )}%`,
197
+ }}
198
+ />
199
+ </div>
200
+ <div className="flex justify-between text-[10px] text-muted-foreground">
201
+ <span>
202
+ ${(suggestion.data.totalEstimatedCostUsd as number).toFixed(4)}
203
+ </span>
204
+ <span>
205
+ ${(suggestion.data.totalBudgetCapUsd as number).toFixed(2)} cap
206
+ </span>
207
+ </div>
208
+ </div>
209
+ )}
210
+
211
+ {/* Action button */}
212
+ {suggestion.action && onApplySuggestion && (
213
+ <Button
214
+ size="sm"
215
+ variant="default"
216
+ className="h-7 text-xs w-full"
217
+ onClick={() => handleApply(suggestion)}
218
+ >
219
+ {suggestion.action.label}
220
+ </Button>
221
+ )}
222
+ </div>
223
+ );
224
+ })}
225
+ </div>
226
+ );
227
+ }
@@ -454,8 +454,8 @@ export async function executeClaudeTask(taskId: string): Promise<void> {
454
454
  : { type: "preset" as const, preset: "claude_code" as const },
455
455
  // F9: Bounded turn limit from profile or default
456
456
  maxTurns: ctx.maxTurns,
457
- // F4: Per-execution budget cap
458
- maxBudgetUsd: DEFAULT_MAX_BUDGET_USD,
457
+ // F4: Per-execution budget cap — use task-specific override if set
458
+ maxBudgetUsd: task.maxBudgetUsd ?? DEFAULT_MAX_BUDGET_USD,
459
459
  ...(ctx.payload?.allowedTools && { allowedTools: ctx.payload.allowedTools }),
460
460
  ...(Object.keys(mergedMcpServers).length > 0 && {
461
461
  mcpServers: mergedMcpServers,
@@ -568,8 +568,8 @@ export async function resumeClaudeTask(taskId: string): Promise<void> {
568
568
  : { type: "preset" as const, preset: "claude_code" as const },
569
569
  // F9: Bounded turn limit from profile or default
570
570
  maxTurns: ctx.maxTurns,
571
- // F4: Per-execution budget cap
572
- maxBudgetUsd: DEFAULT_MAX_BUDGET_USD,
571
+ // F4: Per-execution budget cap — use task-specific override if set
572
+ maxBudgetUsd: task.maxBudgetUsd ?? DEFAULT_MAX_BUDGET_USD,
573
573
  ...(ctx.payload?.allowedTools && { allowedTools: ctx.payload.allowedTools }),
574
574
  ...(Object.keys(mergedMcpServers).length > 0 && {
575
575
  mcpServers: mergedMcpServers,
@@ -145,7 +145,7 @@ async function callAnthropicModel(
145
145
  emitEvent: (event: AgentStreamEvent) => void,
146
146
  options: AnthropicCallOptions = {},
147
147
  ): Promise<ModelTurnResult> {
148
- const modelId = options.modelId ?? "claude-sonnet-4-20250514";
148
+ const modelId = options.modelId ?? getRuntimeCatalogEntry("anthropic-direct").models.default;
149
149
  const maxTokens = options.maxTokens ?? 8192;
150
150
 
151
151
  // Build system content with optional caching
@@ -315,7 +315,7 @@ async function executeAnthropicDirectTask(taskId: string, isResume = false): Pro
315
315
 
316
316
  // Resolve model from settings
317
317
  const { getSetting } = await import("@/lib/settings/helpers");
318
- const modelId = (await getSetting("anthropic_direct_model")) ?? "claude-sonnet-4-20250514";
318
+ const modelId = (await getSetting("anthropic_direct_model")) ?? getRuntimeCatalogEntry("anthropic-direct").models.default;
319
319
 
320
320
  const maxTurns = ctx.maxTurns ?? DEFAULT_MAX_TURNS;
321
321
 
@@ -407,7 +407,7 @@ async function executeAnthropicDirectTask(taskId: string, isResume = false): Pro
407
407
  },
408
408
 
409
409
  maxTurns,
410
- maxBudgetUsd: DEFAULT_MAX_BUDGET_USD,
410
+ maxBudgetUsd: task.maxBudgetUsd ?? DEFAULT_MAX_BUDGET_USD,
411
411
  signal: abortController.signal,
412
412
  });
413
413
 
@@ -21,12 +21,21 @@ export interface RuntimeCapabilities {
21
21
  authHealthCheck: boolean;
22
22
  }
23
23
 
24
+ export interface RuntimeModelConfig {
25
+ /** Default model ID for this runtime */
26
+ default: string;
27
+ /** All supported model IDs for this runtime */
28
+ supported: string[];
29
+ }
30
+
24
31
  export interface RuntimeCatalogEntry {
25
32
  id: AgentRuntimeId;
26
33
  label: string;
27
34
  description: string;
28
35
  providerId: "anthropic" | "openai" | "ollama";
29
36
  capabilities: RuntimeCapabilities;
37
+ /** Model catalog — default and supported model IDs for this runtime */
38
+ models: RuntimeModelConfig;
30
39
  }
31
40
 
32
41
  const RUNTIME_CATALOG: Record<AgentRuntimeId, RuntimeCatalogEntry> = {
@@ -45,6 +54,10 @@ const RUNTIME_CATALOG: Record<AgentRuntimeId, RuntimeCatalogEntry> = {
45
54
  profileAssist: true,
46
55
  authHealthCheck: true,
47
56
  },
57
+ models: {
58
+ default: "sonnet",
59
+ supported: ["haiku", "sonnet", "opus"],
60
+ },
48
61
  },
49
62
  "openai-codex-app-server": {
50
63
  id: "openai-codex-app-server",
@@ -55,12 +68,16 @@ const RUNTIME_CATALOG: Record<AgentRuntimeId, RuntimeCatalogEntry> = {
55
68
  resume: true,
56
69
  cancel: true,
57
70
  approvals: true,
58
- mcpServers: false, // Not yet wired — configs not passed to codex subprocess
71
+ mcpServers: false,
59
72
  profileTests: false,
60
73
  taskAssist: true,
61
74
  profileAssist: false,
62
75
  authHealthCheck: true,
63
76
  },
77
+ models: {
78
+ default: "gpt-5.4",
79
+ supported: ["gpt-5.4", "gpt-5.4-mini", "gpt-5.3-codex"],
80
+ },
64
81
  },
65
82
  "anthropic-direct": {
66
83
  id: "anthropic-direct",
@@ -77,6 +94,10 @@ const RUNTIME_CATALOG: Record<AgentRuntimeId, RuntimeCatalogEntry> = {
77
94
  profileAssist: true,
78
95
  authHealthCheck: true,
79
96
  },
97
+ models: {
98
+ default: "claude-sonnet-4-20250514",
99
+ supported: ["claude-haiku-4-5-20251001", "claude-sonnet-4-20250514", "claude-opus-4-20250514"],
100
+ },
80
101
  },
81
102
  "openai-direct": {
82
103
  id: "openai-direct",
@@ -93,6 +114,10 @@ const RUNTIME_CATALOG: Record<AgentRuntimeId, RuntimeCatalogEntry> = {
93
114
  profileAssist: false,
94
115
  authHealthCheck: true,
95
116
  },
117
+ models: {
118
+ default: "gpt-4.1",
119
+ supported: ["gpt-4.1", "gpt-4.1-mini", "gpt-4.1-nano"],
120
+ },
96
121
  },
97
122
  ollama: {
98
123
  id: "ollama",
@@ -109,6 +134,10 @@ const RUNTIME_CATALOG: Record<AgentRuntimeId, RuntimeCatalogEntry> = {
109
134
  profileAssist: false,
110
135
  authHealthCheck: true,
111
136
  },
137
+ models: {
138
+ default: "llama3",
139
+ supported: [], // Dynamic — populated from Ollama API at runtime
140
+ },
112
141
  },
113
142
  };
114
143
 
@@ -85,7 +85,7 @@ async function callOpenAIModel(
85
85
  emitEvent: (event: AgentStreamEvent) => void,
86
86
  options: OpenAICallOptions = {},
87
87
  ): Promise<ModelTurnResult & { responseId?: string }> {
88
- const modelId = options.modelId ?? "gpt-4.1";
88
+ const modelId = options.modelId ?? getRuntimeCatalogEntry("openai-direct").models.default;
89
89
 
90
90
  // Build tool array: Stagent function tools + enabled server-side tools
91
91
  const serverToolConfig = options.serverTools ?? { web_search_preview: true };
@@ -231,7 +231,7 @@ async function executeOpenAIDirectTask(taskId: string, isResume = false): Promis
231
231
 
232
232
  // Resolve model
233
233
  const { getSetting } = await import("@/lib/settings/helpers");
234
- const modelId = (await getSetting("openai_direct_model")) ?? "gpt-4.1";
234
+ const modelId = (await getSetting("openai_direct_model")) ?? getRuntimeCatalogEntry("openai-direct").models.default;
235
235
  const maxTurns = ctx.maxTurns ?? DEFAULT_MAX_TURNS;
236
236
 
237
237
  // For resume: load previous response ID
@@ -324,7 +324,7 @@ async function executeOpenAIDirectTask(taskId: string, isResume = false): Promis
324
324
  },
325
325
 
326
326
  maxTurns,
327
- maxBudgetUsd: DEFAULT_MAX_BUDGET_USD,
327
+ maxBudgetUsd: task.maxBudgetUsd ?? DEFAULT_MAX_BUDGET_USD,
328
328
  signal: abortController.signal,
329
329
  });
330
330
 
@@ -35,8 +35,8 @@ export const STRIPE_PRODUCTS: StripeProduct[] = [
35
35
  annual: { id: "price_1TJ2d5RCxnzBPkIXjjiyc7lb", amount: 19000, currency: "usd" },
36
36
  },
37
37
  paymentLinks: {
38
- monthly: "https://buy.stripe.com/fZufZjgKC4q9azrgDzdwc06",
39
- annual: "https://buy.stripe.com/bJe00l1PI7Cl7nf1IFdwc0b",
38
+ monthly: "https://buy.stagent.io/fZufZjgKC4q9azrgDzdwc06",
39
+ annual: "https://buy.stagent.io/bJe00l1PI7Cl7nf1IFdwc0b",
40
40
  },
41
41
  },
42
42
  {
@@ -48,8 +48,8 @@ export const STRIPE_PRODUCTS: StripeProduct[] = [
48
48
  annual: { id: "price_1TJ2e1RCxnzBPkIXODs5fZW2", amount: 49000, currency: "usd" },
49
49
  },
50
50
  paymentLinks: {
51
- monthly: "https://buy.stripe.com/aFa4gB0LE9Kt22Vevrdwc07",
52
- annual: "https://buy.stripe.com/bJe6oJdyq2i1bDv1IFdwc0a",
51
+ monthly: "https://buy.stagent.io/aFa4gB0LE9Kt22Vevrdwc07",
52
+ annual: "https://buy.stagent.io/bJe6oJdyq2i1bDv1IFdwc0a",
53
53
  },
54
54
  },
55
55
  {
@@ -61,8 +61,8 @@ export const STRIPE_PRODUCTS: StripeProduct[] = [
61
61
  annual: { id: "price_1TJ2evRCxnzBPkIXqIRaDxQp", amount: 99000, currency: "usd" },
62
62
  },
63
63
  paymentLinks: {
64
- monthly: "https://buy.stripe.com/9B628t2TM5udazr72Zdwc08",
65
- annual: "https://buy.stripe.com/dRmfZjbqicWF5f7873dwc09",
64
+ monthly: "https://buy.stagent.io/9B628t2TM5udazr72Zdwc08",
65
+ annual: "https://buy.stagent.io/dRmfZjbqicWF5f7873dwc09",
66
66
  },
67
67
  },
68
68
  ];
@@ -18,6 +18,7 @@ export const CHAPTER_SLUGS: Record<string, string> = {
18
18
  "ch-10": "ch-10-the-world-model",
19
19
  "ch-11": "ch-11-the-machine-that-builds-machines",
20
20
  "ch-12": "ch-12-the-road-ahead",
21
+ "ch-13": "ch-13-the-wealth-manager",
21
22
  };
22
23
 
23
24
  interface ChapterMapping {
@@ -99,6 +100,11 @@ export const CHAPTER_MAPPING: Record<string, ChapterMapping> = {
99
100
  sourceFiles: [],
100
101
  caseStudies: ["stripe-minions", "ramp-background-agent", "harvey-legal-is-next", "sequoa-hierarchy-to-intelligence", "karpathy-one-gpu-research-lab", "making-machine-that-builds-machines"],
101
102
  },
103
+ "ch-13": {
104
+ docs: ["workflows", "profiles", "schedules"],
105
+ sourceFiles: ["src/lib/workflows/engine.ts", "src/lib/schedules/scheduler.ts", "src/lib/agents/profiles/registry.ts"],
106
+ caseStudies: ["making-machine-that-builds-machines"],
107
+ },
102
108
  };
103
109
 
104
110
  /** Get related Playbook doc slugs for a chapter */
@@ -156,6 +156,16 @@ export const CHAPTERS: BookChapter[] = [
156
156
  readingTime: 10,
157
157
  sections: [],
158
158
  },
159
+ {
160
+ id: "ch-13",
161
+ number: 13,
162
+ title: "The Wealth Manager",
163
+ subtitle: "When a Solo Founder Builds a Domain Application in a Day Using the Machine That Built Itself",
164
+ part: PARTS[3],
165
+ readingTime: 16,
166
+ relatedDocs: ["workflows", "profiles", "schedules"],
167
+ sections: [],
168
+ },
159
169
  ];
160
170
 
161
171
  /**
@@ -39,7 +39,7 @@ export const READING_PATHS: ReadingPath[] = [
39
39
  name: "Developer",
40
40
  description: "The complete journey — every chapter, thesis to roadmap",
41
41
  persona: "developer",
42
- chapterIds: ["ch-1", "ch-2", "ch-3", "ch-4", "ch-5", "ch-6", "ch-7", "ch-8", "ch-9", "ch-10", "ch-11", "ch-12"],
42
+ chapterIds: ["ch-1", "ch-2", "ch-3", "ch-4", "ch-5", "ch-6", "ch-7", "ch-8", "ch-9", "ch-10", "ch-11", "ch-12", "ch-13"],
43
43
  usageStage: "power",
44
44
  },
45
45
  ];