stagent 0.1.9 → 0.1.11

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 (81) hide show
  1. package/README.md +144 -62
  2. package/package.json +1 -2
  3. package/public/readme/cost-usage-list.png +0 -0
  4. package/public/readme/dashboard-bulk-select.png +0 -0
  5. package/public/readme/dashboard-card-edit.png +0 -0
  6. package/public/readme/dashboard-create-form-ai-applied.png +0 -0
  7. package/public/readme/dashboard-create-form-ai-assist.png +0 -0
  8. package/public/readme/dashboard-create-form-empty.png +0 -0
  9. package/public/readme/dashboard-create-form-filled.png +0 -0
  10. package/public/readme/dashboard-filtered.png +0 -0
  11. package/public/readme/dashboard-list.png +0 -0
  12. package/public/readme/dashboard-sorted.png +0 -0
  13. package/public/readme/dashboard-workflow-confirm.png +0 -0
  14. package/public/readme/documents-grid.png +0 -0
  15. package/public/readme/documents-list.png +0 -0
  16. package/public/readme/home-below-fold.png +0 -0
  17. package/public/readme/home-list.png +0 -0
  18. package/public/readme/inbox-list.png +0 -0
  19. package/public/readme/monitor-list.png +0 -0
  20. package/public/readme/profiles-list.png +0 -0
  21. package/public/readme/projects-detail.png +0 -0
  22. package/public/readme/projects-list.png +0 -0
  23. package/public/readme/schedules-list.png +0 -0
  24. package/public/readme/settings-list.png +0 -0
  25. package/public/readme/workflows-list.png +0 -0
  26. package/src/app/api/profiles/route.ts +0 -1
  27. package/src/app/api/workflows/from-assist/route.ts +143 -0
  28. package/src/app/dashboard/page.tsx +24 -2
  29. package/src/app/globals.css +0 -5
  30. package/src/app/tasks/page.tsx +5 -0
  31. package/src/app/workflows/from-assist/page.tsx +35 -0
  32. package/src/components/profiles/profile-detail-view.tsx +1 -16
  33. package/src/components/profiles/profile-form-view.tsx +0 -22
  34. package/src/components/projects/project-card.tsx +47 -35
  35. package/src/components/tasks/ai-assist-panel.tsx +31 -10
  36. package/src/components/tasks/task-card.tsx +16 -1
  37. package/src/components/tasks/task-create-panel.tsx +39 -0
  38. package/src/components/workflows/workflow-confirmation-view.tsx +447 -0
  39. package/src/lib/agents/__tests__/claude-agent.test.ts +7 -2
  40. package/src/lib/agents/__tests__/learned-context.test.ts +500 -0
  41. package/src/lib/agents/__tests__/pattern-extractor.test.ts +243 -0
  42. package/src/lib/agents/__tests__/sweep.test.ts +202 -0
  43. package/src/lib/agents/claude-agent.ts +104 -78
  44. package/src/lib/agents/learned-context.ts +5 -13
  45. package/src/lib/agents/pattern-extractor.ts +15 -64
  46. package/src/lib/agents/profiles/__tests__/suggest.test.ts +67 -0
  47. package/src/lib/agents/profiles/builtins/code-reviewer/profile.yaml +0 -1
  48. package/src/lib/agents/profiles/builtins/data-analyst/profile.yaml +0 -1
  49. package/src/lib/agents/profiles/builtins/devops-engineer/profile.yaml +0 -1
  50. package/src/lib/agents/profiles/builtins/document-writer/profile.yaml +0 -1
  51. package/src/lib/agents/profiles/builtins/general/profile.yaml +0 -1
  52. package/src/lib/agents/profiles/builtins/health-fitness-coach/profile.yaml +0 -1
  53. package/src/lib/agents/profiles/builtins/learning-coach/profile.yaml +0 -1
  54. package/src/lib/agents/profiles/builtins/project-manager/profile.yaml +0 -1
  55. package/src/lib/agents/profiles/builtins/researcher/profile.yaml +0 -1
  56. package/src/lib/agents/profiles/builtins/shopping-assistant/profile.yaml +0 -1
  57. package/src/lib/agents/profiles/builtins/sweep/profile.yaml +0 -1
  58. package/src/lib/agents/profiles/builtins/technical-writer/profile.yaml +0 -1
  59. package/src/lib/agents/profiles/builtins/travel-planner/profile.yaml +0 -1
  60. package/src/lib/agents/profiles/builtins/wealth-manager/profile.yaml +0 -1
  61. package/src/lib/agents/profiles/registry.ts +0 -1
  62. package/src/lib/agents/profiles/suggest.ts +36 -0
  63. package/src/lib/agents/profiles/types.ts +0 -1
  64. package/src/lib/agents/runtime/catalog.ts +1 -1
  65. package/src/lib/agents/runtime/claude.ts +102 -6
  66. package/src/lib/agents/runtime/task-assist-types.ts +12 -2
  67. package/src/lib/constants/task-status.ts +6 -0
  68. package/src/lib/data/__tests__/clear.test.ts +42 -0
  69. package/src/lib/data/clear.ts +3 -0
  70. package/src/lib/data/seed-data/profiles.ts +0 -3
  71. package/src/lib/notifications/permissions.ts +6 -2
  72. package/src/lib/usage/__tests__/ledger.test.ts +29 -5
  73. package/src/lib/usage/ledger.ts +3 -1
  74. package/src/lib/usage/pricing.ts +61 -7
  75. package/src/lib/validators/__tests__/profile.test.ts +0 -15
  76. package/src/lib/validators/profile.ts +0 -1
  77. package/src/lib/workflows/__tests__/assist-builder.test.ts +255 -0
  78. package/src/lib/workflows/__tests__/engine.test.ts +2 -0
  79. package/src/lib/workflows/assist-builder.ts +248 -0
  80. package/src/lib/workflows/assist-session.ts +78 -0
  81. package/src/lib/workflows/engine.ts +47 -1
