stagent 0.1.9 → 0.1.10

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 (44) hide show
  1. package/README.md +129 -47
  2. package/package.json +1 -1
  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/workflows/from-assist/route.ts +143 -0
  27. package/src/app/dashboard/page.tsx +24 -2
  28. package/src/app/workflows/from-assist/page.tsx +35 -0
  29. package/src/components/projects/project-card.tsx +47 -35
  30. package/src/components/tasks/ai-assist-panel.tsx +31 -10
  31. package/src/components/tasks/task-card.tsx +16 -1
  32. package/src/components/tasks/task-create-panel.tsx +39 -0
  33. package/src/components/workflows/workflow-confirmation-view.tsx +447 -0
  34. package/src/lib/agents/profiles/__tests__/suggest.test.ts +67 -0
  35. package/src/lib/agents/profiles/suggest.ts +36 -0
  36. package/src/lib/agents/runtime/claude.ts +36 -6
  37. package/src/lib/agents/runtime/task-assist-types.ts +12 -2
  38. package/src/lib/data/__tests__/clear.test.ts +42 -0
  39. package/src/lib/data/clear.ts +3 -0
  40. package/src/lib/notifications/permissions.ts +6 -2
  41. package/src/lib/workflows/__tests__/assist-builder.test.ts +255 -0
  42. package/src/lib/workflows/assist-builder.ts +248 -0
  43. package/src/lib/workflows/assist-session.ts +78 -0
  44. package/src/lib/workflows/engine.ts +46 -1
