stagent 0.9.5 → 0.10.0
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 +5 -42
- package/dist/cli.js +42 -18
- package/docs/.coverage-gaps.json +13 -55
- package/docs/.last-generated +1 -1
- package/docs/features/provider-runtimes.md +4 -0
- package/docs/features/schedules.md +32 -4
- package/docs/features/settings.md +28 -5
- package/docs/features/tables.md +9 -2
- package/docs/features/workflows.md +10 -4
- package/docs/journeys/developer.md +15 -1
- package/docs/journeys/personal-use.md +21 -4
- package/docs/superpowers/plans/2026-04-07-instance-bootstrap.md +1691 -0
- package/docs/superpowers/plans/2026-04-08-schedule-orchestration.md +2983 -0
- package/docs/superpowers/plans/2026-04-11-schedule-maxturns-api-control.md +551 -0
- package/docs/superpowers/plans/2026-04-11-task-create-profile-validation.md +864 -0
- package/docs/superpowers/plans/2026-04-11-task-runtime-stagent-mcp-injection.md +739 -0
- package/docs/superpowers/specs/2026-04-08-chat-sse-resilience-hotfix-design.md +201 -0
- package/docs/superpowers/specs/2026-04-08-schedule-orchestration-design.md +371 -0
- package/docs/superpowers/specs/2026-04-08-swarm-visibility-design.md +213 -0
- package/package.json +3 -2
- package/src/__tests__/instrumentation-smoke.test.ts +15 -0
- package/src/app/analytics/page.tsx +1 -21
- package/src/app/api/chat/conversations/[id]/messages/route.ts +22 -1
- package/src/app/api/diagnostics/chat-streams/route.ts +65 -0
- package/src/app/api/instance/config/route.ts +41 -0
- package/src/app/api/instance/init/route.ts +34 -0
- package/src/app/api/instance/upgrade/check/route.ts +26 -0
- package/src/app/api/instance/upgrade/route.ts +96 -0
- package/src/app/api/instance/upgrade/status/route.ts +35 -0
- package/src/app/api/memory/route.ts +0 -11
- package/src/app/api/notifications/route.ts +4 -2
- package/src/app/api/projects/[id]/route.ts +5 -155
- package/src/app/api/projects/__tests__/delete-project.test.ts +10 -19
- package/src/app/api/schedules/[id]/execute/route.ts +111 -0
- package/src/app/api/schedules/[id]/route.ts +9 -1
- package/src/app/api/schedules/__tests__/execute-route.test.ts +118 -0
- package/src/app/api/schedules/route.ts +3 -12
- package/src/app/api/settings/openai/login/route.ts +22 -0
- package/src/app/api/settings/openai/logout/route.ts +7 -0
- package/src/app/api/settings/openai/route.ts +21 -1
- package/src/app/api/settings/providers/route.ts +35 -8
- package/src/app/api/tables/[id]/enrich/__tests__/route.test.ts +153 -0
- package/src/app/api/tables/[id]/enrich/plan/route.ts +98 -0
- package/src/app/api/tables/[id]/enrich/route.ts +147 -0
- package/src/app/api/tables/[id]/enrich/runs/route.ts +25 -0
- package/src/app/api/tasks/[id]/execute/route.ts +0 -21
- package/src/app/api/workflows/[id]/resume/route.ts +59 -0
- package/src/app/api/workflows/[id]/status/route.ts +22 -8
- package/src/app/api/workspace/context/route.ts +2 -0
- package/src/app/api/workspace/fix-data-dir/route.ts +81 -0
- package/src/app/chat/page.tsx +11 -0
- package/src/app/inbox/page.tsx +12 -5
- package/src/app/layout.tsx +42 -21
- package/src/app/page.tsx +0 -2
- package/src/app/settings/page.tsx +6 -9
- package/src/components/chat/__tests__/chat-session-provider.test.tsx +408 -0
- package/src/components/chat/chat-command-popover.tsx +2 -2
- package/src/components/chat/chat-input.tsx +2 -3
- package/src/components/chat/chat-session-provider.tsx +720 -0
- package/src/components/chat/chat-shell.tsx +92 -401
- package/src/components/instance/__tests__/instance-section.test.tsx +125 -0
- package/src/components/instance/instance-section.tsx +382 -0
- package/src/components/instance/upgrade-badge.tsx +219 -0
- package/src/components/notifications/__tests__/batch-proposal-review.test.tsx +95 -0
- package/src/components/notifications/__tests__/notification-item.test.tsx +106 -0
- package/src/components/notifications/batch-proposal-review.tsx +20 -5
- package/src/components/notifications/inbox-list.tsx +11 -2
- package/src/components/notifications/notification-item.tsx +56 -2
- package/src/components/notifications/pending-approval-host.tsx +56 -37
- package/src/components/schedules/schedule-create-sheet.tsx +19 -1
- package/src/components/schedules/schedule-edit-sheet.tsx +20 -1
- package/src/components/schedules/schedule-form.tsx +31 -0
- package/src/components/settings/__tests__/providers-runtimes-section.test.tsx +149 -0
- package/src/components/settings/auth-method-selector.tsx +19 -4
- package/src/components/settings/auth-status-badge.tsx +28 -3
- package/src/components/settings/openai-chatgpt-auth-control.tsx +278 -0
- package/src/components/settings/openai-runtime-section.tsx +7 -1
- package/src/components/settings/providers-runtimes-section.tsx +138 -19
- package/src/components/shared/app-sidebar.tsx +4 -3
- package/src/components/shared/command-palette.tsx +4 -5
- package/src/components/shared/theme-toggle.tsx +5 -24
- package/src/components/shared/workspace-indicator.tsx +61 -2
- package/src/components/tables/__tests__/table-enrichment-sheet.test.tsx +130 -0
- package/src/components/tables/table-create-sheet.tsx +4 -0
- package/src/components/tables/table-enrichment-runs.tsx +103 -0
- package/src/components/tables/table-enrichment-sheet.tsx +538 -0
- package/src/components/tables/table-spreadsheet.tsx +29 -5
- package/src/components/tables/table-toolbar.tsx +10 -1
- package/src/components/tasks/kanban-board.tsx +1 -0
- package/src/components/tasks/kanban-column.tsx +53 -14
- package/src/components/tasks/task-bento-grid.tsx +19 -0
- package/src/components/tasks/task-card.tsx +26 -3
- package/src/components/tasks/task-chip-bar.tsx +24 -0
- package/src/components/tasks/task-result-renderer.tsx +1 -1
- package/src/components/workflows/delay-step-body.tsx +109 -0
- package/src/components/workflows/hooks/use-workflow-status.ts +50 -0
- package/src/components/workflows/loop-status-view.tsx +1 -1
- package/src/components/workflows/shared/step-result.tsx +78 -0
- package/src/components/workflows/shared/workflow-header.tsx +141 -0
- package/src/components/workflows/shared/workflow-loading-skeleton.tsx +36 -0
- package/src/components/workflows/swarm-dashboard.tsx +2 -15
- package/src/components/workflows/views/loop-pattern-view.tsx +137 -0
- package/src/components/workflows/views/sequence-pattern-view.tsx +511 -0
- package/src/components/workflows/workflow-form-view.tsx +133 -16
- package/src/components/workflows/workflow-status-view.tsx +30 -740
- package/src/instrumentation-node.ts +94 -0
- package/src/instrumentation.ts +4 -48
- package/src/lib/agents/__tests__/claude-agent.test.ts +199 -0
- package/src/lib/agents/__tests__/execution-manager.test.ts +1 -27
- package/src/lib/agents/__tests__/failure-reason.test.ts +68 -0
- package/src/lib/agents/__tests__/learned-context.test.ts +0 -11
- package/src/lib/agents/__tests__/learning-session.test.ts +158 -0
- package/src/lib/agents/__tests__/pattern-extractor.test.ts +48 -0
- package/src/lib/agents/claude-agent.ts +155 -18
- package/src/lib/agents/execution-manager.ts +0 -35
- package/src/lib/agents/learned-context.ts +0 -12
- package/src/lib/agents/learning-session.ts +18 -5
- package/src/lib/agents/profiles/__tests__/registry.test.ts +6 -4
- package/src/lib/agents/profiles/builtins/upgrade-assistant/SKILL.md +70 -0
- package/src/lib/agents/profiles/builtins/upgrade-assistant/profile.yaml +32 -0
- package/src/lib/agents/runtime/__tests__/openai-codex-auth.test.ts +118 -0
- package/src/lib/agents/runtime/codex-app-server-client.ts +11 -5
- package/src/lib/agents/runtime/openai-codex-auth.ts +389 -0
- package/src/lib/agents/runtime/openai-codex.ts +29 -60
- package/src/lib/agents/runtime/types.ts +8 -0
- package/src/lib/book/chapter-mapping.ts +11 -0
- package/src/lib/book/content.ts +10 -0
- package/src/lib/chat/__tests__/active-streams.test.ts +49 -0
- package/src/lib/chat/__tests__/finalize-safety-net.test.ts +139 -0
- package/src/lib/chat/__tests__/reconcile.test.ts +137 -0
- package/src/lib/chat/__tests__/stream-telemetry.test.ts +151 -0
- package/src/lib/chat/active-streams.ts +27 -0
- package/src/lib/chat/codex-engine.ts +16 -17
- package/src/lib/chat/context-builder.ts +5 -3
- package/src/lib/chat/engine.ts +50 -3
- package/src/lib/chat/reconcile.ts +117 -0
- package/src/lib/chat/stagent-tools.ts +1 -0
- package/src/lib/chat/stream-telemetry.ts +132 -0
- package/src/lib/chat/suggested-prompts.ts +28 -1
- package/src/lib/chat/system-prompt.ts +26 -1
- package/src/lib/chat/tool-catalog.ts +2 -1
- package/src/lib/chat/tools/__tests__/enrich-table-tool.test.ts +127 -0
- package/src/lib/chat/tools/__tests__/schedule-tools.test.ts +261 -0
- package/src/lib/chat/tools/__tests__/task-tools.test.ts +352 -0
- package/src/lib/chat/tools/__tests__/workflow-tools-dedup.test.ts +217 -0
- package/src/lib/chat/tools/document-tools.ts +29 -13
- package/src/lib/chat/tools/helpers.ts +39 -0
- package/src/lib/chat/tools/notification-tools.ts +9 -5
- package/src/lib/chat/tools/project-tools.ts +33 -0
- package/src/lib/chat/tools/schedule-tools.ts +44 -11
- package/src/lib/chat/tools/table-tools.ts +71 -0
- package/src/lib/chat/tools/task-tools.ts +84 -20
- package/src/lib/chat/tools/workflow-tools.ts +234 -32
- package/src/lib/constants/settings.ts +8 -18
- package/src/lib/data/__tests__/clear.test.ts +56 -2
- package/src/lib/data/clear.ts +20 -15
- package/src/lib/data/delete-project.ts +171 -0
- package/src/lib/db/__tests__/bootstrap.test.ts +1 -1
- package/src/lib/db/bootstrap.ts +45 -16
- package/src/lib/db/index.ts +5 -0
- package/src/lib/db/migrations/0009_add_app_instances.sql +25 -0
- package/src/lib/db/migrations/0024_add_workflow_resume_at.sql +10 -0
- package/src/lib/db/migrations/0025_drop_app_instances.sql +3 -0
- package/src/lib/db/migrations/0026_drop_license.sql +3 -0
- package/src/lib/db/migrations/meta/_journal.json +21 -0
- package/src/lib/db/schema.ts +68 -23
- package/src/lib/environment/workspace-context.ts +13 -1
- package/src/lib/import/dedup.ts +4 -54
- package/src/lib/instance/__tests__/bootstrap.test.ts +362 -0
- package/src/lib/instance/__tests__/detect.test.ts +115 -0
- package/src/lib/instance/__tests__/fingerprint.test.ts +48 -0
- package/src/lib/instance/__tests__/git-ops.test.ts +95 -0
- package/src/lib/instance/__tests__/settings.test.ts +83 -0
- package/src/lib/instance/__tests__/upgrade-poller.test.ts +131 -0
- package/src/lib/instance/bootstrap.ts +270 -0
- package/src/lib/instance/detect.ts +49 -0
- package/src/lib/instance/fingerprint.ts +78 -0
- package/src/lib/instance/git-ops.ts +95 -0
- package/src/lib/instance/settings.ts +61 -0
- package/src/lib/instance/types.ts +77 -0
- package/src/lib/instance/upgrade-poller.ts +153 -0
- package/src/lib/notifications/__tests__/visibility.test.ts +51 -0
- package/src/lib/notifications/visibility.ts +33 -0
- package/src/lib/schedules/__tests__/collision-check.test.ts +93 -0
- package/src/lib/schedules/__tests__/config.test.ts +62 -0
- package/src/lib/schedules/__tests__/firing-metrics.test.ts +99 -0
- package/src/lib/schedules/__tests__/integration.test.ts +82 -0
- package/src/lib/schedules/__tests__/slot-claim.test.ts +242 -0
- package/src/lib/schedules/__tests__/tick-scheduler.test.ts +102 -0
- package/src/lib/schedules/__tests__/turn-budget.test.ts +228 -0
- package/src/lib/schedules/collision-check.ts +105 -0
- package/src/lib/schedules/config.ts +53 -0
- package/src/lib/schedules/scheduler.ts +232 -13
- package/src/lib/schedules/slot-claim.ts +105 -0
- package/src/lib/settings/__tests__/openai-auth.test.ts +101 -0
- package/src/lib/settings/__tests__/openai-login-manager.test.ts +64 -0
- package/src/lib/settings/__tests__/runtime-setup.test.ts +33 -0
- package/src/lib/settings/openai-auth.ts +105 -10
- package/src/lib/settings/openai-login-manager.ts +260 -0
- package/src/lib/settings/runtime-setup.ts +14 -4
- package/src/lib/tables/__tests__/enrichment-planner.test.ts +124 -0
- package/src/lib/tables/__tests__/enrichment.test.ts +147 -0
- package/src/lib/tables/enrichment-planner.ts +454 -0
- package/src/lib/tables/enrichment.ts +328 -0
- package/src/lib/tables/query-builder.ts +5 -2
- package/src/lib/tables/trigger-evaluator.ts +3 -2
- package/src/lib/theme.ts +71 -0
- package/src/lib/usage/ledger.ts +2 -18
- package/src/lib/util/__tests__/similarity.test.ts +106 -0
- package/src/lib/util/similarity.ts +77 -0
- package/src/lib/utils/format-timestamp.ts +24 -0
- package/src/lib/utils/stagent-paths.ts +12 -0
- package/src/lib/validators/__tests__/blueprint.test.ts +172 -0
- package/src/lib/validators/__tests__/settings.test.ts +10 -0
- package/src/lib/validators/blueprint.ts +70 -9
- package/src/lib/validators/profile.ts +2 -2
- package/src/lib/validators/settings.ts +3 -1
- package/src/lib/workflows/__tests__/delay.test.ts +196 -0
- package/src/lib/workflows/__tests__/engine.test.ts +8 -0
- package/src/lib/workflows/__tests__/loop-executor.test.ts +54 -0
- package/src/lib/workflows/__tests__/post-action.test.ts +108 -0
- package/src/lib/workflows/blueprints/instantiator.ts +22 -1
- package/src/lib/workflows/blueprints/types.ts +10 -2
- package/src/lib/workflows/delay.ts +106 -0
- package/src/lib/workflows/engine.ts +207 -4
- package/src/lib/workflows/loop-executor.ts +349 -24
- package/src/lib/workflows/post-action.ts +91 -0
- package/src/lib/workflows/types.ts +166 -1
- package/src/app/api/license/checkout/route.ts +0 -28
- package/src/app/api/license/portal/route.ts +0 -26
- package/src/app/api/license/route.ts +0 -89
- package/src/app/api/license/usage/route.ts +0 -63
- package/src/app/api/marketplace/browse/route.ts +0 -15
- package/src/app/api/marketplace/import/route.ts +0 -28
- package/src/app/api/marketplace/publish/route.ts +0 -40
- package/src/app/api/onboarding/email/route.ts +0 -53
- package/src/app/api/settings/telemetry/route.ts +0 -14
- package/src/app/api/sync/export/route.ts +0 -54
- package/src/app/api/sync/restore/route.ts +0 -37
- package/src/app/api/sync/sessions/route.ts +0 -24
- package/src/app/auth/callback/route.ts +0 -73
- package/src/app/marketplace/page.tsx +0 -19
- package/src/components/analytics/analytics-gate-card.tsx +0 -101
- package/src/components/marketplace/blueprint-card.tsx +0 -61
- package/src/components/marketplace/marketplace-browser.tsx +0 -131
- package/src/components/onboarding/email-capture-card.tsx +0 -104
- package/src/components/settings/activation-form.tsx +0 -95
- package/src/components/settings/cloud-account-section.tsx +0 -147
- package/src/components/settings/cloud-sync-section.tsx +0 -155
- package/src/components/settings/subscription-section.tsx +0 -410
- package/src/components/settings/telemetry-section.tsx +0 -80
- package/src/components/shared/premium-gate-overlay.tsx +0 -50
- package/src/components/shared/schedule-gate-dialog.tsx +0 -64
- package/src/components/shared/upgrade-banner.tsx +0 -112
- package/src/hooks/use-supabase-auth.ts +0 -79
- package/src/lib/billing/email.ts +0 -54
- package/src/lib/billing/products.ts +0 -80
- package/src/lib/billing/stripe.ts +0 -101
- package/src/lib/cloud/supabase-browser.ts +0 -32
- package/src/lib/cloud/supabase-client.ts +0 -56
- package/src/lib/license/__tests__/features.test.ts +0 -56
- package/src/lib/license/__tests__/key-format.test.ts +0 -88
- package/src/lib/license/__tests__/manager.test.ts +0 -64
- package/src/lib/license/__tests__/tier-limits.test.ts +0 -79
- package/src/lib/license/cloud-validation.ts +0 -60
- package/src/lib/license/features.ts +0 -44
- package/src/lib/license/key-format.ts +0 -101
- package/src/lib/license/limit-check.ts +0 -111
- package/src/lib/license/limit-queries.ts +0 -51
- package/src/lib/license/manager.ts +0 -345
- package/src/lib/license/notifications.ts +0 -59
- package/src/lib/license/tier-limits.ts +0 -71
- package/src/lib/marketplace/marketplace-client.ts +0 -107
- package/src/lib/sync/cloud-sync.ts +0 -235
- package/src/lib/telemetry/conversion-events.ts +0 -71
- package/src/lib/telemetry/queue.ts +0 -122
- package/src/lib/validators/license.ts +0 -33
|
@@ -0,0 +1,217 @@
|
|
|
1
|
+
import { describe, it, expect, vi, beforeEach } from "vitest";
|
|
2
|
+
|
|
3
|
+
interface WorkflowRow {
|
|
4
|
+
id: string;
|
|
5
|
+
name: string;
|
|
6
|
+
definition: string | null;
|
|
7
|
+
projectId: string | null;
|
|
8
|
+
}
|
|
9
|
+
|
|
10
|
+
const { mockWorkflowRows } = vi.hoisted(() => ({
|
|
11
|
+
mockWorkflowRows: { value: [] as WorkflowRow[] },
|
|
12
|
+
}));
|
|
13
|
+
|
|
14
|
+
// Minimal drizzle query builder stub — supports
|
|
15
|
+
// db.select({...}).from(table).where(...)
|
|
16
|
+
// by returning a thenable that resolves to mockWorkflowRows.value.
|
|
17
|
+
vi.mock("@/lib/db", () => {
|
|
18
|
+
const builder = {
|
|
19
|
+
from() {
|
|
20
|
+
return this;
|
|
21
|
+
},
|
|
22
|
+
where() {
|
|
23
|
+
return this;
|
|
24
|
+
},
|
|
25
|
+
then<TResolve>(resolve: (rows: WorkflowRow[]) => TResolve) {
|
|
26
|
+
return Promise.resolve(mockWorkflowRows.value).then(resolve);
|
|
27
|
+
},
|
|
28
|
+
};
|
|
29
|
+
return {
|
|
30
|
+
db: {
|
|
31
|
+
select: () => builder,
|
|
32
|
+
},
|
|
33
|
+
};
|
|
34
|
+
});
|
|
35
|
+
|
|
36
|
+
// Stub the schema import so drizzle-orm doesn't try to read a real table.
|
|
37
|
+
vi.mock("@/lib/db/schema", () => ({
|
|
38
|
+
workflows: { projectId: "projectId" },
|
|
39
|
+
tasks: {},
|
|
40
|
+
agentLogs: {},
|
|
41
|
+
notifications: {},
|
|
42
|
+
documents: {},
|
|
43
|
+
workflowDocumentInputs: {},
|
|
44
|
+
}));
|
|
45
|
+
|
|
46
|
+
// Stub drizzle-orm operators used in workflow-tools.ts — the tests only
|
|
47
|
+
// care about the return value of the builder, not the operator objects.
|
|
48
|
+
vi.mock("drizzle-orm", () => ({
|
|
49
|
+
eq: () => ({}),
|
|
50
|
+
and: () => ({}),
|
|
51
|
+
desc: () => ({}),
|
|
52
|
+
inArray: () => ({}),
|
|
53
|
+
like: () => ({}),
|
|
54
|
+
}));
|
|
55
|
+
|
|
56
|
+
import { findSimilarWorkflows } from "../workflow-tools";
|
|
57
|
+
|
|
58
|
+
function setRows(rows: WorkflowRow[]) {
|
|
59
|
+
mockWorkflowRows.value = rows;
|
|
60
|
+
}
|
|
61
|
+
|
|
62
|
+
describe("findSimilarWorkflows", () => {
|
|
63
|
+
beforeEach(() => {
|
|
64
|
+
setRows([]);
|
|
65
|
+
});
|
|
66
|
+
|
|
67
|
+
it("returns [] when projectId is null (no cross-project dedup)", async () => {
|
|
68
|
+
setRows([
|
|
69
|
+
{
|
|
70
|
+
id: "wf1",
|
|
71
|
+
name: "Research Customer Feedback",
|
|
72
|
+
definition: JSON.stringify({
|
|
73
|
+
pattern: "sequence",
|
|
74
|
+
steps: [{ id: "s1", name: "Research customer feedback", prompt: "do research" }],
|
|
75
|
+
}),
|
|
76
|
+
projectId: null,
|
|
77
|
+
},
|
|
78
|
+
]);
|
|
79
|
+
|
|
80
|
+
const result = await findSimilarWorkflows(
|
|
81
|
+
null,
|
|
82
|
+
"Research Customer Feedback",
|
|
83
|
+
JSON.stringify({ pattern: "sequence", steps: [] })
|
|
84
|
+
);
|
|
85
|
+
expect(result).toEqual([]);
|
|
86
|
+
});
|
|
87
|
+
|
|
88
|
+
it("returns [] when no workflows exist in the project", async () => {
|
|
89
|
+
setRows([]);
|
|
90
|
+
const result = await findSimilarWorkflows(
|
|
91
|
+
"proj_a",
|
|
92
|
+
"Any name",
|
|
93
|
+
JSON.stringify({ pattern: "sequence", steps: [] })
|
|
94
|
+
);
|
|
95
|
+
expect(result).toEqual([]);
|
|
96
|
+
});
|
|
97
|
+
|
|
98
|
+
it("matches exact name (case-insensitive) with similarity 1.0", async () => {
|
|
99
|
+
setRows([
|
|
100
|
+
{
|
|
101
|
+
id: "wf1",
|
|
102
|
+
name: "Research Customer Feedback",
|
|
103
|
+
definition: null,
|
|
104
|
+
projectId: "proj_a",
|
|
105
|
+
},
|
|
106
|
+
]);
|
|
107
|
+
|
|
108
|
+
const result = await findSimilarWorkflows(
|
|
109
|
+
"proj_a",
|
|
110
|
+
"research customer feedback",
|
|
111
|
+
JSON.stringify({ pattern: "sequence", steps: [] })
|
|
112
|
+
);
|
|
113
|
+
|
|
114
|
+
expect(result).toHaveLength(1);
|
|
115
|
+
expect(result[0]).toMatchObject({
|
|
116
|
+
id: "wf1",
|
|
117
|
+
similarity: 1,
|
|
118
|
+
});
|
|
119
|
+
expect(result[0].reason).toContain("Same name");
|
|
120
|
+
});
|
|
121
|
+
|
|
122
|
+
it("matches on Jaccard similarity over step names + prompts (redesign scenario)", async () => {
|
|
123
|
+
// Simulates the bug scenario: LLM "redesigns" a workflow mid-conversation,
|
|
124
|
+
// using mostly the same vocabulary as the original. The definitions are
|
|
125
|
+
// near-identical (as redesigns typically are in practice) so Jaccard
|
|
126
|
+
// should exceed the 0.7 threshold.
|
|
127
|
+
const sharedSteps = [
|
|
128
|
+
{ id: "s1", name: "Research customer cohort", prompt: "Investigate customer research cohort feedback insights" },
|
|
129
|
+
{ id: "s2", name: "Interview protocol draft", prompt: "Draft customer interview questions protocol script" },
|
|
130
|
+
{ id: "s3", name: "Synthesize findings", prompt: "Summarize customer research findings insights report" },
|
|
131
|
+
];
|
|
132
|
+
setRows([
|
|
133
|
+
{
|
|
134
|
+
id: "wf1",
|
|
135
|
+
name: "Customer Discovery Pipeline",
|
|
136
|
+
definition: JSON.stringify({ pattern: "sequence", steps: sharedSteps }),
|
|
137
|
+
projectId: "proj_a",
|
|
138
|
+
},
|
|
139
|
+
]);
|
|
140
|
+
|
|
141
|
+
const result = await findSimilarWorkflows(
|
|
142
|
+
"proj_a",
|
|
143
|
+
"Customer Discovery Workflow v2",
|
|
144
|
+
JSON.stringify({ pattern: "sequence", steps: sharedSteps })
|
|
145
|
+
);
|
|
146
|
+
|
|
147
|
+
expect(result.length).toBeGreaterThanOrEqual(1);
|
|
148
|
+
expect(result[0].id).toBe("wf1");
|
|
149
|
+
expect(result[0].similarity).toBeGreaterThanOrEqual(0.7);
|
|
150
|
+
});
|
|
151
|
+
|
|
152
|
+
it("does NOT match when names and step text are completely different", async () => {
|
|
153
|
+
setRows([
|
|
154
|
+
{
|
|
155
|
+
id: "wf1",
|
|
156
|
+
name: "Deploy frontend release",
|
|
157
|
+
definition: JSON.stringify({
|
|
158
|
+
pattern: "sequence",
|
|
159
|
+
steps: [{ id: "s1", name: "Deploy staging", prompt: "Push release artifact to staging environment" }],
|
|
160
|
+
}),
|
|
161
|
+
projectId: "proj_a",
|
|
162
|
+
},
|
|
163
|
+
]);
|
|
164
|
+
|
|
165
|
+
const result = await findSimilarWorkflows(
|
|
166
|
+
"proj_a",
|
|
167
|
+
"Customer interview analysis",
|
|
168
|
+
JSON.stringify({
|
|
169
|
+
pattern: "sequence",
|
|
170
|
+
steps: [{ id: "s2", name: "Summarize interviews", prompt: "Pull insights from recent customer interviews" }],
|
|
171
|
+
})
|
|
172
|
+
);
|
|
173
|
+
|
|
174
|
+
expect(result).toEqual([]);
|
|
175
|
+
});
|
|
176
|
+
|
|
177
|
+
it("caps results at 3 and sorts by similarity descending", async () => {
|
|
178
|
+
// Four rows, all exact-name matches (similarity 1.0). Expect exactly 3 returned.
|
|
179
|
+
setRows(
|
|
180
|
+
Array.from({ length: 4 }).map((_, i) => ({
|
|
181
|
+
id: `wf${i}`,
|
|
182
|
+
name: "Duplicate Workflow",
|
|
183
|
+
definition: null,
|
|
184
|
+
projectId: "proj_a",
|
|
185
|
+
}))
|
|
186
|
+
);
|
|
187
|
+
|
|
188
|
+
const result = await findSimilarWorkflows(
|
|
189
|
+
"proj_a",
|
|
190
|
+
"Duplicate Workflow",
|
|
191
|
+
JSON.stringify({ pattern: "sequence", steps: [] })
|
|
192
|
+
);
|
|
193
|
+
|
|
194
|
+
expect(result).toHaveLength(3);
|
|
195
|
+
expect(result.every((r) => r.similarity === 1)).toBe(true);
|
|
196
|
+
});
|
|
197
|
+
|
|
198
|
+
it("handles malformed definition JSON without crashing", async () => {
|
|
199
|
+
setRows([
|
|
200
|
+
{
|
|
201
|
+
id: "wf1",
|
|
202
|
+
name: "Legit Workflow",
|
|
203
|
+
definition: "not-json-at-all",
|
|
204
|
+
projectId: "proj_a",
|
|
205
|
+
},
|
|
206
|
+
]);
|
|
207
|
+
|
|
208
|
+
// Should not throw — just degrades to name-only comparison.
|
|
209
|
+
const result = await findSimilarWorkflows(
|
|
210
|
+
"proj_a",
|
|
211
|
+
"Legit Workflow",
|
|
212
|
+
"also not json"
|
|
213
|
+
);
|
|
214
|
+
expect(result).toHaveLength(1);
|
|
215
|
+
expect(result[0].similarity).toBe(1); // exact name match
|
|
216
|
+
});
|
|
217
|
+
});
|
|
@@ -3,7 +3,7 @@ import { z } from "zod";
|
|
|
3
3
|
import { db } from "@/lib/db";
|
|
4
4
|
import { documents } from "@/lib/db/schema";
|
|
5
5
|
import { eq, and, desc } from "drizzle-orm";
|
|
6
|
-
import { ok, err, type ToolContext } from "./helpers";
|
|
6
|
+
import { ok, err, resolveEntityId, type ToolContext } from "./helpers";
|
|
7
7
|
import { access, stat, copyFile, mkdir } from "fs/promises";
|
|
8
8
|
import { basename, extname, join } from "path";
|
|
9
9
|
import crypto from "crypto";
|
|
@@ -105,6 +105,10 @@ export function documentTools(ctx: ToolContext) {
|
|
|
105
105
|
},
|
|
106
106
|
async (args) => {
|
|
107
107
|
try {
|
|
108
|
+
const resolved = await resolveEntityId(documents, documents.id, args.documentId);
|
|
109
|
+
if ("error" in resolved) return err(resolved.error);
|
|
110
|
+
const documentId = resolved.id;
|
|
111
|
+
|
|
108
112
|
const doc = await db
|
|
109
113
|
.select({
|
|
110
114
|
id: documents.id,
|
|
@@ -122,10 +126,10 @@ export function documentTools(ctx: ToolContext) {
|
|
|
122
126
|
updatedAt: documents.updatedAt,
|
|
123
127
|
})
|
|
124
128
|
.from(documents)
|
|
125
|
-
.where(eq(documents.id,
|
|
129
|
+
.where(eq(documents.id, documentId))
|
|
126
130
|
.get();
|
|
127
131
|
|
|
128
|
-
if (!doc) return err(`Document not found: ${
|
|
132
|
+
if (!doc) return err(`Document not found: ${documentId}`);
|
|
129
133
|
ctx.onToolResult?.("get_document", doc);
|
|
130
134
|
return ok(doc);
|
|
131
135
|
} catch (e) {
|
|
@@ -204,13 +208,17 @@ export function documentTools(ctx: ToolContext) {
|
|
|
204
208
|
},
|
|
205
209
|
async (args) => {
|
|
206
210
|
try {
|
|
211
|
+
const resolved = await resolveEntityId(documents, documents.id, args.documentId);
|
|
212
|
+
if ("error" in resolved) return err(resolved.error);
|
|
213
|
+
const documentId = resolved.id;
|
|
214
|
+
|
|
207
215
|
const doc = await db
|
|
208
216
|
.select()
|
|
209
217
|
.from(documents)
|
|
210
|
-
.where(eq(documents.id,
|
|
218
|
+
.where(eq(documents.id, documentId))
|
|
211
219
|
.get();
|
|
212
220
|
|
|
213
|
-
if (!doc) return err(`Document not found: ${
|
|
221
|
+
if (!doc) return err(`Document not found: ${documentId}`);
|
|
214
222
|
|
|
215
223
|
const updates: Record<string, unknown> = { updatedAt: new Date() };
|
|
216
224
|
|
|
@@ -232,10 +240,10 @@ export function documentTools(ctx: ToolContext) {
|
|
|
232
240
|
await db
|
|
233
241
|
.update(documents)
|
|
234
242
|
.set(updates)
|
|
235
|
-
.where(eq(documents.id,
|
|
243
|
+
.where(eq(documents.id, documentId));
|
|
236
244
|
|
|
237
245
|
if (args.reprocess) {
|
|
238
|
-
processDocument(
|
|
246
|
+
processDocument(documentId).catch(() => {});
|
|
239
247
|
}
|
|
240
248
|
|
|
241
249
|
const updatedFields = [];
|
|
@@ -243,7 +251,7 @@ export function documentTools(ctx: ToolContext) {
|
|
|
243
251
|
if (args.reprocess) updatedFields.push("processingStatus");
|
|
244
252
|
|
|
245
253
|
const result = {
|
|
246
|
-
documentId
|
|
254
|
+
documentId,
|
|
247
255
|
updatedFields,
|
|
248
256
|
processingStatus: args.reprocess ? "queued" : doc.status,
|
|
249
257
|
};
|
|
@@ -264,13 +272,17 @@ export function documentTools(ctx: ToolContext) {
|
|
|
264
272
|
},
|
|
265
273
|
async (args) => {
|
|
266
274
|
try {
|
|
275
|
+
const resolved = await resolveEntityId(documents, documents.id, args.documentId);
|
|
276
|
+
if ("error" in resolved) return err(resolved.error);
|
|
277
|
+
const documentId = resolved.id;
|
|
278
|
+
|
|
267
279
|
const doc = await db
|
|
268
280
|
.select()
|
|
269
281
|
.from(documents)
|
|
270
|
-
.where(eq(documents.id,
|
|
282
|
+
.where(eq(documents.id, documentId))
|
|
271
283
|
.get();
|
|
272
284
|
|
|
273
|
-
if (!doc) return err(`Document not found: ${
|
|
285
|
+
if (!doc) return err(`Document not found: ${documentId}`);
|
|
274
286
|
|
|
275
287
|
// Check task linkage
|
|
276
288
|
if (doc.taskId && !args.cascadeDelete) {
|
|
@@ -285,7 +297,7 @@ export function documentTools(ctx: ToolContext) {
|
|
|
285
297
|
// File may already be deleted
|
|
286
298
|
}
|
|
287
299
|
|
|
288
|
-
await db.delete(documents).where(eq(documents.id,
|
|
300
|
+
await db.delete(documents).where(eq(documents.id, documentId));
|
|
289
301
|
|
|
290
302
|
const result = {
|
|
291
303
|
success: true,
|
|
@@ -308,6 +320,10 @@ export function documentTools(ctx: ToolContext) {
|
|
|
308
320
|
},
|
|
309
321
|
async (args) => {
|
|
310
322
|
try {
|
|
323
|
+
const resolved = await resolveEntityId(documents, documents.id, args.documentId);
|
|
324
|
+
if ("error" in resolved) return err(resolved.error);
|
|
325
|
+
const documentId = resolved.id;
|
|
326
|
+
|
|
311
327
|
const doc = await db
|
|
312
328
|
.select({
|
|
313
329
|
id: documents.id,
|
|
@@ -316,10 +332,10 @@ export function documentTools(ctx: ToolContext) {
|
|
|
316
332
|
extractedText: documents.extractedText,
|
|
317
333
|
})
|
|
318
334
|
.from(documents)
|
|
319
|
-
.where(eq(documents.id,
|
|
335
|
+
.where(eq(documents.id, documentId))
|
|
320
336
|
.get();
|
|
321
337
|
|
|
322
|
-
if (!doc) return err(`Document not found: ${
|
|
338
|
+
if (!doc) return err(`Document not found: ${documentId}`);
|
|
323
339
|
if (doc.status !== "ready")
|
|
324
340
|
return err(`Document not ready (status: ${doc.status}). Wait for preprocessing to complete.`);
|
|
325
341
|
if (!doc.extractedText)
|
|
@@ -2,6 +2,11 @@
|
|
|
2
2
|
* Shared helpers and types for Stagent chat MCP tools.
|
|
3
3
|
*/
|
|
4
4
|
|
|
5
|
+
import { db } from "@/lib/db";
|
|
6
|
+
import { like } from "drizzle-orm";
|
|
7
|
+
import type { SQLiteTableWithColumns } from "drizzle-orm/sqlite-core";
|
|
8
|
+
import type { SQLiteColumn } from "drizzle-orm/sqlite-core";
|
|
9
|
+
|
|
5
10
|
/** Context passed to each tool factory — provides project scoping and entity callbacks. */
|
|
6
11
|
export interface ToolContext {
|
|
7
12
|
projectId?: string | null;
|
|
@@ -22,3 +27,37 @@ export function err(message: string) {
|
|
|
22
27
|
isError: true as const,
|
|
23
28
|
};
|
|
24
29
|
}
|
|
30
|
+
|
|
31
|
+
/**
|
|
32
|
+
* Resolve an entity ID that may be a prefix (8+ chars) to the full UUID.
|
|
33
|
+
* Uses LIKE 'prefix%' which hits the primary key B-tree index on SQLite.
|
|
34
|
+
*
|
|
35
|
+
* Fast path: IDs >=32 chars are returned as-is (already full UUIDs).
|
|
36
|
+
*/
|
|
37
|
+
export async function resolveEntityId(
|
|
38
|
+
// eslint-disable-next-line @typescript-eslint/no-explicit-any
|
|
39
|
+
table: SQLiteTableWithColumns<any>,
|
|
40
|
+
idColumn: SQLiteColumn,
|
|
41
|
+
rawId: string,
|
|
42
|
+
): Promise<{ id: string } | { error: string }> {
|
|
43
|
+
// Full UUIDs are 36 chars (with hyphens) or 32 (without) — skip prefix search
|
|
44
|
+
if (rawId.length >= 32) {
|
|
45
|
+
return { id: rawId };
|
|
46
|
+
}
|
|
47
|
+
|
|
48
|
+
const matches = await db
|
|
49
|
+
.select({ id: idColumn })
|
|
50
|
+
.from(table)
|
|
51
|
+
.where(like(idColumn, `${rawId}%`))
|
|
52
|
+
.limit(2);
|
|
53
|
+
|
|
54
|
+
if (matches.length === 0) {
|
|
55
|
+
return { error: `No entity found matching ID prefix: ${rawId}` };
|
|
56
|
+
}
|
|
57
|
+
if (matches.length > 1) {
|
|
58
|
+
return {
|
|
59
|
+
error: `Ambiguous ID prefix "${rawId}" matches multiple entities: ${matches.map((m) => m.id).join(", ")}. Use the full ID.`,
|
|
60
|
+
};
|
|
61
|
+
}
|
|
62
|
+
return { id: matches[0].id as string };
|
|
63
|
+
}
|
|
@@ -3,7 +3,7 @@ import { z } from "zod";
|
|
|
3
3
|
import { db } from "@/lib/db";
|
|
4
4
|
import { notifications } from "@/lib/db/schema";
|
|
5
5
|
import { eq, isNull, desc } from "drizzle-orm";
|
|
6
|
-
import { ok, err, type ToolContext } from "./helpers";
|
|
6
|
+
import { ok, err, resolveEntityId, type ToolContext } from "./helpers";
|
|
7
7
|
|
|
8
8
|
export function notificationTools(_ctx: ToolContext) {
|
|
9
9
|
return [
|
|
@@ -73,13 +73,17 @@ export function notificationTools(_ctx: ToolContext) {
|
|
|
73
73
|
},
|
|
74
74
|
async (args) => {
|
|
75
75
|
try {
|
|
76
|
+
const resolved = await resolveEntityId(notifications, notifications.id, args.notificationId);
|
|
77
|
+
if ("error" in resolved) return err(resolved.error);
|
|
78
|
+
const notificationId = resolved.id;
|
|
79
|
+
|
|
76
80
|
const notification = await db
|
|
77
81
|
.select()
|
|
78
82
|
.from(notifications)
|
|
79
|
-
.where(eq(notifications.id,
|
|
83
|
+
.where(eq(notifications.id, notificationId))
|
|
80
84
|
.get();
|
|
81
85
|
|
|
82
|
-
if (!notification) return err(`Notification not found: ${
|
|
86
|
+
if (!notification) return err(`Notification not found: ${notificationId}`);
|
|
83
87
|
if (notification.response) return err("Already responded to this notification");
|
|
84
88
|
|
|
85
89
|
const responseData = {
|
|
@@ -98,7 +102,7 @@ export function notificationTools(_ctx: ToolContext) {
|
|
|
98
102
|
respondedAt: new Date(),
|
|
99
103
|
read: true,
|
|
100
104
|
})
|
|
101
|
-
.where(eq(notifications.id,
|
|
105
|
+
.where(eq(notifications.id, notificationId));
|
|
102
106
|
|
|
103
107
|
// Save permanent permission if requested
|
|
104
108
|
if (args.behavior === "allow" && args.alwaysAllow && notification.toolName && notification.toolInput) {
|
|
@@ -115,7 +119,7 @@ export function notificationTools(_ctx: ToolContext) {
|
|
|
115
119
|
|
|
116
120
|
return ok({
|
|
117
121
|
message: `Notification ${args.behavior === "allow" ? "approved" : "denied"}`,
|
|
118
|
-
notificationId
|
|
122
|
+
notificationId,
|
|
119
123
|
alwaysAllow: args.alwaysAllow ?? false,
|
|
120
124
|
});
|
|
121
125
|
} catch (e) {
|
|
@@ -85,5 +85,38 @@ export function projectTools(ctx: ToolContext) {
|
|
|
85
85
|
}
|
|
86
86
|
}
|
|
87
87
|
),
|
|
88
|
+
|
|
89
|
+
defineTool(
|
|
90
|
+
"delete_project",
|
|
91
|
+
"Permanently delete a project and all its resources (tasks, tables, schedules, " +
|
|
92
|
+
"documents, app instances). This is irreversible. Use to clean up orphaned or " +
|
|
93
|
+
"unwanted projects. Always confirm with the user before calling.",
|
|
94
|
+
{
|
|
95
|
+
projectId: z
|
|
96
|
+
.string()
|
|
97
|
+
.describe("The ID of the project to delete"),
|
|
98
|
+
},
|
|
99
|
+
async (args) => {
|
|
100
|
+
try {
|
|
101
|
+
const { deleteProjectCascade } = await import(
|
|
102
|
+
"@/lib/data/delete-project"
|
|
103
|
+
);
|
|
104
|
+
|
|
105
|
+
const deleted = deleteProjectCascade(args.projectId);
|
|
106
|
+
if (!deleted) {
|
|
107
|
+
return err(`Project "${args.projectId}" not found`);
|
|
108
|
+
}
|
|
109
|
+
|
|
110
|
+
return ok({
|
|
111
|
+
projectId: args.projectId,
|
|
112
|
+
message: `Project "${args.projectId}" and all its resources have been deleted.`,
|
|
113
|
+
});
|
|
114
|
+
} catch (e) {
|
|
115
|
+
return err(
|
|
116
|
+
e instanceof Error ? e.message : "Failed to delete project",
|
|
117
|
+
);
|
|
118
|
+
}
|
|
119
|
+
}
|
|
120
|
+
),
|
|
88
121
|
];
|
|
89
122
|
}
|
|
@@ -3,7 +3,7 @@ import { z } from "zod";
|
|
|
3
3
|
import { db } from "@/lib/db";
|
|
4
4
|
import { schedules } from "@/lib/db/schema";
|
|
5
5
|
import { eq, and, desc } from "drizzle-orm";
|
|
6
|
-
import { ok, err, type ToolContext } from "./helpers";
|
|
6
|
+
import { ok, err, resolveEntityId, type ToolContext } from "./helpers";
|
|
7
7
|
import { analyzePromptEfficiency } from "@/lib/schedules/prompt-analyzer";
|
|
8
8
|
|
|
9
9
|
const VALID_SCHEDULE_STATUSES = [
|
|
@@ -69,6 +69,13 @@ export function scheduleTools(ctx: ToolContext) {
|
|
|
69
69
|
.number()
|
|
70
70
|
.optional()
|
|
71
71
|
.describe("Auto-expire after this many hours"),
|
|
72
|
+
maxTurns: z
|
|
73
|
+
.number()
|
|
74
|
+
.int()
|
|
75
|
+
.min(10)
|
|
76
|
+
.max(500)
|
|
77
|
+
.optional()
|
|
78
|
+
.describe("Hard cap on turns per firing (10-500). Omit to inherit the system default."),
|
|
72
79
|
},
|
|
73
80
|
async (args) => {
|
|
74
81
|
try {
|
|
@@ -147,6 +154,8 @@ export function scheduleTools(ctx: ToolContext) {
|
|
|
147
154
|
recurs: true,
|
|
148
155
|
status: "active",
|
|
149
156
|
maxFirings: args.maxFirings ?? null,
|
|
157
|
+
maxTurns: args.maxTurns ?? null,
|
|
158
|
+
maxTurnsSetAt: args.maxTurns !== undefined ? now : null,
|
|
150
159
|
firingCount: 0,
|
|
151
160
|
expiresAt,
|
|
152
161
|
nextFireAt,
|
|
@@ -184,13 +193,17 @@ export function scheduleTools(ctx: ToolContext) {
|
|
|
184
193
|
},
|
|
185
194
|
async (args) => {
|
|
186
195
|
try {
|
|
196
|
+
const resolved = await resolveEntityId(schedules, schedules.id, args.scheduleId);
|
|
197
|
+
if ("error" in resolved) return err(resolved.error);
|
|
198
|
+
const scheduleId = resolved.id;
|
|
199
|
+
|
|
187
200
|
const schedule = await db
|
|
188
201
|
.select()
|
|
189
202
|
.from(schedules)
|
|
190
|
-
.where(eq(schedules.id,
|
|
203
|
+
.where(eq(schedules.id, scheduleId))
|
|
191
204
|
.get();
|
|
192
205
|
|
|
193
|
-
if (!schedule) return err(`Schedule not found: ${
|
|
206
|
+
if (!schedule) return err(`Schedule not found: ${scheduleId}`);
|
|
194
207
|
ctx.onToolResult?.("get_schedule", schedule);
|
|
195
208
|
return ok(schedule);
|
|
196
209
|
} catch (e) {
|
|
@@ -216,16 +229,28 @@ export function scheduleTools(ctx: ToolContext) {
|
|
|
216
229
|
.describe("New status (use 'paused' to pause, 'active' to resume)"),
|
|
217
230
|
assignedAgent: z.string().optional().describe("New runtime ID"),
|
|
218
231
|
agentProfile: z.string().optional().describe("New agent profile"),
|
|
232
|
+
maxTurns: z
|
|
233
|
+
.number()
|
|
234
|
+
.int()
|
|
235
|
+
.min(10)
|
|
236
|
+
.max(500)
|
|
237
|
+
.optional()
|
|
238
|
+
.nullable()
|
|
239
|
+
.describe("Hard cap on turns per firing (10-500). Pass null to clear an override back to the system default."),
|
|
219
240
|
},
|
|
220
241
|
async (args) => {
|
|
221
242
|
try {
|
|
243
|
+
const resolved = await resolveEntityId(schedules, schedules.id, args.scheduleId);
|
|
244
|
+
if ("error" in resolved) return err(resolved.error);
|
|
245
|
+
const scheduleId = resolved.id;
|
|
246
|
+
|
|
222
247
|
const existing = await db
|
|
223
248
|
.select()
|
|
224
249
|
.from(schedules)
|
|
225
|
-
.where(eq(schedules.id,
|
|
250
|
+
.where(eq(schedules.id, scheduleId))
|
|
226
251
|
.get();
|
|
227
252
|
|
|
228
|
-
if (!existing) return err(`Schedule not found: ${
|
|
253
|
+
if (!existing) return err(`Schedule not found: ${scheduleId}`);
|
|
229
254
|
|
|
230
255
|
const updates: Record<string, unknown> = { updatedAt: new Date() };
|
|
231
256
|
if (args.name !== undefined) updates.name = args.name;
|
|
@@ -233,6 +258,10 @@ export function scheduleTools(ctx: ToolContext) {
|
|
|
233
258
|
if (args.status !== undefined) updates.status = args.status;
|
|
234
259
|
if (args.assignedAgent !== undefined) updates.assignedAgent = args.assignedAgent;
|
|
235
260
|
if (args.agentProfile !== undefined) updates.agentProfile = args.agentProfile;
|
|
261
|
+
if (args.maxTurns !== undefined) {
|
|
262
|
+
updates.maxTurns = args.maxTurns;
|
|
263
|
+
updates.maxTurnsSetAt = args.maxTurns === null ? null : new Date();
|
|
264
|
+
}
|
|
236
265
|
|
|
237
266
|
if (args.interval) {
|
|
238
267
|
const { parseInterval, computeNextFireTime } = await import(
|
|
@@ -267,12 +296,12 @@ export function scheduleTools(ctx: ToolContext) {
|
|
|
267
296
|
await db
|
|
268
297
|
.update(schedules)
|
|
269
298
|
.set(updates)
|
|
270
|
-
.where(eq(schedules.id,
|
|
299
|
+
.where(eq(schedules.id, scheduleId));
|
|
271
300
|
|
|
272
301
|
const [schedule] = await db
|
|
273
302
|
.select()
|
|
274
303
|
.from(schedules)
|
|
275
|
-
.where(eq(schedules.id,
|
|
304
|
+
.where(eq(schedules.id, scheduleId));
|
|
276
305
|
|
|
277
306
|
ctx.onToolResult?.("update_schedule", schedule);
|
|
278
307
|
return ok(schedule);
|
|
@@ -290,16 +319,20 @@ export function scheduleTools(ctx: ToolContext) {
|
|
|
290
319
|
},
|
|
291
320
|
async (args) => {
|
|
292
321
|
try {
|
|
322
|
+
const resolved = await resolveEntityId(schedules, schedules.id, args.scheduleId);
|
|
323
|
+
if ("error" in resolved) return err(resolved.error);
|
|
324
|
+
const scheduleId = resolved.id;
|
|
325
|
+
|
|
293
326
|
const existing = await db
|
|
294
327
|
.select()
|
|
295
328
|
.from(schedules)
|
|
296
|
-
.where(eq(schedules.id,
|
|
329
|
+
.where(eq(schedules.id, scheduleId))
|
|
297
330
|
.get();
|
|
298
331
|
|
|
299
|
-
if (!existing) return err(`Schedule not found: ${
|
|
332
|
+
if (!existing) return err(`Schedule not found: ${scheduleId}`);
|
|
300
333
|
|
|
301
|
-
await db.delete(schedules).where(eq(schedules.id,
|
|
302
|
-
return ok({ message: "Schedule deleted", scheduleId
|
|
334
|
+
await db.delete(schedules).where(eq(schedules.id, scheduleId));
|
|
335
|
+
return ok({ message: "Schedule deleted", scheduleId, name: existing.name });
|
|
303
336
|
} catch (e) {
|
|
304
337
|
return err(e instanceof Error ? e.message : "Failed to delete schedule");
|
|
305
338
|
}
|
|
@@ -25,6 +25,7 @@ import {
|
|
|
25
25
|
importRows,
|
|
26
26
|
createImportRecord,
|
|
27
27
|
} from "@/lib/tables/import";
|
|
28
|
+
import { createEnrichmentWorkflow } from "@/lib/tables/enrichment";
|
|
28
29
|
import type { ColumnDef } from "@/lib/tables/types";
|
|
29
30
|
|
|
30
31
|
export function tableTools(ctx: ToolContext) {
|
|
@@ -301,6 +302,76 @@ export function tableTools(ctx: ToolContext) {
|
|
|
301
302
|
}
|
|
302
303
|
),
|
|
303
304
|
|
|
305
|
+
// ── Bulk row enrichment ──────────────────────────────────────────
|
|
306
|
+
|
|
307
|
+
defineTool(
|
|
308
|
+
"enrich_table",
|
|
309
|
+
`Bulk-enrich rows in a user table by running an agent task per row and writing the result back to a target column. Creates a row-driven loop workflow that fans out one task per matching row.
|
|
310
|
+
|
|
311
|
+
The prompt may reference row fields with {{row.fieldName}} placeholders — they are passed to the agent as JSON context. To skip a row at agent-time, return the literal string "NOT_FOUND". Already-populated rows (target column has a non-empty value) are skipped automatically for idempotency.
|
|
312
|
+
|
|
313
|
+
Returns the workflowId so the caller can poll status, plus the rowCount that will actually be processed.`,
|
|
314
|
+
{
|
|
315
|
+
tableId: z.string().describe("Table ID to enrich"),
|
|
316
|
+
prompt: z
|
|
317
|
+
.string()
|
|
318
|
+
.min(1)
|
|
319
|
+
.max(8192)
|
|
320
|
+
.describe(
|
|
321
|
+
"Per-row prompt template. Use {{row.fieldName}} to reference row fields. Instruct the agent to return NOT_FOUND when no value can be determined."
|
|
322
|
+
),
|
|
323
|
+
targetColumn: z
|
|
324
|
+
.string()
|
|
325
|
+
.min(1)
|
|
326
|
+
.describe("Column name to write the agent's result into"),
|
|
327
|
+
filter: z
|
|
328
|
+
.object({
|
|
329
|
+
column: z.string(),
|
|
330
|
+
operator: z.enum([
|
|
331
|
+
"eq", "neq", "gt", "gte", "lt", "lte",
|
|
332
|
+
"contains", "starts_with", "in", "is_empty", "is_not_empty",
|
|
333
|
+
]),
|
|
334
|
+
value: z
|
|
335
|
+
.union([z.string(), z.number(), z.boolean(), z.array(z.string())])
|
|
336
|
+
.optional(),
|
|
337
|
+
})
|
|
338
|
+
.optional()
|
|
339
|
+
.describe(
|
|
340
|
+
"Optional row filter — typically {column: targetColumn, operator: 'is_empty'} to enrich only blank cells"
|
|
341
|
+
),
|
|
342
|
+
agentProfile: z
|
|
343
|
+
.string()
|
|
344
|
+
.optional()
|
|
345
|
+
.describe("Agent profile to use (defaults to 'sales-researcher')"),
|
|
346
|
+
projectId: z
|
|
347
|
+
.string()
|
|
348
|
+
.optional()
|
|
349
|
+
.describe("Project ID. Omit to use the active project."),
|
|
350
|
+
batchSize: z
|
|
351
|
+
.number()
|
|
352
|
+
.int()
|
|
353
|
+
.min(1)
|
|
354
|
+
.optional()
|
|
355
|
+
.describe("Maximum rows to process in this run (default 50, capped at 200)"),
|
|
356
|
+
},
|
|
357
|
+
async (args) => {
|
|
358
|
+
try {
|
|
359
|
+
const effectiveProjectId = args.projectId ?? ctx.projectId ?? undefined;
|
|
360
|
+
const result = await createEnrichmentWorkflow(args.tableId, {
|
|
361
|
+
prompt: args.prompt,
|
|
362
|
+
targetColumn: args.targetColumn,
|
|
363
|
+
filter: args.filter,
|
|
364
|
+
agentProfile: args.agentProfile,
|
|
365
|
+
projectId: effectiveProjectId,
|
|
366
|
+
batchSize: args.batchSize,
|
|
367
|
+
});
|
|
368
|
+
return ok(result);
|
|
369
|
+
} catch (e) {
|
|
370
|
+
return err(e instanceof Error ? e.message : "Failed to start enrichment");
|
|
371
|
+
}
|
|
372
|
+
}
|
|
373
|
+
),
|
|
374
|
+
|
|
304
375
|
// ── Creation operations ──────────────────────────────────────────
|
|
305
376
|
|
|
306
377
|
defineTool(
|