@@ -0,0 +1,248 @@
1
+ import type { WorkflowDefinition, WorkflowStep, WorkflowPattern } from "./types";
2
+ import type { TaskAssistResponse, TaskAssistBreakdownStep } from "@/lib/agents/runtime/task-assist-types";
3
+ import { validateWorkflowDefinition } from "./definition-validation";
4
+
5
+ interface AssistBuilderInput {
6
+ mainTask: {
7
+ title: string;
8
+ description: string;
9
+ agentProfile?: string;
10
+ };
11
+ assistResponse: TaskAssistResponse;
12
+ overrides?: {
13
+ pattern?: WorkflowPattern;
14
+ steps?: Partial<WorkflowStep>[];
15
+ loopConfig?: { maxIterations?: number; timeBudgetMs?: number };
16
+ swarmConfig?: { workerConcurrencyLimit?: number };
17
+ };
18
+ }
19
+
20
+ function stepId(index: number): string {
21
+ return `step_${index + 1}`;
22
+ }
23
+
24
+ function buildStep(
25
+ index: number,
26
+ name: string,
27
+ prompt: string,
28
+ options?: {
29
+ agentProfile?: string;
30
+ requiresApproval?: boolean;
31
+ dependsOn?: string[];
32
+ }
33
+ ): WorkflowStep {
34
+ return {
35
+ id: stepId(index),
36
+ name,
37
+ prompt,
38
+ agentProfile: options?.agentProfile,
39
+ requiresApproval: options?.requiresApproval,
40
+ dependsOn: options?.dependsOn,
41
+ };
42
+ }
43
+
44
+ function resolveProfile(
45
+ suggestedProfile: string | undefined,
46
+ fallbackProfile: string | undefined
47
+ ): string | undefined {
48
+ const profile = suggestedProfile ?? fallbackProfile;
49
+ if (!profile || profile === "auto") return undefined;
50
+ return profile;
51
+ }
52
+
53
+ function breakdownToSteps(
54
+ mainTask: AssistBuilderInput["mainTask"],
55
+ breakdown: TaskAssistBreakdownStep[],
56
+ pattern: WorkflowPattern
57
+ ): WorkflowStep[] {
58
+ const steps: WorkflowStep[] = [];
59
+
60
+ // Step 1 is always the main task
61
+ steps.push(
62
+ buildStep(0, mainTask.title, mainTask.description, {
63
+ agentProfile: resolveProfile(undefined, mainTask.agentProfile),
64
+ })
65
+ );
66
+
67
+ // Remaining steps from breakdown
68
+ for (let i = 0; i < breakdown.length; i++) {
69
+ const sub = breakdown[i];
70
+ const dependsOn = sub.dependsOn?.map((depIdx) => stepId(depIdx));
71
+ steps.push(
72
+ buildStep(i + 1, sub.title, sub.description, {
73
+ agentProfile: resolveProfile(sub.suggestedProfile, undefined),
74
+ requiresApproval: pattern === "checkpoint" ? sub.requiresApproval : undefined,
75
+ dependsOn,
76
+ })
77
+ );
78
+ }
79
+
80
+ return steps;
81
+ }
82
+
83
+ function buildSequenceDefinition(
84
+ mainTask: AssistBuilderInput["mainTask"],
85
+ assist: TaskAssistResponse,
86
+ pattern: "sequence" | "planner-executor" | "checkpoint"
87
+ ): WorkflowDefinition {
88
+ const steps = breakdownToSteps(mainTask, assist.breakdown, pattern);
89
+ return { pattern, steps };
90
+ }
91
+
92
+ function buildParallelDefinition(
93
+ mainTask: AssistBuilderInput["mainTask"],
94
+ assist: TaskAssistResponse
95
+ ): WorkflowDefinition {
96
+ const breakdown = assist.breakdown;
97
+
98
+ // Steps with no dependsOn are branches; steps with dependsOn are synthesis
99
+ const hasSynthesis = breakdown.some((s) => s.dependsOn && s.dependsOn.length > 0);
100
+
101
+ const steps: WorkflowStep[] = [];
102
+
103
+ // Main task as first branch
104
+ steps.push(
105
+ buildStep(0, mainTask.title, mainTask.description, {
106
+ agentProfile: resolveProfile(undefined, mainTask.agentProfile),
107
+ })
108
+ );
109
+
110
+ // Add breakdown items as branches (no dependsOn) or synthesis (with dependsOn)
111
+ for (let i = 0; i < breakdown.length; i++) {
112
+ const sub = breakdown[i];
113
+ const dependsOn = sub.dependsOn?.map((depIdx) => stepId(depIdx));
114
+ steps.push(
115
+ buildStep(i + 1, sub.title, sub.description, {
116
+ agentProfile: resolveProfile(sub.suggestedProfile, undefined),
117
+ dependsOn,
118
+ })
119
+ );
120
+ }
121
+
122
+ // Auto-generate synthesis step if none exists
123
+ if (!hasSynthesis) {
124
+ const branchIds = steps.map((s) => s.id);
125
+ steps.push(
126
+ buildStep(steps.length, "Synthesize results", "Combine and synthesize the results from all parallel branches into a coherent summary.", {
127
+ dependsOn: branchIds,
128
+ })
129
+ );
130
+ }
131
+
132
+ return { pattern: "parallel", steps };
133
+ }
134
+
135
+ function buildLoopDefinition(
136
+ mainTask: AssistBuilderInput["mainTask"],
137
+ assist: TaskAssistResponse,
138
+ overrides?: AssistBuilderInput["overrides"]
139
+ ): WorkflowDefinition {
140
+ const loopConfig = {
141
+ maxIterations: overrides?.loopConfig?.maxIterations
142
+ ?? assist.suggestedLoopConfig?.maxIterations
143
+ ?? 5,
144
+ timeBudgetMs: overrides?.loopConfig?.timeBudgetMs
145
+ ?? assist.suggestedLoopConfig?.timeBudgetMs,
146
+ agentProfile: resolveProfile(undefined, mainTask.agentProfile),
147
+ };
148
+
149
+ const steps: WorkflowStep[] = [
150
+ buildStep(0, mainTask.title, mainTask.description, {
151
+ agentProfile: resolveProfile(undefined, mainTask.agentProfile),
152
+ }),
153
+ ];
154
+
155
+ return { pattern: "loop", steps, loopConfig };
156
+ }
157
+
158
+ function buildSwarmDefinition(
159
+ mainTask: AssistBuilderInput["mainTask"],
160
+ assist: TaskAssistResponse,
161
+ overrides?: AssistBuilderInput["overrides"]
162
+ ): WorkflowDefinition {
163
+ const breakdown = assist.breakdown;
164
+ const steps: WorkflowStep[] = [];
165
+
166
+ // Step 1 = mayor (main task)
167
+ steps.push(
168
+ buildStep(0, mainTask.title, mainTask.description, {
169
+ agentProfile: resolveProfile(undefined, mainTask.agentProfile),
170
+ })
171
+ );
172
+
173
+ // Steps 2..N-1 = workers (from breakdown)
174
+ for (let i = 0; i < breakdown.length; i++) {
175
+ const sub = breakdown[i];
176
+ steps.push(
177
+ buildStep(i + 1, sub.title, sub.description, {
178
+ agentProfile: resolveProfile(sub.suggestedProfile, undefined),
179
+ })
180
+ );
181
+ }
182
+
183
+ // Step N = refinery (auto-generated)
184
+ steps.push(
185
+ buildStep(steps.length, "Refine and merge results", "Review all worker outputs, resolve conflicts, and produce a unified final result.", {})
186
+ );
187
+
188
+ const swarmConfig = {
189
+ workerConcurrencyLimit: overrides?.swarmConfig?.workerConcurrencyLimit
190
+ ?? assist.suggestedSwarmConfig?.workerConcurrencyLimit
191
+ ?? 2,
192
+ };
193
+
194
+ return { pattern: "swarm", steps, swarmConfig };
195
+ }
196
+
197
+ /**
198
+ * Convert an AI Assist response into a validated WorkflowDefinition.
199
+ * Pure function — no side effects.
200
+ */
201
+ export function buildWorkflowDefinitionFromAssist(
202
+ input: AssistBuilderInput
203
+ ): WorkflowDefinition {
204
+ const pattern = input.overrides?.pattern ?? input.assistResponse.recommendedPattern as WorkflowPattern;
205
+ const { mainTask, assistResponse, overrides } = input;
206
+
207
+ let definition: WorkflowDefinition;
208
+
209
+ switch (pattern) {
210
+ case "sequence":
211
+ case "planner-executor":
212
+ case "checkpoint":
213
+ definition = buildSequenceDefinition(mainTask, assistResponse, pattern);
214
+ break;
215
+ case "parallel":
216
+ definition = buildParallelDefinition(mainTask, assistResponse);
217
+ break;
218
+ case "loop":
219
+ definition = buildLoopDefinition(mainTask, assistResponse, overrides);
220
+ break;
221
+ case "swarm":
222
+ definition = buildSwarmDefinition(mainTask, assistResponse, overrides);
223
+ break;
224
+ default:
225
+ // Fallback to sequence for unknown patterns
226
+ definition = buildSequenceDefinition(mainTask, assistResponse, "sequence");
227
+ }
228
+
229
+ // Apply step-level overrides
230
+ if (overrides?.steps) {
231
+ for (let i = 0; i < overrides.steps.length && i < definition.steps.length; i++) {
232
+ const stepOverride = overrides.steps[i];
233
+ if (stepOverride) {
234
+ Object.assign(definition.steps[i], stepOverride);
235
+ }
236
+ }
237
+ }
238
+
239
+ // Validate
240
+ const validationError = validateWorkflowDefinition(definition);
241
+ if (validationError) {
242
+ throw new Error(`Invalid workflow definition: ${validationError}`);
243
+ }
244
+
245
+ return definition;
246
+ }
247
+
248
+ export type { AssistBuilderInput };
@@ -0,0 +1,78 @@
1
+ import type { TaskAssistResponse } from "@/lib/agents/runtime/task-assist-types";
2
+
3
+ const STORAGE_KEY = "stagent:workflow-from-assist";
4
+
5
+ export interface WorkflowAssistState {
6
+ assistResult: TaskAssistResponse;
7
+ formState: {
8
+ title: string;
9
+ description: string;
10
+ projectId: string;
11
+ priority: string;
12
+ agentProfile: string;
13
+ assignedAgent: string;
14
+ };
15
+ }
16
+
17
+ export function saveAssistState(state: WorkflowAssistState): void {
18
+ try {
19
+ sessionStorage.setItem(STORAGE_KEY, JSON.stringify(state));
20
+ } catch {
21
+ // sessionStorage may be unavailable (e.g. SSR)
22
+ }
23
+ }
24
+
25
+ export function loadAssistState(): WorkflowAssistState | null {
26
+ try {
27
+ const raw = sessionStorage.getItem(STORAGE_KEY);
28
+ if (!raw) return null;
29
+ return JSON.parse(raw) as WorkflowAssistState;
30
+ } catch {
31
+ return null;
32
+ }
33
+ }
34
+
35
+ export function clearAssistState(): void {
36
+ try {
37
+ sessionStorage.removeItem(STORAGE_KEY);
38
+ } catch {
39
+ // noop
40
+ }
41
+ }
42
+
43
+ const FORM_RESTORE_KEY = "stagent:task-form-restore";
44
+
45
+ export interface TaskFormState {
46
+ title: string;
47
+ description: string;
48
+ projectId: string;
49
+ priority: string;
50
+ agentProfile: string;
51
+ assignedAgent: string;
52
+ }
53
+
54
+ export function saveTaskFormState(state: TaskFormState): void {
55
+ try {
56
+ sessionStorage.setItem(FORM_RESTORE_KEY, JSON.stringify(state));
57
+ } catch {
58
+ // noop
59
+ }
60
+ }
61
+
62
+ export function loadTaskFormState(): TaskFormState | null {
63
+ try {
64
+ const raw = sessionStorage.getItem(FORM_RESTORE_KEY);
65
+ if (!raw) return null;
66
+ return JSON.parse(raw) as TaskFormState;
67
+ } catch {
68
+ return null;
69
+ }
70
+ }
71
+
72
+ export function clearTaskFormState(): void {
73
+ try {
74
+ sessionStorage.removeItem(FORM_RESTORE_KEY);
75
+ } catch {
76
+ // noop
77
+ }
78
+ }
@@ -2,6 +2,7 @@ import { db } from "@/lib/db";
2
2
  import { workflows, tasks, agentLogs, notifications } from "@/lib/db/schema";