@@ -3,6 +3,7 @@ import {
3
3
  agentLogs,
4
4
  notifications,
5
5
  documents,
6
+ learnedContext,
6
7
  tasks,
7
8
  workflows,
8
9
  schedules,
@@ -31,6 +32,7 @@ export function clearAllData() {
31
32
  const logsDeleted = db.delete(agentLogs).run().changes;
32
33
  const notificationsDeleted = db.delete(notifications).run().changes;
33
34
  const documentsDeleted = db.delete(documents).run().changes;
35
+ const learnedContextDeleted = db.delete(learnedContext).run().changes;
34
36
  const tasksDeleted = db.delete(tasks).run().changes;
35
37
  const workflowsDeleted = db.delete(workflows).run().changes;
36
38
  const schedulesDeleted = db.delete(schedules).run().changes;
@@ -58,6 +60,7 @@ export function clearAllData() {
58
60
  agentLogs: logsDeleted,
59
61
  notifications: notificationsDeleted,
60
62
  documents: documentsDeleted,
63
+ learnedContext: learnedContextDeleted,
61
64
  files: filesDeleted,
62
65
  };
63
66
  }
@@ -149,6 +149,11 @@ export function getPermissionDetailEntries(
149
149
  export function getPermissionResponseLabel(response: string | null): string | null {
150
150
  if (!response) return null;
151
151
 
152
+ // Handle legacy plain-string responses (pre-JSON format)
153
+ const legacy = response.toLowerCase();
154
+ if (legacy === "approved" || legacy === "allowed") return "Allowed";
155
+ if (legacy === "denied" || legacy === "rejected") return "Denied";
156
+
152
157
  try {
153
158
  const parsed = JSON.parse(response) as {
154
159
  behavior?: "allow" | "deny";
@@ -164,8 +169,7 @@ export function getPermissionResponseLabel(response: string | null): string | nu
164
169
  }
165
170
 
166
171
  return null;
167
- } catch (err) {
168
- console.error("[permissions] Failed to parse permission response:", err);
172
+ } catch {
169
173
  return null;
170
174
  }
171
175
  }
@@ -0,0 +1,255 @@
1
+ import { describe, it, expect } from "vitest";
2
+ import { buildWorkflowDefinitionFromAssist } from "../assist-builder";
3
+ import type { TaskAssistResponse } from "@/lib/agents/runtime/task-assist-types";
4
+
5
+ const MAIN_TASK = {
6
+ title: "Build Auth System",
7
+ description: "Implement authentication with OAuth2",
8
+ agentProfile: "general",
9
+ };
10
+
11
+ function makeAssistResponse(
12
+ overrides: Partial<TaskAssistResponse> = {}
13
+ ): TaskAssistResponse {
14
+ return {
15
+ improvedDescription: "Build a complete auth system",
16
+ breakdown: [
17
+ { title: "Set up middleware", description: "Create auth middleware" },
18
+ { title: "Create endpoints", description: "Build user API endpoints" },
19
+ { title: "Write tests", description: "Integration tests for auth" },
20
+ ],
21
+ recommendedPattern: "sequence",
22
+ complexity: "complex",
23
+ needsCheckpoint: false,
24
+ reasoning: "Multi-step ordered work",
25
+ ...overrides,
26
+ };
27
+ }
28
+
29
+ describe("buildWorkflowDefinitionFromAssist", () => {
30
+ describe("sequence pattern", () => {
31
+ it("creates a sequence workflow with main task as step 1", () => {
32
+ const result = buildWorkflowDefinitionFromAssist({
33
+ mainTask: MAIN_TASK,
34
+ assistResponse: makeAssistResponse(),
35
+ });
36
+
37
+ expect(result.pattern).toBe("sequence");
38
+ expect(result.steps).toHaveLength(4); // main + 3 breakdown
39
+ expect(result.steps[0].name).toBe("Build Auth System");
40
+ expect(result.steps[1].name).toBe("Set up middleware");
41
+ expect(result.steps[3].name).toBe("Write tests");
42
+ });
43
+
44
+ it("assigns profiles from main task and suggestions", () => {
45
+ const result = buildWorkflowDefinitionFromAssist({
46
+ mainTask: MAIN_TASK,
47
+ assistResponse: makeAssistResponse({
48
+ breakdown: [
49
+ { title: "Research", description: "Research patterns", suggestedProfile: "researcher" },
50
+ { title: "Code", description: "Write code" },
51
+ ],
52
+ }),
53
+ });
54
+
55
+ expect(result.steps[0].agentProfile).toBe("general"); // from mainTask
56
+ expect(result.steps[1].agentProfile).toBe("researcher"); // from suggestion
57
+ expect(result.steps[2].agentProfile).toBeUndefined(); // no suggestion = undefined
58
+ });
59
+ });
60
+
61
+ describe("checkpoint pattern", () => {
62
+ it("preserves requiresApproval on steps", () => {
63
+ const result = buildWorkflowDefinitionFromAssist({
64
+ mainTask: MAIN_TASK,
65
+ assistResponse: makeAssistResponse({
66
+ recommendedPattern: "checkpoint",
67
+ breakdown: [
68
+ { title: "Plan", description: "Plan deployment", requiresApproval: true },
69
+ { title: "Deploy", description: "Execute deployment" },
70
+ ],
71
+ }),
72
+ });
73
+
74
+ expect(result.pattern).toBe("checkpoint");
75
+ expect(result.steps[1].requiresApproval).toBe(true);
76
+ expect(result.steps[2].requiresApproval).toBeUndefined();
77
+ });
78
+ });
79
+
80
+ describe("parallel pattern", () => {
81
+ it("auto-generates synthesis step when none provided", () => {
82
+ const result = buildWorkflowDefinitionFromAssist({
83
+ mainTask: MAIN_TASK,
84
+ assistResponse: makeAssistResponse({
85
+ recommendedPattern: "parallel",
86
+ breakdown: [
87
+ { title: "Branch A", description: "Research area A" },
88
+ { title: "Branch B", description: "Research area B" },
89
+ ],
90
+ }),
91
+ });
92
+
93
+ expect(result.pattern).toBe("parallel");
94
+ // main + 2 branches + auto-synthesis = 4
95
+ expect(result.steps).toHaveLength(4);
96
+ expect(result.steps[3].name).toBe("Synthesize results");
97
+ expect(result.steps[3].dependsOn).toEqual(["step_1", "step_2", "step_3"]);
98
+ });
99
+
100
+ it("preserves explicit synthesis step with dependsOn", () => {
101
+ const result = buildWorkflowDefinitionFromAssist({
102
+ mainTask: MAIN_TASK,
103
+ assistResponse: makeAssistResponse({
104
+ recommendedPattern: "parallel",
105
+ breakdown: [
106
+ { title: "Branch A", description: "Research A" },
107
+ { title: "Merge", description: "Merge results", dependsOn: [0, 1] },
108
+ ],
109
+ }),
110
+ });
111
+
112
+ // main + Branch A + Merge = 3 (no auto-synthesis because dependsOn exists)
113
+ expect(result.steps).toHaveLength(3);
114
+ expect(result.steps[2].dependsOn).toEqual(["step_1", "step_2"]);
115
+ });
116
+ });
117
+
118
+ describe("loop pattern", () => {
119
+ it("creates single-step loop with config", () => {
120
+ const result = buildWorkflowDefinitionFromAssist({
121
+ mainTask: MAIN_TASK,
122
+ assistResponse: makeAssistResponse({
123
+ recommendedPattern: "loop",
124
+ suggestedLoopConfig: { maxIterations: 3, timeBudgetMs: 60000 },
125
+ }),
126
+ });
127
+
128
+ expect(result.pattern).toBe("loop");
129
+ expect(result.steps).toHaveLength(1);
130
+ expect(result.loopConfig?.maxIterations).toBe(3);
131
+ expect(result.loopConfig?.timeBudgetMs).toBe(60000);
132
+ });
133
+
134
+ it("defaults to 5 iterations", () => {
135
+ const result = buildWorkflowDefinitionFromAssist({
136
+ mainTask: MAIN_TASK,
137
+ assistResponse: makeAssistResponse({ recommendedPattern: "loop" }),
138
+ });
139
+
140
+ expect(result.loopConfig?.maxIterations).toBe(5);
141
+ });
142
+
143
+ it("applies loop config overrides", () => {
144
+ const result = buildWorkflowDefinitionFromAssist({
145
+ mainTask: MAIN_TASK,
146
+ assistResponse: makeAssistResponse({
147
+ recommendedPattern: "loop",
148
+ suggestedLoopConfig: { maxIterations: 3 },
149
+ }),
150
+ overrides: { loopConfig: { maxIterations: 10 } },
151
+ });
152
+
153
+ expect(result.loopConfig?.maxIterations).toBe(10);
154
+ });
155
+ });
156
+
157
+ describe("swarm pattern", () => {
158
+ it("creates mayor/workers/refinery structure", () => {
159
+ const result = buildWorkflowDefinitionFromAssist({
160
+ mainTask: MAIN_TASK,
161
+ assistResponse: makeAssistResponse({
162
+ recommendedPattern: "swarm",
163
+ breakdown: [
164
+ { title: "Worker 1", description: "Task 1" },
165
+ { title: "Worker 2", description: "Task 2" },
166
+ ],
167
+ }),
168
+ });
169
+
170
+ expect(result.pattern).toBe("swarm");
171
+ // mayor + 2 workers + refinery = 4
172
+ expect(result.steps).toHaveLength(4);
173
+ expect(result.steps[0].name).toBe("Build Auth System"); // mayor
174
+ expect(result.steps[3].name).toBe("Refine and merge results"); // refinery
175
+ expect(result.swarmConfig?.workerConcurrencyLimit).toBe(2);
176
+ });
177
+
178
+ it("applies swarm config overrides", () => {
179
+ const result = buildWorkflowDefinitionFromAssist({
180
+ mainTask: MAIN_TASK,
181
+ assistResponse: makeAssistResponse({
182
+ recommendedPattern: "swarm",
183
+ breakdown: [
184
+ { title: "W1", description: "T1" },
185
+ { title: "W2", description: "T2" },
186
+ ],
187
+ suggestedSwarmConfig: { workerConcurrencyLimit: 1 },
188
+ }),
189
+ overrides: { swarmConfig: { workerConcurrencyLimit: 2 } },
190
+ });
191
+
192
+ expect(result.swarmConfig?.workerConcurrencyLimit).toBe(2);
193
+ });
194
+ });
195
+
196
+ describe("pattern override", () => {
197
+ it("overrides AI-recommended pattern", () => {
198
+ const result = buildWorkflowDefinitionFromAssist({
199
+ mainTask: MAIN_TASK,
200
+ assistResponse: makeAssistResponse({ recommendedPattern: "sequence" }),
201
+ overrides: { pattern: "checkpoint" },
202
+ });
203
+
204
+ expect(result.pattern).toBe("checkpoint");
205
+ });
206
+ });
207
+
208
+ describe("step overrides", () => {
209
+ it("applies partial step overrides", () => {
210
+ const result = buildWorkflowDefinitionFromAssist({
211
+ mainTask: MAIN_TASK,
212
+ assistResponse: makeAssistResponse(),
213
+ overrides: {
214
+ steps: [
215
+ undefined,
216
+ { agentProfile: "code-reviewer" },
217
+ ] as Partial<import("../types").WorkflowStep>[],
218
+ },
219
+ });
220
+
221
+ expect(result.steps[1].agentProfile).toBe("code-reviewer");
222
+ });
223
+ });
224
+
225
+ describe("validation", () => {
226
+ it("throws on invalid definition", () => {
227
+ expect(() =>
228
+ buildWorkflowDefinitionFromAssist({
229
+ mainTask: MAIN_TASK,
230
+ assistResponse: makeAssistResponse({
231
+ recommendedPattern: "loop",
232
+ // Missing loopConfig
233
+ }),
234
+ overrides: { loopConfig: { maxIterations: 0 } },
235
+ })
236
+ ).toThrow("Invalid workflow definition");
237
+ });
238
+ });
239
+
240
+ describe("auto profile handling", () => {
241
+ it('treats "auto" suggestedProfile as undefined', () => {
242
+ const result = buildWorkflowDefinitionFromAssist({
243
+ mainTask: { ...MAIN_TASK, agentProfile: undefined },
244
+ assistResponse: makeAssistResponse({
245
+ breakdown: [
246
+ { title: "Step", description: "Do thing", suggestedProfile: "auto" },
247
+ ],
248
+ }),
249
+ });
250
+
251
+ expect(result.steps[0].agentProfile).toBeUndefined();
252
+ expect(result.steps[1].agentProfile).toBeUndefined();
253
+ });
254
+ });
255
+ });
@@ -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
+ }