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
|
@@ -1,17 +1,39 @@
|
|
|
1
1
|
import { NextResponse } from "next/server";
|
|
2
|
+
import { readStagentCodexAuthState } from "@/lib/agents/runtime/openai-codex-auth";
|
|
2
3
|
import { getRuntimeSetupStates } from "@/lib/settings/runtime-setup";
|
|
3
4
|
import { getRoutingPreference } from "@/lib/settings/routing";
|
|
4
5
|
import { getAuthSettings } from "@/lib/settings/auth";
|
|
5
6
|
import { getOpenAIAuthSettings } from "@/lib/settings/openai-auth";
|
|
7
|
+
import { getOpenAILoginState } from "@/lib/settings/openai-login-manager";
|
|
6
8
|
|
|
7
9
|
export async function GET() {
|
|
8
|
-
const [
|
|
9
|
-
|
|
10
|
-
|
|
11
|
-
|
|
12
|
-
|
|
13
|
-
|
|
14
|
-
|
|
10
|
+
const [routingPreference, anthropicAuth, initialOpenaiAuth] = await Promise.all([
|
|
11
|
+
getRoutingPreference(),
|
|
12
|
+
getAuthSettings(),
|
|
13
|
+
getOpenAIAuthSettings(),
|
|
14
|
+
]);
|
|
15
|
+
|
|
16
|
+
let openaiAuth = initialOpenaiAuth;
|
|
17
|
+
if (openaiAuth.method === "oauth") {
|
|
18
|
+
try {
|
|
19
|
+
const current = await readStagentCodexAuthState({ refreshToken: true });
|
|
20
|
+
openaiAuth = {
|
|
21
|
+
...openaiAuth,
|
|
22
|
+
oauthConnected: current.connected,
|
|
23
|
+
account: current.account,
|
|
24
|
+
rateLimits: current.rateLimits,
|
|
25
|
+
};
|
|
26
|
+
} catch {
|
|
27
|
+
openaiAuth = {
|
|
28
|
+
...openaiAuth,
|
|
29
|
+
oauthConnected: false,
|
|
30
|
+
account: null,
|
|
31
|
+
rateLimits: null,
|
|
32
|
+
};
|
|
33
|
+
}
|
|
34
|
+
}
|
|
35
|
+
|
|
36
|
+
const runtimeStates = await getRuntimeSetupStates();
|
|
15
37
|
|
|
16
38
|
const anthropicConfigured =
|
|
17
39
|
runtimeStates["claude-code"].configured ||
|
|
@@ -42,9 +64,14 @@ export async function GET() {
|
|
|
42
64
|
},
|
|
43
65
|
openai: {
|
|
44
66
|
configured: openaiConfigured,
|
|
67
|
+
authMethod: openaiAuth.method,
|
|
45
68
|
hasKey: openaiAuth.hasKey,
|
|
46
69
|
apiKeySource: openaiAuth.apiKeySource,
|
|
47
|
-
|
|
70
|
+
oauthConnected: openaiAuth.oauthConnected,
|
|
71
|
+
account: openaiAuth.account,
|
|
72
|
+
rateLimits: openaiAuth.rateLimits,
|
|
73
|
+
login: getOpenAILoginState(),
|
|
74
|
+
dualBilling: openaiAuth.oauthConnected && openaiAuth.hasKey,
|
|
48
75
|
runtimes: [
|
|
49
76
|
runtimeStates["openai-codex-app-server"],
|
|
50
77
|
runtimeStates["openai-direct"],
|
|
@@ -0,0 +1,153 @@
|
|
|
1
|
+
import { describe, it, expect, vi, beforeEach } from "vitest";
|
|
2
|
+
|
|
3
|
+
const { mockCreateEnrichmentWorkflow } = vi.hoisted(() => ({
|
|
4
|
+
mockCreateEnrichmentWorkflow: vi.fn(),
|
|
5
|
+
}));
|
|
6
|
+
|
|
7
|
+
vi.mock("@/lib/tables/enrichment", () => ({
|
|
8
|
+
createEnrichmentWorkflow: mockCreateEnrichmentWorkflow,
|
|
9
|
+
}));
|
|
10
|
+
|
|
11
|
+
import { POST } from "../route";
|
|
12
|
+
|
|
13
|
+
function makeRequest(body: unknown): Request {
|
|
14
|
+
return new Request("http://test/api/tables/tbl_x/enrich", {
|
|
15
|
+
method: "POST",
|
|
16
|
+
headers: { "content-type": "application/json" },
|
|
17
|
+
body: JSON.stringify(body),
|
|
18
|
+
});
|
|
19
|
+
}
|
|
20
|
+
|
|
21
|
+
const params = Promise.resolve({ id: "tbl_x" });
|
|
22
|
+
|
|
23
|
+
describe("POST /api/tables/[id]/enrich", () => {
|
|
24
|
+
beforeEach(() => {
|
|
25
|
+
mockCreateEnrichmentWorkflow.mockReset();
|
|
26
|
+
});
|
|
27
|
+
|
|
28
|
+
it("rejects requests with missing required fields (400)", async () => {
|
|
29
|
+
const res = await POST(makeRequest({}) as never, {
|
|
30
|
+
params,
|
|
31
|
+
});
|
|
32
|
+
expect(res.status).toBe(400);
|
|
33
|
+
expect(mockCreateEnrichmentWorkflow).not.toHaveBeenCalled();
|
|
34
|
+
});
|
|
35
|
+
|
|
36
|
+
it("returns 202 with workflowId and rowCount on success", async () => {
|
|
37
|
+
mockCreateEnrichmentWorkflow.mockResolvedValueOnce({
|
|
38
|
+
workflowId: "wf_123",
|
|
39
|
+
rowCount: 3,
|
|
40
|
+
});
|
|
41
|
+
|
|
42
|
+
const res = await POST(
|
|
43
|
+
makeRequest({
|
|
44
|
+
prompt: "Find LinkedIn for {{row.name}}",
|
|
45
|
+
targetColumn: "linkedin",
|
|
46
|
+
}) as never,
|
|
47
|
+
{ params }
|
|
48
|
+
);
|
|
49
|
+
|
|
50
|
+
expect(res.status).toBe(202);
|
|
51
|
+
const json = (await res.json()) as { workflowId: string; rowCount: number };
|
|
52
|
+
expect(json.workflowId).toBe("wf_123");
|
|
53
|
+
expect(json.rowCount).toBe(3);
|
|
54
|
+
expect(mockCreateEnrichmentWorkflow).toHaveBeenCalledWith(
|
|
55
|
+
"tbl_x",
|
|
56
|
+
expect.objectContaining({
|
|
57
|
+
prompt: "Find LinkedIn for {{row.name}}",
|
|
58
|
+
targetColumn: "linkedin",
|
|
59
|
+
})
|
|
60
|
+
);
|
|
61
|
+
});
|
|
62
|
+
|
|
63
|
+
it("caps batchSize to 200 before delegating", async () => {
|
|
64
|
+
mockCreateEnrichmentWorkflow.mockResolvedValueOnce({
|
|
65
|
+
workflowId: "wf_456",
|
|
66
|
+
rowCount: 200,
|
|
67
|
+
});
|
|
68
|
+
|
|
69
|
+
await POST(
|
|
70
|
+
makeRequest({
|
|
71
|
+
prompt: "Enrich {{row.name}}",
|
|
72
|
+
targetColumn: "linkedin",
|
|
73
|
+
batchSize: 5000,
|
|
74
|
+
}) as never,
|
|
75
|
+
{ params }
|
|
76
|
+
);
|
|
77
|
+
|
|
78
|
+
const callArg = mockCreateEnrichmentWorkflow.mock.calls[0][1] as {
|
|
79
|
+
batchSize: number;
|
|
80
|
+
};
|
|
81
|
+
expect(callArg.batchSize).toBe(200);
|
|
82
|
+
});
|
|
83
|
+
|
|
84
|
+
it("rejects batchSize less than 1", async () => {
|
|
85
|
+
const res = await POST(
|
|
86
|
+
makeRequest({
|
|
87
|
+
prompt: "x",
|
|
88
|
+
targetColumn: "linkedin",
|
|
89
|
+
batchSize: 0,
|
|
90
|
+
}) as never,
|
|
91
|
+
{ params }
|
|
92
|
+
);
|
|
93
|
+
expect(res.status).toBe(400);
|
|
94
|
+
expect(mockCreateEnrichmentWorkflow).not.toHaveBeenCalled();
|
|
95
|
+
});
|
|
96
|
+
|
|
97
|
+
it("returns 404 when the table is missing", async () => {
|
|
98
|
+
mockCreateEnrichmentWorkflow.mockRejectedValueOnce(
|
|
99
|
+
new Error("Table tbl_x not found")
|
|
100
|
+
);
|
|
101
|
+
|
|
102
|
+
const res = await POST(
|
|
103
|
+
makeRequest({
|
|
104
|
+
prompt: "x",
|
|
105
|
+
targetColumn: "linkedin",
|
|
106
|
+
}) as never,
|
|
107
|
+
{ params }
|
|
108
|
+
);
|
|
109
|
+
expect(res.status).toBe(404);
|
|
110
|
+
});
|
|
111
|
+
|
|
112
|
+
it("returns 400 when the column does not exist on the table", async () => {
|
|
113
|
+
mockCreateEnrichmentWorkflow.mockRejectedValueOnce(
|
|
114
|
+
new Error('Column "ghost" does not exist on table tbl_x')
|
|
115
|
+
);
|
|
116
|
+
|
|
117
|
+
const res = await POST(
|
|
118
|
+
makeRequest({
|
|
119
|
+
prompt: "x",
|
|
120
|
+
targetColumn: "ghost",
|
|
121
|
+
}) as never,
|
|
122
|
+
{ params }
|
|
123
|
+
);
|
|
124
|
+
expect(res.status).toBe(400);
|
|
125
|
+
});
|
|
126
|
+
|
|
127
|
+
it("forwards filter, agentProfile, and projectId to the generator", async () => {
|
|
128
|
+
mockCreateEnrichmentWorkflow.mockResolvedValueOnce({
|
|
129
|
+
workflowId: "wf_789",
|
|
130
|
+
rowCount: 1,
|
|
131
|
+
});
|
|
132
|
+
|
|
133
|
+
await POST(
|
|
134
|
+
makeRequest({
|
|
135
|
+
prompt: "x",
|
|
136
|
+
targetColumn: "linkedin",
|
|
137
|
+
filter: { column: "linkedin", operator: "is_empty" },
|
|
138
|
+
agentProfile: "researcher",
|
|
139
|
+
projectId: "proj_1",
|
|
140
|
+
}) as never,
|
|
141
|
+
{ params }
|
|
142
|
+
);
|
|
143
|
+
|
|
144
|
+
const callArg = mockCreateEnrichmentWorkflow.mock.calls[0][1] as {
|
|
145
|
+
filter: unknown;
|
|
146
|
+
agentProfile: string;
|
|
147
|
+
projectId: string;
|
|
148
|
+
};
|
|
149
|
+
expect(callArg.filter).toEqual({ column: "linkedin", operator: "is_empty" });
|
|
150
|
+
expect(callArg.agentProfile).toBe("researcher");
|
|
151
|
+
expect(callArg.projectId).toBe("proj_1");
|
|
152
|
+
});
|
|
153
|
+
});
|
|
@@ -0,0 +1,98 @@
|
|
|
1
|
+
import { NextRequest, NextResponse } from "next/server";
|
|
2
|
+
import { z } from "zod";
|
|
3
|
+
import { previewEnrichmentPlan } from "@/lib/tables/enrichment";
|
|
4
|
+
|
|
5
|
+
const MAX_BATCH_SIZE = 200;
|
|
6
|
+
|
|
7
|
+
const filterSchema = z.object({
|
|
8
|
+
column: z.string().min(1),
|
|
9
|
+
operator: z.enum([
|
|
10
|
+
"eq",
|
|
11
|
+
"neq",
|
|
12
|
+
"gt",
|
|
13
|
+
"gte",
|
|
14
|
+
"lt",
|
|
15
|
+
"lte",
|
|
16
|
+
"contains",
|
|
17
|
+
"starts_with",
|
|
18
|
+
"in",
|
|
19
|
+
"is_empty",
|
|
20
|
+
"is_not_empty",
|
|
21
|
+
]),
|
|
22
|
+
value: z
|
|
23
|
+
.union([z.string(), z.number(), z.boolean(), z.array(z.string())])
|
|
24
|
+
.optional(),
|
|
25
|
+
});
|
|
26
|
+
|
|
27
|
+
const previewRequestSchema = z.object({
|
|
28
|
+
targetColumn: z.string().min(1).max(128),
|
|
29
|
+
promptMode: z.enum(["auto", "custom"]).optional(),
|
|
30
|
+
prompt: z.string().min(1).max(8192).optional(),
|
|
31
|
+
filter: filterSchema.optional(),
|
|
32
|
+
agentProfile: z.string().min(1).max(128).optional(),
|
|
33
|
+
agentProfileOverride: z.string().min(1).max(128).optional(),
|
|
34
|
+
batchSize: z.number().int().min(1).optional(),
|
|
35
|
+
}).superRefine((value, ctx) => {
|
|
36
|
+
const mode = value.promptMode ?? (value.prompt ? "custom" : "auto");
|
|
37
|
+
if (mode === "custom" && !value.prompt?.trim()) {
|
|
38
|
+
ctx.addIssue({
|
|
39
|
+
code: z.ZodIssueCode.custom,
|
|
40
|
+
path: ["prompt"],
|
|
41
|
+
message: "Custom enrichment requires a prompt",
|
|
42
|
+
});
|
|
43
|
+
}
|
|
44
|
+
});
|
|
45
|
+
|
|
46
|
+
export async function POST(
|
|
47
|
+
req: NextRequest,
|
|
48
|
+
{ params }: { params: Promise<{ id: string }> }
|
|
49
|
+
) {
|
|
50
|
+
const { id } = await params;
|
|
51
|
+
|
|
52
|
+
let body: unknown;
|
|
53
|
+
try {
|
|
54
|
+
body = await req.json();
|
|
55
|
+
} catch {
|
|
56
|
+
return NextResponse.json({ error: "Invalid JSON body" }, { status: 400 });
|
|
57
|
+
}
|
|
58
|
+
|
|
59
|
+
const parsed = previewRequestSchema.safeParse(body);
|
|
60
|
+
if (!parsed.success) {
|
|
61
|
+
return NextResponse.json(
|
|
62
|
+
{
|
|
63
|
+
error: parsed.error.issues.map((issue) => ({
|
|
64
|
+
path: issue.path.join("."),
|
|
65
|
+
message: issue.message,
|
|
66
|
+
})),
|
|
67
|
+
},
|
|
68
|
+
{ status: 400 }
|
|
69
|
+
);
|
|
70
|
+
}
|
|
71
|
+
|
|
72
|
+
const { batchSize, ...rest } = parsed.data;
|
|
73
|
+
const cappedBatchSize =
|
|
74
|
+
batchSize !== undefined ? Math.min(batchSize, MAX_BATCH_SIZE) : undefined;
|
|
75
|
+
|
|
76
|
+
try {
|
|
77
|
+
const preview = await previewEnrichmentPlan(id, {
|
|
78
|
+
...rest,
|
|
79
|
+
batchSize: cappedBatchSize,
|
|
80
|
+
});
|
|
81
|
+
return NextResponse.json(preview);
|
|
82
|
+
} catch (err) {
|
|
83
|
+
const message = err instanceof Error ? err.message : String(err);
|
|
84
|
+
|
|
85
|
+
if (/not found/i.test(message)) {
|
|
86
|
+
return NextResponse.json({ error: message }, { status: 404 });
|
|
87
|
+
}
|
|
88
|
+
if (/does not exist|unsupported/i.test(message)) {
|
|
89
|
+
return NextResponse.json({ error: message }, { status: 400 });
|
|
90
|
+
}
|
|
91
|
+
|
|
92
|
+
console.error("[tables/enrich/plan] POST error:", err);
|
|
93
|
+
return NextResponse.json(
|
|
94
|
+
{ error: "Failed to build enrichment plan" },
|
|
95
|
+
{ status: 500 }
|
|
96
|
+
);
|
|
97
|
+
}
|
|
98
|
+
}
|
|
@@ -0,0 +1,147 @@
|
|
|
1
|
+
import { NextRequest, NextResponse } from "next/server";
|
|
2
|
+
import { z } from "zod";
|
|
3
|
+
import { createEnrichmentWorkflow } from "@/lib/tables/enrichment";
|
|
4
|
+
|
|
5
|
+
const MAX_BATCH_SIZE = 200;
|
|
6
|
+
|
|
7
|
+
const filterSchema = z.object({
|
|
8
|
+
column: z.string().min(1),
|
|
9
|
+
operator: z.enum([
|
|
10
|
+
"eq",
|
|
11
|
+
"neq",
|
|
12
|
+
"gt",
|
|
13
|
+
"gte",
|
|
14
|
+
"lt",
|
|
15
|
+
"lte",
|
|
16
|
+
"contains",
|
|
17
|
+
"starts_with",
|
|
18
|
+
"in",
|
|
19
|
+
"is_empty",
|
|
20
|
+
"is_not_empty",
|
|
21
|
+
]),
|
|
22
|
+
value: z
|
|
23
|
+
.union([z.string(), z.number(), z.boolean(), z.array(z.string())])
|
|
24
|
+
.optional(),
|
|
25
|
+
});
|
|
26
|
+
|
|
27
|
+
const enrichRequestSchema = z.object({
|
|
28
|
+
targetColumn: z.string().min(1).max(128),
|
|
29
|
+
promptMode: z.enum(["auto", "custom"]).optional(),
|
|
30
|
+
prompt: z.string().min(1).max(8192).optional(),
|
|
31
|
+
filter: filterSchema.optional(),
|
|
32
|
+
agentProfile: z.string().min(1).max(128).optional(),
|
|
33
|
+
agentProfileOverride: z.string().min(1).max(128).optional(),
|
|
34
|
+
projectId: z.string().nullable().optional(),
|
|
35
|
+
// Reject non-positive ints; the upper bound is *clamped* in the handler so
|
|
36
|
+
// callers asking for too much get a working (smaller) batch instead of a 400.
|
|
37
|
+
batchSize: z.number().int().min(1).optional(),
|
|
38
|
+
itemVariable: z.string().min(1).max(64).optional(),
|
|
39
|
+
workflowName: z.string().min(1).max(256).optional(),
|
|
40
|
+
plan: z
|
|
41
|
+
.object({
|
|
42
|
+
promptMode: z.enum(["auto", "custom"]),
|
|
43
|
+
strategy: z.enum([
|
|
44
|
+
"single-pass-lookup",
|
|
45
|
+
"single-pass-classify",
|
|
46
|
+
"research-and-synthesize",
|
|
47
|
+
]),
|
|
48
|
+
agentProfile: z.string().min(1),
|
|
49
|
+
reasoning: z.string(),
|
|
50
|
+
steps: z.array(
|
|
51
|
+
z.object({
|
|
52
|
+
id: z.string().min(1),
|
|
53
|
+
name: z.string().min(1),
|
|
54
|
+
purpose: z.string().min(1),
|
|
55
|
+
prompt: z.string().min(1),
|
|
56
|
+
agentProfile: z.string().min(1).optional(),
|
|
57
|
+
})
|
|
58
|
+
),
|
|
59
|
+
targetContract: z.object({
|
|
60
|
+
columnName: z.string().min(1),
|
|
61
|
+
columnLabel: z.string().min(1),
|
|
62
|
+
dataType: z.enum(["text", "number", "boolean", "select", "url", "email"]),
|
|
63
|
+
allowedOptions: z.array(z.string()).optional(),
|
|
64
|
+
}),
|
|
65
|
+
eligibleRowCount: z.number().int().min(0),
|
|
66
|
+
sampleBindings: z.array(z.record(z.string(), z.unknown())),
|
|
67
|
+
})
|
|
68
|
+
.optional(),
|
|
69
|
+
}).superRefine((value, ctx) => {
|
|
70
|
+
const hasPlan = Boolean(value.plan);
|
|
71
|
+
const mode = value.promptMode ?? (value.prompt ? "custom" : "auto");
|
|
72
|
+
if (!hasPlan && mode === "custom" && !value.prompt?.trim()) {
|
|
73
|
+
ctx.addIssue({
|
|
74
|
+
code: z.ZodIssueCode.custom,
|
|
75
|
+
path: ["prompt"],
|
|
76
|
+
message: "Custom enrichment requires a prompt",
|
|
77
|
+
});
|
|
78
|
+
}
|
|
79
|
+
});
|
|
80
|
+
|
|
81
|
+
/**
|
|
82
|
+
* POST /api/tables/[id]/enrich
|
|
83
|
+
*
|
|
84
|
+
* Kicks off a row-driven enrichment workflow for a user table. The workflow
|
|
85
|
+
* runs fire-and-forget (TDR-001); the response includes the workflow id and
|
|
86
|
+
* the number of rows that will actually be processed (already-populated rows
|
|
87
|
+
* are filtered out for idempotency).
|
|
88
|
+
*
|
|
89
|
+
* Status codes:
|
|
90
|
+
* - 202: workflow created and queued
|
|
91
|
+
* - 400: invalid body, unknown column, or other validation failure
|
|
92
|
+
* - 404: table not found
|
|
93
|
+
* - 500: unexpected error
|
|
94
|
+
*/
|
|
95
|
+
export async function POST(
|
|
96
|
+
req: NextRequest,
|
|
97
|
+
{ params }: { params: Promise<{ id: string }> }
|
|
98
|
+
) {
|
|
99
|
+
const { id } = await params;
|
|
100
|
+
|
|
101
|
+
let body: unknown;
|
|
102
|
+
try {
|
|
103
|
+
body = await req.json();
|
|
104
|
+
} catch {
|
|
105
|
+
return NextResponse.json({ error: "Invalid JSON body" }, { status: 400 });
|
|
106
|
+
}
|
|
107
|
+
|
|
108
|
+
const parsed = enrichRequestSchema.safeParse(body);
|
|
109
|
+
if (!parsed.success) {
|
|
110
|
+
return NextResponse.json(
|
|
111
|
+
{
|
|
112
|
+
error: parsed.error.issues.map((issue) => ({
|
|
113
|
+
path: issue.path.join("."),
|
|
114
|
+
message: issue.message,
|
|
115
|
+
})),
|
|
116
|
+
},
|
|
117
|
+
{ status: 400 }
|
|
118
|
+
);
|
|
119
|
+
}
|
|
120
|
+
|
|
121
|
+
const { batchSize, ...rest } = parsed.data;
|
|
122
|
+
const cappedBatchSize =
|
|
123
|
+
batchSize !== undefined ? Math.min(batchSize, MAX_BATCH_SIZE) : undefined;
|
|
124
|
+
|
|
125
|
+
try {
|
|
126
|
+
const result = await createEnrichmentWorkflow(id, {
|
|
127
|
+
...rest,
|
|
128
|
+
batchSize: cappedBatchSize,
|
|
129
|
+
});
|
|
130
|
+
return NextResponse.json(result, { status: 202 });
|
|
131
|
+
} catch (err) {
|
|
132
|
+
const message = err instanceof Error ? err.message : String(err);
|
|
133
|
+
|
|
134
|
+
if (/not found/i.test(message)) {
|
|
135
|
+
return NextResponse.json({ error: message }, { status: 404 });
|
|
136
|
+
}
|
|
137
|
+
if (/does not exist|unsupported/i.test(message)) {
|
|
138
|
+
return NextResponse.json({ error: message }, { status: 400 });
|
|
139
|
+
}
|
|
140
|
+
|
|
141
|
+
console.error("[tables/enrich] POST error:", err);
|
|
142
|
+
return NextResponse.json(
|
|
143
|
+
{ error: "Failed to start enrichment workflow" },
|
|
144
|
+
{ status: 500 }
|
|
145
|
+
);
|
|
146
|
+
}
|
|
147
|
+
}
|
|
@@ -0,0 +1,25 @@
|
|
|
1
|
+
import { NextRequest, NextResponse } from "next/server";
|
|
2
|
+
import { listRecentEnrichmentRuns } from "@/lib/tables/enrichment";
|
|
3
|
+
|
|
4
|
+
export async function GET(
|
|
5
|
+
req: NextRequest,
|
|
6
|
+
{ params }: { params: Promise<{ id: string }> }
|
|
7
|
+
) {
|
|
8
|
+
const { id } = await params;
|
|
9
|
+
const searchParams = req.nextUrl.searchParams;
|
|
10
|
+
const rawLimit = Number(searchParams.get("limit") ?? "5");
|
|
11
|
+
const limit = Number.isFinite(rawLimit)
|
|
12
|
+
? Math.min(Math.max(Math.trunc(rawLimit), 1), 10)
|
|
13
|
+
: 5;
|
|
14
|
+
|
|
15
|
+
try {
|
|
16
|
+
const runs = await listRecentEnrichmentRuns(id, limit);
|
|
17
|
+
return NextResponse.json(runs);
|
|
18
|
+
} catch (err) {
|
|
19
|
+
console.error("[tables/enrich/runs] GET error:", err);
|
|
20
|
+
return NextResponse.json(
|
|
21
|
+
{ error: "Failed to load recent enrichment runs" },
|
|
22
|
+
{ status: 500 }
|
|
23
|
+
);
|
|
24
|
+
}
|
|
25
|
+
}
|
|
@@ -10,8 +10,6 @@ import {
|
|
|
10
10
|
enforceTaskBudgetGuardrails,
|
|
11
11
|
} from "@/lib/settings/budget-guardrails";
|
|
12
12
|
import { ensureFreshScan } from "@/lib/environment/auto-scan";
|
|
13
|
-
import { getAllExecutions } from "@/lib/agents/execution-manager";
|
|
14
|
-
import { licenseManager } from "@/lib/license/manager";
|
|
15
13
|
|
|
16
14
|
export async function POST(
|
|
17
15
|
_req: NextRequest,
|
|
@@ -95,25 +93,6 @@ export async function POST(
|
|
|
95
93
|
return NextResponse.json({ error: compatibilityError }, { status: 400 });
|
|
96
94
|
}
|
|
97
95
|
|
|
98
|
-
// Pre-check parallel workflow limit before fire-and-forget
|
|
99
|
-
const parallelLimit = licenseManager.getLimit("parallelWorkflows");
|
|
100
|
-
if (Number.isFinite(parallelLimit) && getAllExecutions().size >= parallelLimit) {
|
|
101
|
-
// Revert task to queued since we can't execute it
|
|
102
|
-
db.update(tasks)
|
|
103
|
-
.set({ status: "queued", updatedAt: new Date() })
|
|
104
|
-
.where(eq(tasks.id, id))
|
|
105
|
-
.run();
|
|
106
|
-
return NextResponse.json(
|
|
107
|
-
{
|
|
108
|
-
error: `Parallel workflow limit reached (${getAllExecutions().size}/${parallelLimit}). Wait for a running task to finish or upgrade.`,
|
|
109
|
-
limitType: "parallelWorkflows",
|
|
110
|
-
current: getAllExecutions().size,
|
|
111
|
-
max: parallelLimit,
|
|
112
|
-
},
|
|
113
|
-
{ status: 429 }
|
|
114
|
-
);
|
|
115
|
-
}
|
|
116
|
-
|
|
117
96
|
// Fire-and-forget — task already marked as running
|
|
118
97
|
executeTaskWithAgent(id, task.assignedAgent ?? DEFAULT_AGENT_RUNTIME).catch(
|
|
119
98
|
(err) => console.error(`Task ${id} execution error:`, err)
|
|
@@ -0,0 +1,59 @@
|
|
|
1
|
+
import { NextRequest, NextResponse } from "next/server";
|
|
2
|
+
import { db } from "@/lib/db";
|
|
3
|
+
import { workflows } from "@/lib/db/schema";
|
|
4
|
+
import { eq } from "drizzle-orm";
|
|
5
|
+
import { resumeWorkflow } from "@/lib/workflows/engine";
|
|
6
|
+
|
|
7
|
+
/**
|
|
8
|
+
* POST /api/workflows/[id]/resume
|
|
9
|
+
*
|
|
10
|
+
* Manually resume a workflow that was paused at a delay step. Called by the
|
|
11
|
+
* "Resume Now" button in WorkflowStatusView to skip the remaining delay. The
|
|
12
|
+
* scheduler tick also calls resumeWorkflow() directly (not through this route)
|
|
13
|
+
* when workflows.resume_at is reached.
|
|
14
|
+
*
|
|
15
|
+
* Response codes:
|
|
16
|
+
* 202 Accepted — resume dispatched (fire-and-forget)
|
|
17
|
+
* 404 Not Found — workflow does not exist
|
|
18
|
+
* 409 Conflict — workflow is not in paused state (already resumed, racing
|
|
19
|
+
* scheduler, or was never paused). resumeWorkflow handles
|
|
20
|
+
* this internally with its atomic status transition, so the
|
|
21
|
+
* conflict is reported here for correct UX feedback.
|
|
22
|
+
*/
|
|
23
|
+
export async function POST(
|
|
24
|
+
_req: NextRequest,
|
|
25
|
+
{ params }: { params: Promise<{ id: string }> },
|
|
26
|
+
) {
|
|
27
|
+
const { id } = await params;
|
|
28
|
+
|
|
29
|
+
const [workflow] = await db
|
|
30
|
+
.select()
|
|
31
|
+
.from(workflows)
|
|
32
|
+
.where(eq(workflows.id, id));
|
|
33
|
+
|
|
34
|
+
if (!workflow) {
|
|
35
|
+
return NextResponse.json({ error: "Workflow not found" }, { status: 404 });
|
|
36
|
+
}
|
|
37
|
+
|
|
38
|
+
if (workflow.status !== "paused") {
|
|
39
|
+
return NextResponse.json(
|
|
40
|
+
{
|
|
41
|
+
error: `Workflow is not paused (current status: ${workflow.status})`,
|
|
42
|
+
status: workflow.status,
|
|
43
|
+
},
|
|
44
|
+
{ status: 409 },
|
|
45
|
+
);
|
|
46
|
+
}
|
|
47
|
+
|
|
48
|
+
// Fire-and-forget. resumeWorkflow performs its own atomic status check, so
|
|
49
|
+
// if the scheduler tick beats this request by microseconds, the DB UPDATE
|
|
50
|
+
// will match zero rows and resumeWorkflow returns silently without harm.
|
|
51
|
+
resumeWorkflow(id).catch((error) => {
|
|
52
|
+
console.error(`Workflow ${id} resume failed:`, error);
|
|
53
|
+
});
|
|
54
|
+
|
|
55
|
+
return NextResponse.json(
|
|
56
|
+
{ status: "resuming", workflowId: id },
|
|
57
|
+
{ status: 202 },
|
|
58
|
+
);
|
|
59
|
+
}
|
|
@@ -3,6 +3,11 @@ import { db } from "@/lib/db";
|
|
|
3
3
|
import { workflows, tasks, documents } from "@/lib/db/schema";
|
|
4
4
|
import { eq, and, inArray, count, desc, sql as drizzleSql } from "drizzle-orm";
|
|
5
5
|
import { parseWorkflowState } from "@/lib/workflows/engine";
|
|
6
|
+
import type {
|
|
7
|
+
WorkflowStatusResponse,
|
|
8
|
+
NonLoopPattern,
|
|
9
|
+
StepWithState,
|
|
10
|
+
} from "@/lib/workflows/types";
|
|
6
11
|
|
|
7
12
|
/** Collect output documents for workflow step tasks + input documents from parent task */
|
|
8
13
|
async function getWorkflowDocuments(
|
|
@@ -97,15 +102,18 @@ export async function GET(
|
|
|
97
102
|
const sourceTaskId: string | undefined = definition.sourceTaskId;
|
|
98
103
|
const { stepDocuments, parentDocuments } = await getWorkflowDocuments(state, sourceTaskId);
|
|
99
104
|
|
|
100
|
-
// Loop pattern returns loop-specific data instead of step states
|
|
105
|
+
// Loop pattern returns loop-specific data instead of step states.
|
|
106
|
+
// The `satisfies` annotation enforces the TDR-031 contract: the loop arm
|
|
107
|
+
// of WorkflowStatusResponse cannot emit `workflowState` or `resumeAt`, and
|
|
108
|
+
// its `steps` field is raw WorkflowStep[] (not StepWithState[]).
|
|
101
109
|
if (definition.pattern === "loop") {
|
|
102
|
-
|
|
110
|
+
const loopBody = {
|
|
103
111
|
id: workflow.id,
|
|
104
112
|
name: workflow.name,
|
|
105
113
|
status: workflow.status,
|
|
106
114
|
projectId: workflow.projectId,
|
|
107
115
|
definition: workflow.definition,
|
|
108
|
-
pattern:
|
|
116
|
+
pattern: "loop" as const,
|
|
109
117
|
loopConfig: definition.loopConfig,
|
|
110
118
|
swarmConfig: definition.swarmConfig,
|
|
111
119
|
loopState,
|
|
@@ -114,18 +122,23 @@ export async function GET(
|
|
|
114
122
|
parentDocuments,
|
|
115
123
|
runNumber: workflow.runNumber,
|
|
116
124
|
runHistory,
|
|
117
|
-
}
|
|
125
|
+
} satisfies WorkflowStatusResponse;
|
|
126
|
+
return NextResponse.json(loopBody);
|
|
118
127
|
}
|
|
119
128
|
|
|
120
|
-
|
|
129
|
+
// Non-loop arm: sequence, parallel, swarm, planner-executor, checkpoint all
|
|
130
|
+
// share the step-state rendering path. `satisfies` enforces that this branch
|
|
131
|
+
// cannot accidentally emit `loopState`, and that every step has `.state`.
|
|
132
|
+
const nonLoopBody = {
|
|
121
133
|
id: workflow.id,
|
|
122
134
|
name: workflow.name,
|
|
123
135
|
status: workflow.status,
|
|
136
|
+
resumeAt: workflow.resumeAt ?? null,
|
|
124
137
|
projectId: workflow.projectId,
|
|
125
138
|
definition: workflow.definition,
|
|
126
|
-
pattern: definition.pattern,
|
|
139
|
+
pattern: definition.pattern as NonLoopPattern,
|
|
127
140
|
swarmConfig: definition.swarmConfig,
|
|
128
|
-
steps: definition.steps.map((step, i) => ({
|
|
141
|
+
steps: definition.steps.map((step, i): StepWithState => ({
|
|
129
142
|
...step,
|
|
130
143
|
state: state?.stepStates[i] ?? { stepId: step.id, status: "pending" },
|
|
131
144
|
})),
|
|
@@ -134,5 +147,6 @@ export async function GET(
|
|
|
134
147
|
parentDocuments,
|
|
135
148
|
runNumber: workflow.runNumber,
|
|
136
149
|
runHistory,
|
|
137
|
-
}
|
|
150
|
+
} satisfies WorkflowStatusResponse;
|
|
151
|
+
return NextResponse.json(nonLoopBody);
|
|
138
152
|
}
|
|
@@ -1,6 +1,8 @@
|
|
|
1
1
|
import { NextResponse } from "next/server";
|
|
2
2
|
import { getWorkspaceContext } from "@/lib/environment/workspace-context";
|
|
3
3
|
|
|
4
|
+
export const dynamic = "force-dynamic";
|
|
5
|
+
|
|
4
6
|
export function GET() {
|
|
5
7
|
const context = getWorkspaceContext();
|
|
6
8
|
return NextResponse.json(context, {
|