3
3
  import { eq } from "drizzle-orm";
4
4
  import { executeTaskWithRuntime } from "@/lib/agents/runtime";
5
+ import { classifyTaskProfile } from "@/lib/agents/router";
5
6
  import type { WorkflowDefinition, WorkflowState, StepState, LoopState } from "./types";
6
7
  import { createInitialState } from "./types";
7
8
  import { executeLoop } from "./loop-executor";
@@ -47,6 +48,8 @@ export async function executeWorkflow(workflowId: string): Promise<void> {
47
48
  try {
48
49
  await executeLoop(workflowId, definition);
49
50
 
51
+ await syncSourceTaskStatus(workflowId, "completed");
52
+
50
53
  await db.insert(agentLogs).values({
51
54
  id: crypto.randomUUID(),
52
55
  taskId: null,
@@ -56,6 +59,8 @@ export async function executeWorkflow(workflowId: string): Promise<void> {
56
59
  timestamp: new Date(),
57
60
  });
58
61
  } catch (error) {
62
+ await syncSourceTaskStatus(workflowId, "failed");
63
+
59
64
  await db.insert(agentLogs).values({
60
65
  id: crypto.randomUUID(),
61
66
  taskId: null,
@@ -94,6 +99,9 @@ export async function executeWorkflow(workflowId: string): Promise<void> {
94
99
  state.completedAt = new Date().toISOString();
95
100
  await updateWorkflowState(workflowId, state, "completed");
96
101
 
102
+ // Sync parent task status
103
+ await syncSourceTaskStatus(workflowId, "completed");
104
+
97
105
  await db.insert(agentLogs).values({
98
106
  id: crypto.randomUUID(),
99
107
  taskId: null,
@@ -106,6 +114,9 @@ export async function executeWorkflow(workflowId: string): Promise<void> {
106
114
  state.status = "failed";
107
115
  await updateWorkflowState(workflowId, state, "failed");
108
116
 
117
+ // Sync parent task status
118
+ await syncSourceTaskStatus(workflowId, "failed");
119
+
109
120
  await db.insert(agentLogs).values({
110
121
  id: crypto.randomUUID(),
111
122
  taskId: null,
@@ -682,6 +693,12 @@ export async function executeChildTask(
682
693
  .from(workflows)
683
694
  .where(eq(workflows.id, workflowId));
684
695
 
696
+ // Resolve "auto" profile via multi-agent router
697
+ const resolvedProfile =
698
+ !agentProfile || agentProfile === "auto"
699
+ ? classifyTaskProfile(name, prompt, assignedAgent)
700
+ : agentProfile;
701
+
685
702
  const taskId = crypto.randomUUID();
686
703
  await db.insert(tasks).values({
687
704
  id: taskId,
@@ -693,7 +710,7 @@ export async function executeChildTask(
693
710
  status: "queued",
694
711
  priority: 1,
695
712
  assignedAgent: assignedAgent ?? null,
696
- agentProfile: agentProfile ?? null,
713
+ agentProfile: resolvedProfile ?? null,
697
714
  createdAt: new Date(),
698
715
  updatedAt: new Date(),
699
716
  });
@@ -811,6 +828,35 @@ async function waitForApproval(
811
828
  return false; // Timeout — treat as denied
812
829
  }
813
830
 
831
+ /**
832
+ * Sync the parent (source) task's status with the workflow's final status.
833
+ * The parent task is linked via `sourceTaskId` in the workflow's definition JSON.
834
+ */
835
+ async function syncSourceTaskStatus(
836
+ workflowId: string,
837
+ status: "completed" | "failed"
838
+ ): Promise<void> {
839
+ try {
840
+ const result = await db
841
+ .select()
842
+ .from(workflows)
843
+ .where(eq(workflows.id, workflowId));
844
+
845
+ const workflow = Array.isArray(result) ? result[0] : undefined;
846
+ if (!workflow) return;
847
+
848
+ const def = JSON.parse(workflow.definition);
849
+ if (!def.sourceTaskId) return;
850
+
851
+ await db
852
+ .update(tasks)
853
+ .set({ status, updatedAt: new Date() })
854
+ .where(eq(tasks.id, def.sourceTaskId));
855
+ } catch (error) {
856
+ console.error(`[workflow-engine] Failed to sync source task status for workflow ${workflowId}:`, error);
857
+ }
858
+ }
859
+
814
860
  /**
815
861
  * Update workflow state in the database.
816
862
  */