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,447 @@
1
+ "use client";
2
+
3
+ import { useState, useEffect } from "react";
4
+ import { useRouter } from "next/navigation";
5
+ import { Button } from "@/components/ui/button";
6
+ import { Input } from "@/components/ui/input";
7
+ import { Label } from "@/components/ui/label";
8
+ import { Badge } from "@/components/ui/badge";
9
+ import {
10
+ Select,
11
+ SelectContent,
12
+ SelectItem,
13
+ SelectTrigger,
14
+ SelectValue,
15
+ } from "@/components/ui/select";
16
+ import {
17
+ ArrowUp,
18
+ ArrowDown,
19
+ Star,
20
+ Trash2,
21
+ Loader2,
22
+ GitBranch,
23
+ ListOrdered,
24
+ Settings,
25
+ } from "lucide-react";
26
+ import { toast } from "sonner";
27
+ import { FormSectionCard } from "@/components/shared/form-section-card";
28
+ import type { TaskAssistResponse } from "@/lib/agents/runtime/task-assist-types";
29
+ import type { WorkflowPattern } from "@/lib/workflows/types";
30
+ import { suggestProfileForStep } from "@/lib/agents/profiles/suggest";
31
+ import {
32
+ loadAssistState,
33
+ clearAssistState,
34
+ saveTaskFormState,
35
+ } from "@/lib/workflows/assist-session";
36
+
37
+ const PATTERN_OPTIONS: { value: WorkflowPattern; label: string; description: string }[] = [
38
+ { value: "sequence", label: "Sequence", description: "Steps run one after another" },
39
+ { value: "planner-executor", label: "Planner → Executor", description: "First step plans, rest execute" },
40
+ { value: "checkpoint", label: "Checkpoint", description: "Steps pause for human approval" },
41
+ { value: "parallel", label: "Parallel", description: "Independent steps run concurrently" },
42
+ { value: "loop", label: "Loop", description: "Single step repeats iteratively" },
43
+ { value: "swarm", label: "Swarm", description: "Mayor coordinates workers" },
44
+ ];
45
+
46
+ interface StepItem {
47
+ title: string;
48
+ description: string;
49
+ profile: string;
50
+ requiresApproval?: boolean;
51
+ }
52
+
53
+ interface ProfileOption {
54
+ id: string;
55
+ name: string;
56
+ }
57
+
58
+ interface WorkflowConfirmationViewProps {
59
+ projects: { id: string; name: string }[];
60
+ profiles: ProfileOption[];
61
+ }
62
+
63
+ export function WorkflowConfirmationView({
64
+ projects,
65
+ profiles,
66
+ }: WorkflowConfirmationViewProps) {
67
+ const router = useRouter();
68
+ const [workflowName, setWorkflowName] = useState("");
69
+ const [pattern, setPattern] = useState<WorkflowPattern>("sequence");
70
+ const [selectedProjectId, setSelectedProjectId] = useState("");
71
+ const [steps, setSteps] = useState<StepItem[]>([]);
72
+ const [maxIterations, setMaxIterations] = useState(5);
73
+ const [workerConcurrencyLimit, setWorkerConcurrencyLimit] = useState(2);
74
+ const [submitting, setSubmitting] = useState(false);
75
+ const [priority, setPriority] = useState(2);
76
+ const [assignedAgent, setAssignedAgent] = useState<string | undefined>();
77
+ const [loaded, setLoaded] = useState(false);
78
+
79
+ // Load state from sessionStorage on mount
80
+ useEffect(() => {
81
+ const state = loadAssistState();
82
+ if (!state) {
83
+ router.replace("/tasks/new");
84
+ return;
85
+ }
86
+
87
+ const { assistResult, formState } = state;
88
+ const recommended = assistResult.recommendedPattern;
89
+ const validPattern = recommended === "single" ? "sequence" : (recommended as WorkflowPattern);
90
+
91
+ setPattern(validPattern);
92
+ setWorkflowName(`Workflow: ${formState.title}`);
93
+ setSelectedProjectId(formState.projectId);
94
+ setPriority(parseInt(formState.priority, 10) || 2);
95
+ setAssignedAgent(formState.assignedAgent || undefined);
96
+ setMaxIterations(assistResult.suggestedLoopConfig?.maxIterations ?? 5);
97
+ setWorkerConcurrencyLimit(assistResult.suggestedSwarmConfig?.workerConcurrencyLimit ?? 2);
98
+
99
+ const profileIds = profiles.map((p) => p.id);
100
+
101
+ const newSteps: StepItem[] = [
102
+ {
103
+ title: formState.title,
104
+ description: formState.description,
105
+ profile: formState.agentProfile || "auto",
106
+ },
107
+ ...assistResult.breakdown.map((sub) => ({
108
+ title: sub.title,
109
+ description: sub.description,
110
+ profile: sub.suggestedProfile || suggestProfileForStep(sub.title, sub.description, profileIds),
111
+ requiresApproval: sub.requiresApproval,
112
+ })),
113
+ ];
114
+
115
+ setSteps(newSteps);
116
+ setLoaded(true);
117
+ }, [router, profiles]);
118
+
119
+ function navigateBackToTask() {
120
+ // Save form state so task form can restore
121
+ const state = loadAssistState();
122
+ if (state) {
123
+ saveTaskFormState(state.formState);
124
+ }
125
+ clearAssistState();
126
+ router.push("/tasks/new?restore=1");
127
+ }
128
+
129
+ function moveStep(index: number, direction: -1 | 1) {
130
+ if (index === 0) return;
131
+ const newIndex = index + direction;
132
+ if (newIndex < 1 || newIndex >= steps.length) return;
133
+ const newSteps = [...steps];
134
+ [newSteps[index], newSteps[newIndex]] = [newSteps[newIndex], newSteps[index]];
135
+ setSteps(newSteps);
136
+ }
137
+
138
+ function removeStep(index: number) {
139
+ if (index === 0 || steps.length <= 2) return;
140
+ setSteps(steps.filter((_, i) => i !== index));
141
+ }
142
+
143
+ function updateStepProfile(index: number, profile: string) {
144
+ const newSteps = [...steps];
145
+ newSteps[index] = { ...newSteps[index], profile };
146
+ setSteps(newSteps);
147
+ }
148
+
149
+ async function handleSubmit(executeImmediately: boolean) {
150
+ if (!workflowName.trim() || steps.length < 2) return;
151
+ setSubmitting(true);
152
+
153
+ try {
154
+ const definitionSteps = steps.map((step, i) => ({
155
+ id: `step_${i + 1}`,
156
+ name: step.title,
157
+ prompt: step.description,
158
+ agentProfile: step.profile === "auto" ? undefined : step.profile,
159
+ requiresApproval: pattern === "checkpoint" ? step.requiresApproval : undefined,
160
+ ...(pattern === "parallel" && i === steps.length - 1
161
+ ? { dependsOn: steps.slice(0, -1).map((_, j) => `step_${j + 1}`) }
162
+ : {}),
163
+ }));
164
+
165
+ const definition: Record<string, unknown> = {
166
+ pattern,
167
+ steps: definitionSteps,
168
+ };
169
+
170
+ if (pattern === "loop") {
171
+ definition.loopConfig = {
172
+ maxIterations,
173
+ agentProfile: steps[0]?.profile === "auto" ? undefined : steps[0]?.profile,
174
+ };
175
+ }
176
+
177
+ if (pattern === "swarm") {
178
+ definition.swarmConfig = { workerConcurrencyLimit };
179
+ }
180
+
181
+ const res = await fetch("/api/workflows/from-assist", {
182
+ method: "POST",
183
+ headers: { "Content-Type": "application/json" },
184
+ body: JSON.stringify({
185
+ name: workflowName.trim(),
186
+ projectId: selectedProjectId || undefined,
187
+ definition,
188
+ priority,
189
+ assignedAgent: assignedAgent || undefined,
190
+ executeImmediately,
191
+ parentTask: {
192
+ title: steps[0].title,
193
+ description: steps[0].description,
194
+ agentProfile: steps[0].profile === "auto" ? undefined : steps[0].profile,
195
+ },
196
+ }),
197
+ });
198
+
199
+ if (!res.ok) {
200
+ const data = await res.json().catch(() => null);
201
+ toast.error(data?.error ?? "Failed to create workflow");
202
+ return;
203
+ }
204
+
205
+ const data = await res.json();
206
+ toast.success(
207
+ executeImmediately
208
+ ? `Task created with workflow (${data.taskIds.length} steps)`
209
+ : `Task created with draft workflow (${data.taskIds.length} steps)`,
210
+ {
211
+ action: {
212
+ label: "View workflow",
213
+ onClick: () => window.open(`/workflows/${data.workflow.id}`, "_self"),
214
+ },
215
+ }
216
+ );
217
+ clearAssistState();
218
+ router.push("/dashboard");
219
+ } catch {
220
+ toast.error("Network error — could not create workflow");
221
+ } finally {
222
+ setSubmitting(false);
223
+ }
224
+ }
225
+
226
+ const isReorderDisabled = pattern === "parallel";
227
+
228
+ if (!loaded) {
229
+ return (
230
+ <div className="flex items-center justify-center py-12 text-muted-foreground">
231
+ <Loader2 className="h-5 w-5 animate-spin mr-2" />
232
+ Loading workflow data...
233
+ </div>
234
+ );
235
+ }
236
+
237
+ return (
238
+ <div className="space-y-6">
239
+ <div>
240
+ <h1 className="text-2xl font-bold">Create Workflow from AI Assist</h1>
241
+ <p className="text-sm text-muted-foreground mt-1">
242
+ Review and customize the AI-suggested workflow before creating it.
243
+ </p>
244
+ </div>
245
+
246
+ {/* Workflow Identity */}
247
+ <FormSectionCard icon={GitBranch} title="Workflow Identity">
248
+ <div className="space-y-3">
249
+ <div className="space-y-1.5">
250
+ <Label htmlFor="workflow-name">Name</Label>
251
+ <Input
252
+ id="workflow-name"
253
+ value={workflowName}
254
+ onChange={(e) => setWorkflowName(e.target.value)}
255
+ placeholder="Workflow name"
256
+ />
257
+ </div>
258
+
259
+ <div className="grid grid-cols-2 gap-3">
260
+ <div className="space-y-1.5">
261
+ <Label>Pattern</Label>
262
+ <Select value={pattern} onValueChange={(v) => setPattern(v as WorkflowPattern)}>
263
+ <SelectTrigger>
264
+ <SelectValue />
265
+ </SelectTrigger>
266
+ <SelectContent>
267
+ {PATTERN_OPTIONS.map((opt) => (
268
+ <SelectItem key={opt.value} value={opt.value}>
269
+ {opt.label}
270
+ </SelectItem>
271
+ ))}
272
+ </SelectContent>
273
+ </Select>
274
+ <p className="text-xs text-muted-foreground">
275
+ {PATTERN_OPTIONS.find((o) => o.value === pattern)?.description}
276
+ </p>
277
+ </div>
278
+ <div className="space-y-1.5">
279
+ <Label>Project</Label>
280
+ <Select
281
+ value={selectedProjectId || "none"}
282
+ onValueChange={(v) => setSelectedProjectId(v === "none" ? "" : v)}
283
+ >
284
+ <SelectTrigger>
285
+ <SelectValue placeholder="None" />
286
+ </SelectTrigger>
287
+ <SelectContent>
288
+ <SelectItem value="none">None</SelectItem>
289
+ {projects.map((p) => (
290
+ <SelectItem key={p.id} value={p.id}>{p.name}</SelectItem>
291
+ ))}
292
+ </SelectContent>
293
+ </Select>
294
+ </div>
295
+ </div>
296
+ </div>
297
+ </FormSectionCard>
298
+
299
+ {/* Steps */}
300
+ <FormSectionCard icon={ListOrdered} title={`Steps (${steps.length})`}>
301
+ <div className="space-y-2">
302
+ {steps.map((step, i) => (
303
+ <div
304
+ key={i}
305
+ className="flex items-center gap-2 p-2.5 rounded-md border bg-card text-sm"
306
+ >
307
+ <span className="text-xs text-muted-foreground w-5 shrink-0">
308
+ {i + 1}.
309
+ </span>
310
+ {i === 0 && (
311
+ <Badge variant="outline" className="text-xs shrink-0 px-1">
312
+ <Star className="h-2.5 w-2.5" />
313
+ </Badge>
314
+ )}
315
+ <div className="flex-1 min-w-0">
316
+ <span className="font-medium block truncate">{step.title}</span>
317
+ {step.description && (
318
+ <span className="text-xs text-muted-foreground block truncate">
319
+ {step.description}
320
+ </span>
321
+ )}
322
+ </div>
323
+
324
+ {/* Profile selector */}
325
+ <Select
326
+ value={step.profile}
327
+ onValueChange={(v) => updateStepProfile(i, v)}
328
+ >
329
+ <SelectTrigger className="w-32 h-7 text-xs shrink-0">
330
+ <SelectValue />
331
+ </SelectTrigger>
332
+ <SelectContent>
333
+ <SelectItem value="auto">Auto</SelectItem>
334
+ {profiles.map((p) => (
335
+ <SelectItem key={p.id} value={p.id}>
336
+ {p.name}
337
+ </SelectItem>
338
+ ))}
339
+ </SelectContent>
340
+ </Select>
341
+
342
+ {/* Reorder buttons */}
343
+ {!isReorderDisabled && (
344
+ <div className="flex flex-col gap-0.5">
345
+ <button
346
+ type="button"
347
+ onClick={() => moveStep(i, -1)}
348
+ disabled={i <= 1}
349
+ className="p-0.5 rounded hover:bg-muted disabled:opacity-30"
350
+ aria-label="Move step up"
351
+ >
352
+ <ArrowUp className="h-3 w-3" />
353
+ </button>
354
+ <button
355
+ type="button"
356
+ onClick={() => moveStep(i, 1)}
357
+ disabled={i === 0 || i === steps.length - 1}
358
+ className="p-0.5 rounded hover:bg-muted disabled:opacity-30"
359
+ aria-label="Move step down"
360
+ >
361
+ <ArrowDown className="h-3 w-3" />
362
+ </button>
363
+ </div>
364
+ )}
365
+
366
+ {/* Remove button */}
367
+ {i > 0 && steps.length > 2 && (
368
+ <button
369
+ type="button"
370
+ onClick={() => removeStep(i)}
371
+ className="p-0.5 rounded hover:bg-destructive/10 text-muted-foreground hover:text-destructive"
372
+ aria-label={`Remove step ${step.title}`}
373
+ >
374
+ <Trash2 className="h-3 w-3" />
375
+ </button>
376
+ )}
377
+ </div>
378
+ ))}
379
+ </div>
380
+ </FormSectionCard>
381
+
382
+ {/* Pattern-specific config */}
383
+ {(pattern === "loop" || pattern === "swarm") && (
384
+ <FormSectionCard icon={Settings} title="Configuration">
385
+ {pattern === "loop" && (
386
+ <div className="space-y-1.5">
387
+ <Label htmlFor="max-iterations">Max Iterations</Label>
388
+ <Input
389
+ id="max-iterations"
390
+ type="number"
391
+ min={1}
392
+ max={50}
393
+ value={maxIterations}
394
+ onChange={(e) => setMaxIterations(parseInt(e.target.value, 10) || 5)}
395
+ className="w-32"
396
+ />
397
+ </div>
398
+ )}
399
+ {pattern === "swarm" && (
400
+ <div className="space-y-1.5">
401
+ <Label htmlFor="concurrency-limit">Worker Concurrency Limit</Label>
402
+ <Input
403
+ id="concurrency-limit"
404
+ type="number"
405
+ min={1}
406
+ max={5}
407
+ value={workerConcurrencyLimit}
408
+ onChange={(e) => setWorkerConcurrencyLimit(parseInt(e.target.value, 10) || 2)}
409
+ className="w-32"
410
+ />
411
+ </div>
412
+ )}
413
+ <div className="flex items-center gap-2 text-xs text-muted-foreground opacity-50 mt-2">
414
+ <input type="checkbox" disabled />
415
+ <span>Save as Blueprint (coming soon)</span>
416
+ </div>
417
+ </FormSectionCard>
418
+ )}
419
+
420
+ {/* Action bar */}
421
+ <div className="flex justify-end gap-2 pt-2">
422
+ <Button
423
+ variant="ghost"
424
+ onClick={navigateBackToTask}
425
+ disabled={submitting}
426
+ >
427
+ Dismiss
428
+ </Button>
429
+ <Button
430
+ variant="outline"
431
+ onClick={() => handleSubmit(false)}
432
+ disabled={submitting || !workflowName.trim() || steps.length < 2}
433
+ >
434
+ {submitting ? <Loader2 className="h-3 w-3 mr-1 animate-spin" /> : null}
435
+ Accept
436
+ </Button>
437
+ <Button
438
+ onClick={() => handleSubmit(true)}
439
+ disabled={submitting || !workflowName.trim() || steps.length < 2}
440
+ >
441
+ {submitting ? <Loader2 className="h-3 w-3 mr-1 animate-spin" /> : null}
442
+ Accept & Run
443
+ </Button>
444
+ </div>
445
+ </div>
446
+ );
447
+ }
@@ -315,12 +315,17 @@ describe("executeClaudeTask", () => {
315
315
 
316
316
  await executeClaudeTask("task-1");
317
317
 
318
- // query prompt should include output instructions and fall back to the title
318
+ // F1: prompt contains only user task text (title fallback); system instructions in systemPrompt
319
319
  expect(mockQuery).toHaveBeenCalledWith(
320
320
  expect.objectContaining({
321
- prompt: "Write outputs to /tmp/stagent-outputs/task-1\n\nTest Task",
321
+ prompt: "Test Task",
322
322
  })
323
323
  );
324
+ // System instructions (including output instructions) are in the systemPrompt option
325
+ const callOptions = mockQuery.mock.calls[0][0].options;
326
+ expect(callOptions.systemPrompt).toBeDefined();
327
+ expect(callOptions.maxTurns).toBeDefined();
328
+ expect(callOptions.maxBudgetUsd).toBeDefined();
324
329
  });
325
330
  });
326
331