stagent 0.1.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/LICENSE +191 -0
- package/README.md +399 -0
- package/components.json +21 -0
- package/dist/cli.js +171 -0
- package/drizzle.config.ts +12 -0
- package/next.config.mjs +15 -0
- package/package.json +114 -0
- package/postcss.config.mjs +8 -0
- package/public/icon-512.png +0 -0
- package/public/icon.svg +13 -0
- package/public/readme/home-workspace.png +0 -0
- package/public/readme/inbox-approvals.png +0 -0
- package/public/readme/workflow-blueprints.png +0 -0
- package/public/stagent-s-128.png +0 -0
- package/public/stagent-s-64.png +0 -0
- package/src/app/api/blueprints/[id]/instantiate/route.ts +27 -0
- package/src/app/api/blueprints/[id]/route.ts +39 -0
- package/src/app/api/blueprints/import/route.ts +68 -0
- package/src/app/api/blueprints/route.ts +29 -0
- package/src/app/api/command-palette/recent/route.ts +31 -0
- package/src/app/api/data/clear/route.ts +22 -0
- package/src/app/api/data/seed/route.ts +22 -0
- package/src/app/api/documents/[id]/file/route.ts +44 -0
- package/src/app/api/documents/[id]/route.ts +123 -0
- package/src/app/api/documents/route.ts +59 -0
- package/src/app/api/logs/stream/route.ts +101 -0
- package/src/app/api/notifications/[id]/route.ts +36 -0
- package/src/app/api/notifications/mark-all-read/route.ts +13 -0
- package/src/app/api/notifications/pending-approvals/route.ts +10 -0
- package/src/app/api/notifications/pending-approvals/stream/route.ts +101 -0
- package/src/app/api/notifications/route.ts +34 -0
- package/src/app/api/permissions/route.ts +46 -0
- package/src/app/api/profiles/[id]/route.ts +79 -0
- package/src/app/api/profiles/[id]/test/route.ts +42 -0
- package/src/app/api/profiles/import/route.ts +108 -0
- package/src/app/api/profiles/route.ts +50 -0
- package/src/app/api/projects/[id]/route.ts +72 -0
- package/src/app/api/projects/route.ts +53 -0
- package/src/app/api/schedules/[id]/route.ts +185 -0
- package/src/app/api/schedules/route.ts +117 -0
- package/src/app/api/settings/budgets/route.ts +24 -0
- package/src/app/api/settings/openai/route.ts +24 -0
- package/src/app/api/settings/route.ts +21 -0
- package/src/app/api/settings/test/route.ts +26 -0
- package/src/app/api/tasks/[id]/cancel/route.ts +21 -0
- package/src/app/api/tasks/[id]/execute/route.ts +90 -0
- package/src/app/api/tasks/[id]/logs/route.ts +95 -0
- package/src/app/api/tasks/[id]/output/route.ts +47 -0
- package/src/app/api/tasks/[id]/respond/route.ts +64 -0
- package/src/app/api/tasks/[id]/resume/route.ts +76 -0
- package/src/app/api/tasks/[id]/route.ts +77 -0
- package/src/app/api/tasks/assist/route.ts +35 -0
- package/src/app/api/tasks/route.ts +82 -0
- package/src/app/api/uploads/[id]/route.ts +81 -0
- package/src/app/api/uploads/cleanup/route.ts +7 -0
- package/src/app/api/uploads/route.ts +66 -0
- package/src/app/api/workflows/[id]/execute/route.ts +82 -0
- package/src/app/api/workflows/[id]/route.ts +133 -0
- package/src/app/api/workflows/[id]/status/route.ts +54 -0
- package/src/app/api/workflows/[id]/steps/[stepId]/retry/route.ts +22 -0
- package/src/app/api/workflows/route.ts +61 -0
- package/src/app/apple-icon.tsx +31 -0
- package/src/app/costs/page.tsx +256 -0
- package/src/app/dashboard/page.tsx +44 -0
- package/src/app/documents/[id]/page.tsx +46 -0
- package/src/app/documents/page.tsx +45 -0
- package/src/app/error.tsx +26 -0
- package/src/app/global-error.tsx +23 -0
- package/src/app/globals.css +733 -0
- package/src/app/icon.tsx +30 -0
- package/src/app/inbox/loading.tsx +15 -0
- package/src/app/inbox/page.tsx +35 -0
- package/src/app/layout.tsx +78 -0
- package/src/app/manifest.ts +32 -0
- package/src/app/monitor/page.tsx +37 -0
- package/src/app/page.tsx +162 -0
- package/src/app/profiles/[id]/edit/page.tsx +39 -0
- package/src/app/profiles/[id]/page.tsx +33 -0
- package/src/app/profiles/new/page.tsx +22 -0
- package/src/app/profiles/page.tsx +19 -0
- package/src/app/projects/[id]/page.tsx +134 -0
- package/src/app/projects/loading.tsx +17 -0
- package/src/app/projects/page.tsx +32 -0
- package/src/app/schedules/[id]/page.tsx +47 -0
- package/src/app/schedules/page.tsx +18 -0
- package/src/app/settings/loading.tsx +24 -0
- package/src/app/settings/page.tsx +27 -0
- package/src/app/tasks/[id]/page.tsx +45 -0
- package/src/app/tasks/new/page.tsx +27 -0
- package/src/app/workflows/[id]/edit/page.tsx +66 -0
- package/src/app/workflows/[id]/page.tsx +37 -0
- package/src/app/workflows/blueprints/[id]/page.tsx +40 -0
- package/src/app/workflows/blueprints/new/page.tsx +20 -0
- package/src/app/workflows/blueprints/page.tsx +11 -0
- package/src/app/workflows/new/page.tsx +36 -0
- package/src/app/workflows/page.tsx +18 -0
- package/src/components/charts/donut-ring.tsx +64 -0
- package/src/components/charts/mini-bar.tsx +75 -0
- package/src/components/charts/sparkline.tsx +107 -0
- package/src/components/costs/cost-dashboard.tsx +877 -0
- package/src/components/costs/cost-filters.tsx +179 -0
- package/src/components/dashboard/activity-feed.tsx +95 -0
- package/src/components/dashboard/greeting.tsx +30 -0
- package/src/components/dashboard/priority-queue.tsx +79 -0
- package/src/components/dashboard/quick-actions.tsx +62 -0
- package/src/components/dashboard/recent-projects.tsx +79 -0
- package/src/components/dashboard/stats-cards.tsx +114 -0
- package/src/components/documents/document-browser.tsx +235 -0
- package/src/components/documents/document-detail-view.tsx +367 -0
- package/src/components/documents/document-grid.tsx +78 -0
- package/src/components/documents/document-preview.tsx +68 -0
- package/src/components/documents/document-table.tsx +119 -0
- package/src/components/documents/document-upload-dialog.tsx +153 -0
- package/src/components/documents/types.ts +6 -0
- package/src/components/documents/utils.ts +57 -0
- package/src/components/monitoring/connection-indicator.tsx +14 -0
- package/src/components/monitoring/log-entry.tsx +79 -0
- package/src/components/monitoring/log-filters.tsx +57 -0
- package/src/components/monitoring/log-stream.tsx +144 -0
- package/src/components/monitoring/monitor-overview-wrapper.tsx +64 -0
- package/src/components/monitoring/monitor-overview.tsx +119 -0
- package/src/components/notifications/failure-action.tsx +38 -0
- package/src/components/notifications/inbox-list.tsx +165 -0
- package/src/components/notifications/message-response.tsx +196 -0
- package/src/components/notifications/notification-item.tsx +250 -0
- package/src/components/notifications/pending-approval-host.tsx +478 -0
- package/src/components/notifications/permission-action.tsx +37 -0
- package/src/components/notifications/permission-response-actions.tsx +126 -0
- package/src/components/notifications/unread-badge.tsx +35 -0
- package/src/components/profiles/profile-browser.tsx +117 -0
- package/src/components/profiles/profile-card.tsx +78 -0
- package/src/components/profiles/profile-detail-view.tsx +564 -0
- package/src/components/profiles/profile-form-view.tsx +480 -0
- package/src/components/profiles/profile-import-dialog.tsx +113 -0
- package/src/components/projects/project-card.tsx +58 -0
- package/src/components/projects/project-create-dialog.tsx +140 -0
- package/src/components/projects/project-detail.tsx +68 -0
- package/src/components/projects/project-edit-dialog.tsx +219 -0
- package/src/components/projects/project-list.tsx +108 -0
- package/src/components/schedules/schedule-create-dialog.tsx +403 -0
- package/src/components/schedules/schedule-detail-view.tsx +274 -0
- package/src/components/schedules/schedule-list.tsx +242 -0
- package/src/components/schedules/schedule-status-badge.tsx +16 -0
- package/src/components/settings/api-key-form.tsx +141 -0
- package/src/components/settings/auth-config-section.tsx +141 -0
- package/src/components/settings/auth-method-selector.tsx +67 -0
- package/src/components/settings/auth-status-badge.tsx +40 -0
- package/src/components/settings/auth-status-dot.tsx +59 -0
- package/src/components/settings/budget-guardrails-section.tsx +842 -0
- package/src/components/settings/data-management-section.tsx +141 -0
- package/src/components/settings/openai-runtime-section.tsx +104 -0
- package/src/components/settings/permissions-section.tsx +91 -0
- package/src/components/shared/app-sidebar.tsx +123 -0
- package/src/components/shared/card-skeleton.tsx +42 -0
- package/src/components/shared/command-palette.tsx +250 -0
- package/src/components/shared/confirm-dialog.tsx +52 -0
- package/src/components/shared/empty-state.tsx +24 -0
- package/src/components/shared/error-state.tsx +32 -0
- package/src/components/shared/form-section-card.tsx +33 -0
- package/src/components/shared/section-heading.tsx +14 -0
- package/src/components/shared/stagent-logo.tsx +21 -0
- package/src/components/shared/theme-toggle.tsx +46 -0
- package/src/components/tasks/ai-assist-panel.tsx +210 -0
- package/src/components/tasks/content-preview.tsx +89 -0
- package/src/components/tasks/empty-board.tsx +12 -0
- package/src/components/tasks/file-upload.tsx +120 -0
- package/src/components/tasks/kanban-board.tsx +275 -0
- package/src/components/tasks/kanban-column.tsx +75 -0
- package/src/components/tasks/skeleton-board.tsx +21 -0
- package/src/components/tasks/task-attachments.tsx +114 -0
- package/src/components/tasks/task-card.tsx +101 -0
- package/src/components/tasks/task-create-panel.tsx +360 -0
- package/src/components/tasks/task-detail-view.tsx +356 -0
- package/src/components/ui/alert-dialog.tsx +196 -0
- package/src/components/ui/badge.tsx +50 -0
- package/src/components/ui/button.tsx +71 -0
- package/src/components/ui/card.tsx +92 -0
- package/src/components/ui/checkbox.tsx +32 -0
- package/src/components/ui/command.tsx +184 -0
- package/src/components/ui/dialog.tsx +158 -0
- package/src/components/ui/dropdown-menu.tsx +257 -0
- package/src/components/ui/form.tsx +167 -0
- package/src/components/ui/input.tsx +21 -0
- package/src/components/ui/label.tsx +24 -0
- package/src/components/ui/popover.tsx +89 -0
- package/src/components/ui/progress.tsx +31 -0
- package/src/components/ui/radio-group.tsx +45 -0
- package/src/components/ui/scroll-area.tsx +58 -0
- package/src/components/ui/select.tsx +190 -0
- package/src/components/ui/separator.tsx +28 -0
- package/src/components/ui/sheet.tsx +143 -0
- package/src/components/ui/sidebar.tsx +726 -0
- package/src/components/ui/skeleton.tsx +13 -0
- package/src/components/ui/slider.tsx +63 -0
- package/src/components/ui/sonner.tsx +36 -0
- package/src/components/ui/switch.tsx +35 -0
- package/src/components/ui/table.tsx +116 -0
- package/src/components/ui/tabs.tsx +91 -0
- package/src/components/ui/textarea.tsx +18 -0
- package/src/components/ui/tooltip.tsx +57 -0
- package/src/components/workflows/blueprint-editor.tsx +109 -0
- package/src/components/workflows/blueprint-gallery.tsx +155 -0
- package/src/components/workflows/blueprint-preview.tsx +240 -0
- package/src/components/workflows/loop-status-view.tsx +272 -0
- package/src/components/workflows/swarm-dashboard.tsx +185 -0
- package/src/components/workflows/workflow-form-view.tsx +1376 -0
- package/src/components/workflows/workflow-list.tsx +230 -0
- package/src/components/workflows/workflow-status-view.tsx +477 -0
- package/src/hooks/use-mobile.ts +19 -0
- package/src/instrumentation.ts +7 -0
- package/src/lib/agents/claude-agent.ts +737 -0
- package/src/lib/agents/execution-manager.ts +27 -0
- package/src/lib/agents/profiles/assignment-validation.ts +75 -0
- package/src/lib/agents/profiles/builtins/code-reviewer/SKILL.md +21 -0
- package/src/lib/agents/profiles/builtins/code-reviewer/profile.yaml +28 -0
- package/src/lib/agents/profiles/builtins/data-analyst/SKILL.md +25 -0
- package/src/lib/agents/profiles/builtins/data-analyst/profile.yaml +27 -0
- package/src/lib/agents/profiles/builtins/devops-engineer/SKILL.md +34 -0
- package/src/lib/agents/profiles/builtins/devops-engineer/profile.yaml +27 -0
- package/src/lib/agents/profiles/builtins/document-writer/SKILL.md +16 -0
- package/src/lib/agents/profiles/builtins/document-writer/profile.yaml +27 -0
- package/src/lib/agents/profiles/builtins/general/SKILL.md +13 -0
- package/src/lib/agents/profiles/builtins/general/profile.yaml +18 -0
- package/src/lib/agents/profiles/builtins/health-fitness-coach/SKILL.md +34 -0
- package/src/lib/agents/profiles/builtins/health-fitness-coach/profile.yaml +26 -0
- package/src/lib/agents/profiles/builtins/learning-coach/SKILL.md +35 -0
- package/src/lib/agents/profiles/builtins/learning-coach/profile.yaml +26 -0
- package/src/lib/agents/profiles/builtins/project-manager/SKILL.md +26 -0
- package/src/lib/agents/profiles/builtins/project-manager/profile.yaml +26 -0
- package/src/lib/agents/profiles/builtins/researcher/SKILL.md +15 -0
- package/src/lib/agents/profiles/builtins/researcher/profile.yaml +27 -0
- package/src/lib/agents/profiles/builtins/shopping-assistant/SKILL.md +34 -0
- package/src/lib/agents/profiles/builtins/shopping-assistant/profile.yaml +26 -0
- package/src/lib/agents/profiles/builtins/technical-writer/SKILL.md +31 -0
- package/src/lib/agents/profiles/builtins/technical-writer/profile.yaml +29 -0
- package/src/lib/agents/profiles/builtins/travel-planner/SKILL.md +23 -0
- package/src/lib/agents/profiles/builtins/travel-planner/profile.yaml +26 -0
- package/src/lib/agents/profiles/builtins/wealth-manager/SKILL.md +24 -0
- package/src/lib/agents/profiles/builtins/wealth-manager/profile.yaml +26 -0
- package/src/lib/agents/profiles/compatibility.ts +109 -0
- package/src/lib/agents/profiles/registry.ts +293 -0
- package/src/lib/agents/profiles/test-runner.ts +18 -0
- package/src/lib/agents/profiles/test-types.ts +20 -0
- package/src/lib/agents/profiles/types.ts +43 -0
- package/src/lib/agents/router.ts +56 -0
- package/src/lib/agents/runtime/catalog.ts +85 -0
- package/src/lib/agents/runtime/claude-sdk.ts +12 -0
- package/src/lib/agents/runtime/claude.ts +370 -0
- package/src/lib/agents/runtime/codex-app-server-client.ts +289 -0
- package/src/lib/agents/runtime/index.ts +167 -0
- package/src/lib/agents/runtime/openai-codex.ts +1089 -0
- package/src/lib/agents/runtime/task-assist-types.ts +8 -0
- package/src/lib/agents/runtime/types.ts +30 -0
- package/src/lib/constants/settings.ts +13 -0
- package/src/lib/constants/status-colors.ts +44 -0
- package/src/lib/constants/task-status.ts +49 -0
- package/src/lib/data/clear.ts +63 -0
- package/src/lib/data/seed-data/documents.ts +715 -0
- package/src/lib/data/seed-data/logs.ts +195 -0
- package/src/lib/data/seed-data/notifications.ts +141 -0
- package/src/lib/data/seed-data/profiles.ts +175 -0
- package/src/lib/data/seed-data/projects.ts +61 -0
- package/src/lib/data/seed-data/schedules.ts +108 -0
- package/src/lib/data/seed-data/tasks.ts +341 -0
- package/src/lib/data/seed-data/usage-ledger.ts +130 -0
- package/src/lib/data/seed-data/workflows.ts +213 -0
- package/src/lib/data/seed.ts +129 -0
- package/src/lib/db/index.ts +221 -0
- package/src/lib/db/migrations/0000_aromatic_gargoyle.sql +59 -0
- package/src/lib/db/migrations/0001_first_iron_patriot.sql +6 -0
- package/src/lib/db/migrations/0002_add_resume_count.sql +1 -0
- package/src/lib/db/migrations/0003_add_settings.sql +5 -0
- package/src/lib/db/migrations/0004_add_documents.sql +20 -0
- package/src/lib/db/migrations/0005_add_document_preprocessing.sql +4 -0
- package/src/lib/db/migrations/0006_add_agent_profile.sql +2 -0
- package/src/lib/db/migrations/0007_add_usage_metering_ledger.sql +30 -0
- package/src/lib/db/migrations/0008_add_document_version.sql +1 -0
- package/src/lib/db/migrations/meta/0000_snapshot.json +416 -0
- package/src/lib/db/migrations/meta/0001_snapshot.json +461 -0
- package/src/lib/db/migrations/meta/0002_snapshot.json +469 -0
- package/src/lib/db/migrations/meta/_journal.json +27 -0
- package/src/lib/db/schema.ts +227 -0
- package/src/lib/documents/cleanup.ts +50 -0
- package/src/lib/documents/context-builder.ts +75 -0
- package/src/lib/documents/output-scanner.ts +166 -0
- package/src/lib/documents/processor.ts +120 -0
- package/src/lib/documents/processors/image.ts +21 -0
- package/src/lib/documents/processors/office.ts +36 -0
- package/src/lib/documents/processors/pdf.ts +12 -0
- package/src/lib/documents/processors/spreadsheet.ts +18 -0
- package/src/lib/documents/processors/text.ts +8 -0
- package/src/lib/documents/registry.ts +25 -0
- package/src/lib/notifications/actionable.ts +108 -0
- package/src/lib/notifications/permissions.ts +169 -0
- package/src/lib/queries/chart-data.ts +184 -0
- package/src/lib/schedules/interval-parser.ts +110 -0
- package/src/lib/schedules/scheduler.ts +220 -0
- package/src/lib/settings/auth.ts +98 -0
- package/src/lib/settings/budget-guardrails.ts +590 -0
- package/src/lib/settings/helpers.ts +23 -0
- package/src/lib/settings/openai-auth.ts +80 -0
- package/src/lib/settings/permissions.ts +102 -0
- package/src/lib/usage/ledger.ts +489 -0
- package/src/lib/usage/pricing.ts +68 -0
- package/src/lib/utils/crypto.ts +90 -0
- package/src/lib/utils/format-timestamp.ts +46 -0
- package/src/lib/utils/session-cleanup.ts +26 -0
- package/src/lib/utils/stagent-paths.ts +18 -0
- package/src/lib/utils.ts +6 -0
- package/src/lib/validators/blueprint.ts +43 -0
- package/src/lib/validators/profile.ts +64 -0
- package/src/lib/validators/project.ts +17 -0
- package/src/lib/validators/settings.ts +57 -0
- package/src/lib/validators/task.ts +30 -0
- package/src/lib/workflows/blueprints/builtins/code-review-pipeline.yaml +72 -0
- package/src/lib/workflows/blueprints/builtins/documentation-generation.yaml +62 -0
- package/src/lib/workflows/blueprints/builtins/investment-research.yaml +81 -0
- package/src/lib/workflows/blueprints/builtins/meal-planning.yaml +73 -0
- package/src/lib/workflows/blueprints/builtins/product-research.yaml +72 -0
- package/src/lib/workflows/blueprints/builtins/research-report.yaml +77 -0
- package/src/lib/workflows/blueprints/builtins/sprint-planning.yaml +77 -0
- package/src/lib/workflows/blueprints/builtins/travel-planning.yaml +80 -0
- package/src/lib/workflows/blueprints/instantiator.ts +131 -0
- package/src/lib/workflows/blueprints/registry.ts +128 -0
- package/src/lib/workflows/blueprints/template.ts +58 -0
- package/src/lib/workflows/blueprints/types.ts +38 -0
- package/src/lib/workflows/definition-validation.ts +121 -0
- package/src/lib/workflows/engine.ts +1113 -0
- package/src/lib/workflows/loop-executor.ts +270 -0
- package/src/lib/workflows/parallel.ts +55 -0
- package/src/lib/workflows/swarm.ts +97 -0
- package/src/lib/workflows/types.ts +112 -0
- package/tsconfig.json +41 -0
|
@@ -0,0 +1,1113 @@
|
|
|
1
|
+
import { db } from "@/lib/db";
|
|
2
|
+
import { workflows, tasks, agentLogs, notifications } from "@/lib/db/schema";
|
|
3
|
+
import { eq } from "drizzle-orm";
|
|
4
|
+
import { executeTaskWithRuntime } from "@/lib/agents/runtime";
|
|
5
|
+
import type { WorkflowDefinition, WorkflowState, StepState, LoopState } from "./types";
|
|
6
|
+
import { createInitialState } from "./types";
|
|
7
|
+
import { executeLoop } from "./loop-executor";
|
|
8
|
+
import {
|
|
9
|
+
buildParallelSynthesisPrompt,
|
|
10
|
+
getParallelWorkflowStructure,
|
|
11
|
+
PARALLEL_BRANCH_CONCURRENCY_LIMIT,
|
|
12
|
+
} from "./parallel";
|
|
13
|
+
import {
|
|
14
|
+
buildSwarmRefineryPrompt,
|
|
15
|
+
buildSwarmWorkerPrompt,
|
|
16
|
+
getSwarmWorkflowStructure,
|
|
17
|
+
} from "./swarm";
|
|
18
|
+
|
|
19
|
+
/**
|
|
20
|
+
* Execute a workflow by advancing through its steps according to the pattern.
|
|
21
|
+
* Fire-and-forget — call this from the API route and don't await.
|
|
22
|
+
*/
|
|
23
|
+
export async function executeWorkflow(workflowId: string): Promise<void> {
|
|
24
|
+
const [workflow] = await db
|
|
25
|
+
.select()
|
|
26
|
+
.from(workflows)
|
|
27
|
+
.where(eq(workflows.id, workflowId));
|
|
28
|
+
|
|
29
|
+
if (!workflow) throw new Error(`Workflow ${workflowId} not found`);
|
|
30
|
+
|
|
31
|
+
const definition: WorkflowDefinition = JSON.parse(workflow.definition);
|
|
32
|
+
const state = createInitialState(definition);
|
|
33
|
+
|
|
34
|
+
await updateWorkflowState(workflowId, state, "active");
|
|
35
|
+
|
|
36
|
+
await db.insert(agentLogs).values({
|
|
37
|
+
id: crypto.randomUUID(),
|
|
38
|
+
taskId: null,
|
|
39
|
+
agentType: "workflow-engine",
|
|
40
|
+
event: "workflow_started",
|
|
41
|
+
payload: JSON.stringify({ workflowId, pattern: definition.pattern }),
|
|
42
|
+
timestamp: new Date(),
|
|
43
|
+
});
|
|
44
|
+
|
|
45
|
+
// Loop pattern manages its own lifecycle — delegate fully
|
|
46
|
+
if (definition.pattern === "loop") {
|
|
47
|
+
try {
|
|
48
|
+
await executeLoop(workflowId, definition);
|
|
49
|
+
|
|
50
|
+
await db.insert(agentLogs).values({
|
|
51
|
+
id: crypto.randomUUID(),
|
|
52
|
+
taskId: null,
|
|
53
|
+
agentType: "workflow-engine",
|
|
54
|
+
event: "workflow_completed",
|
|
55
|
+
payload: JSON.stringify({ workflowId }),
|
|
56
|
+
timestamp: new Date(),
|
|
57
|
+
});
|
|
58
|
+
} catch (error) {
|
|
59
|
+
await db.insert(agentLogs).values({
|
|
60
|
+
id: crypto.randomUUID(),
|
|
61
|
+
taskId: null,
|
|
62
|
+
agentType: "workflow-engine",
|
|
63
|
+
event: "workflow_failed",
|
|
64
|
+
payload: JSON.stringify({
|
|
65
|
+
workflowId,
|
|
66
|
+
error: error instanceof Error ? error.message : String(error),
|
|
67
|
+
}),
|
|
68
|
+
timestamp: new Date(),
|
|
69
|
+
});
|
|
70
|
+
}
|
|
71
|
+
return;
|
|
72
|
+
}
|
|
73
|
+
|
|
74
|
+
try {
|
|
75
|
+
switch (definition.pattern) {
|
|
76
|
+
case "sequence":
|
|
77
|
+
await executeSequence(workflowId, definition, state);
|
|
78
|
+
break;
|
|
79
|
+
case "planner-executor":
|
|
80
|
+
await executePlannerExecutor(workflowId, definition, state);
|
|
81
|
+
break;
|
|
82
|
+
case "checkpoint":
|
|
83
|
+
await executeCheckpoint(workflowId, definition, state);
|
|
84
|
+
break;
|
|
85
|
+
case "parallel":
|
|
86
|
+
await executeParallel(workflowId, definition, state);
|
|
87
|
+
break;
|
|
88
|
+
case "swarm":
|
|
89
|
+
await executeSwarm(workflowId, definition, state);
|
|
90
|
+
break;
|
|
91
|
+
}
|
|
92
|
+
|
|
93
|
+
state.status = "completed";
|
|
94
|
+
state.completedAt = new Date().toISOString();
|
|
95
|
+
await updateWorkflowState(workflowId, state, "completed");
|
|
96
|
+
|
|
97
|
+
await db.insert(agentLogs).values({
|
|
98
|
+
id: crypto.randomUUID(),
|
|
99
|
+
taskId: null,
|
|
100
|
+
agentType: "workflow-engine",
|
|
101
|
+
event: "workflow_completed",
|
|
102
|
+
payload: JSON.stringify({ workflowId }),
|
|
103
|
+
timestamp: new Date(),
|
|
104
|
+
});
|
|
105
|
+
} catch (error) {
|
|
106
|
+
state.status = "failed";
|
|
107
|
+
await updateWorkflowState(workflowId, state, "failed");
|
|
108
|
+
|
|
109
|
+
await db.insert(agentLogs).values({
|
|
110
|
+
id: crypto.randomUUID(),
|
|
111
|
+
taskId: null,
|
|
112
|
+
agentType: "workflow-engine",
|
|
113
|
+
event: "workflow_failed",
|
|
114
|
+
payload: JSON.stringify({
|
|
115
|
+
workflowId,
|
|
116
|
+
error: error instanceof Error ? error.message : String(error),
|
|
117
|
+
}),
|
|
118
|
+
timestamp: new Date(),
|
|
119
|
+
});
|
|
120
|
+
}
|
|
121
|
+
}
|
|
122
|
+
|
|
123
|
+
/**
|
|
124
|
+
* Sequence pattern: execute steps one after another, passing output forward.
|
|
125
|
+
*/
|
|
126
|
+
async function executeSequence(
|
|
127
|
+
workflowId: string,
|
|
128
|
+
definition: WorkflowDefinition,
|
|
129
|
+
state: WorkflowState
|
|
130
|
+
): Promise<void> {
|
|
131
|
+
let previousOutput = "";
|
|
132
|
+
|
|
133
|
+
for (let i = 0; i < definition.steps.length; i++) {
|
|
134
|
+
const step = definition.steps[i];
|
|
135
|
+
state.currentStepIndex = i;
|
|
136
|
+
|
|
137
|
+
// Build prompt with context from previous step
|
|
138
|
+
const contextPrompt = previousOutput
|
|
139
|
+
? `Previous step output:\n${previousOutput}\n\n---\n\n${step.prompt}`
|
|
140
|
+
: step.prompt;
|
|
141
|
+
|
|
142
|
+
const result = await executeStep(
|
|
143
|
+
workflowId,
|
|
144
|
+
step.id,
|
|
145
|
+
step.name,
|
|
146
|
+
contextPrompt,
|
|
147
|
+
state,
|
|
148
|
+
step.assignedAgent,
|
|
149
|
+
step.agentProfile
|
|
150
|
+
);
|
|
151
|
+
|
|
152
|
+
if (result.status === "failed") {
|
|
153
|
+
throw new Error(`Step "${step.name}" failed: ${result.error}`);
|
|
154
|
+
}
|
|
155
|
+
|
|
156
|
+
previousOutput = result.result ?? "";
|
|
157
|
+
}
|
|
158
|
+
}
|
|
159
|
+
|
|
160
|
+
/**
|
|
161
|
+
* Planner-Executor pattern: first step generates a plan, subsequent steps execute it.
|
|
162
|
+
*/
|
|
163
|
+
async function executePlannerExecutor(
|
|
164
|
+
workflowId: string,
|
|
165
|
+
definition: WorkflowDefinition,
|
|
166
|
+
state: WorkflowState
|
|
167
|
+
): Promise<void> {
|
|
168
|
+
if (definition.steps.length < 2) {
|
|
169
|
+
throw new Error("Planner-Executor requires at least 2 steps (planner + executor)");
|
|
170
|
+
}
|
|
171
|
+
|
|
172
|
+
// Step 1: Planner
|
|
173
|
+
const plannerStep = definition.steps[0];
|
|
174
|
+
state.currentStepIndex = 0;
|
|
175
|
+
const planResult = await executeStep(
|
|
176
|
+
workflowId,
|
|
177
|
+
plannerStep.id,
|
|
178
|
+
plannerStep.name,
|
|
179
|
+
plannerStep.prompt,
|
|
180
|
+
state,
|
|
181
|
+
plannerStep.assignedAgent,
|
|
182
|
+
plannerStep.agentProfile
|
|
183
|
+
);
|
|
184
|
+
|
|
185
|
+
if (planResult.status === "failed") {
|
|
186
|
+
throw new Error(`Planner step failed: ${planResult.error}`);
|
|
187
|
+
}
|
|
188
|
+
|
|
189
|
+
// Execute remaining steps with plan context
|
|
190
|
+
for (let i = 1; i < definition.steps.length; i++) {
|
|
191
|
+
const step = definition.steps[i];
|
|
192
|
+
state.currentStepIndex = i;
|
|
193
|
+
|
|
194
|
+
const contextPrompt = `Plan from planner:\n${planResult.result}\n\n---\n\n${step.prompt}`;
|
|
195
|
+
const result = await executeStep(
|
|
196
|
+
workflowId,
|
|
197
|
+
step.id,
|
|
198
|
+
step.name,
|
|
199
|
+
contextPrompt,
|
|
200
|
+
state,
|
|
201
|
+
step.assignedAgent,
|
|
202
|
+
step.agentProfile
|
|
203
|
+
);
|
|
204
|
+
|
|
205
|
+
if (result.status === "failed") {
|
|
206
|
+
throw new Error(`Executor step "${step.name}" failed: ${result.error}`);
|
|
207
|
+
}
|
|
208
|
+
}
|
|
209
|
+
}
|
|
210
|
+
|
|
211
|
+
/**
|
|
212
|
+
* Checkpoint pattern: execute steps with human approval gates between them.
|
|
213
|
+
*/
|
|
214
|
+
async function executeCheckpoint(
|
|
215
|
+
workflowId: string,
|
|
216
|
+
definition: WorkflowDefinition,
|
|
217
|
+
state: WorkflowState
|
|
218
|
+
): Promise<void> {
|
|
219
|
+
let previousOutput = "";
|
|
220
|
+
|
|
221
|
+
for (let i = 0; i < definition.steps.length; i++) {
|
|
222
|
+
const step = definition.steps[i];
|
|
223
|
+
state.currentStepIndex = i;
|
|
224
|
+
|
|
225
|
+
// If step requires approval and we have previous output, wait for approval
|
|
226
|
+
if (step.requiresApproval && i > 0) {
|
|
227
|
+
state.stepStates[i].status = "waiting_approval";
|
|
228
|
+
await updateWorkflowState(workflowId, state, "active");
|
|
229
|
+
|
|
230
|
+
const approved = await waitForApproval(workflowId, step.name, previousOutput);
|
|
231
|
+
if (!approved) {
|
|
232
|
+
state.stepStates[i].status = "failed";
|
|
233
|
+
state.stepStates[i].error = "Approval denied by user";
|
|
234
|
+
throw new Error(`Step "${step.name}" was denied approval`);
|
|
235
|
+
}
|
|
236
|
+
}
|
|
237
|
+
|
|
238
|
+
const contextPrompt = previousOutput
|
|
239
|
+
? `Previous step output:\n${previousOutput}\n\n---\n\n${step.prompt}`
|
|
240
|
+
: step.prompt;
|
|
241
|
+
|
|
242
|
+
const result = await executeStep(
|
|
243
|
+
workflowId,
|
|
244
|
+
step.id,
|
|
245
|
+
step.name,
|
|
246
|
+
contextPrompt,
|
|
247
|
+
state,
|
|
248
|
+
step.assignedAgent,
|
|
249
|
+
step.agentProfile
|
|
250
|
+
);
|
|
251
|
+
|
|
252
|
+
if (result.status === "failed") {
|
|
253
|
+
throw new Error(`Step "${step.name}" failed: ${result.error}`);
|
|
254
|
+
}
|
|
255
|
+
|
|
256
|
+
previousOutput = result.result ?? "";
|
|
257
|
+
}
|
|
258
|
+
}
|
|
259
|
+
|
|
260
|
+
/**
|
|
261
|
+
* Parallel pattern: execute branch steps concurrently, then run a synthesis step.
|
|
262
|
+
*/
|
|
263
|
+
async function executeParallel(
|
|
264
|
+
workflowId: string,
|
|
265
|
+
definition: WorkflowDefinition,
|
|
266
|
+
state: WorkflowState
|
|
267
|
+
): Promise<void> {
|
|
268
|
+
const structure = getParallelWorkflowStructure(definition);
|
|
269
|
+
if (!structure) {
|
|
270
|
+
throw new Error(
|
|
271
|
+
"Parallel workflows require branch steps and exactly one synthesis step"
|
|
272
|
+
);
|
|
273
|
+
}
|
|
274
|
+
|
|
275
|
+
const { branchSteps, synthesisStep } = structure;
|
|
276
|
+
const synthesisIndex = definition.steps.findIndex(
|
|
277
|
+
(step) => step.id === synthesisStep.id
|
|
278
|
+
);
|
|
279
|
+
|
|
280
|
+
if (synthesisIndex === -1) {
|
|
281
|
+
throw new Error(`Synthesis step "${synthesisStep.id}" not found`);
|
|
282
|
+
}
|
|
283
|
+
|
|
284
|
+
let stateWriteQueue = Promise.resolve();
|
|
285
|
+
const commitState = (
|
|
286
|
+
mutate: (draft: WorkflowState) => void,
|
|
287
|
+
status: "draft" | "active" | "paused" | "completed" = "active"
|
|
288
|
+
) => {
|
|
289
|
+
stateWriteQueue = stateWriteQueue.then(async () => {
|
|
290
|
+
mutate(state);
|
|
291
|
+
await updateWorkflowState(workflowId, state, status);
|
|
292
|
+
});
|
|
293
|
+
return stateWriteQueue;
|
|
294
|
+
};
|
|
295
|
+
|
|
296
|
+
await commitState((draft) => {
|
|
297
|
+
draft.currentStepIndex = 0;
|
|
298
|
+
const joinState = draft.stepStates[synthesisIndex];
|
|
299
|
+
joinState.status = "waiting_dependencies";
|
|
300
|
+
joinState.error = undefined;
|
|
301
|
+
joinState.result = undefined;
|
|
302
|
+
});
|
|
303
|
+
|
|
304
|
+
const branchResults = await mapWithConcurrency(
|
|
305
|
+
branchSteps,
|
|
306
|
+
PARALLEL_BRANCH_CONCURRENCY_LIMIT,
|
|
307
|
+
async (step) => {
|
|
308
|
+
const stepIndex = definition.steps.findIndex(
|
|
309
|
+
(candidate) => candidate.id === step.id
|
|
310
|
+
);
|
|
311
|
+
if (stepIndex === -1) {
|
|
312
|
+
throw new Error(`Parallel branch "${step.id}" not found`);
|
|
313
|
+
}
|
|
314
|
+
|
|
315
|
+
const startedAt = new Date().toISOString();
|
|
316
|
+
await commitState((draft) => {
|
|
317
|
+
const stepState = draft.stepStates[stepIndex];
|
|
318
|
+
stepState.status = "running";
|
|
319
|
+
stepState.startedAt = startedAt;
|
|
320
|
+
stepState.completedAt = undefined;
|
|
321
|
+
stepState.error = undefined;
|
|
322
|
+
stepState.result = undefined;
|
|
323
|
+
});
|
|
324
|
+
|
|
325
|
+
const result = await executeChildTask(
|
|
326
|
+
workflowId,
|
|
327
|
+
step.name,
|
|
328
|
+
step.prompt,
|
|
329
|
+
step.assignedAgent,
|
|
330
|
+
step.agentProfile
|
|
331
|
+
);
|
|
332
|
+
|
|
333
|
+
const completedAt = new Date().toISOString();
|
|
334
|
+
await commitState((draft) => {
|
|
335
|
+
const stepState = draft.stepStates[stepIndex];
|
|
336
|
+
stepState.taskId = result.taskId;
|
|
337
|
+
stepState.completedAt = completedAt;
|
|
338
|
+
|
|
339
|
+
if (result.status === "completed") {
|
|
340
|
+
stepState.status = "completed";
|
|
341
|
+
stepState.result = result.result ?? "";
|
|
342
|
+
} else {
|
|
343
|
+
stepState.status = "failed";
|
|
344
|
+
stepState.error =
|
|
345
|
+
result.error ?? "Task did not complete successfully";
|
|
346
|
+
}
|
|
347
|
+
});
|
|
348
|
+
|
|
349
|
+
return { step, result };
|
|
350
|
+
}
|
|
351
|
+
);
|
|
352
|
+
|
|
353
|
+
await stateWriteQueue;
|
|
354
|
+
|
|
355
|
+
const failedBranches = branchResults.filter(
|
|
356
|
+
(branch) => branch.result.status !== "completed"
|
|
357
|
+
);
|
|
358
|
+
|
|
359
|
+
if (failedBranches.length > 0) {
|
|
360
|
+
const failureSummary = failedBranches
|
|
361
|
+
.map(
|
|
362
|
+
(branch) =>
|
|
363
|
+
`${branch.step.name}: ${
|
|
364
|
+
branch.result.error ?? "Task did not complete successfully"
|
|
365
|
+
}`
|
|
366
|
+
)
|
|
367
|
+
.join("; ");
|
|
368
|
+
|
|
369
|
+
await commitState((draft) => {
|
|
370
|
+
const joinState = draft.stepStates[synthesisIndex];
|
|
371
|
+
joinState.status = "failed";
|
|
372
|
+
joinState.error = `Blocked by failed branches: ${failureSummary}`;
|
|
373
|
+
});
|
|
374
|
+
await stateWriteQueue;
|
|
375
|
+
|
|
376
|
+
throw new Error(`Parallel branches failed: ${failureSummary}`);
|
|
377
|
+
}
|
|
378
|
+
|
|
379
|
+
const synthesisStartedAt = new Date().toISOString();
|
|
380
|
+
await commitState((draft) => {
|
|
381
|
+
draft.currentStepIndex = synthesisIndex;
|
|
382
|
+
const joinState = draft.stepStates[synthesisIndex];
|
|
383
|
+
joinState.status = "running";
|
|
384
|
+
joinState.startedAt = synthesisStartedAt;
|
|
385
|
+
joinState.completedAt = undefined;
|
|
386
|
+
joinState.error = undefined;
|
|
387
|
+
joinState.result = undefined;
|
|
388
|
+
});
|
|
389
|
+
|
|
390
|
+
const synthesisPrompt = buildParallelSynthesisPrompt({
|
|
391
|
+
branchOutputs: branchResults.map((branch) => ({
|
|
392
|
+
stepName: branch.step.name,
|
|
393
|
+
result: branch.result.result ?? "",
|
|
394
|
+
})),
|
|
395
|
+
synthesisPrompt: synthesisStep.prompt,
|
|
396
|
+
});
|
|
397
|
+
|
|
398
|
+
const synthesisResult = await executeChildTask(
|
|
399
|
+
workflowId,
|
|
400
|
+
synthesisStep.name,
|
|
401
|
+
synthesisPrompt,
|
|
402
|
+
synthesisStep.assignedAgent,
|
|
403
|
+
synthesisStep.agentProfile
|
|
404
|
+
);
|
|
405
|
+
|
|
406
|
+
await commitState((draft) => {
|
|
407
|
+
const joinState = draft.stepStates[synthesisIndex];
|
|
408
|
+
joinState.taskId = synthesisResult.taskId;
|
|
409
|
+
joinState.completedAt = new Date().toISOString();
|
|
410
|
+
|
|
411
|
+
if (synthesisResult.status === "completed") {
|
|
412
|
+
joinState.status = "completed";
|
|
413
|
+
joinState.result = synthesisResult.result ?? "";
|
|
414
|
+
} else {
|
|
415
|
+
joinState.status = "failed";
|
|
416
|
+
joinState.error =
|
|
417
|
+
synthesisResult.error ?? "Task did not complete successfully";
|
|
418
|
+
}
|
|
419
|
+
});
|
|
420
|
+
await stateWriteQueue;
|
|
421
|
+
|
|
422
|
+
if (synthesisResult.status !== "completed") {
|
|
423
|
+
throw new Error(
|
|
424
|
+
`Synthesis step "${synthesisStep.name}" failed: ${
|
|
425
|
+
synthesisResult.error ?? "Task did not complete successfully"
|
|
426
|
+
}`
|
|
427
|
+
);
|
|
428
|
+
}
|
|
429
|
+
}
|
|
430
|
+
|
|
431
|
+
/**
|
|
432
|
+
* Swarm pattern: run a mayor planning step, execute worker steps in parallel,
|
|
433
|
+
* then merge the results through a refinery step.
|
|
434
|
+
*/
|
|
435
|
+
async function executeSwarm(
|
|
436
|
+
workflowId: string,
|
|
437
|
+
definition: WorkflowDefinition,
|
|
438
|
+
state: WorkflowState
|
|
439
|
+
): Promise<void> {
|
|
440
|
+
const structure = getSwarmWorkflowStructure(definition);
|
|
441
|
+
if (!structure) {
|
|
442
|
+
throw new Error(
|
|
443
|
+
"Swarm workflows require a mayor step, 2-5 worker steps, and a refinery step"
|
|
444
|
+
);
|
|
445
|
+
}
|
|
446
|
+
|
|
447
|
+
const { mayorStep, workerSteps, refineryStep, workerConcurrencyLimit } =
|
|
448
|
+
structure;
|
|
449
|
+
const refineryIndex = definition.steps.findIndex(
|
|
450
|
+
(step) => step.id === refineryStep.id
|
|
451
|
+
);
|
|
452
|
+
|
|
453
|
+
if (refineryIndex === -1) {
|
|
454
|
+
throw new Error(`Refinery step "${refineryStep.id}" not found`);
|
|
455
|
+
}
|
|
456
|
+
|
|
457
|
+
const mayorResult = await executeStep(
|
|
458
|
+
workflowId,
|
|
459
|
+
mayorStep.id,
|
|
460
|
+
mayorStep.name,
|
|
461
|
+
mayorStep.prompt,
|
|
462
|
+
state,
|
|
463
|
+
mayorStep.assignedAgent,
|
|
464
|
+
mayorStep.agentProfile
|
|
465
|
+
);
|
|
466
|
+
|
|
467
|
+
if (mayorResult.status === "failed") {
|
|
468
|
+
throw new Error(`Mayor step "${mayorStep.name}" failed: ${mayorResult.error}`);
|
|
469
|
+
}
|
|
470
|
+
|
|
471
|
+
let stateWriteQueue = Promise.resolve();
|
|
472
|
+
const commitState = (
|
|
473
|
+
mutate: (draft: WorkflowState) => void,
|
|
474
|
+
status: "draft" | "active" | "paused" | "completed" = "active"
|
|
475
|
+
) => {
|
|
476
|
+
stateWriteQueue = stateWriteQueue.then(async () => {
|
|
477
|
+
mutate(state);
|
|
478
|
+
await updateWorkflowState(workflowId, state, status);
|
|
479
|
+
});
|
|
480
|
+
return stateWriteQueue;
|
|
481
|
+
};
|
|
482
|
+
|
|
483
|
+
await commitState((draft) => {
|
|
484
|
+
draft.currentStepIndex = 1;
|
|
485
|
+
const refineryState = draft.stepStates[refineryIndex];
|
|
486
|
+
refineryState.status = "waiting_dependencies";
|
|
487
|
+
refineryState.error = undefined;
|
|
488
|
+
refineryState.result = undefined;
|
|
489
|
+
refineryState.startedAt = undefined;
|
|
490
|
+
refineryState.completedAt = undefined;
|
|
491
|
+
});
|
|
492
|
+
|
|
493
|
+
const workerResults = await mapWithConcurrency(
|
|
494
|
+
workerSteps,
|
|
495
|
+
workerConcurrencyLimit,
|
|
496
|
+
async (step) => {
|
|
497
|
+
const stepIndex = definition.steps.findIndex(
|
|
498
|
+
(candidate) => candidate.id === step.id
|
|
499
|
+
);
|
|
500
|
+
if (stepIndex === -1) {
|
|
501
|
+
throw new Error(`Swarm worker "${step.id}" not found`);
|
|
502
|
+
}
|
|
503
|
+
|
|
504
|
+
const workerPrompt = buildSwarmWorkerPrompt({
|
|
505
|
+
mayorName: mayorStep.name,
|
|
506
|
+
mayorResult: mayorResult.result ?? "",
|
|
507
|
+
workerName: step.name,
|
|
508
|
+
workerPrompt: step.prompt,
|
|
509
|
+
});
|
|
510
|
+
|
|
511
|
+
const startedAt = new Date().toISOString();
|
|
512
|
+
await commitState((draft) => {
|
|
513
|
+
const stepState = draft.stepStates[stepIndex];
|
|
514
|
+
stepState.status = "running";
|
|
515
|
+
stepState.startedAt = startedAt;
|
|
516
|
+
stepState.completedAt = undefined;
|
|
517
|
+
stepState.error = undefined;
|
|
518
|
+
stepState.result = undefined;
|
|
519
|
+
});
|
|
520
|
+
|
|
521
|
+
const result = await executeChildTask(
|
|
522
|
+
workflowId,
|
|
523
|
+
step.name,
|
|
524
|
+
workerPrompt,
|
|
525
|
+
step.assignedAgent,
|
|
526
|
+
step.agentProfile
|
|
527
|
+
);
|
|
528
|
+
|
|
529
|
+
const completedAt = new Date().toISOString();
|
|
530
|
+
await commitState((draft) => {
|
|
531
|
+
const stepState = draft.stepStates[stepIndex];
|
|
532
|
+
stepState.taskId = result.taskId;
|
|
533
|
+
stepState.completedAt = completedAt;
|
|
534
|
+
|
|
535
|
+
if (result.status === "completed") {
|
|
536
|
+
stepState.status = "completed";
|
|
537
|
+
stepState.result = result.result ?? "";
|
|
538
|
+
} else {
|
|
539
|
+
stepState.status = "failed";
|
|
540
|
+
stepState.error =
|
|
541
|
+
result.error ?? "Task did not complete successfully";
|
|
542
|
+
}
|
|
543
|
+
});
|
|
544
|
+
|
|
545
|
+
return { step, result };
|
|
546
|
+
}
|
|
547
|
+
);
|
|
548
|
+
|
|
549
|
+
await stateWriteQueue;
|
|
550
|
+
|
|
551
|
+
const failedWorkers = workerResults.filter(
|
|
552
|
+
(worker) => worker.result.status !== "completed"
|
|
553
|
+
);
|
|
554
|
+
if (failedWorkers.length > 0) {
|
|
555
|
+
const failureSummary = summarizeFailedWorkers(failedWorkers);
|
|
556
|
+
|
|
557
|
+
await commitState((draft) => {
|
|
558
|
+
const refineryState = draft.stepStates[refineryIndex];
|
|
559
|
+
refineryState.status = "failed";
|
|
560
|
+
refineryState.error = `Blocked by failed workers: ${failureSummary}`;
|
|
561
|
+
});
|
|
562
|
+
await stateWriteQueue;
|
|
563
|
+
|
|
564
|
+
throw new Error(`Swarm workers failed: ${failureSummary}`);
|
|
565
|
+
}
|
|
566
|
+
|
|
567
|
+
await runSwarmRefinery({
|
|
568
|
+
workflowId,
|
|
569
|
+
state,
|
|
570
|
+
mayorStep,
|
|
571
|
+
mayorResult: mayorResult.result ?? "",
|
|
572
|
+
refineryStep,
|
|
573
|
+
refineryIndex,
|
|
574
|
+
workerOutputs: workerResults.map((worker) => ({
|
|
575
|
+
stepName: worker.step.name,
|
|
576
|
+
result: worker.result.result ?? "",
|
|
577
|
+
})),
|
|
578
|
+
});
|
|
579
|
+
}
|
|
580
|
+
|
|
581
|
+
function summarizeFailedWorkers(
|
|
582
|
+
failedWorkers: Array<{
|
|
583
|
+
step: { name: string };
|
|
584
|
+
result: { error?: string };
|
|
585
|
+
}>
|
|
586
|
+
): string {
|
|
587
|
+
return failedWorkers
|
|
588
|
+
.map(
|
|
589
|
+
(worker) =>
|
|
590
|
+
`${worker.step.name}: ${
|
|
591
|
+
worker.result.error ?? "Task did not complete successfully"
|
|
592
|
+
}`
|
|
593
|
+
)
|
|
594
|
+
.join("; ");
|
|
595
|
+
}
|
|
596
|
+
|
|
597
|
+
async function runSwarmRefinery(input: {
|
|
598
|
+
workflowId: string;
|
|
599
|
+
state: WorkflowState;
|
|
600
|
+
mayorStep: { name: string };
|
|
601
|
+
mayorResult: string;
|
|
602
|
+
refineryStep: {
|
|
603
|
+
id: string;
|
|
604
|
+
name: string;
|
|
605
|
+
prompt: string;
|
|
606
|
+
assignedAgent?: string;
|
|
607
|
+
agentProfile?: string;
|
|
608
|
+
};
|
|
609
|
+
refineryIndex: number;
|
|
610
|
+
workerOutputs: Array<{ stepName: string; result: string }>;
|
|
611
|
+
}): Promise<void> {
|
|
612
|
+
const {
|
|
613
|
+
workflowId,
|
|
614
|
+
state,
|
|
615
|
+
mayorStep,
|
|
616
|
+
mayorResult,
|
|
617
|
+
refineryStep,
|
|
618
|
+
refineryIndex,
|
|
619
|
+
workerOutputs,
|
|
620
|
+
} = input;
|
|
621
|
+
|
|
622
|
+
state.currentStepIndex = refineryIndex;
|
|
623
|
+
const refineryState = state.stepStates[refineryIndex];
|
|
624
|
+
refineryState.status = "running";
|
|
625
|
+
refineryState.startedAt = new Date().toISOString();
|
|
626
|
+
refineryState.completedAt = undefined;
|
|
627
|
+
refineryState.error = undefined;
|
|
628
|
+
refineryState.result = undefined;
|
|
629
|
+
await updateWorkflowState(workflowId, state, "active");
|
|
630
|
+
|
|
631
|
+
const refineryPrompt = buildSwarmRefineryPrompt({
|
|
632
|
+
mayorName: mayorStep.name,
|
|
633
|
+
mayorResult,
|
|
634
|
+
workerOutputs,
|
|
635
|
+
refineryPrompt: refineryStep.prompt,
|
|
636
|
+
});
|
|
637
|
+
|
|
638
|
+
const refineryResult = await executeChildTask(
|
|
639
|
+
workflowId,
|
|
640
|
+
refineryStep.name,
|
|
641
|
+
refineryPrompt,
|
|
642
|
+
refineryStep.assignedAgent,
|
|
643
|
+
refineryStep.agentProfile
|
|
644
|
+
);
|
|
645
|
+
|
|
646
|
+
refineryState.taskId = refineryResult.taskId;
|
|
647
|
+
refineryState.completedAt = new Date().toISOString();
|
|
648
|
+
|
|
649
|
+
if (refineryResult.status === "completed") {
|
|
650
|
+
refineryState.status = "completed";
|
|
651
|
+
refineryState.result = refineryResult.result ?? "";
|
|
652
|
+
} else {
|
|
653
|
+
refineryState.status = "failed";
|
|
654
|
+
refineryState.error =
|
|
655
|
+
refineryResult.error ?? "Task did not complete successfully";
|
|
656
|
+
}
|
|
657
|
+
|
|
658
|
+
await updateWorkflowState(workflowId, state, "active");
|
|
659
|
+
|
|
660
|
+
if (refineryResult.status !== "completed") {
|
|
661
|
+
throw new Error(
|
|
662
|
+
`Refinery step "${refineryStep.name}" failed: ${
|
|
663
|
+
refineryResult.error ?? "Task did not complete successfully"
|
|
664
|
+
}`
|
|
665
|
+
);
|
|
666
|
+
}
|
|
667
|
+
}
|
|
668
|
+
|
|
669
|
+
/**
|
|
670
|
+
* Create and execute a child task, returning its result.
|
|
671
|
+
* Shared by step-based patterns and the loop executor.
|
|
672
|
+
*/
|
|
673
|
+
export async function executeChildTask(
|
|
674
|
+
workflowId: string,
|
|
675
|
+
name: string,
|
|
676
|
+
prompt: string,
|
|
677
|
+
assignedAgent?: string,
|
|
678
|
+
agentProfile?: string
|
|
679
|
+
): Promise<{ taskId: string; status: string; result?: string; error?: string }> {
|
|
680
|
+
const [workflow] = await db
|
|
681
|
+
.select()
|
|
682
|
+
.from(workflows)
|
|
683
|
+
.where(eq(workflows.id, workflowId));
|
|
684
|
+
|
|
685
|
+
const taskId = crypto.randomUUID();
|
|
686
|
+
await db.insert(tasks).values({
|
|
687
|
+
id: taskId,
|
|
688
|
+
projectId: workflow?.projectId ?? null,
|
|
689
|
+
workflowId,
|
|
690
|
+
scheduleId: null,
|
|
691
|
+
title: `[Workflow] ${name}`,
|
|
692
|
+
description: prompt,
|
|
693
|
+
status: "queued",
|
|
694
|
+
priority: 1,
|
|
695
|
+
assignedAgent: assignedAgent ?? null,
|
|
696
|
+
agentProfile: agentProfile ?? null,
|
|
697
|
+
createdAt: new Date(),
|
|
698
|
+
updatedAt: new Date(),
|
|
699
|
+
});
|
|
700
|
+
|
|
701
|
+
await db
|
|
702
|
+
.update(tasks)
|
|
703
|
+
.set({ status: "running", updatedAt: new Date() })
|
|
704
|
+
.where(eq(tasks.id, taskId));
|
|
705
|
+
|
|
706
|
+
try {
|
|
707
|
+
await executeTaskWithRuntime(taskId);
|
|
708
|
+
} catch {
|
|
709
|
+
// Runtime adapter handles its own error logging
|
|
710
|
+
}
|
|
711
|
+
|
|
712
|
+
const [completedTask] = await db
|
|
713
|
+
.select()
|
|
714
|
+
.from(tasks)
|
|
715
|
+
.where(eq(tasks.id, taskId));
|
|
716
|
+
|
|
717
|
+
if (completedTask?.status === "completed") {
|
|
718
|
+
return { taskId, status: "completed", result: completedTask.result ?? "" };
|
|
719
|
+
}
|
|
720
|
+
return {
|
|
721
|
+
taskId,
|
|
722
|
+
status: "failed",
|
|
723
|
+
error: completedTask?.result ?? "Task did not complete successfully",
|
|
724
|
+
};
|
|
725
|
+
}
|
|
726
|
+
|
|
727
|
+
/**
|
|
728
|
+
* Execute a single workflow step by creating a task and waiting for completion.
|
|
729
|
+
*/
|
|
730
|
+
async function executeStep(
|
|
731
|
+
workflowId: string,
|
|
732
|
+
stepId: string,
|
|
733
|
+
stepName: string,
|
|
734
|
+
prompt: string,
|
|
735
|
+
state: WorkflowState,
|
|
736
|
+
assignedAgent?: string,
|
|
737
|
+
agentProfile?: string
|
|
738
|
+
): Promise<StepState> {
|
|
739
|
+
const stepState = state.stepStates.find((s) => s.stepId === stepId);
|
|
740
|
+
if (!stepState) throw new Error(`Step ${stepId} not found in state`);
|
|
741
|
+
|
|
742
|
+
stepState.status = "running";
|
|
743
|
+
stepState.startedAt = new Date().toISOString();
|
|
744
|
+
await updateWorkflowState(workflowId, state, "active");
|
|
745
|
+
|
|
746
|
+
const result = await executeChildTask(
|
|
747
|
+
workflowId,
|
|
748
|
+
stepName,
|
|
749
|
+
prompt,
|
|
750
|
+
assignedAgent,
|
|
751
|
+
agentProfile
|
|
752
|
+
);
|
|
753
|
+
|
|
754
|
+
stepState.taskId = result.taskId;
|
|
755
|
+
if (result.status === "completed") {
|
|
756
|
+
stepState.status = "completed";
|
|
757
|
+
stepState.result = result.result ?? "";
|
|
758
|
+
stepState.completedAt = new Date().toISOString();
|
|
759
|
+
} else {
|
|
760
|
+
stepState.status = "failed";
|
|
761
|
+
stepState.error = result.error ?? "Task did not complete successfully";
|
|
762
|
+
}
|
|
763
|
+
|
|
764
|
+
await updateWorkflowState(workflowId, state, "active");
|
|
765
|
+
return stepState;
|
|
766
|
+
}
|
|
767
|
+
|
|
768
|
+
/**
|
|
769
|
+
* Wait for human approval via the notifications system.
|
|
770
|
+
*/
|
|
771
|
+
async function waitForApproval(
|
|
772
|
+
workflowId: string,
|
|
773
|
+
stepName: string,
|
|
774
|
+
previousOutput: string
|
|
775
|
+
): Promise<boolean> {
|
|
776
|
+
const notificationId = crypto.randomUUID();
|
|
777
|
+
|
|
778
|
+
await db.insert(notifications).values({
|
|
779
|
+
id: notificationId,
|
|
780
|
+
taskId: null,
|
|
781
|
+
type: "permission_required",
|
|
782
|
+
title: `Workflow checkpoint: ${stepName}`,
|
|
783
|
+
body: `Previous step output:\n${previousOutput.slice(0, 500)}`,
|
|
784
|
+
toolName: "WorkflowCheckpoint",
|
|
785
|
+
toolInput: JSON.stringify({ workflowId, stepName }),
|
|
786
|
+
createdAt: new Date(),
|
|
787
|
+
});
|
|
788
|
+
|
|
789
|
+
// Poll for response with 5-minute timeout for human approval
|
|
790
|
+
const deadline = Date.now() + 5 * 60 * 1000;
|
|
791
|
+
const pollInterval = 2000;
|
|
792
|
+
|
|
793
|
+
while (Date.now() < deadline) {
|
|
794
|
+
const [notification] = await db
|
|
795
|
+
.select()
|
|
796
|
+
.from(notifications)
|
|
797
|
+
.where(eq(notifications.id, notificationId));
|
|
798
|
+
|
|
799
|
+
if (notification?.response) {
|
|
800
|
+
try {
|
|
801
|
+
const parsed = JSON.parse(notification.response);
|
|
802
|
+
return parsed.behavior === "allow";
|
|
803
|
+
} catch {
|
|
804
|
+
return false;
|
|
805
|
+
}
|
|
806
|
+
}
|
|
807
|
+
|
|
808
|
+
await new Promise((resolve) => setTimeout(resolve, pollInterval));
|
|
809
|
+
}
|
|
810
|
+
|
|
811
|
+
return false; // Timeout — treat as denied
|
|
812
|
+
}
|
|
813
|
+
|
|
814
|
+
/**
|
|
815
|
+
* Update workflow state in the database.
|
|
816
|
+
*/
|
|
817
|
+
export async function updateWorkflowState(
|
|
818
|
+
workflowId: string,
|
|
819
|
+
state: WorkflowState,
|
|
820
|
+
status: "draft" | "active" | "paused" | "completed" | "failed"
|
|
821
|
+
): Promise<void> {
|
|
822
|
+
// Store state in the definition field as a combined object
|
|
823
|
+
const [workflow] = await db
|
|
824
|
+
.select()
|
|
825
|
+
.from(workflows)
|
|
826
|
+
.where(eq(workflows.id, workflowId));
|
|
827
|
+
|
|
828
|
+
if (!workflow) return;
|
|
829
|
+
|
|
830
|
+
const definition = JSON.parse(workflow.definition);
|
|
831
|
+
const combined = { ...definition, _state: state };
|
|
832
|
+
|
|
833
|
+
await db
|
|
834
|
+
.update(workflows)
|
|
835
|
+
.set({
|
|
836
|
+
definition: JSON.stringify(combined),
|
|
837
|
+
status,
|
|
838
|
+
updatedAt: new Date(),
|
|
839
|
+
})
|
|
840
|
+
.where(eq(workflows.id, workflowId));
|
|
841
|
+
}
|
|
842
|
+
|
|
843
|
+
/**
|
|
844
|
+
* Get the current state of a workflow.
|
|
845
|
+
*/
|
|
846
|
+
export function parseWorkflowState(
|
|
847
|
+
definitionJson: string
|
|
848
|
+
): { definition: WorkflowDefinition; state: WorkflowState | null; loopState: LoopState | null } {
|
|
849
|
+
const parsed = JSON.parse(definitionJson);
|
|
850
|
+
const { _state, _loopState, ...definition } = parsed;
|
|
851
|
+
return { definition, state: _state ?? null, loopState: _loopState ?? null };
|
|
852
|
+
}
|
|
853
|
+
|
|
854
|
+
/**
|
|
855
|
+
* Retry a failed step in a workflow.
|
|
856
|
+
*/
|
|
857
|
+
export async function retryWorkflowStep(
|
|
858
|
+
workflowId: string,
|
|
859
|
+
stepId: string
|
|
860
|
+
): Promise<void> {
|
|
861
|
+
const [workflow] = await db
|
|
862
|
+
.select()
|
|
863
|
+
.from(workflows)
|
|
864
|
+
.where(eq(workflows.id, workflowId));
|
|
865
|
+
|
|
866
|
+
if (!workflow) throw new Error(`Workflow ${workflowId} not found`);
|
|
867
|
+
|
|
868
|
+
const { definition, state } = parseWorkflowState(workflow.definition);
|
|
869
|
+
if (!state) throw new Error("Workflow has no execution state");
|
|
870
|
+
|
|
871
|
+
const stepIndex = state.stepStates.findIndex((s) => s.stepId === stepId);
|
|
872
|
+
if (stepIndex === -1) throw new Error(`Step ${stepId} not found`);
|
|
873
|
+
|
|
874
|
+
const stepState = state.stepStates[stepIndex];
|
|
875
|
+
if (stepState.status !== "failed") {
|
|
876
|
+
throw new Error(`Step ${stepId} is not in failed state`);
|
|
877
|
+
}
|
|
878
|
+
|
|
879
|
+
if (workflow.status === "active") {
|
|
880
|
+
throw new Error("Cannot retry a step while the workflow is active");
|
|
881
|
+
}
|
|
882
|
+
|
|
883
|
+
if (definition.pattern === "swarm") {
|
|
884
|
+
await retrySwarmStep(workflowId, definition, state, stepIndex);
|
|
885
|
+
return;
|
|
886
|
+
}
|
|
887
|
+
|
|
888
|
+
// Reset step state
|
|
889
|
+
stepState.status = "pending";
|
|
890
|
+
stepState.error = undefined;
|
|
891
|
+
stepState.taskId = undefined;
|
|
892
|
+
state.status = "running";
|
|
893
|
+
state.currentStepIndex = stepIndex;
|
|
894
|
+
await updateWorkflowState(workflowId, state, "active");
|
|
895
|
+
|
|
896
|
+
// Re-execute from this step
|
|
897
|
+
const step = definition.steps[stepIndex];
|
|
898
|
+
const result = await executeStep(
|
|
899
|
+
workflowId,
|
|
900
|
+
step.id,
|
|
901
|
+
step.name,
|
|
902
|
+
step.prompt,
|
|
903
|
+
state,
|
|
904
|
+
step.assignedAgent,
|
|
905
|
+
step.agentProfile
|
|
906
|
+
);
|
|
907
|
+
|
|
908
|
+
if (result.status === "completed") {
|
|
909
|
+
// Continue with remaining steps if this was a sequence
|
|
910
|
+
if (definition.pattern === "sequence") {
|
|
911
|
+
let previousOutput = result.result ?? "";
|
|
912
|
+
for (let i = stepIndex + 1; i < definition.steps.length; i++) {
|
|
913
|
+
const nextStep = definition.steps[i];
|
|
914
|
+
state.currentStepIndex = i;
|
|
915
|
+
const contextPrompt = `Previous step output:\n${previousOutput}\n\n---\n\n${nextStep.prompt}`;
|
|
916
|
+
const nextResult = await executeStep(
|
|
917
|
+
workflowId,
|
|
918
|
+
nextStep.id,
|
|
919
|
+
nextStep.name,
|
|
920
|
+
contextPrompt,
|
|
921
|
+
state,
|
|
922
|
+
nextStep.assignedAgent,
|
|
923
|
+
nextStep.agentProfile
|
|
924
|
+
);
|
|
925
|
+
if (nextResult.status === "failed") break;
|
|
926
|
+
previousOutput = nextResult.result ?? "";
|
|
927
|
+
}
|
|
928
|
+
}
|
|
929
|
+
|
|
930
|
+
const allCompleted = state.stepStates.every((s) => s.status === "completed");
|
|
931
|
+
state.status = allCompleted ? "completed" : "failed";
|
|
932
|
+
state.completedAt = allCompleted ? new Date().toISOString() : undefined;
|
|
933
|
+
await updateWorkflowState(workflowId, state, allCompleted ? "completed" : "failed");
|
|
934
|
+
}
|
|
935
|
+
}
|
|
936
|
+
|
|
937
|
+
function resetStepState(stepState: StepState): void {
|
|
938
|
+
stepState.status = "pending";
|
|
939
|
+
stepState.error = undefined;
|
|
940
|
+
stepState.result = undefined;
|
|
941
|
+
stepState.taskId = undefined;
|
|
942
|
+
stepState.startedAt = undefined;
|
|
943
|
+
stepState.completedAt = undefined;
|
|
944
|
+
}
|
|
945
|
+
|
|
946
|
+
async function retrySwarmStep(
|
|
947
|
+
workflowId: string,
|
|
948
|
+
definition: WorkflowDefinition,
|
|
949
|
+
state: WorkflowState,
|
|
950
|
+
stepIndex: number
|
|
951
|
+
): Promise<void> {
|
|
952
|
+
const structure = getSwarmWorkflowStructure(definition);
|
|
953
|
+
if (!structure) {
|
|
954
|
+
throw new Error(
|
|
955
|
+
"Swarm workflows require a mayor step, 2-5 worker steps, and a refinery step"
|
|
956
|
+
);
|
|
957
|
+
}
|
|
958
|
+
|
|
959
|
+
const { mayorStep, workerSteps, refineryStep } = structure;
|
|
960
|
+
const refineryIndex = definition.steps.length - 1;
|
|
961
|
+
const mayorState = state.stepStates[0];
|
|
962
|
+
const refineryState = state.stepStates[refineryIndex];
|
|
963
|
+
const targetStep = definition.steps[stepIndex];
|
|
964
|
+
const targetState = state.stepStates[stepIndex];
|
|
965
|
+
|
|
966
|
+
if (stepIndex === 0) {
|
|
967
|
+
for (const currentStepState of state.stepStates) {
|
|
968
|
+
resetStepState(currentStepState);
|
|
969
|
+
}
|
|
970
|
+
|
|
971
|
+
state.status = "running";
|
|
972
|
+
state.currentStepIndex = 0;
|
|
973
|
+
state.completedAt = undefined;
|
|
974
|
+
await updateWorkflowState(workflowId, state, "active");
|
|
975
|
+
await executeSwarm(workflowId, definition, state);
|
|
976
|
+
return;
|
|
977
|
+
}
|
|
978
|
+
|
|
979
|
+
if (mayorState.status !== "completed" || !mayorState.result) {
|
|
980
|
+
throw new Error("Swarm mayor output must complete before retrying downstream steps");
|
|
981
|
+
}
|
|
982
|
+
|
|
983
|
+
if (stepIndex === refineryIndex) {
|
|
984
|
+
const incompleteWorkers = workerSteps.filter((_, workerIndex) => {
|
|
985
|
+
const workerState = state.stepStates[workerIndex + 1];
|
|
986
|
+
return workerState.status !== "completed" || !workerState.result;
|
|
987
|
+
});
|
|
988
|
+
|
|
989
|
+
if (incompleteWorkers.length > 0) {
|
|
990
|
+
throw new Error("All swarm workers must complete before retrying the refinery");
|
|
991
|
+
}
|
|
992
|
+
|
|
993
|
+
resetStepState(refineryState);
|
|
994
|
+
state.status = "running";
|
|
995
|
+
state.currentStepIndex = refineryIndex;
|
|
996
|
+
state.completedAt = undefined;
|
|
997
|
+
await updateWorkflowState(workflowId, state, "active");
|
|
998
|
+
|
|
999
|
+
await runSwarmRefinery({
|
|
1000
|
+
workflowId,
|
|
1001
|
+
state,
|
|
1002
|
+
mayorStep,
|
|
1003
|
+
mayorResult: mayorState.result,
|
|
1004
|
+
refineryStep,
|
|
1005
|
+
refineryIndex,
|
|
1006
|
+
workerOutputs: workerSteps.map((worker, workerIndex) => ({
|
|
1007
|
+
stepName: worker.name,
|
|
1008
|
+
result: state.stepStates[workerIndex + 1].result ?? "",
|
|
1009
|
+
})),
|
|
1010
|
+
});
|
|
1011
|
+
|
|
1012
|
+
state.status = "completed";
|
|
1013
|
+
state.completedAt = new Date().toISOString();
|
|
1014
|
+
await updateWorkflowState(workflowId, state, "completed");
|
|
1015
|
+
return;
|
|
1016
|
+
}
|
|
1017
|
+
|
|
1018
|
+
resetStepState(targetState);
|
|
1019
|
+
resetStepState(refineryState);
|
|
1020
|
+
refineryState.status = "waiting_dependencies";
|
|
1021
|
+
state.status = "running";
|
|
1022
|
+
state.currentStepIndex = stepIndex;
|
|
1023
|
+
state.completedAt = undefined;
|
|
1024
|
+
await updateWorkflowState(workflowId, state, "active");
|
|
1025
|
+
|
|
1026
|
+
const retriedWorker = await executeStep(
|
|
1027
|
+
workflowId,
|
|
1028
|
+
targetStep.id,
|
|
1029
|
+
targetStep.name,
|
|
1030
|
+
buildSwarmWorkerPrompt({
|
|
1031
|
+
mayorName: mayorStep.name,
|
|
1032
|
+
mayorResult: mayorState.result,
|
|
1033
|
+
workerName: targetStep.name,
|
|
1034
|
+
workerPrompt: targetStep.prompt,
|
|
1035
|
+
}),
|
|
1036
|
+
state,
|
|
1037
|
+
targetStep.assignedAgent,
|
|
1038
|
+
targetStep.agentProfile
|
|
1039
|
+
);
|
|
1040
|
+
|
|
1041
|
+
if (retriedWorker.status !== "completed") {
|
|
1042
|
+
state.status = "failed";
|
|
1043
|
+
await updateWorkflowState(workflowId, state, "failed");
|
|
1044
|
+
throw new Error(
|
|
1045
|
+
`Swarm worker "${targetStep.name}" failed: ${
|
|
1046
|
+
retriedWorker.error ?? "Task did not complete successfully"
|
|
1047
|
+
}`
|
|
1048
|
+
);
|
|
1049
|
+
}
|
|
1050
|
+
|
|
1051
|
+
const failedWorkers = workerSteps
|
|
1052
|
+
.map((worker, workerIndex) => ({
|
|
1053
|
+
step: worker,
|
|
1054
|
+
result: state.stepStates[workerIndex + 1],
|
|
1055
|
+
}))
|
|
1056
|
+
.filter((worker) => worker.result.status !== "completed");
|
|
1057
|
+
|
|
1058
|
+
if (failedWorkers.length > 0) {
|
|
1059
|
+
refineryState.status = "failed";
|
|
1060
|
+
refineryState.error = `Blocked by failed workers: ${summarizeFailedWorkers(
|
|
1061
|
+
failedWorkers.map((worker) => ({
|
|
1062
|
+
step: worker.step,
|
|
1063
|
+
result: { error: worker.result.error },
|
|
1064
|
+
}))
|
|
1065
|
+
)}`;
|
|
1066
|
+
state.status = "failed";
|
|
1067
|
+
await updateWorkflowState(workflowId, state, "failed");
|
|
1068
|
+
return;
|
|
1069
|
+
}
|
|
1070
|
+
|
|
1071
|
+
await runSwarmRefinery({
|
|
1072
|
+
workflowId,
|
|
1073
|
+
state,
|
|
1074
|
+
mayorStep,
|
|
1075
|
+
mayorResult: mayorState.result,
|
|
1076
|
+
refineryStep,
|
|
1077
|
+
refineryIndex,
|
|
1078
|
+
workerOutputs: workerSteps.map((worker, workerIndex) => ({
|
|
1079
|
+
stepName: worker.name,
|
|
1080
|
+
result: state.stepStates[workerIndex + 1].result ?? "",
|
|
1081
|
+
})),
|
|
1082
|
+
});
|
|
1083
|
+
|
|
1084
|
+
state.status = "completed";
|
|
1085
|
+
state.completedAt = new Date().toISOString();
|
|
1086
|
+
await updateWorkflowState(workflowId, state, "completed");
|
|
1087
|
+
}
|
|
1088
|
+
|
|
1089
|
+
async function mapWithConcurrency<T, TResult>(
|
|
1090
|
+
items: T[],
|
|
1091
|
+
limit: number,
|
|
1092
|
+
worker: (item: T) => Promise<TResult>
|
|
1093
|
+
): Promise<TResult[]> {
|
|
1094
|
+
if (items.length === 0) {
|
|
1095
|
+
return [];
|
|
1096
|
+
}
|
|
1097
|
+
|
|
1098
|
+
const results = new Array<TResult>(items.length);
|
|
1099
|
+
let nextIndex = 0;
|
|
1100
|
+
const workerCount = Math.min(limit, items.length);
|
|
1101
|
+
|
|
1102
|
+
await Promise.all(
|
|
1103
|
+
Array.from({ length: workerCount }, async () => {
|
|
1104
|
+
while (nextIndex < items.length) {
|
|
1105
|
+
const currentIndex = nextIndex;
|
|
1106
|
+
nextIndex += 1;
|
|
1107
|
+
results[currentIndex] = await worker(items[currentIndex]);
|
|
1108
|
+
}
|
|
1109
|
+
})
|
|
1110
|
+
);
|
|
1111
|
+
|
|
1112
|
+
return results;
|
|
1113
|
+
}
|