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,9 +1,22 @@
|
|
|
1
1
|
import { db } from "@/lib/db";
|
|
2
2
|
import { workflows, agentLogs } from "@/lib/db/schema";
|
|
3
3
|
import { eq } from "drizzle-orm";
|
|
4
|
-
import { executeChildTask
|
|
5
|
-
import type {
|
|
4
|
+
import { executeChildTask } from "./engine";
|
|
5
|
+
import type {
|
|
6
|
+
WorkflowDefinition,
|
|
7
|
+
LoopState,
|
|
8
|
+
IterationState,
|
|
9
|
+
LoopStopReason,
|
|
10
|
+
WorkflowEnrichmentTargetContract,
|
|
11
|
+
} from "./types";
|
|
6
12
|
import { createInitialLoopState } from "./types";
|
|
13
|
+
import {
|
|
14
|
+
resolvePostAction,
|
|
15
|
+
shouldSkipPostActionValue,
|
|
16
|
+
extractPostActionValue,
|
|
17
|
+
} from "./post-action";
|
|
18
|
+
import { updateRow } from "@/lib/data/tables";
|
|
19
|
+
import { normalizeEnrichmentOutput } from "@/lib/tables/enrichment-planner";
|
|
7
20
|
|
|
8
21
|
/**
|
|
9
22
|
* Execute the loop pattern — autonomous iteration with stop conditions.
|
|
@@ -27,8 +40,18 @@ export async function executeLoop(
|
|
|
27
40
|
assignedAgent,
|
|
28
41
|
agentProfile,
|
|
29
42
|
completionSignals,
|
|
43
|
+
items,
|
|
44
|
+
itemVariable,
|
|
30
45
|
} = definition.loopConfig;
|
|
31
|
-
|
|
46
|
+
|
|
47
|
+
// Row-driven loop: iterate exactly once per item (capped at maxIterations).
|
|
48
|
+
// Items array presence flips the loop into a finite fan-out pattern.
|
|
49
|
+
const isRowDriven = Array.isArray(items);
|
|
50
|
+
const rowItems = isRowDriven ? (items as unknown[]) : [];
|
|
51
|
+
const boundVarName = itemVariable && itemVariable.length > 0 ? itemVariable : "item";
|
|
52
|
+
const effectiveMax = isRowDriven
|
|
53
|
+
? Math.min(rowItems.length, maxIterations)
|
|
54
|
+
: maxIterations;
|
|
32
55
|
|
|
33
56
|
// Restore existing state (resume) or create fresh
|
|
34
57
|
const loopState = await restoreOrCreateLoopState(workflowId);
|
|
@@ -49,7 +72,7 @@ export async function executeLoop(
|
|
|
49
72
|
}
|
|
50
73
|
|
|
51
74
|
try {
|
|
52
|
-
while (loopState.currentIteration <
|
|
75
|
+
while (loopState.currentIteration < effectiveMax) {
|
|
53
76
|
// Check pause: re-fetch workflow status from DB
|
|
54
77
|
const [workflow] = await db
|
|
55
78
|
.select()
|
|
@@ -81,14 +104,6 @@ export async function executeLoop(
|
|
|
81
104
|
|
|
82
105
|
const iterationNum = loopState.currentIteration + 1;
|
|
83
106
|
|
|
84
|
-
// Build iteration prompt
|
|
85
|
-
const prompt = buildIterationPrompt(
|
|
86
|
-
loopPrompt,
|
|
87
|
-
previousOutput,
|
|
88
|
-
iterationNum,
|
|
89
|
-
maxIterations
|
|
90
|
-
);
|
|
91
|
-
|
|
92
107
|
// Create iteration state
|
|
93
108
|
const iterationState: IterationState = {
|
|
94
109
|
iteration: iterationNum,
|
|
@@ -112,14 +127,29 @@ export async function executeLoop(
|
|
|
112
127
|
timestamp: new Date(),
|
|
113
128
|
});
|
|
114
129
|
|
|
115
|
-
|
|
116
|
-
|
|
117
|
-
|
|
118
|
-
|
|
119
|
-
|
|
120
|
-
|
|
121
|
-
|
|
122
|
-
|
|
130
|
+
const result = isRowDriven
|
|
131
|
+
? await executeRowDrivenIteration({
|
|
132
|
+
workflowId,
|
|
133
|
+
definition,
|
|
134
|
+
row: rowItems[loopState.currentIteration],
|
|
135
|
+
itemVariable: boundVarName,
|
|
136
|
+
iteration: iterationNum,
|
|
137
|
+
totalRows: effectiveMax,
|
|
138
|
+
loopAssignedAgent: assignedAgent,
|
|
139
|
+
loopAgentProfile: agentProfile,
|
|
140
|
+
})
|
|
141
|
+
: await executeChildTask(
|
|
142
|
+
workflowId,
|
|
143
|
+
`Loop Iteration ${iterationNum}`,
|
|
144
|
+
buildIterationPrompt(
|
|
145
|
+
definition.steps[0].prompt,
|
|
146
|
+
previousOutput,
|
|
147
|
+
iterationNum,
|
|
148
|
+
maxIterations
|
|
149
|
+
),
|
|
150
|
+
assignedAgent ?? definition.steps[0].assignedAgent,
|
|
151
|
+
agentProfile ?? definition.steps[0].agentProfile
|
|
152
|
+
);
|
|
123
153
|
|
|
124
154
|
// Update iteration state
|
|
125
155
|
const iterStartTime = new Date(iterationState.startedAt!).getTime();
|
|
@@ -130,19 +160,29 @@ export async function executeLoop(
|
|
|
130
160
|
if (result.status === "completed") {
|
|
131
161
|
iterationState.status = "completed";
|
|
132
162
|
iterationState.result = result.result;
|
|
133
|
-
|
|
163
|
+
if (!isRowDriven) {
|
|
164
|
+
previousOutput = result.result ?? "";
|
|
165
|
+
}
|
|
134
166
|
} else {
|
|
135
167
|
iterationState.status = "failed";
|
|
136
168
|
iterationState.error = result.error;
|
|
137
|
-
|
|
138
|
-
|
|
169
|
+
loopState.currentIteration = iterationNum;
|
|
170
|
+
await updateLoopState(workflowId, loopState, "active");
|
|
171
|
+
if (!isRowDriven) {
|
|
172
|
+
await finalizeLoop(workflowId, loopState, "error");
|
|
173
|
+
return;
|
|
174
|
+
}
|
|
175
|
+
continue;
|
|
139
176
|
}
|
|
140
177
|
|
|
141
178
|
loopState.currentIteration = iterationNum;
|
|
142
179
|
await updateLoopState(workflowId, loopState, "active");
|
|
143
180
|
|
|
144
|
-
// Check completion signal
|
|
181
|
+
// Check completion signal — only for autonomous loops. Row-driven
|
|
182
|
+
// loops always run through every item; per-row completion text like
|
|
183
|
+
// "NOT_FOUND" must not abort the fan-out.
|
|
145
184
|
if (
|
|
185
|
+
!isRowDriven &&
|
|
146
186
|
result.result &&
|
|
147
187
|
detectCompletionSignal(result.result, completionSignals)
|
|
148
188
|
) {
|
|
@@ -164,6 +204,62 @@ export async function executeLoop(
|
|
|
164
204
|
}
|
|
165
205
|
}
|
|
166
206
|
|
|
207
|
+
/**
|
|
208
|
+
* Build the prompt for a single row-driven iteration.
|
|
209
|
+
*
|
|
210
|
+
* Row-driven loops fan out one iteration per item in `loopConfig.items` and
|
|
211
|
+
* stop when items are exhausted (no LOOP_COMPLETE signal needed). The row
|
|
212
|
+
* payload is serialized as JSON under the bound variable name so the agent
|
|
213
|
+
* can read every field without us pre-committing to a templating syntax.
|
|
214
|
+
*/
|
|
215
|
+
export function buildRowIterationPrompt(
|
|
216
|
+
template: string,
|
|
217
|
+
row: unknown,
|
|
218
|
+
itemVariable: string,
|
|
219
|
+
iteration: number,
|
|
220
|
+
totalRows: number,
|
|
221
|
+
previousStepOutput: string,
|
|
222
|
+
stepOutputs: Record<string, string>
|
|
223
|
+
): string {
|
|
224
|
+
const resolvedTemplate = resolveRowTemplate(template, {
|
|
225
|
+
[itemVariable]: row,
|
|
226
|
+
previous: previousStepOutput,
|
|
227
|
+
stepOutputs,
|
|
228
|
+
});
|
|
229
|
+
const parts: string[] = [];
|
|
230
|
+
parts.push(`Row ${iteration} of ${totalRows}.`);
|
|
231
|
+
parts.push(
|
|
232
|
+
`\nCurrent ${itemVariable}:\n\`\`\`json\n${JSON.stringify(row, null, 2)}\n\`\`\``
|
|
233
|
+
);
|
|
234
|
+
if (previousStepOutput) {
|
|
235
|
+
parts.push(`\nPrevious step output:\n${previousStepOutput}`);
|
|
236
|
+
}
|
|
237
|
+
parts.push(`\n---\n\n${resolvedTemplate}`);
|
|
238
|
+
return parts.join("");
|
|
239
|
+
}
|
|
240
|
+
|
|
241
|
+
function resolveRowTemplate(
|
|
242
|
+
template: string,
|
|
243
|
+
context: Record<string, unknown>
|
|
244
|
+
): string {
|
|
245
|
+
return template.replace(/\{\{\s*([^}]+)\s*\}\}/g, (_match, path: string) => {
|
|
246
|
+
const value = readContextPath(context, path.trim());
|
|
247
|
+
if (value === undefined || value === null) return "";
|
|
248
|
+
return typeof value === "string" ? value : JSON.stringify(value);
|
|
249
|
+
});
|
|
250
|
+
}
|
|
251
|
+
|
|
252
|
+
function readContextPath(value: unknown, path: string): unknown {
|
|
253
|
+
const parts = path.split(".");
|
|
254
|
+
let current = value;
|
|
255
|
+
for (const part of parts) {
|
|
256
|
+
if (current === null || current === undefined) return undefined;
|
|
257
|
+
if (typeof current !== "object") return undefined;
|
|
258
|
+
current = (current as Record<string, unknown>)[part];
|
|
259
|
+
}
|
|
260
|
+
return current;
|
|
261
|
+
}
|
|
262
|
+
|
|
167
263
|
/**
|
|
168
264
|
* Build the prompt for a single iteration, including previous output context.
|
|
169
265
|
*/
|
|
@@ -253,6 +349,235 @@ async function restoreOrCreateLoopState(
|
|
|
253
349
|
return createInitialLoopState();
|
|
254
350
|
}
|
|
255
351
|
|
|
352
|
+
/**
|
|
353
|
+
* Apply a `postAction` for a single row-driven iteration. Resolves any
|
|
354
|
+
* `{{row.field}}` placeholders, runs the skip rules, and writes the value
|
|
355
|
+
* via `updateRow`. Every outcome is logged to `agent_logs` so enrichment
|
|
356
|
+
* runs are auditable end-to-end. Errors are caught and logged — never
|
|
357
|
+
* thrown — so a single bad row can't abort the fan-out.
|
|
358
|
+
*/
|
|
359
|
+
async function applyRowPostAction(params: {
|
|
360
|
+
workflowId: string;
|
|
361
|
+
taskId: string;
|
|
362
|
+
postAction: NonNullable<import("./types").WorkflowStep["postAction"]>;
|
|
363
|
+
row: unknown;
|
|
364
|
+
itemVariable: string;
|
|
365
|
+
taskResult: string;
|
|
366
|
+
targetContract?: WorkflowEnrichmentTargetContract;
|
|
367
|
+
}): Promise<void> {
|
|
368
|
+
const {
|
|
369
|
+
workflowId,
|
|
370
|
+
taskId,
|
|
371
|
+
postAction,
|
|
372
|
+
row,
|
|
373
|
+
itemVariable,
|
|
374
|
+
taskResult,
|
|
375
|
+
targetContract,
|
|
376
|
+
} = params;
|
|
377
|
+
|
|
378
|
+
try {
|
|
379
|
+
if (postAction.type !== "update_row") {
|
|
380
|
+
// Future-proofing: unknown variants log + return rather than throwing.
|
|
381
|
+
await db.insert(agentLogs).values({
|
|
382
|
+
id: crypto.randomUUID(),
|
|
383
|
+
taskId,
|
|
384
|
+
agentType: "loop-executor",
|
|
385
|
+
event: "post_action_unknown_type",
|
|
386
|
+
payload: JSON.stringify({ workflowId, postAction }),
|
|
387
|
+
timestamp: new Date(),
|
|
388
|
+
});
|
|
389
|
+
return;
|
|
390
|
+
}
|
|
391
|
+
|
|
392
|
+
const resolved = resolvePostAction(postAction, row, itemVariable);
|
|
393
|
+
const rawValue = extractPostActionValue(taskResult);
|
|
394
|
+
|
|
395
|
+
if (!targetContract && shouldSkipPostActionValue(rawValue)) {
|
|
396
|
+
await db.insert(agentLogs).values({
|
|
397
|
+
id: crypto.randomUUID(),
|
|
398
|
+
taskId,
|
|
399
|
+
agentType: "loop-executor",
|
|
400
|
+
event: "post_action_skipped",
|
|
401
|
+
payload: JSON.stringify({
|
|
402
|
+
workflowId,
|
|
403
|
+
rowId: resolved.rowId,
|
|
404
|
+
column: resolved.column,
|
|
405
|
+
reason: rawValue.trim() === "" ? "empty" : "not_found",
|
|
406
|
+
}),
|
|
407
|
+
timestamp: new Date(),
|
|
408
|
+
});
|
|
409
|
+
return;
|
|
410
|
+
}
|
|
411
|
+
|
|
412
|
+
const normalized = targetContract
|
|
413
|
+
? normalizeEnrichmentOutput(rawValue, targetContract)
|
|
414
|
+
: { kind: "valid" as const, value: rawValue };
|
|
415
|
+
|
|
416
|
+
if (normalized.kind === "skip") {
|
|
417
|
+
await db.insert(agentLogs).values({
|
|
418
|
+
id: crypto.randomUUID(),
|
|
419
|
+
taskId,
|
|
420
|
+
agentType: "loop-executor",
|
|
421
|
+
event: "post_action_skipped",
|
|
422
|
+
payload: JSON.stringify({
|
|
423
|
+
workflowId,
|
|
424
|
+
rowId: resolved.rowId,
|
|
425
|
+
column: resolved.column,
|
|
426
|
+
reason: normalized.reason,
|
|
427
|
+
}),
|
|
428
|
+
timestamp: new Date(),
|
|
429
|
+
});
|
|
430
|
+
return;
|
|
431
|
+
}
|
|
432
|
+
|
|
433
|
+
if (normalized.kind === "invalid") {
|
|
434
|
+
await db.insert(agentLogs).values({
|
|
435
|
+
id: crypto.randomUUID(),
|
|
436
|
+
taskId,
|
|
437
|
+
agentType: "loop-executor",
|
|
438
|
+
event: "post_action_contract_invalid",
|
|
439
|
+
payload: JSON.stringify({
|
|
440
|
+
workflowId,
|
|
441
|
+
rowId: resolved.rowId,
|
|
442
|
+
column: resolved.column,
|
|
443
|
+
error: normalized.reason,
|
|
444
|
+
rawValue,
|
|
445
|
+
}),
|
|
446
|
+
timestamp: new Date(),
|
|
447
|
+
});
|
|
448
|
+
return;
|
|
449
|
+
}
|
|
450
|
+
|
|
451
|
+
if (!resolved.rowId) {
|
|
452
|
+
await db.insert(agentLogs).values({
|
|
453
|
+
id: crypto.randomUUID(),
|
|
454
|
+
taskId,
|
|
455
|
+
agentType: "loop-executor",
|
|
456
|
+
event: "post_action_failed",
|
|
457
|
+
payload: JSON.stringify({
|
|
458
|
+
workflowId,
|
|
459
|
+
error: "rowId resolved to empty string — check postAction template",
|
|
460
|
+
postAction,
|
|
461
|
+
}),
|
|
462
|
+
timestamp: new Date(),
|
|
463
|
+
});
|
|
464
|
+
return;
|
|
465
|
+
}
|
|
466
|
+
|
|
467
|
+
const updated = await updateRow(resolved.rowId, {
|
|
468
|
+
data: { [resolved.column]: normalized.value },
|
|
469
|
+
});
|
|
470
|
+
|
|
471
|
+
await db.insert(agentLogs).values({
|
|
472
|
+
id: crypto.randomUUID(),
|
|
473
|
+
taskId,
|
|
474
|
+
agentType: "loop-executor",
|
|
475
|
+
event: updated ? "post_action_applied" : "post_action_failed",
|
|
476
|
+
payload: JSON.stringify({
|
|
477
|
+
workflowId,
|
|
478
|
+
rowId: resolved.rowId,
|
|
479
|
+
column: resolved.column,
|
|
480
|
+
tableId: resolved.tableId,
|
|
481
|
+
...(updated ? {} : { error: "row not found" }),
|
|
482
|
+
}),
|
|
483
|
+
timestamp: new Date(),
|
|
484
|
+
});
|
|
485
|
+
} catch (err) {
|
|
486
|
+
// Never let postAction failures abort the loop iteration.
|
|
487
|
+
await db.insert(agentLogs).values({
|
|
488
|
+
id: crypto.randomUUID(),
|
|
489
|
+
taskId,
|
|
490
|
+
agentType: "loop-executor",
|
|
491
|
+
event: "post_action_failed",
|
|
492
|
+
payload: JSON.stringify({
|
|
493
|
+
workflowId,
|
|
494
|
+
error: err instanceof Error ? err.message : String(err),
|
|
495
|
+
}),
|
|
496
|
+
timestamp: new Date(),
|
|
497
|
+
}).catch(() => {});
|
|
498
|
+
}
|
|
499
|
+
}
|
|
500
|
+
|
|
501
|
+
async function executeRowDrivenIteration(params: {
|
|
502
|
+
workflowId: string;
|
|
503
|
+
definition: WorkflowDefinition;
|
|
504
|
+
row: unknown;
|
|
505
|
+
itemVariable: string;
|
|
506
|
+
iteration: number;
|
|
507
|
+
totalRows: number;
|
|
508
|
+
loopAssignedAgent?: string;
|
|
509
|
+
loopAgentProfile?: string;
|
|
510
|
+
}): Promise<{ taskId: string; status: string; result?: string; error?: string }> {
|
|
511
|
+
const {
|
|
512
|
+
workflowId,
|
|
513
|
+
definition,
|
|
514
|
+
row,
|
|
515
|
+
itemVariable,
|
|
516
|
+
iteration,
|
|
517
|
+
totalRows,
|
|
518
|
+
loopAssignedAgent,
|
|
519
|
+
loopAgentProfile,
|
|
520
|
+
} = params;
|
|
521
|
+
|
|
522
|
+
let previousStepOutput = "";
|
|
523
|
+
let lastTaskId = "";
|
|
524
|
+
const stepOutputs: Record<string, string> = {};
|
|
525
|
+
|
|
526
|
+
for (const step of definition.steps) {
|
|
527
|
+
const prompt = buildRowIterationPrompt(
|
|
528
|
+
step.prompt,
|
|
529
|
+
row,
|
|
530
|
+
itemVariable,
|
|
531
|
+
iteration,
|
|
532
|
+
totalRows,
|
|
533
|
+
previousStepOutput,
|
|
534
|
+
stepOutputs
|
|
535
|
+
);
|
|
536
|
+
|
|
537
|
+
const result = await executeChildTask(
|
|
538
|
+
workflowId,
|
|
539
|
+
`${step.name} · Row ${iteration}`,
|
|
540
|
+
prompt,
|
|
541
|
+
loopAssignedAgent ?? step.assignedAgent,
|
|
542
|
+
loopAgentProfile ?? step.agentProfile,
|
|
543
|
+
undefined,
|
|
544
|
+
step.id,
|
|
545
|
+
step.budgetUsd,
|
|
546
|
+
step.runtimeId
|
|
547
|
+
);
|
|
548
|
+
lastTaskId = result.taskId;
|
|
549
|
+
|
|
550
|
+
if (result.status !== "completed") {
|
|
551
|
+
return {
|
|
552
|
+
taskId: lastTaskId,
|
|
553
|
+
status: "failed",
|
|
554
|
+
error: `${step.name}: ${result.error ?? "Task did not complete successfully"}`,
|
|
555
|
+
};
|
|
556
|
+
}
|
|
557
|
+
|
|
558
|
+
previousStepOutput = result.result ?? "";
|
|
559
|
+
stepOutputs[step.id] = previousStepOutput;
|
|
560
|
+
|
|
561
|
+
if (step.postAction) {
|
|
562
|
+
await applyRowPostAction({
|
|
563
|
+
workflowId,
|
|
564
|
+
taskId: result.taskId,
|
|
565
|
+
postAction: step.postAction,
|
|
566
|
+
row,
|
|
567
|
+
itemVariable,
|
|
568
|
+
taskResult: previousStepOutput,
|
|
569
|
+
targetContract: definition.metadata?.enrichment?.targetContract,
|
|
570
|
+
});
|
|
571
|
+
}
|
|
572
|
+
}
|
|
573
|
+
|
|
574
|
+
return {
|
|
575
|
+
taskId: lastTaskId,
|
|
576
|
+
status: "completed",
|
|
577
|
+
result: previousStepOutput,
|
|
578
|
+
};
|
|
579
|
+
}
|
|
580
|
+
|
|
256
581
|
/**
|
|
257
582
|
* Finalize a loop with a stop reason and mark the workflow as completed.
|
|
258
583
|
*/
|
|
@@ -0,0 +1,91 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Post-step action helpers — pure logic for the declarative side-effect
|
|
3
|
+
* framework that bulk row enrichment uses to write agent results back into
|
|
4
|
+
* user table cells.
|
|
5
|
+
*
|
|
6
|
+
* Dispatch (the actual `updateRow` call) lives in the loop-executor where
|
|
7
|
+
* it has DB access; this module stays pure so the resolution + skip rules
|
|
8
|
+
* can be unit-tested without mocking DB.
|
|
9
|
+
*
|
|
10
|
+
* See features/bulk-row-enrichment.md.
|
|
11
|
+
*/
|
|
12
|
+
|
|
13
|
+
import type { StepPostAction } from "./types";
|
|
14
|
+
|
|
15
|
+
/**
|
|
16
|
+
* Substitute `{{itemVariable.field}}` placeholders in a postAction definition
|
|
17
|
+
* against the current loop iteration's row. Supports nested paths via dotted
|
|
18
|
+
* field names (e.g. `{{row.meta.id}}`). Only `rowId` is templated today —
|
|
19
|
+
* `tableId` and `column` are static, and templating them would invite SQL
|
|
20
|
+
* surprises.
|
|
21
|
+
*/
|
|
22
|
+
export function resolvePostAction(
|
|
23
|
+
action: StepPostAction,
|
|
24
|
+
row: unknown,
|
|
25
|
+
itemVariable: string
|
|
26
|
+
): StepPostAction {
|
|
27
|
+
return {
|
|
28
|
+
...action,
|
|
29
|
+
rowId: substituteRowPath(action.rowId, row, itemVariable),
|
|
30
|
+
};
|
|
31
|
+
}
|
|
32
|
+
|
|
33
|
+
/**
|
|
34
|
+
* Replace `{{itemVariable.path.to.field}}` with the value at that path on row.
|
|
35
|
+
* Multiple placeholders in the same string are all replaced. Missing paths
|
|
36
|
+
* resolve to an empty string (caller should validate the result).
|
|
37
|
+
*/
|
|
38
|
+
function substituteRowPath(
|
|
39
|
+
template: string,
|
|
40
|
+
row: unknown,
|
|
41
|
+
itemVariable: string
|
|
42
|
+
): string {
|
|
43
|
+
// Match {{<itemVariable>.<dotted.path>}} — escape itemVariable for safety
|
|
44
|
+
const escaped = itemVariable.replace(/[.*+?^${}()|[\]\\]/g, "\\$&");
|
|
45
|
+
const pattern = new RegExp(`\\{\\{\\s*${escaped}\\.([\\w.]+)\\s*\\}\\}`, "g");
|
|
46
|
+
|
|
47
|
+
return template.replace(pattern, (_match, path: string) => {
|
|
48
|
+
const value = readPath(row, path);
|
|
49
|
+
if (value === undefined || value === null) return "";
|
|
50
|
+
return String(value);
|
|
51
|
+
});
|
|
52
|
+
}
|
|
53
|
+
|
|
54
|
+
function readPath(obj: unknown, path: string): unknown {
|
|
55
|
+
const parts = path.split(".");
|
|
56
|
+
let current: unknown = obj;
|
|
57
|
+
for (const part of parts) {
|
|
58
|
+
if (current === null || current === undefined) return undefined;
|
|
59
|
+
if (typeof current !== "object") return undefined;
|
|
60
|
+
current = (current as Record<string, unknown>)[part];
|
|
61
|
+
}
|
|
62
|
+
return current;
|
|
63
|
+
}
|
|
64
|
+
|
|
65
|
+
/**
|
|
66
|
+
* Decide whether the agent's result should be written back to the cell, or
|
|
67
|
+
* skipped silently. We skip empty/whitespace-only results and the literal
|
|
68
|
+
* `NOT_FOUND` sentinel (case-insensitive) so an enrichment workflow can
|
|
69
|
+
* gracefully say "no value for this row" without overwriting a real value
|
|
70
|
+
* with garbage.
|
|
71
|
+
*
|
|
72
|
+
* Substring matches are intentionally NOT skipped — only the trimmed value
|
|
73
|
+
* being exactly `NOT_FOUND` triggers the skip. This avoids dropping a long
|
|
74
|
+
* answer that happens to mention the sentinel.
|
|
75
|
+
*/
|
|
76
|
+
export function shouldSkipPostActionValue(value: string): boolean {
|
|
77
|
+
const trimmed = value.trim();
|
|
78
|
+
if (trimmed === "") return true;
|
|
79
|
+
if (trimmed.toUpperCase() === "NOT_FOUND") return true;
|
|
80
|
+
return false;
|
|
81
|
+
}
|
|
82
|
+
|
|
83
|
+
/**
|
|
84
|
+
* Pull the writable value out of a task result. Trims whitespace and tolerates
|
|
85
|
+
* undefined/null without throwing. The caller should run the result through
|
|
86
|
+
* `shouldSkipPostActionValue` before writing.
|
|
87
|
+
*/
|
|
88
|
+
export function extractPostActionValue(result: string | undefined | null): string {
|
|
89
|
+
if (result === undefined || result === null) return "";
|
|
90
|
+
return result.trim();
|
|
91
|
+
}
|