stagent 0.6.2 → 0.6.3
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/dist/cli.js +47 -1
- package/package.json +1 -2
- package/src/app/api/documents/[id]/route.ts +5 -1
- package/src/app/api/documents/[id]/versions/route.ts +53 -0
- package/src/app/api/documents/route.ts +5 -1
- package/src/app/api/projects/[id]/documents/route.ts +124 -0
- package/src/app/api/projects/[id]/route.ts +35 -3
- package/src/app/api/projects/__tests__/delete-project.test.ts +1 -0
- package/src/app/api/schedules/route.ts +19 -1
- package/src/app/api/tasks/[id]/route.ts +37 -2
- package/src/app/api/tasks/[id]/siblings/route.ts +48 -0
- package/src/app/api/tasks/route.ts +8 -9
- package/src/app/api/workflows/[id]/documents/route.ts +209 -0
- package/src/app/api/workflows/[id]/execute/route.ts +6 -2
- package/src/app/api/workflows/[id]/route.ts +16 -3
- package/src/app/api/workflows/[id]/status/route.ts +18 -2
- package/src/app/api/workflows/route.ts +13 -2
- package/src/app/documents/page.tsx +5 -1
- package/src/app/layout.tsx +0 -1
- package/src/app/manifest.ts +3 -3
- package/src/app/projects/[id]/page.tsx +62 -2
- package/src/components/documents/document-chip-bar.tsx +17 -1
- package/src/components/documents/document-detail-view.tsx +51 -0
- package/src/components/documents/document-grid.tsx +5 -0
- package/src/components/documents/document-table.tsx +4 -0
- package/src/components/documents/types.ts +3 -0
- package/src/components/projects/project-form-sheet.tsx +133 -2
- package/src/components/schedules/schedule-form.tsx +113 -1
- package/src/components/shared/document-picker-sheet.tsx +283 -0
- package/src/components/tasks/task-card.tsx +8 -1
- package/src/components/tasks/task-create-panel.tsx +137 -14
- package/src/components/tasks/task-detail-view.tsx +47 -0
- package/src/components/tasks/task-edit-dialog.tsx +125 -2
- package/src/components/workflows/workflow-form-view.tsx +231 -7
- package/src/components/workflows/workflow-kanban-card.tsx +8 -1
- package/src/components/workflows/workflow-list.tsx +90 -45
- package/src/components/workflows/workflow-status-view.tsx +167 -22
- package/src/lib/__tests__/npx-process-cwd.test.ts +17 -2
- package/src/lib/agents/profiles/registry.ts +6 -3
- package/src/lib/book/__tests__/chapter-slugs.test.ts +80 -0
- package/src/lib/book/chapter-generator.ts +4 -19
- package/src/lib/book/chapter-mapping.ts +17 -0
- package/src/lib/book/content.ts +5 -16
- package/src/lib/book/update-detector.ts +3 -16
- package/src/lib/chat/engine.ts +1 -0
- package/src/lib/chat/system-prompt.ts +9 -1
- package/src/lib/chat/tool-catalog.ts +1 -0
- package/src/lib/chat/tools/settings-tools.ts +109 -0
- package/src/lib/chat/tools/workflow-tools.ts +145 -2
- package/src/lib/data/clear.ts +12 -0
- package/src/lib/db/bootstrap.ts +48 -0
- package/src/lib/db/migrations/0016_add_workflow_document_inputs.sql +13 -0
- package/src/lib/db/migrations/0017_add_document_picker_tables.sql +25 -0
- package/src/lib/db/migrations/0018_add_workflow_run_number.sql +2 -0
- package/src/lib/db/schema.ts +77 -0
- package/src/lib/docs/reader.ts +2 -3
- package/src/lib/documents/context-builder.ts +75 -2
- package/src/lib/documents/document-resolver.ts +119 -0
- package/src/lib/documents/processors/spreadsheet.ts +2 -1
- package/src/lib/schedules/scheduler.ts +31 -1
- package/src/lib/utils/app-root.ts +20 -0
- package/src/lib/validators/__tests__/task.test.ts +43 -10
- package/src/lib/validators/task.ts +7 -1
- package/src/lib/workflows/blueprints/registry.ts +3 -3
- package/src/lib/workflows/engine.ts +24 -8
- package/src/lib/workflows/types.ts +14 -0
- package/public/icon.svg +0 -13
- package/src/components/tasks/file-upload.tsx +0 -120
|
@@ -0,0 +1,119 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Resolve document selectors against the project document pool.
|
|
3
|
+
* Used at workflow creation time for auto-discovery of relevant documents.
|
|
4
|
+
*/
|
|
5
|
+
|
|
6
|
+
import { db } from "@/lib/db";
|
|
7
|
+
import { documents, workflows, tasks } from "@/lib/db/schema";
|
|
8
|
+
import { and, eq, desc, like, inArray } from "drizzle-orm";
|
|
9
|
+
import type { DocumentRow } from "@/lib/db/schema";
|
|
10
|
+
import type { DocumentSelector } from "@/lib/workflows/types";
|
|
11
|
+
|
|
12
|
+
/**
|
|
13
|
+
* Resolve a DocumentSelector against the project pool, returning matching documents.
|
|
14
|
+
* Used for auto-discovery at workflow creation time (not at execution time).
|
|
15
|
+
*/
|
|
16
|
+
export async function resolveDocumentSelector(
|
|
17
|
+
projectId: string,
|
|
18
|
+
selector: DocumentSelector
|
|
19
|
+
): Promise<DocumentRow[]> {
|
|
20
|
+
const conditions = [eq(documents.projectId, projectId)];
|
|
21
|
+
|
|
22
|
+
if (selector.direction) {
|
|
23
|
+
conditions.push(eq(documents.direction, selector.direction));
|
|
24
|
+
}
|
|
25
|
+
|
|
26
|
+
if (selector.category) {
|
|
27
|
+
conditions.push(eq(documents.category, selector.category));
|
|
28
|
+
}
|
|
29
|
+
|
|
30
|
+
if (selector.mimeType) {
|
|
31
|
+
conditions.push(eq(documents.mimeType, selector.mimeType));
|
|
32
|
+
}
|
|
33
|
+
|
|
34
|
+
if (selector.namePattern) {
|
|
35
|
+
// Convert glob pattern to SQL LIKE: * → %, ? → _
|
|
36
|
+
const likePattern = selector.namePattern
|
|
37
|
+
.replace(/\*/g, "%")
|
|
38
|
+
.replace(/\?/g, "_");
|
|
39
|
+
conditions.push(like(documents.originalName, likePattern));
|
|
40
|
+
}
|
|
41
|
+
|
|
42
|
+
// Filter by source workflow (via task → workflow relationship)
|
|
43
|
+
if (selector.fromWorkflowId) {
|
|
44
|
+
const workflowTaskIds = await db
|
|
45
|
+
.select({ id: tasks.id })
|
|
46
|
+
.from(tasks)
|
|
47
|
+
.where(eq(tasks.workflowId, selector.fromWorkflowId));
|
|
48
|
+
|
|
49
|
+
const taskIds = workflowTaskIds.map((t) => t.id);
|
|
50
|
+
if (taskIds.length === 0) return [];
|
|
51
|
+
conditions.push(inArray(documents.taskId, taskIds));
|
|
52
|
+
} else if (selector.fromWorkflowName) {
|
|
53
|
+
// Look up workflow by name, then get its task IDs
|
|
54
|
+
const matchingWorkflows = await db
|
|
55
|
+
.select({ id: workflows.id })
|
|
56
|
+
.from(workflows)
|
|
57
|
+
.where(
|
|
58
|
+
and(
|
|
59
|
+
eq(workflows.projectId, projectId),
|
|
60
|
+
like(workflows.name, `%${selector.fromWorkflowName}%`)
|
|
61
|
+
)
|
|
62
|
+
);
|
|
63
|
+
|
|
64
|
+
if (matchingWorkflows.length === 0) return [];
|
|
65
|
+
|
|
66
|
+
const wfIds = matchingWorkflows.map((w) => w.id);
|
|
67
|
+
const workflowTaskIds = await db
|
|
68
|
+
.select({ id: tasks.id })
|
|
69
|
+
.from(tasks)
|
|
70
|
+
.where(inArray(tasks.workflowId, wfIds));
|
|
71
|
+
|
|
72
|
+
const taskIds = workflowTaskIds.map((t) => t.id);
|
|
73
|
+
if (taskIds.length === 0) return [];
|
|
74
|
+
conditions.push(inArray(documents.taskId, taskIds));
|
|
75
|
+
}
|
|
76
|
+
|
|
77
|
+
// Only return ready documents (processed and available)
|
|
78
|
+
conditions.push(eq(documents.status, "ready"));
|
|
79
|
+
|
|
80
|
+
let query = db
|
|
81
|
+
.select()
|
|
82
|
+
.from(documents)
|
|
83
|
+
.where(and(...conditions))
|
|
84
|
+
.orderBy(desc(documents.createdAt));
|
|
85
|
+
|
|
86
|
+
if (selector.latest) {
|
|
87
|
+
query = query.limit(selector.latest) as typeof query;
|
|
88
|
+
}
|
|
89
|
+
|
|
90
|
+
return query;
|
|
91
|
+
}
|
|
92
|
+
|
|
93
|
+
/**
|
|
94
|
+
* Get all output documents from completed workflows in a project.
|
|
95
|
+
* Useful for browsing the project document pool.
|
|
96
|
+
*/
|
|
97
|
+
export async function getProjectDocumentPool(
|
|
98
|
+
projectId: string,
|
|
99
|
+
options?: { direction?: "input" | "output"; search?: string }
|
|
100
|
+
): Promise<DocumentRow[]> {
|
|
101
|
+
const conditions = [
|
|
102
|
+
eq(documents.projectId, projectId),
|
|
103
|
+
eq(documents.status, "ready"),
|
|
104
|
+
];
|
|
105
|
+
|
|
106
|
+
if (options?.direction) {
|
|
107
|
+
conditions.push(eq(documents.direction, options.direction));
|
|
108
|
+
}
|
|
109
|
+
|
|
110
|
+
if (options?.search) {
|
|
111
|
+
conditions.push(like(documents.originalName, `%${options.search}%`));
|
|
112
|
+
}
|
|
113
|
+
|
|
114
|
+
return db
|
|
115
|
+
.select()
|
|
116
|
+
.from(documents)
|
|
117
|
+
.where(and(...conditions))
|
|
118
|
+
.orderBy(desc(documents.createdAt));
|
|
119
|
+
}
|
|
@@ -6,7 +6,8 @@ export async function processSpreadsheet(filePath: string): Promise<ProcessorRes
|
|
|
6
6
|
const ExcelJS = await import("exceljs");
|
|
7
7
|
const workbook = new ExcelJS.Workbook();
|
|
8
8
|
const buffer = await readFile(filePath);
|
|
9
|
-
|
|
9
|
+
// eslint-disable-next-line @typescript-eslint/no-explicit-any
|
|
10
|
+
await workbook.xlsx.load(buffer as any);
|
|
10
11
|
|
|
11
12
|
const sheets: string[] = [];
|
|
12
13
|
workbook.eachSheet((worksheet) => {
|
|
@@ -12,7 +12,7 @@
|
|
|
12
12
|
*/
|
|
13
13
|
|
|
14
14
|
import { db } from "@/lib/db";
|
|
15
|
-
import { schedules, tasks, agentLogs } from "@/lib/db/schema";
|
|
15
|
+
import { schedules, tasks, agentLogs, scheduleDocumentInputs, documents } from "@/lib/db/schema";
|
|
16
16
|
import { eq, and, lte, inArray, sql } from "drizzle-orm";
|
|
17
17
|
import { computeNextFireTime } from "./interval-parser";
|
|
18
18
|
import { executeTaskWithRuntime } from "@/lib/agents/runtime";
|
|
@@ -178,6 +178,21 @@ async function fireSchedule(
|
|
|
178
178
|
updatedAt: now,
|
|
179
179
|
});
|
|
180
180
|
|
|
181
|
+
// Link schedule's documents to the created task
|
|
182
|
+
try {
|
|
183
|
+
const schedDocs = await db
|
|
184
|
+
.select({ documentId: scheduleDocumentInputs.documentId })
|
|
185
|
+
.from(scheduleDocumentInputs)
|
|
186
|
+
.where(eq(scheduleDocumentInputs.scheduleId, schedule.id));
|
|
187
|
+
for (const { documentId } of schedDocs) {
|
|
188
|
+
await db.update(documents)
|
|
189
|
+
.set({ taskId, projectId: schedule.projectId, updatedAt: now })
|
|
190
|
+
.where(eq(documents.id, documentId));
|
|
191
|
+
}
|
|
192
|
+
} catch (err) {
|
|
193
|
+
console.error(`[scheduler] Document linking failed for schedule ${schedule.id}:`, err);
|
|
194
|
+
}
|
|
195
|
+
|
|
181
196
|
// Update schedule counters
|
|
182
197
|
const isOneShot = !schedule.recurs;
|
|
183
198
|
const reachedMax =
|
|
@@ -335,6 +350,21 @@ async function fireHeartbeat(
|
|
|
335
350
|
updatedAt: now,
|
|
336
351
|
});
|
|
337
352
|
|
|
353
|
+
// Link schedule's documents to the heartbeat task
|
|
354
|
+
try {
|
|
355
|
+
const schedDocs = await db
|
|
356
|
+
.select({ documentId: scheduleDocumentInputs.documentId })
|
|
357
|
+
.from(scheduleDocumentInputs)
|
|
358
|
+
.where(eq(scheduleDocumentInputs.scheduleId, schedule.id));
|
|
359
|
+
for (const { documentId } of schedDocs) {
|
|
360
|
+
await db.update(documents)
|
|
361
|
+
.set({ taskId: evalTaskId, projectId: schedule.projectId, updatedAt: now })
|
|
362
|
+
.where(eq(documents.id, documentId));
|
|
363
|
+
}
|
|
364
|
+
} catch (err) {
|
|
365
|
+
console.error(`[scheduler] Document linking failed for heartbeat ${schedule.id}:`, err);
|
|
366
|
+
}
|
|
367
|
+
|
|
338
368
|
// 5. Execute and wait for result (with timeout)
|
|
339
369
|
try {
|
|
340
370
|
await executeTaskWithRuntime(evalTaskId);
|
|
@@ -0,0 +1,20 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Resolve the app root directory.
|
|
3
|
+
* - import.meta.dirname works under npx (real path to installed package)
|
|
4
|
+
* - Turbopack compiles it to /ROOT/... (virtual, doesn't exist) → fall back to process.cwd()
|
|
5
|
+
*
|
|
6
|
+
* Uses dynamic require() for fs/path to avoid bundling Node built-ins
|
|
7
|
+
* into client components (content.ts is shared with book-reader.tsx).
|
|
8
|
+
*/
|
|
9
|
+
export function getAppRoot(metaDirname: string | undefined, depth: number): string {
|
|
10
|
+
// eslint-disable-next-line @typescript-eslint/no-require-imports
|
|
11
|
+
const { existsSync } = require("fs") as typeof import("fs");
|
|
12
|
+
// eslint-disable-next-line @typescript-eslint/no-require-imports
|
|
13
|
+
const { join } = require("path") as typeof import("path");
|
|
14
|
+
|
|
15
|
+
if (metaDirname) {
|
|
16
|
+
const candidate = join(metaDirname, ...Array(depth).fill(".."));
|
|
17
|
+
if (existsSync(join(candidate, "package.json"))) return candidate;
|
|
18
|
+
}
|
|
19
|
+
return process.cwd();
|
|
20
|
+
}
|
|
@@ -72,40 +72,63 @@ describe("createTaskSchema", () => {
|
|
|
72
72
|
}
|
|
73
73
|
});
|
|
74
74
|
|
|
75
|
-
it("accepts
|
|
75
|
+
it("accepts documentIds as optional array of strings", () => {
|
|
76
76
|
const result = createTaskSchema.safeParse({
|
|
77
77
|
title: "Test",
|
|
78
|
-
|
|
78
|
+
documentIds: ["abc-123", "def-456"],
|
|
79
79
|
});
|
|
80
80
|
expect(result.success).toBe(true);
|
|
81
81
|
if (result.success) {
|
|
82
|
-
expect(result.data.
|
|
82
|
+
expect(result.data.documentIds).toEqual(["abc-123", "def-456"]);
|
|
83
83
|
}
|
|
84
84
|
});
|
|
85
85
|
|
|
86
|
-
it("accepts task without
|
|
86
|
+
it("accepts task without documentIds", () => {
|
|
87
87
|
const result = createTaskSchema.safeParse({ title: "Test" });
|
|
88
88
|
expect(result.success).toBe(true);
|
|
89
89
|
if (result.success) {
|
|
90
|
-
expect(result.data.
|
|
90
|
+
expect(result.data.documentIds).toBeUndefined();
|
|
91
91
|
}
|
|
92
92
|
});
|
|
93
93
|
|
|
94
|
-
it("accepts empty
|
|
95
|
-
const result = createTaskSchema.safeParse({ title: "Test",
|
|
94
|
+
it("accepts empty documentIds array", () => {
|
|
95
|
+
const result = createTaskSchema.safeParse({ title: "Test", documentIds: [] });
|
|
96
96
|
expect(result.success).toBe(true);
|
|
97
97
|
if (result.success) {
|
|
98
|
-
expect(result.data.
|
|
98
|
+
expect(result.data.documentIds).toEqual([]);
|
|
99
99
|
}
|
|
100
100
|
});
|
|
101
101
|
|
|
102
|
-
it("rejects
|
|
102
|
+
it("rejects documentIds with non-string elements", () => {
|
|
103
103
|
const result = createTaskSchema.safeParse({
|
|
104
104
|
title: "Test",
|
|
105
|
-
|
|
105
|
+
documentIds: [123, true],
|
|
106
106
|
});
|
|
107
107
|
expect(result.success).toBe(false);
|
|
108
108
|
});
|
|
109
|
+
|
|
110
|
+
it("accepts deprecated fileIds and transforms to documentIds", () => {
|
|
111
|
+
const result = createTaskSchema.safeParse({
|
|
112
|
+
title: "Test",
|
|
113
|
+
fileIds: ["file-1", "file-2"],
|
|
114
|
+
});
|
|
115
|
+
expect(result.success).toBe(true);
|
|
116
|
+
if (result.success) {
|
|
117
|
+
expect(result.data.documentIds).toEqual(["file-1", "file-2"]);
|
|
118
|
+
}
|
|
119
|
+
});
|
|
120
|
+
|
|
121
|
+
it("prefers documentIds over fileIds when both provided", () => {
|
|
122
|
+
const result = createTaskSchema.safeParse({
|
|
123
|
+
title: "Test",
|
|
124
|
+
documentIds: ["doc-1"],
|
|
125
|
+
fileIds: ["file-1"],
|
|
126
|
+
});
|
|
127
|
+
expect(result.success).toBe(true);
|
|
128
|
+
if (result.success) {
|
|
129
|
+
expect(result.data.documentIds).toEqual(["doc-1"]);
|
|
130
|
+
}
|
|
131
|
+
});
|
|
109
132
|
});
|
|
110
133
|
|
|
111
134
|
describe("updateTaskSchema", () => {
|
|
@@ -141,4 +164,14 @@ describe("updateTaskSchema", () => {
|
|
|
141
164
|
});
|
|
142
165
|
expect(result.success).toBe(false);
|
|
143
166
|
});
|
|
167
|
+
|
|
168
|
+
it("accepts documentIds in update", () => {
|
|
169
|
+
const result = updateTaskSchema.safeParse({
|
|
170
|
+
documentIds: ["doc-1", "doc-2"],
|
|
171
|
+
});
|
|
172
|
+
expect(result.success).toBe(true);
|
|
173
|
+
if (result.success) {
|
|
174
|
+
expect(result.data.documentIds).toEqual(["doc-1", "doc-2"]);
|
|
175
|
+
}
|
|
176
|
+
});
|
|
144
177
|
});
|
|
@@ -10,8 +10,13 @@ export const createTaskSchema = z.object({
|
|
|
10
10
|
priority: z.number().min(0).max(3).default(2),
|
|
11
11
|
assignedAgent: assignedAgentSchema.optional(),
|
|
12
12
|
agentProfile: z.string().optional(),
|
|
13
|
+
documentIds: z.array(z.string()).optional(),
|
|
14
|
+
/** @deprecated Use documentIds instead */
|
|
13
15
|
fileIds: z.array(z.string()).optional(),
|
|
14
|
-
})
|
|
16
|
+
}).transform((data) => ({
|
|
17
|
+
...data,
|
|
18
|
+
documentIds: data.documentIds ?? data.fileIds,
|
|
19
|
+
}));
|
|
15
20
|
|
|
16
21
|
export const updateTaskSchema = z.object({
|
|
17
22
|
title: z.string().min(1).max(200).optional(),
|
|
@@ -24,6 +29,7 @@ export const updateTaskSchema = z.object({
|
|
|
24
29
|
agentProfile: z.string().optional(),
|
|
25
30
|
result: z.string().optional(),
|
|
26
31
|
sessionId: z.string().optional(),
|
|
32
|
+
documentIds: z.array(z.string()).optional(),
|
|
27
33
|
});
|
|
28
34
|
|
|
29
35
|
export type CreateTaskInput = z.infer<typeof createTaskSchema>;
|
|
@@ -3,12 +3,12 @@ import path from "node:path";
|
|
|
3
3
|
import yaml from "js-yaml";
|
|
4
4
|
import { BlueprintSchema } from "@/lib/validators/blueprint";
|
|
5
5
|
import { getStagentBlueprintsDir } from "@/lib/utils/stagent-paths";
|
|
6
|
+
import { getAppRoot } from "@/lib/utils/app-root";
|
|
6
7
|
import type { WorkflowBlueprint } from "./types";
|
|
7
8
|
|
|
8
|
-
// Use fileURLToPath for ESM compatibility in Next.js
|
|
9
9
|
const BUILTINS_DIR = path.resolve(
|
|
10
|
-
import.meta.dirname
|
|
11
|
-
"builtins"
|
|
10
|
+
getAppRoot(import.meta.dirname, 4),
|
|
11
|
+
"src", "lib", "workflows", "blueprints", "builtins"
|
|
12
12
|
);
|
|
13
13
|
|
|
14
14
|
const USER_BLUEPRINTS_DIR = getStagentBlueprintsDir();
|
|
@@ -20,7 +20,10 @@ import {
|
|
|
20
20
|
openLearningSession,
|
|
21
21
|
closeLearningSession,
|
|
22
22
|
} from "@/lib/agents/learning-session";
|
|
23
|
-
import {
|
|
23
|
+
import {
|
|
24
|
+
buildWorkflowDocumentContext,
|
|
25
|
+
buildPoolDocumentContext,
|
|
26
|
+
} from "@/lib/documents/context-builder";
|
|
24
27
|
|
|
25
28
|
/**
|
|
26
29
|
* Execute a workflow by advancing through its steps according to the pattern.
|
|
@@ -359,7 +362,8 @@ async function executeParallel(
|
|
|
359
362
|
step.prompt,
|
|
360
363
|
step.assignedAgent,
|
|
361
364
|
step.agentProfile,
|
|
362
|
-
parentTaskId
|
|
365
|
+
parentTaskId,
|
|
366
|
+
step.id
|
|
363
367
|
);
|
|
364
368
|
|
|
365
369
|
const completedAt = new Date().toISOString();
|
|
@@ -433,7 +437,8 @@ async function executeParallel(
|
|
|
433
437
|
synthesisPrompt,
|
|
434
438
|
synthesisStep.assignedAgent,
|
|
435
439
|
synthesisStep.agentProfile,
|
|
436
|
-
parentTaskId
|
|
440
|
+
parentTaskId,
|
|
441
|
+
synthesisStep.id
|
|
437
442
|
);
|
|
438
443
|
|
|
439
444
|
await commitState((draft) => {
|
|
@@ -559,7 +564,8 @@ async function executeSwarm(
|
|
|
559
564
|
workerPrompt,
|
|
560
565
|
step.assignedAgent,
|
|
561
566
|
step.agentProfile,
|
|
562
|
-
parentTaskId
|
|
567
|
+
parentTaskId,
|
|
568
|
+
step.id
|
|
563
569
|
);
|
|
564
570
|
|
|
565
571
|
const completedAt = new Date().toISOString();
|
|
@@ -680,7 +686,8 @@ async function runSwarmRefinery(input: {
|
|
|
680
686
|
refineryPrompt,
|
|
681
687
|
refineryStep.assignedAgent,
|
|
682
688
|
refineryStep.agentProfile,
|
|
683
|
-
parentTaskId
|
|
689
|
+
parentTaskId,
|
|
690
|
+
refineryStep.id
|
|
684
691
|
);
|
|
685
692
|
|
|
686
693
|
refineryState.taskId = refineryResult.taskId;
|
|
@@ -716,7 +723,8 @@ export async function executeChildTask(
|
|
|
716
723
|
prompt: string,
|
|
717
724
|
assignedAgent?: string,
|
|
718
725
|
agentProfile?: string,
|
|
719
|
-
parentTaskId?: string
|
|
726
|
+
parentTaskId?: string,
|
|
727
|
+
stepId?: string
|
|
720
728
|
): Promise<{ taskId: string; status: string; result?: string; error?: string }> {
|
|
721
729
|
const [workflow] = await db
|
|
722
730
|
.select()
|
|
@@ -735,10 +743,16 @@ export async function executeChildTask(
|
|
|
735
743
|
if (parentTaskId) {
|
|
736
744
|
const docContext = await buildWorkflowDocumentContext(parentTaskId);
|
|
737
745
|
if (docContext) {
|
|
738
|
-
enrichedPrompt = `${docContext}\n\n${
|
|
746
|
+
enrichedPrompt = `${docContext}\n\n${enrichedPrompt}`;
|
|
739
747
|
}
|
|
740
748
|
}
|
|
741
749
|
|
|
750
|
+
// Inject pool document context from workflow_document_inputs junction table
|
|
751
|
+
const poolContext = await buildPoolDocumentContext(workflowId, stepId);
|
|
752
|
+
if (poolContext) {
|
|
753
|
+
enrichedPrompt = `${poolContext}\n\n${enrichedPrompt}`;
|
|
754
|
+
}
|
|
755
|
+
|
|
742
756
|
const taskId = crypto.randomUUID();
|
|
743
757
|
await db.insert(tasks).values({
|
|
744
758
|
id: taskId,
|
|
@@ -751,6 +765,7 @@ export async function executeChildTask(
|
|
|
751
765
|
priority: 1,
|
|
752
766
|
assignedAgent: assignedAgent ?? null,
|
|
753
767
|
agentProfile: resolvedProfile ?? null,
|
|
768
|
+
workflowRunNumber: workflow?.runNumber ?? null,
|
|
754
769
|
createdAt: new Date(),
|
|
755
770
|
updatedAt: new Date(),
|
|
756
771
|
});
|
|
@@ -807,7 +822,8 @@ async function executeStep(
|
|
|
807
822
|
prompt,
|
|
808
823
|
assignedAgent,
|
|
809
824
|
agentProfile,
|
|
810
|
-
parentTaskId
|
|
825
|
+
parentTaskId,
|
|
826
|
+
stepId
|
|
811
827
|
);
|
|
812
828
|
|
|
813
829
|
stepState.taskId = result.taskId;
|
|
@@ -14,6 +14,20 @@ export interface WorkflowStep {
|
|
|
14
14
|
dependsOn?: string[];
|
|
15
15
|
assignedAgent?: string;
|
|
16
16
|
agentProfile?: string;
|
|
17
|
+
/** Document IDs from the project pool to inject as context for this step */
|
|
18
|
+
documentIds?: string[];
|
|
19
|
+
}
|
|
20
|
+
|
|
21
|
+
/** Selector for auto-discovering documents from the project pool */
|
|
22
|
+
export interface DocumentSelector {
|
|
23
|
+
fromWorkflowId?: string;
|
|
24
|
+
fromWorkflowName?: string;
|
|
25
|
+
category?: string;
|
|
26
|
+
direction?: "input" | "output";
|
|
27
|
+
mimeType?: string;
|
|
28
|
+
namePattern?: string;
|
|
29
|
+
/** Take only the N most recent matching documents */
|
|
30
|
+
latest?: number;
|
|
17
31
|
}
|
|
18
32
|
|
|
19
33
|
export interface LoopConfig {
|
package/public/icon.svg
DELETED
|
@@ -1,13 +0,0 @@
|
|
|
1
|
-
<svg width="512" height="512" viewBox="0 0 512 512" fill="none" xmlns="http://www.w3.org/2000/svg">
|
|
2
|
-
<rect width="512" height="512" rx="112" fill="#0f172a"/>
|
|
3
|
-
<defs>
|
|
4
|
-
<linearGradient id="s-grad" x1="150" y1="80" x2="362" y2="432" gradientUnits="userSpaceOnUse">
|
|
5
|
-
<stop offset="0%" stop-color="#22d3ee"/>
|
|
6
|
-
<stop offset="50%" stop-color="#3b82f6"/>
|
|
7
|
-
<stop offset="100%" stop-color="#2563eb"/>
|
|
8
|
-
</linearGradient>
|
|
9
|
-
</defs>
|
|
10
|
-
<path d="M340 130c0 0-60-10-110 30s-50 90-10 115c40 25 70 30 70 30s50 15 30 65c-20 50-80 40-80 40" stroke="url(#s-grad)" stroke-width="56" stroke-linecap="round" fill="none"/>
|
|
11
|
-
<circle cx="340" cy="130" r="36" fill="#22d3ee"/>
|
|
12
|
-
<circle cx="240" cy="410" r="36" fill="#2563eb"/>
|
|
13
|
-
</svg>
|
|
@@ -1,120 +0,0 @@
|
|
|
1
|
-
"use client";
|
|
2
|
-
|
|
3
|
-
import { useState, useRef } from "react";
|
|
4
|
-
import { Button } from "@/components/ui/button";
|
|
5
|
-
import { Upload, X, FileText, Image, FileCode } from "lucide-react";
|
|
6
|
-
|
|
7
|
-
interface UploadedFile {
|
|
8
|
-
id: string;
|
|
9
|
-
filename: string;
|
|
10
|
-
originalName: string;
|
|
11
|
-
size: number;
|
|
12
|
-
type: string;
|
|
13
|
-
}
|
|
14
|
-
|
|
15
|
-
interface FileUploadProps {
|
|
16
|
-
onUploaded: (file: UploadedFile) => void;
|
|
17
|
-
uploads: UploadedFile[];
|
|
18
|
-
onRemove: (id: string) => void;
|
|
19
|
-
}
|
|
20
|
-
|
|
21
|
-
function formatSize(bytes: number): string {
|
|
22
|
-
if (bytes < 1024) return `${bytes} B`;
|
|
23
|
-
if (bytes < 1024 * 1024) return `${(bytes / 1024).toFixed(1)} KB`;
|
|
24
|
-
return `${(bytes / (1024 * 1024)).toFixed(1)} MB`;
|
|
25
|
-
}
|
|
26
|
-
|
|
27
|
-
function getFileIcon(type: string) {
|
|
28
|
-
if (type.startsWith("image/")) return Image;
|
|
29
|
-
if (type.includes("pdf") || type.includes("document") || type.includes("text")) return FileText;
|
|
30
|
-
if (type.includes("javascript") || type.includes("typescript") || type.includes("json") || type.includes("xml")) return FileCode;
|
|
31
|
-
return FileText;
|
|
32
|
-
}
|
|
33
|
-
|
|
34
|
-
export function FileUpload({ onUploaded, uploads, onRemove }: FileUploadProps) {
|
|
35
|
-
const [uploading, setUploading] = useState(false);
|
|
36
|
-
const inputRef = useRef<HTMLInputElement>(null);
|
|
37
|
-
|
|
38
|
-
async function handleFile(file: File) {
|
|
39
|
-
setUploading(true);
|
|
40
|
-
try {
|
|
41
|
-
const formData = new FormData();
|
|
42
|
-
formData.append("file", file);
|
|
43
|
-
|
|
44
|
-
const res = await fetch("/api/uploads", {
|
|
45
|
-
method: "POST",
|
|
46
|
-
body: formData,
|
|
47
|
-
});
|
|
48
|
-
|
|
49
|
-
if (res.ok) {
|
|
50
|
-
const data = await res.json();
|
|
51
|
-
onUploaded(data);
|
|
52
|
-
}
|
|
53
|
-
} finally {
|
|
54
|
-
setUploading(false);
|
|
55
|
-
}
|
|
56
|
-
}
|
|
57
|
-
|
|
58
|
-
function handleChange(e: React.ChangeEvent<HTMLInputElement>) {
|
|
59
|
-
const file = e.target.files?.[0];
|
|
60
|
-
if (file) handleFile(file);
|
|
61
|
-
e.target.value = "";
|
|
62
|
-
}
|
|
63
|
-
|
|
64
|
-
function handleDrop(e: React.DragEvent) {
|
|
65
|
-
e.preventDefault();
|
|
66
|
-
const file = e.dataTransfer.files?.[0];
|
|
67
|
-
if (file) handleFile(file);
|
|
68
|
-
}
|
|
69
|
-
|
|
70
|
-
return (
|
|
71
|
-
<div className="space-y-2">
|
|
72
|
-
<div
|
|
73
|
-
role="button"
|
|
74
|
-
tabIndex={0}
|
|
75
|
-
aria-label="Upload a file — click or drag and drop"
|
|
76
|
-
className="border-2 border-dashed rounded-lg p-4 text-center cursor-pointer hover:bg-accent/50 transition-colors focus-visible:outline-none focus-visible:ring-2 focus-visible:ring-ring"
|
|
77
|
-
onClick={() => inputRef.current?.click()}
|
|
78
|
-
onKeyDown={(e) => { if (e.key === "Enter" || e.key === " ") { e.preventDefault(); inputRef.current?.click(); } }}
|
|
79
|
-
onDrop={handleDrop}
|
|
80
|
-
onDragOver={(e) => e.preventDefault()}
|
|
81
|
-
>
|
|
82
|
-
<Upload className="h-5 w-5 mx-auto text-muted-foreground mb-1" />
|
|
83
|
-
<p className="text-xs text-muted-foreground">
|
|
84
|
-
{uploading ? "Uploading..." : "Click or drop a file"}
|
|
85
|
-
</p>
|
|
86
|
-
<p className="text-xs text-muted-foreground mt-0.5">Max 50MB per file</p>
|
|
87
|
-
<input
|
|
88
|
-
ref={inputRef}
|
|
89
|
-
type="file"
|
|
90
|
-
className="hidden"
|
|
91
|
-
onChange={handleChange}
|
|
92
|
-
/>
|
|
93
|
-
</div>
|
|
94
|
-
{uploads.length > 0 && (
|
|
95
|
-
<div className="space-y-1">
|
|
96
|
-
{uploads.map((f) => {
|
|
97
|
-
const Icon = getFileIcon(f.type);
|
|
98
|
-
return (
|
|
99
|
-
<div key={f.id} className="flex items-center gap-2 text-sm">
|
|
100
|
-
<Icon className="h-3 w-3 text-muted-foreground" />
|
|
101
|
-
<span className="flex-1 truncate">{f.originalName}</span>
|
|
102
|
-
<span className="text-xs text-muted-foreground">{formatSize(f.size)}</span>
|
|
103
|
-
<Button
|
|
104
|
-
type="button"
|
|
105
|
-
variant="ghost"
|
|
106
|
-
size="icon"
|
|
107
|
-
className="h-5 w-5"
|
|
108
|
-
onClick={() => onRemove(f.id)}
|
|
109
|
-
aria-label={`Remove ${f.originalName}`}
|
|
110
|
-
>
|
|
111
|
-
<X className="h-3 w-3" />
|
|
112
|
-
</Button>
|
|
113
|
-
</div>
|
|
114
|
-
);
|
|
115
|
-
})}
|
|
116
|
-
</div>
|
|
117
|
-
)}
|
|
118
|
-
</div>
|
|
119
|
-
);
|
|
120
|
-
}
|