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.
- package/README.md +129 -47
- package/package.json +1 -1
- package/public/readme/cost-usage-list.png +0 -0
- package/public/readme/dashboard-bulk-select.png +0 -0
- package/public/readme/dashboard-card-edit.png +0 -0
- package/public/readme/dashboard-create-form-ai-applied.png +0 -0
- package/public/readme/dashboard-create-form-ai-assist.png +0 -0
- package/public/readme/dashboard-create-form-empty.png +0 -0
- package/public/readme/dashboard-create-form-filled.png +0 -0
- package/public/readme/dashboard-filtered.png +0 -0
- package/public/readme/dashboard-list.png +0 -0
- package/public/readme/dashboard-sorted.png +0 -0
- package/public/readme/dashboard-workflow-confirm.png +0 -0
- package/public/readme/documents-grid.png +0 -0
- package/public/readme/documents-list.png +0 -0
- package/public/readme/home-below-fold.png +0 -0
- package/public/readme/home-list.png +0 -0
- package/public/readme/inbox-list.png +0 -0
- package/public/readme/monitor-list.png +0 -0
- package/public/readme/profiles-list.png +0 -0
- package/public/readme/projects-detail.png +0 -0
- package/public/readme/projects-list.png +0 -0
- package/public/readme/schedules-list.png +0 -0
- package/public/readme/settings-list.png +0 -0
- package/public/readme/workflows-list.png +0 -0
- package/src/app/api/workflows/from-assist/route.ts +143 -0
- package/src/app/dashboard/page.tsx +24 -2
- package/src/app/workflows/from-assist/page.tsx +35 -0
- package/src/components/projects/project-card.tsx +47 -35
- package/src/components/tasks/ai-assist-panel.tsx +31 -10
- package/src/components/tasks/task-card.tsx +16 -1
- package/src/components/tasks/task-create-panel.tsx +39 -0
- package/src/components/workflows/workflow-confirmation-view.tsx +447 -0
- package/src/lib/agents/profiles/__tests__/suggest.test.ts +67 -0
- package/src/lib/agents/profiles/suggest.ts +36 -0
- package/src/lib/agents/runtime/claude.ts +36 -6
- package/src/lib/agents/runtime/task-assist-types.ts +12 -2
- package/src/lib/data/__tests__/clear.test.ts +42 -0
- package/src/lib/data/clear.ts +3 -0
- package/src/lib/notifications/permissions.ts +6 -2
- package/src/lib/workflows/__tests__/assist-builder.test.ts +255 -0
- package/src/lib/workflows/assist-builder.ts +248 -0
- package/src/lib/workflows/assist-session.ts +78 -0
- package/src/lib/workflows/engine.ts +46 -1
package/src/lib/data/clear.ts
CHANGED
|
@@ -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
|
|
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
|
+
}
|