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
|
@@ -2,6 +2,76 @@ import { defineTool } from "../tool-registry";
|
|
|
2
2
|
import { z } from "zod";
|
|
3
3
|
import { ok, err, type ToolContext } from "./helpers";
|
|
4
4
|
|
|
5
|
+
/* ── Writable settings allowlist ─────────────────────────────────── */
|
|
6
|
+
|
|
7
|
+
interface WritableSetting {
|
|
8
|
+
validate: (value: string) => string | null; // error message or null
|
|
9
|
+
description: string;
|
|
10
|
+
}
|
|
11
|
+
|
|
12
|
+
const WRITABLE_SETTINGS: Record<string, WritableSetting> = {
|
|
13
|
+
"runtime.sdkTimeoutSeconds": {
|
|
14
|
+
description: "SDK timeout in seconds (10–300)",
|
|
15
|
+
validate: (v) => {
|
|
16
|
+
const n = parseInt(v, 10);
|
|
17
|
+
return isNaN(n) || n < 10 || n > 300 ? "Must be integer 10–300" : null;
|
|
18
|
+
},
|
|
19
|
+
},
|
|
20
|
+
"runtime.maxTurns": {
|
|
21
|
+
description: "Max agent turns per task (1–50)",
|
|
22
|
+
validate: (v) => {
|
|
23
|
+
const n = parseInt(v, 10);
|
|
24
|
+
return isNaN(n) || n < 1 || n > 50 ? "Must be integer 1–50" : null;
|
|
25
|
+
},
|
|
26
|
+
},
|
|
27
|
+
"routing.preference": {
|
|
28
|
+
description: "Routing preference: cost | latency | quality | manual",
|
|
29
|
+
validate: (v) =>
|
|
30
|
+
["cost", "latency", "quality", "manual"].includes(v)
|
|
31
|
+
? null
|
|
32
|
+
: "Must be one of: cost, latency, quality, manual",
|
|
33
|
+
},
|
|
34
|
+
"browser.chromeDevtoolsEnabled": {
|
|
35
|
+
description: "Enable Chrome DevTools MCP: true | false",
|
|
36
|
+
validate: (v) =>
|
|
37
|
+
["true", "false"].includes(v) ? null : "Must be 'true' or 'false'",
|
|
38
|
+
},
|
|
39
|
+
"browser.playwrightEnabled": {
|
|
40
|
+
description: "Enable Playwright MCP: true | false",
|
|
41
|
+
validate: (v) =>
|
|
42
|
+
["true", "false"].includes(v) ? null : "Must be 'true' or 'false'",
|
|
43
|
+
},
|
|
44
|
+
"web.exaSearchEnabled": {
|
|
45
|
+
description: "Enable Exa web search: true | false",
|
|
46
|
+
validate: (v) =>
|
|
47
|
+
["true", "false"].includes(v) ? null : "Must be 'true' or 'false'",
|
|
48
|
+
},
|
|
49
|
+
"learning.contextCharLimit": {
|
|
50
|
+
description: "Learning context char limit (2000–32000, step 1000)",
|
|
51
|
+
validate: (v) => {
|
|
52
|
+
const n = parseInt(v, 10);
|
|
53
|
+
return isNaN(n) || n < 2000 || n > 32000 || n % 1000 !== 0
|
|
54
|
+
? "Must be integer 2000–32000, step 1000"
|
|
55
|
+
: null;
|
|
56
|
+
},
|
|
57
|
+
},
|
|
58
|
+
"ollama.baseUrl": {
|
|
59
|
+
description: "Ollama server base URL",
|
|
60
|
+
validate: (v) => (v.trim().length === 0 ? "Must be non-empty URL" : null),
|
|
61
|
+
},
|
|
62
|
+
"ollama.defaultModel": {
|
|
63
|
+
description: "Default Ollama model name",
|
|
64
|
+
validate: (v) =>
|
|
65
|
+
v.trim().length === 0 ? "Must be non-empty string" : null,
|
|
66
|
+
},
|
|
67
|
+
};
|
|
68
|
+
|
|
69
|
+
const WRITABLE_KEYS_DOC = Object.entries(WRITABLE_SETTINGS)
|
|
70
|
+
.map(([k, v]) => `- "${k}": ${v.description}`)
|
|
71
|
+
.join("\n");
|
|
72
|
+
|
|
73
|
+
/* ── Tool definitions ────────────────────────────────────────────── */
|
|
74
|
+
|
|
5
75
|
export function settingsTools(_ctx: ToolContext) {
|
|
6
76
|
return [
|
|
7
77
|
defineTool(
|
|
@@ -69,5 +139,44 @@ export function settingsTools(_ctx: ToolContext) {
|
|
|
69
139
|
}
|
|
70
140
|
}
|
|
71
141
|
),
|
|
142
|
+
|
|
143
|
+
defineTool(
|
|
144
|
+
"set_settings",
|
|
145
|
+
`Update a Stagent setting. Requires user approval.\n\nWritable keys:\n${WRITABLE_KEYS_DOC}`,
|
|
146
|
+
{
|
|
147
|
+
key: z.string().describe("Setting key to update"),
|
|
148
|
+
value: z.string().describe("New value (always a string)"),
|
|
149
|
+
},
|
|
150
|
+
async (args) => {
|
|
151
|
+
const spec = WRITABLE_SETTINGS[args.key];
|
|
152
|
+
if (!spec) {
|
|
153
|
+
return err(
|
|
154
|
+
`Key "${args.key}" is not writable. Valid keys: ${Object.keys(WRITABLE_SETTINGS).join(", ")}`
|
|
155
|
+
);
|
|
156
|
+
}
|
|
157
|
+
const validationError = spec.validate(args.value);
|
|
158
|
+
if (validationError) {
|
|
159
|
+
return err(
|
|
160
|
+
`Invalid value for "${args.key}": ${validationError}`
|
|
161
|
+
);
|
|
162
|
+
}
|
|
163
|
+
try {
|
|
164
|
+
const { getSetting, setSetting } = await import(
|
|
165
|
+
"@/lib/settings/helpers"
|
|
166
|
+
);
|
|
167
|
+
const oldValue = await getSetting(args.key);
|
|
168
|
+
await setSetting(args.key, args.value);
|
|
169
|
+
return ok({
|
|
170
|
+
key: args.key,
|
|
171
|
+
oldValue: oldValue ?? "(unset)",
|
|
172
|
+
newValue: args.value,
|
|
173
|
+
});
|
|
174
|
+
} catch (e) {
|
|
175
|
+
return err(
|
|
176
|
+
e instanceof Error ? e.message : "Failed to update setting"
|
|
177
|
+
);
|
|
178
|
+
}
|
|
179
|
+
}
|
|
180
|
+
),
|
|
72
181
|
];
|
|
73
182
|
}
|
|
@@ -1,8 +1,15 @@
|
|
|
1
1
|
import { defineTool } from "../tool-registry";
|
|
2
2
|
import { z } from "zod";
|
|
3
3
|
import { db } from "@/lib/db";
|
|
4
|
-
import {
|
|
5
|
-
|
|
4
|
+
import {
|
|
5
|
+
workflows,
|
|
6
|
+
tasks,
|
|
7
|
+
agentLogs,
|
|
8
|
+
notifications,
|
|
9
|
+
documents,
|
|
10
|
+
workflowDocumentInputs,
|
|
11
|
+
} from "@/lib/db/schema";
|
|
12
|
+
import { eq, and, desc, inArray, like } from "drizzle-orm";
|
|
6
13
|
import { ok, err, type ToolContext } from "./helpers";
|
|
7
14
|
|
|
8
15
|
const VALID_WORKFLOW_STATUSES = [
|
|
@@ -74,6 +81,12 @@ export function workflowTools(ctx: ToolContext) {
|
|
|
74
81
|
.describe(
|
|
75
82
|
'Workflow definition as JSON string. Must include "pattern" and "steps" array. Example: {"pattern":"sequence","steps":[{"name":"step1","prompt":"Do X","assignedAgent":"claude"}]}'
|
|
76
83
|
),
|
|
84
|
+
documentIds: z
|
|
85
|
+
.array(z.string())
|
|
86
|
+
.optional()
|
|
87
|
+
.describe(
|
|
88
|
+
"Optional array of document IDs from the project pool to attach as input context. These documents will be injected into all workflow steps at execution time."
|
|
89
|
+
),
|
|
77
90
|
},
|
|
78
91
|
async (args) => {
|
|
79
92
|
try {
|
|
@@ -103,6 +116,25 @@ export function workflowTools(ctx: ToolContext) {
|
|
|
103
116
|
updatedAt: now,
|
|
104
117
|
});
|
|
105
118
|
|
|
119
|
+
// Attach pool documents if provided
|
|
120
|
+
const attachedDocs: string[] = [];
|
|
121
|
+
if (args.documentIds && args.documentIds.length > 0) {
|
|
122
|
+
for (const docId of args.documentIds) {
|
|
123
|
+
try {
|
|
124
|
+
await db.insert(workflowDocumentInputs).values({
|
|
125
|
+
id: crypto.randomUUID(),
|
|
126
|
+
workflowId: id,
|
|
127
|
+
documentId: docId,
|
|
128
|
+
stepId: null,
|
|
129
|
+
createdAt: now,
|
|
130
|
+
});
|
|
131
|
+
attachedDocs.push(docId);
|
|
132
|
+
} catch {
|
|
133
|
+
// Skip duplicates or invalid doc IDs
|
|
134
|
+
}
|
|
135
|
+
}
|
|
136
|
+
}
|
|
137
|
+
|
|
106
138
|
const [workflow] = await db
|
|
107
139
|
.select()
|
|
108
140
|
.from(workflows)
|
|
@@ -115,6 +147,7 @@ export function workflowTools(ctx: ToolContext) {
|
|
|
115
147
|
projectId: workflow.projectId,
|
|
116
148
|
status: workflow.status,
|
|
117
149
|
createdAt: workflow.createdAt,
|
|
150
|
+
attachedDocuments: attachedDocs.length,
|
|
118
151
|
});
|
|
119
152
|
} catch (e) {
|
|
120
153
|
return err(e instanceof Error ? e.message : "Failed to create workflow");
|
|
@@ -354,5 +387,115 @@ export function workflowTools(ctx: ToolContext) {
|
|
|
354
387
|
}
|
|
355
388
|
}
|
|
356
389
|
),
|
|
390
|
+
|
|
391
|
+
defineTool(
|
|
392
|
+
"find_related_documents",
|
|
393
|
+
"Search for documents in the project pool that could be used as context for a workflow. Returns output documents from completed workflows and uploaded documents. Use this proactively when creating follow-up workflows to discover relevant context.",
|
|
394
|
+
{
|
|
395
|
+
projectId: z
|
|
396
|
+
.string()
|
|
397
|
+
.optional()
|
|
398
|
+
.describe("Project ID to search in. Omit to use the active project."),
|
|
399
|
+
query: z
|
|
400
|
+
.string()
|
|
401
|
+
.optional()
|
|
402
|
+
.describe("Search query to match against document names"),
|
|
403
|
+
direction: z
|
|
404
|
+
.enum(["input", "output"])
|
|
405
|
+
.optional()
|
|
406
|
+
.describe('Filter by direction. Use "output" to find documents produced by other workflows.'),
|
|
407
|
+
sourceWorkflowId: z
|
|
408
|
+
.string()
|
|
409
|
+
.optional()
|
|
410
|
+
.describe("Filter to documents produced by a specific workflow"),
|
|
411
|
+
limit: z
|
|
412
|
+
.number()
|
|
413
|
+
.optional()
|
|
414
|
+
.describe("Maximum number of documents to return (default: 20)"),
|
|
415
|
+
},
|
|
416
|
+
async (args) => {
|
|
417
|
+
try {
|
|
418
|
+
const effectiveProjectId = args.projectId ?? ctx.projectId ?? undefined;
|
|
419
|
+
if (!effectiveProjectId) {
|
|
420
|
+
return err("No project context — specify a projectId or set an active project");
|
|
421
|
+
}
|
|
422
|
+
|
|
423
|
+
const conditions = [
|
|
424
|
+
eq(documents.projectId, effectiveProjectId),
|
|
425
|
+
eq(documents.status, "ready"),
|
|
426
|
+
];
|
|
427
|
+
|
|
428
|
+
if (args.direction) {
|
|
429
|
+
conditions.push(eq(documents.direction, args.direction));
|
|
430
|
+
}
|
|
431
|
+
|
|
432
|
+
if (args.query) {
|
|
433
|
+
conditions.push(like(documents.originalName, `%${args.query}%`));
|
|
434
|
+
}
|
|
435
|
+
|
|
436
|
+
if (args.sourceWorkflowId) {
|
|
437
|
+
// Find task IDs belonging to the source workflow
|
|
438
|
+
const workflowTasks = await db
|
|
439
|
+
.select({ id: tasks.id })
|
|
440
|
+
.from(tasks)
|
|
441
|
+
.where(eq(tasks.workflowId, args.sourceWorkflowId));
|
|
442
|
+
|
|
443
|
+
const taskIds = workflowTasks.map((t) => t.id);
|
|
444
|
+
if (taskIds.length > 0) {
|
|
445
|
+
conditions.push(inArray(documents.taskId, taskIds));
|
|
446
|
+
} else {
|
|
447
|
+
return ok([]); // No tasks for this workflow
|
|
448
|
+
}
|
|
449
|
+
}
|
|
450
|
+
|
|
451
|
+
const result = await db
|
|
452
|
+
.select({
|
|
453
|
+
id: documents.id,
|
|
454
|
+
originalName: documents.originalName,
|
|
455
|
+
mimeType: documents.mimeType,
|
|
456
|
+
size: documents.size,
|
|
457
|
+
direction: documents.direction,
|
|
458
|
+
category: documents.category,
|
|
459
|
+
status: documents.status,
|
|
460
|
+
taskId: documents.taskId,
|
|
461
|
+
createdAt: documents.createdAt,
|
|
462
|
+
})
|
|
463
|
+
.from(documents)
|
|
464
|
+
.where(and(...conditions))
|
|
465
|
+
.orderBy(desc(documents.createdAt))
|
|
466
|
+
.limit(args.limit ?? 20);
|
|
467
|
+
|
|
468
|
+
// Enrich with source workflow name
|
|
469
|
+
const enriched = await Promise.all(
|
|
470
|
+
result.map(async (doc) => {
|
|
471
|
+
let sourceWorkflowName: string | null = null;
|
|
472
|
+
if (doc.taskId) {
|
|
473
|
+
const [task] = await db
|
|
474
|
+
.select({ workflowId: tasks.workflowId })
|
|
475
|
+
.from(tasks)
|
|
476
|
+
.where(eq(tasks.id, doc.taskId));
|
|
477
|
+
if (task?.workflowId) {
|
|
478
|
+
const [wf] = await db
|
|
479
|
+
.select({ name: workflows.name })
|
|
480
|
+
.from(workflows)
|
|
481
|
+
.where(eq(workflows.id, task.workflowId));
|
|
482
|
+
sourceWorkflowName = wf?.name ?? null;
|
|
483
|
+
}
|
|
484
|
+
}
|
|
485
|
+
return {
|
|
486
|
+
...doc,
|
|
487
|
+
sourceWorkflow: sourceWorkflowName,
|
|
488
|
+
};
|
|
489
|
+
})
|
|
490
|
+
);
|
|
491
|
+
|
|
492
|
+
return ok(enriched);
|
|
493
|
+
} catch (e) {
|
|
494
|
+
return err(
|
|
495
|
+
e instanceof Error ? e.message : "Failed to find documents"
|
|
496
|
+
);
|
|
497
|
+
}
|
|
498
|
+
}
|
|
499
|
+
),
|
|
357
500
|
];
|
|
358
501
|
}
|
package/src/lib/data/clear.ts
CHANGED
|
@@ -25,6 +25,9 @@ import {
|
|
|
25
25
|
channelBindings,
|
|
26
26
|
channelConfigs,
|
|
27
27
|
agentMessages,
|
|
28
|
+
workflowDocumentInputs,
|
|
29
|
+
scheduleDocumentInputs,
|
|
30
|
+
projectDocumentDefaults,
|
|
28
31
|
} from "@/lib/db/schema";
|
|
29
32
|
import { readdirSync, unlinkSync, mkdirSync } from "fs";
|
|
30
33
|
import { join } from "path";
|
|
@@ -71,6 +74,12 @@ export function clearAllData() {
|
|
|
71
74
|
const usageLedgerDeleted = db.delete(usageLedger).run().changes;
|
|
72
75
|
const logsDeleted = db.delete(agentLogs).run().changes;
|
|
73
76
|
const notificationsDeleted = db.delete(notifications).run().changes;
|
|
77
|
+
|
|
78
|
+
// Document junction tables — delete before documents, workflows, schedules, projects
|
|
79
|
+
const workflowDocInputsDeleted = db.delete(workflowDocumentInputs).run().changes;
|
|
80
|
+
const scheduleDocInputsDeleted = db.delete(scheduleDocumentInputs).run().changes;
|
|
81
|
+
const projectDocDefaultsDeleted = db.delete(projectDocumentDefaults).run().changes;
|
|
82
|
+
|
|
74
83
|
const documentsDeleted = db.delete(documents).run().changes;
|
|
75
84
|
const agentMemoryDeleted = db.delete(agentMemory).run().changes;
|
|
76
85
|
const learnedContextDeleted = db.delete(learnedContext).run().changes;
|
|
@@ -130,6 +139,9 @@ export function clearAllData() {
|
|
|
130
139
|
agentMessages: agentMessagesDeleted,
|
|
131
140
|
channelBindings: channelBindingsDeleted,
|
|
132
141
|
channelConfigs: channelConfigsDeleted,
|
|
142
|
+
workflowDocumentInputs: workflowDocInputsDeleted,
|
|
143
|
+
scheduleDocumentInputs: scheduleDocInputsDeleted,
|
|
144
|
+
projectDocumentDefaults: projectDocDefaultsDeleted,
|
|
133
145
|
files: filesDeleted,
|
|
134
146
|
screenshots: screenshotsDeleted,
|
|
135
147
|
};
|
package/src/lib/db/bootstrap.ts
CHANGED
|
@@ -28,6 +28,9 @@ const STAGENT_TABLES = [
|
|
|
28
28
|
"channel_configs",
|
|
29
29
|
"channel_bindings",
|
|
30
30
|
"agent_messages",
|
|
31
|
+
"workflow_document_inputs",
|
|
32
|
+
"schedule_document_inputs",
|
|
33
|
+
"project_document_defaults",
|
|
31
34
|
] as const;
|
|
32
35
|
|
|
33
36
|
export function bootstrapStagentDatabase(sqlite: Database.Database): void {
|
|
@@ -55,6 +58,7 @@ export function bootstrapStagentDatabase(sqlite: Database.Database): void {
|
|
|
55
58
|
result TEXT,
|
|
56
59
|
session_id TEXT,
|
|
57
60
|
resume_count INTEGER DEFAULT 0 NOT NULL,
|
|
61
|
+
workflow_run_number INTEGER,
|
|
58
62
|
created_at INTEGER NOT NULL,
|
|
59
63
|
updated_at INTEGER NOT NULL,
|
|
60
64
|
FOREIGN KEY (project_id) REFERENCES projects(id) ON UPDATE NO ACTION ON DELETE NO ACTION,
|
|
@@ -68,6 +72,7 @@ export function bootstrapStagentDatabase(sqlite: Database.Database): void {
|
|
|
68
72
|
name TEXT NOT NULL,
|
|
69
73
|
definition TEXT NOT NULL,
|
|
70
74
|
status TEXT DEFAULT 'draft' NOT NULL,
|
|
75
|
+
run_number INTEGER DEFAULT 0 NOT NULL,
|
|
71
76
|
created_at INTEGER NOT NULL,
|
|
72
77
|
updated_at INTEGER NOT NULL,
|
|
73
78
|
FOREIGN KEY (project_id) REFERENCES projects(id) ON UPDATE NO ACTION ON DELETE NO ACTION
|
|
@@ -272,6 +277,8 @@ export function bootstrapStagentDatabase(sqlite: Database.Database): void {
|
|
|
272
277
|
|
|
273
278
|
// Task source type
|
|
274
279
|
addColumnIfMissing(`ALTER TABLE tasks ADD COLUMN source_type TEXT;`);
|
|
280
|
+
addColumnIfMissing(`ALTER TABLE workflows ADD COLUMN run_number INTEGER DEFAULT 0 NOT NULL;`);
|
|
281
|
+
addColumnIfMissing(`ALTER TABLE tasks ADD COLUMN workflow_run_number INTEGER;`);
|
|
275
282
|
addColumnIfMissing(`ALTER TABLE documents ADD COLUMN version INTEGER NOT NULL DEFAULT 1;`);
|
|
276
283
|
addColumnIfMissing(`ALTER TABLE documents ADD COLUMN source TEXT DEFAULT 'upload';`);
|
|
277
284
|
addColumnIfMissing(`ALTER TABLE documents ADD COLUMN conversation_id TEXT REFERENCES conversations(id);`);
|
|
@@ -566,6 +573,47 @@ export function bootstrapStagentDatabase(sqlite: Database.Database): void {
|
|
|
566
573
|
CREATE INDEX IF NOT EXISTS idx_agent_messages_to_status ON agent_messages(to_profile_id, status);
|
|
567
574
|
CREATE INDEX IF NOT EXISTS idx_agent_messages_task ON agent_messages(task_id);
|
|
568
575
|
`);
|
|
576
|
+
|
|
577
|
+
// ── Workflow Document Pool ──────────────────────────────────────────
|
|
578
|
+
sqlite.exec(`
|
|
579
|
+
CREATE TABLE IF NOT EXISTS workflow_document_inputs (
|
|
580
|
+
id TEXT PRIMARY KEY NOT NULL,
|
|
581
|
+
workflow_id TEXT NOT NULL,
|
|
582
|
+
document_id TEXT NOT NULL,
|
|
583
|
+
step_id TEXT,
|
|
584
|
+
created_at INTEGER NOT NULL,
|
|
585
|
+
FOREIGN KEY (workflow_id) REFERENCES workflows(id) ON UPDATE NO ACTION ON DELETE NO ACTION,
|
|
586
|
+
FOREIGN KEY (document_id) REFERENCES documents(id) ON UPDATE NO ACTION ON DELETE NO ACTION
|
|
587
|
+
);
|
|
588
|
+
|
|
589
|
+
CREATE INDEX IF NOT EXISTS idx_wdi_workflow ON workflow_document_inputs(workflow_id);
|
|
590
|
+
CREATE INDEX IF NOT EXISTS idx_wdi_document ON workflow_document_inputs(document_id);
|
|
591
|
+
CREATE UNIQUE INDEX IF NOT EXISTS idx_wdi_workflow_doc_step ON workflow_document_inputs(workflow_id, document_id, step_id);
|
|
592
|
+
|
|
593
|
+
CREATE TABLE IF NOT EXISTS schedule_document_inputs (
|
|
594
|
+
id TEXT PRIMARY KEY NOT NULL,
|
|
595
|
+
schedule_id TEXT NOT NULL,
|
|
596
|
+
document_id TEXT NOT NULL,
|
|
597
|
+
created_at INTEGER NOT NULL,
|
|
598
|
+
FOREIGN KEY (schedule_id) REFERENCES schedules(id) ON UPDATE NO ACTION ON DELETE NO ACTION,
|
|
599
|
+
FOREIGN KEY (document_id) REFERENCES documents(id) ON UPDATE NO ACTION ON DELETE NO ACTION
|
|
600
|
+
);
|
|
601
|
+
|
|
602
|
+
CREATE INDEX IF NOT EXISTS idx_sdi_schedule ON schedule_document_inputs(schedule_id);
|
|
603
|
+
CREATE UNIQUE INDEX IF NOT EXISTS idx_sdi_schedule_doc ON schedule_document_inputs(schedule_id, document_id);
|
|
604
|
+
|
|
605
|
+
CREATE TABLE IF NOT EXISTS project_document_defaults (
|
|
606
|
+
id TEXT PRIMARY KEY NOT NULL,
|
|
607
|
+
project_id TEXT NOT NULL,
|
|
608
|
+
document_id TEXT NOT NULL,
|
|
609
|
+
created_at INTEGER NOT NULL,
|
|
610
|
+
FOREIGN KEY (project_id) REFERENCES projects(id) ON UPDATE NO ACTION ON DELETE NO ACTION,
|
|
611
|
+
FOREIGN KEY (document_id) REFERENCES documents(id) ON UPDATE NO ACTION ON DELETE NO ACTION
|
|
612
|
+
);
|
|
613
|
+
|
|
614
|
+
CREATE INDEX IF NOT EXISTS idx_pdd_project ON project_document_defaults(project_id);
|
|
615
|
+
CREATE UNIQUE INDEX IF NOT EXISTS idx_pdd_project_doc ON project_document_defaults(project_id, document_id);
|
|
616
|
+
`);
|
|
569
617
|
}
|
|
570
618
|
|
|
571
619
|
export function hasLegacyStagentTables(sqlite: Database.Database): boolean {
|
|
@@ -0,0 +1,13 @@
|
|
|
1
|
+
CREATE TABLE IF NOT EXISTS workflow_document_inputs (
|
|
2
|
+
id TEXT PRIMARY KEY NOT NULL,
|
|
3
|
+
workflow_id TEXT NOT NULL,
|
|
4
|
+
document_id TEXT NOT NULL,
|
|
5
|
+
step_id TEXT,
|
|
6
|
+
created_at INTEGER NOT NULL,
|
|
7
|
+
FOREIGN KEY (workflow_id) REFERENCES workflows(id) ON UPDATE NO ACTION ON DELETE NO ACTION,
|
|
8
|
+
FOREIGN KEY (document_id) REFERENCES documents(id) ON UPDATE NO ACTION ON DELETE NO ACTION
|
|
9
|
+
);
|
|
10
|
+
|
|
11
|
+
CREATE INDEX IF NOT EXISTS idx_wdi_workflow ON workflow_document_inputs(workflow_id);
|
|
12
|
+
CREATE INDEX IF NOT EXISTS idx_wdi_document ON workflow_document_inputs(document_id);
|
|
13
|
+
CREATE UNIQUE INDEX IF NOT EXISTS idx_wdi_workflow_doc_step ON workflow_document_inputs(workflow_id, document_id, step_id);
|
|
@@ -0,0 +1,25 @@
|
|
|
1
|
+
-- Schedule document inputs (context documents for scheduled tasks)
|
|
2
|
+
CREATE TABLE IF NOT EXISTS `schedule_document_inputs` (
|
|
3
|
+
`id` text PRIMARY KEY NOT NULL,
|
|
4
|
+
`schedule_id` text NOT NULL,
|
|
5
|
+
`document_id` text NOT NULL,
|
|
6
|
+
`created_at` integer NOT NULL,
|
|
7
|
+
FOREIGN KEY (`schedule_id`) REFERENCES `schedules`(`id`) ON UPDATE NO ACTION ON DELETE NO ACTION,
|
|
8
|
+
FOREIGN KEY (`document_id`) REFERENCES `documents`(`id`) ON UPDATE NO ACTION ON DELETE NO ACTION
|
|
9
|
+
);
|
|
10
|
+
|
|
11
|
+
CREATE INDEX IF NOT EXISTS `idx_sdi_schedule` ON `schedule_document_inputs` (`schedule_id`);
|
|
12
|
+
CREATE UNIQUE INDEX IF NOT EXISTS `idx_sdi_schedule_doc` ON `schedule_document_inputs` (`schedule_id`, `document_id`);
|
|
13
|
+
|
|
14
|
+
-- Project document defaults (auto-attached to new tasks/workflows in project)
|
|
15
|
+
CREATE TABLE IF NOT EXISTS `project_document_defaults` (
|
|
16
|
+
`id` text PRIMARY KEY NOT NULL,
|
|
17
|
+
`project_id` text NOT NULL,
|
|
18
|
+
`document_id` text NOT NULL,
|
|
19
|
+
`created_at` integer NOT NULL,
|
|
20
|
+
FOREIGN KEY (`project_id`) REFERENCES `projects`(`id`) ON UPDATE NO ACTION ON DELETE NO ACTION,
|
|
21
|
+
FOREIGN KEY (`document_id`) REFERENCES `documents`(`id`) ON UPDATE NO ACTION ON DELETE NO ACTION
|
|
22
|
+
);
|
|
23
|
+
|
|
24
|
+
CREATE INDEX IF NOT EXISTS `idx_pdd_project` ON `project_document_defaults` (`project_id`);
|
|
25
|
+
CREATE UNIQUE INDEX IF NOT EXISTS `idx_pdd_project_doc` ON `project_document_defaults` (`project_id`, `document_id`);
|
package/src/lib/db/schema.ts
CHANGED
|
@@ -37,6 +37,7 @@ export const tasks = sqliteTable(
|
|
|
37
37
|
sourceType: text("source_type", {
|
|
38
38
|
enum: ["manual", "scheduled", "heartbeat", "workflow"],
|
|
39
39
|
}),
|
|
40
|
+
workflowRunNumber: integer("workflow_run_number"),
|
|
40
41
|
createdAt: integer("created_at", { mode: "timestamp" }).notNull(),
|
|
41
42
|
updatedAt: integer("updated_at", { mode: "timestamp" }).notNull(),
|
|
42
43
|
},
|
|
@@ -59,6 +60,7 @@ export const workflows = sqliteTable("workflows", {
|
|
|
59
60
|
})
|
|
60
61
|
.default("draft")
|
|
61
62
|
.notNull(),
|
|
63
|
+
runNumber: integer("run_number").default(0).notNull(),
|
|
62
64
|
createdAt: integer("created_at", { mode: "timestamp" }).notNull(),
|
|
63
65
|
updatedAt: integer("updated_at", { mode: "timestamp" }).notNull(),
|
|
64
66
|
});
|
|
@@ -678,6 +680,81 @@ export const agentMessages = sqliteTable(
|
|
|
678
680
|
]
|
|
679
681
|
);
|
|
680
682
|
|
|
683
|
+
// ── Workflow Document Pool ───────────────────────────────────────────
|
|
684
|
+
|
|
685
|
+
export const workflowDocumentInputs = sqliteTable(
|
|
686
|
+
"workflow_document_inputs",
|
|
687
|
+
{
|
|
688
|
+
id: text("id").primaryKey(),
|
|
689
|
+
workflowId: text("workflow_id")
|
|
690
|
+
.references(() => workflows.id)
|
|
691
|
+
.notNull(),
|
|
692
|
+
documentId: text("document_id")
|
|
693
|
+
.references(() => documents.id)
|
|
694
|
+
.notNull(),
|
|
695
|
+
/** null = document available to all steps; set = scoped to specific step */
|
|
696
|
+
stepId: text("step_id"),
|
|
697
|
+
createdAt: integer("created_at", { mode: "timestamp" }).notNull(),
|
|
698
|
+
},
|
|
699
|
+
(table) => [
|
|
700
|
+
index("idx_wdi_workflow").on(table.workflowId),
|
|
701
|
+
index("idx_wdi_document").on(table.documentId),
|
|
702
|
+
uniqueIndex("idx_wdi_workflow_doc_step").on(
|
|
703
|
+
table.workflowId,
|
|
704
|
+
table.documentId,
|
|
705
|
+
table.stepId
|
|
706
|
+
),
|
|
707
|
+
]
|
|
708
|
+
);
|
|
709
|
+
|
|
710
|
+
export type WorkflowDocumentInputRow = InferSelectModel<typeof workflowDocumentInputs>;
|
|
711
|
+
|
|
712
|
+
export const scheduleDocumentInputs = sqliteTable(
|
|
713
|
+
"schedule_document_inputs",
|
|
714
|
+
{
|
|
715
|
+
id: text("id").primaryKey(),
|
|
716
|
+
scheduleId: text("schedule_id")
|
|
717
|
+
.references(() => schedules.id)
|
|
718
|
+
.notNull(),
|
|
719
|
+
documentId: text("document_id")
|
|
720
|
+
.references(() => documents.id)
|
|
721
|
+
.notNull(),
|
|
722
|
+
createdAt: integer("created_at", { mode: "timestamp" }).notNull(),
|
|
723
|
+
},
|
|
724
|
+
(table) => [
|
|
725
|
+
index("idx_sdi_schedule").on(table.scheduleId),
|
|
726
|
+
uniqueIndex("idx_sdi_schedule_doc").on(
|
|
727
|
+
table.scheduleId,
|
|
728
|
+
table.documentId
|
|
729
|
+
),
|
|
730
|
+
]
|
|
731
|
+
);
|
|
732
|
+
|
|
733
|
+
export type ScheduleDocumentInputRow = InferSelectModel<typeof scheduleDocumentInputs>;
|
|
734
|
+
|
|
735
|
+
export const projectDocumentDefaults = sqliteTable(
|
|
736
|
+
"project_document_defaults",
|
|
737
|
+
{
|
|
738
|
+
id: text("id").primaryKey(),
|
|
739
|
+
projectId: text("project_id")
|
|
740
|
+
.references(() => projects.id)
|
|
741
|
+
.notNull(),
|
|
742
|
+
documentId: text("document_id")
|
|
743
|
+
.references(() => documents.id)
|
|
744
|
+
.notNull(),
|
|
745
|
+
createdAt: integer("created_at", { mode: "timestamp" }).notNull(),
|
|
746
|
+
},
|
|
747
|
+
(table) => [
|
|
748
|
+
index("idx_pdd_project").on(table.projectId),
|
|
749
|
+
uniqueIndex("idx_pdd_project_doc").on(
|
|
750
|
+
table.projectId,
|
|
751
|
+
table.documentId
|
|
752
|
+
),
|
|
753
|
+
]
|
|
754
|
+
);
|
|
755
|
+
|
|
756
|
+
export type ProjectDocumentDefaultRow = InferSelectModel<typeof projectDocumentDefaults>;
|
|
757
|
+
|
|
681
758
|
// Shared types derived from schema — use these in components instead of `as any`
|
|
682
759
|
export type ProjectRow = InferSelectModel<typeof projects>;
|
|
683
760
|
export type TaskRow = InferSelectModel<typeof tasks>;
|
package/src/lib/docs/reader.ts
CHANGED
|
@@ -1,12 +1,11 @@
|
|
|
1
1
|
import { readFileSync, readdirSync, existsSync } from "fs";
|
|
2
2
|
import { join, basename } from "path";
|
|
3
3
|
import type { DocManifest, ParsedDoc } from "./types";
|
|
4
|
+
import { getAppRoot } from "../utils/app-root";
|
|
4
5
|
|
|
5
6
|
/** Resolve the docs directory relative to this source file (npx-safe) */
|
|
6
7
|
function docsDir(): string {
|
|
7
|
-
|
|
8
|
-
// src/lib/docs/ → project root → docs/
|
|
9
|
-
return join(dir, "..", "..", "..", "docs");
|
|
8
|
+
return join(getAppRoot(import.meta.dirname, 3), "docs");
|
|
10
9
|
}
|
|
11
10
|
|
|
12
11
|
/** Read and parse docs/manifest.json */
|
|
@@ -4,8 +4,8 @@
|
|
|
4
4
|
*/
|
|
5
5
|
|
|
6
6
|
import { db } from "@/lib/db";
|
|
7
|
-
import { documents } from "@/lib/db/schema";
|
|
8
|
-
import { and, eq } from "drizzle-orm";
|
|
7
|
+
import { documents, workflowDocumentInputs } from "@/lib/db/schema";
|
|
8
|
+
import { and, eq, inArray, isNull } from "drizzle-orm";
|
|
9
9
|
import type { DocumentRow } from "@/lib/db/schema";
|
|
10
10
|
|
|
11
11
|
const MAX_INLINE_TEXT = 10_000;
|
|
@@ -114,3 +114,76 @@ export async function buildWorkflowDocumentContext(
|
|
|
114
114
|
return null;
|
|
115
115
|
}
|
|
116
116
|
}
|
|
117
|
+
|
|
118
|
+
/**
|
|
119
|
+
* Build document context from the workflow document pool (junction table).
|
|
120
|
+
* Queries workflow_document_inputs for documents bound to this workflow,
|
|
121
|
+
* optionally scoped to a specific step. Returns null if no pool documents.
|
|
122
|
+
*
|
|
123
|
+
* Includes both workflow-level bindings (stepId=null) and step-specific bindings.
|
|
124
|
+
*/
|
|
125
|
+
export async function buildPoolDocumentContext(
|
|
126
|
+
workflowId: string,
|
|
127
|
+
stepId?: string
|
|
128
|
+
): Promise<string | null> {
|
|
129
|
+
try {
|
|
130
|
+
// Get workflow-level (stepId=null) bindings — available to all steps
|
|
131
|
+
const globalBindings = await db
|
|
132
|
+
.select({ documentId: workflowDocumentInputs.documentId })
|
|
133
|
+
.from(workflowDocumentInputs)
|
|
134
|
+
.where(
|
|
135
|
+
and(
|
|
136
|
+
eq(workflowDocumentInputs.workflowId, workflowId),
|
|
137
|
+
isNull(workflowDocumentInputs.stepId)
|
|
138
|
+
)
|
|
139
|
+
);
|
|
140
|
+
|
|
141
|
+
// If a specific step, also get step-scoped bindings
|
|
142
|
+
let stepBindings: { documentId: string }[] = [];
|
|
143
|
+
if (stepId) {
|
|
144
|
+
stepBindings = await db
|
|
145
|
+
.select({ documentId: workflowDocumentInputs.documentId })
|
|
146
|
+
.from(workflowDocumentInputs)
|
|
147
|
+
.where(
|
|
148
|
+
and(
|
|
149
|
+
eq(workflowDocumentInputs.workflowId, workflowId),
|
|
150
|
+
eq(workflowDocumentInputs.stepId, stepId)
|
|
151
|
+
)
|
|
152
|
+
);
|
|
153
|
+
}
|
|
154
|
+
|
|
155
|
+
// Deduplicate document IDs
|
|
156
|
+
const docIdSet = new Set<string>();
|
|
157
|
+
for (const b of [...globalBindings, ...stepBindings]) {
|
|
158
|
+
docIdSet.add(b.documentId);
|
|
159
|
+
}
|
|
160
|
+
|
|
161
|
+
if (docIdSet.size === 0) return null;
|
|
162
|
+
|
|
163
|
+
const docs = await db
|
|
164
|
+
.select()
|
|
165
|
+
.from(documents)
|
|
166
|
+
.where(inArray(documents.id, [...docIdSet]));
|
|
167
|
+
|
|
168
|
+
if (docs.length === 0) return null;
|
|
169
|
+
|
|
170
|
+
const sections = docs.map((doc, i) => formatDocument(doc, i));
|
|
171
|
+
let result = sections.join("\n\n");
|
|
172
|
+
|
|
173
|
+
if (result.length > MAX_WORKFLOW_DOC_CONTEXT) {
|
|
174
|
+
result = result.slice(0, MAX_WORKFLOW_DOC_CONTEXT);
|
|
175
|
+
result += `\n\n(Pool document context truncated at ${MAX_WORKFLOW_DOC_CONTEXT} chars — use Read tool for full content)`;
|
|
176
|
+
}
|
|
177
|
+
|
|
178
|
+
return [
|
|
179
|
+
"--- Workflow Pool Documents ---",
|
|
180
|
+
"",
|
|
181
|
+
result,
|
|
182
|
+
"",
|
|
183
|
+
"--- End Workflow Pool Documents ---",
|
|
184
|
+
].join("\n");
|
|
185
|
+
} catch (error) {
|
|
186
|
+
console.error("[context-builder] Failed to build pool document context:", error);
|
|
187
|
+
return null;
|
|
188
|
+
}
|
|
189
|
+
}
|