@plumbus/core 0.2.8 → 0.3.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/dist/.tsbuildinfo +1 -1
- package/dist/ai/__tests__/ai-service.test.js +704 -1
- package/dist/ai/__tests__/ai-service.test.js.map +1 -1
- package/dist/ai/__tests__/chunking.test.js +34 -0
- package/dist/ai/__tests__/chunking.test.js.map +1 -1
- package/dist/ai/__tests__/provider.test.d.ts.map +1 -1
- package/dist/ai/__tests__/provider.test.js +482 -4
- package/dist/ai/__tests__/provider.test.js.map +1 -1
- package/dist/ai/__tests__/validation.test.js +238 -1
- package/dist/ai/__tests__/validation.test.js.map +1 -1
- package/dist/ai/__tests__/zod-to-provider-schema.test.d.ts +2 -0
- package/dist/ai/__tests__/zod-to-provider-schema.test.d.ts.map +1 -0
- package/dist/ai/__tests__/zod-to-provider-schema.test.js +60 -0
- package/dist/ai/__tests__/zod-to-provider-schema.test.js.map +1 -0
- package/dist/ai/ai-service.d.ts +19 -2
- package/dist/ai/ai-service.d.ts.map +1 -1
- package/dist/ai/ai-service.js +565 -90
- package/dist/ai/ai-service.js.map +1 -1
- package/dist/ai/cost-tracker.d.ts +26 -1
- package/dist/ai/cost-tracker.d.ts.map +1 -1
- package/dist/ai/cost-tracker.js +1 -0
- package/dist/ai/cost-tracker.js.map +1 -1
- package/dist/ai/index.d.ts +5 -3
- package/dist/ai/index.d.ts.map +1 -1
- package/dist/ai/index.js +5 -1
- package/dist/ai/index.js.map +1 -1
- package/dist/ai/provider.d.ts +59 -1
- package/dist/ai/provider.d.ts.map +1 -1
- package/dist/ai/provider.js +388 -90
- package/dist/ai/provider.js.map +1 -1
- package/dist/ai/rag/chunking.js +53 -3
- package/dist/ai/rag/chunking.js.map +1 -1
- package/dist/ai/refusal.d.ts +28 -0
- package/dist/ai/refusal.d.ts.map +1 -0
- package/dist/ai/refusal.js +36 -0
- package/dist/ai/refusal.js.map +1 -0
- package/dist/ai/validation.d.ts +40 -8
- package/dist/ai/validation.d.ts.map +1 -1
- package/dist/ai/validation.js +212 -6
- package/dist/ai/validation.js.map +1 -1
- package/dist/ai/zod-to-provider-schema.d.ts +15 -0
- package/dist/ai/zod-to-provider-schema.d.ts.map +1 -0
- package/dist/ai/zod-to-provider-schema.js +200 -0
- package/dist/ai/zod-to-provider-schema.js.map +1 -0
- package/dist/api/__tests__/route-generator.test.js +3 -0
- package/dist/api/__tests__/route-generator.test.js.map +1 -1
- package/dist/api/route-generator.d.ts +4 -0
- package/dist/api/route-generator.d.ts.map +1 -1
- package/dist/api/route-generator.js +1 -0
- package/dist/api/route-generator.js.map +1 -1
- package/dist/cli/__tests__/doctor.test.js +104 -2
- package/dist/cli/__tests__/doctor.test.js.map +1 -1
- package/dist/cli/__tests__/init.test.js +127 -7
- package/dist/cli/__tests__/init.test.js.map +1 -1
- package/dist/cli/__tests__/migrate.test.js +159 -2
- package/dist/cli/__tests__/migrate.test.js.map +1 -1
- package/dist/cli/commands/__tests__/commands.test.js +5 -0
- package/dist/cli/commands/__tests__/commands.test.js.map +1 -1
- package/dist/cli/commands/dev.d.ts.map +1 -1
- package/dist/cli/commands/dev.js +22 -1
- package/dist/cli/commands/dev.js.map +1 -1
- package/dist/cli/commands/doctor.d.ts +2 -0
- package/dist/cli/commands/doctor.d.ts.map +1 -1
- package/dist/cli/commands/doctor.js +78 -0
- package/dist/cli/commands/doctor.js.map +1 -1
- package/dist/cli/commands/generate.d.ts.map +1 -1
- package/dist/cli/commands/generate.js +8 -3
- package/dist/cli/commands/generate.js.map +1 -1
- package/dist/cli/commands/init.d.ts +16 -2
- package/dist/cli/commands/init.d.ts.map +1 -1
- package/dist/cli/commands/init.js +226 -25
- package/dist/cli/commands/init.js.map +1 -1
- package/dist/cli/commands/migrate.d.ts.map +1 -1
- package/dist/cli/commands/migrate.js +258 -8
- package/dist/cli/commands/migrate.js.map +1 -1
- package/dist/cli/commands/start.d.ts.map +1 -1
- package/dist/cli/commands/start.js +22 -1
- package/dist/cli/commands/start.js.map +1 -1
- package/dist/config/loader.d.ts.map +1 -1
- package/dist/config/loader.js +3 -0
- package/dist/config/loader.js.map +1 -1
- package/dist/data/__tests__/drift-inspector.test.d.ts +2 -0
- package/dist/data/__tests__/drift-inspector.test.d.ts.map +1 -0
- package/dist/data/__tests__/drift-inspector.test.js +170 -0
- package/dist/data/__tests__/drift-inspector.test.js.map +1 -0
- package/dist/data/__tests__/framework-migration-rewriter.test.d.ts +2 -0
- package/dist/data/__tests__/framework-migration-rewriter.test.d.ts.map +1 -0
- package/dist/data/__tests__/framework-migration-rewriter.test.js +329 -0
- package/dist/data/__tests__/framework-migration-rewriter.test.js.map +1 -0
- package/dist/data/__tests__/migration.test.d.ts +2 -0
- package/dist/data/__tests__/migration.test.d.ts.map +1 -0
- package/dist/data/__tests__/migration.test.js +143 -0
- package/dist/data/__tests__/migration.test.js.map +1 -0
- package/dist/data/__tests__/repository.test.js +126 -0
- package/dist/data/__tests__/repository.test.js.map +1 -1
- package/dist/data/drift-inspector.d.ts +41 -0
- package/dist/data/drift-inspector.d.ts.map +1 -0
- package/dist/data/drift-inspector.js +197 -0
- package/dist/data/drift-inspector.js.map +1 -0
- package/dist/data/framework-migration-rewriter.d.ts +61 -0
- package/dist/data/framework-migration-rewriter.d.ts.map +1 -0
- package/dist/data/framework-migration-rewriter.js +357 -0
- package/dist/data/framework-migration-rewriter.js.map +1 -0
- package/dist/data/index.d.ts +2 -1
- package/dist/data/index.d.ts.map +1 -1
- package/dist/data/index.js +2 -1
- package/dist/data/index.js.map +1 -1
- package/dist/data/migration.d.ts +31 -1
- package/dist/data/migration.d.ts.map +1 -1
- package/dist/data/migration.js +127 -3
- package/dist/data/migration.js.map +1 -1
- package/dist/data/repository.d.ts.map +1 -1
- package/dist/data/repository.js +21 -0
- package/dist/data/repository.js.map +1 -1
- package/dist/define/__tests__/definePrompt.test.js +37 -0
- package/dist/define/__tests__/definePrompt.test.js.map +1 -1
- package/dist/define/definePrompt.d.ts +13 -0
- package/dist/define/definePrompt.d.ts.map +1 -1
- package/dist/define/definePrompt.js +3 -0
- package/dist/define/definePrompt.js.map +1 -1
- package/dist/errors/__tests__/http.test.js +3 -0
- package/dist/errors/__tests__/http.test.js.map +1 -1
- package/dist/errors/http.d.ts.map +1 -1
- package/dist/errors/http.js +14 -0
- package/dist/errors/http.js.map +1 -1
- package/dist/errors/index.d.ts +12 -0
- package/dist/errors/index.d.ts.map +1 -1
- package/dist/errors/index.js +21 -0
- package/dist/errors/index.js.map +1 -1
- package/dist/events/__tests__/event-emitter.test.js +72 -0
- package/dist/events/__tests__/event-emitter.test.js.map +1 -1
- package/dist/events/emitter.d.ts.map +1 -1
- package/dist/events/emitter.js +51 -0
- package/dist/events/emitter.js.map +1 -1
- package/dist/execution/__tests__/capability-executor.test.js +28 -0
- package/dist/execution/__tests__/capability-executor.test.js.map +1 -1
- package/dist/execution/__tests__/context-factory.test.js +1 -0
- package/dist/execution/__tests__/context-factory.test.js.map +1 -1
- package/dist/execution/capability-executor.d.ts.map +1 -1
- package/dist/execution/capability-executor.js +21 -0
- package/dist/execution/capability-executor.js.map +1 -1
- package/dist/execution/context-factory.d.ts.map +1 -1
- package/dist/execution/context-factory.js +2 -0
- package/dist/execution/context-factory.js.map +1 -1
- package/dist/flows/__tests__/flow-engine.test.js +535 -3
- package/dist/flows/__tests__/flow-engine.test.js.map +1 -1
- package/dist/flows/__tests__/flow-service.test.js +8 -0
- package/dist/flows/__tests__/flow-service.test.js.map +1 -1
- package/dist/flows/__tests__/step-executor.test.js +14 -4
- package/dist/flows/__tests__/step-executor.test.js.map +1 -1
- package/dist/flows/engine.d.ts +53 -1
- package/dist/flows/engine.d.ts.map +1 -1
- package/dist/flows/engine.js +462 -25
- package/dist/flows/engine.js.map +1 -1
- package/dist/flows/flow-service.d.ts.map +1 -1
- package/dist/flows/flow-service.js +4 -0
- package/dist/flows/flow-service.js.map +1 -1
- package/dist/flows/index.d.ts +1 -1
- package/dist/flows/index.d.ts.map +1 -1
- package/dist/flows/index.js +1 -1
- package/dist/flows/index.js.map +1 -1
- package/dist/flows/schema.d.ts +34 -0
- package/dist/flows/schema.d.ts.map +1 -1
- package/dist/flows/schema.js +3 -0
- package/dist/flows/schema.js.map +1 -1
- package/dist/flows/step-executor.d.ts +2 -0
- package/dist/flows/step-executor.d.ts.map +1 -1
- package/dist/flows/step-executor.js +6 -6
- package/dist/flows/step-executor.js.map +1 -1
- package/dist/index.d.ts +4 -4
- package/dist/index.d.ts.map +1 -1
- package/dist/index.js +3 -3
- package/dist/index.js.map +1 -1
- package/dist/server/__tests__/bootstrap.test.js +37 -0
- package/dist/server/__tests__/bootstrap.test.js.map +1 -1
- package/dist/server/bootstrap.d.ts +19 -0
- package/dist/server/bootstrap.d.ts.map +1 -1
- package/dist/server/bootstrap.js +48 -4
- package/dist/server/bootstrap.js.map +1 -1
- package/dist/testing/__tests__/simulate-flow.test.js +24 -2
- package/dist/testing/__tests__/simulate-flow.test.js.map +1 -1
- package/dist/testing/context.d.ts.map +1 -1
- package/dist/testing/context.js +37 -1
- package/dist/testing/context.js.map +1 -1
- package/dist/testing/simulate-flow.d.ts.map +1 -1
- package/dist/testing/simulate-flow.js +25 -1
- package/dist/testing/simulate-flow.js.map +1 -1
- package/dist/types/context.d.ts +118 -0
- package/dist/types/context.d.ts.map +1 -1
- package/dist/types/enums.d.ts +2 -0
- package/dist/types/enums.d.ts.map +1 -1
- package/dist/types/enums.js +2 -0
- package/dist/types/enums.js.map +1 -1
- package/dist/types/prompt.d.ts +64 -0
- package/dist/types/prompt.d.ts.map +1 -1
- package/dist/worker/__tests__/bootstrap.test.js +36 -2
- package/dist/worker/__tests__/bootstrap.test.js.map +1 -1
- package/dist/worker/bootstrap.d.ts +7 -0
- package/dist/worker/bootstrap.d.ts.map +1 -1
- package/dist/worker/bootstrap.js +164 -14
- package/dist/worker/bootstrap.js.map +1 -1
- package/instructions/framework.md +7 -0
- package/instructions/guardrails.md +39 -0
- package/instructions/patterns.md +10 -0
- package/package.json +3 -2
package/dist/flows/engine.js
CHANGED
|
@@ -1,15 +1,64 @@
|
|
|
1
|
-
import { and, eq, inArray, isNull, lte, or } from 'drizzle-orm';
|
|
1
|
+
import { and, eq, inArray, isNull, lte, or, sql } from 'drizzle-orm';
|
|
2
2
|
import { randomUUID } from 'node:crypto';
|
|
3
|
+
import { hostname } from 'node:os';
|
|
4
|
+
import { FlowCancelledError, LeaseLostError } from '../errors/index.js';
|
|
3
5
|
import { BackoffStrategy } from '../types/enums.js';
|
|
4
6
|
import { flowExecutionsTable } from './schema.js';
|
|
5
7
|
import { assertTransition, FlowStatus, isTerminal, StepStatus, } from './state-machine.js';
|
|
6
|
-
import { buildHistoryEntry, executeStep } from './step-executor.js';
|
|
8
|
+
import { buildHistoryEntry, executeStep, } from './step-executor.js';
|
|
9
|
+
/** Default lease duration: 5 minutes */
|
|
10
|
+
const DEFAULT_LEASE_DURATION_MS = 300_000;
|
|
11
|
+
/** Default claim batch size */
|
|
12
|
+
const DEFAULT_CLAIM_BATCH_SIZE = 50;
|
|
13
|
+
/**
|
|
14
|
+
* Read the affected-row count from a Drizzle UPDATE/INSERT/DELETE result in a
|
|
15
|
+
* driver-agnostic way. postgres-js exposes this as `.count` (the array-like
|
|
16
|
+
* `Result` has `length === 0` when no RETURNING clause is used), while
|
|
17
|
+
* node-postgres exposes it as `.rowCount`. Returning `length` is only a
|
|
18
|
+
* correct fallback for queries that include `.returning(...)`.
|
|
19
|
+
*
|
|
20
|
+
* Historical bug: reading only `rowCount ?? length` silently returned 0 for
|
|
21
|
+
* every postgres-js UPDATE without RETURNING, which caused `extendLease` to
|
|
22
|
+
* always report "lease lost" even when the row was successfully updated.
|
|
23
|
+
*/
|
|
24
|
+
function getRowsAffected(result) {
|
|
25
|
+
const r = result;
|
|
26
|
+
if (typeof r?.rowCount === 'number')
|
|
27
|
+
return r.rowCount;
|
|
28
|
+
if (typeof r?.count === 'number')
|
|
29
|
+
return r.count;
|
|
30
|
+
if (typeof r?.length === 'number')
|
|
31
|
+
return r.length;
|
|
32
|
+
return 0;
|
|
33
|
+
}
|
|
34
|
+
/**
|
|
35
|
+
* Generate a deterministic-ish worker id for the current process.
|
|
36
|
+
*/
|
|
37
|
+
export function generateWorkerId() {
|
|
38
|
+
return `${hostname()}:${process.pid}:${randomUUID().slice(0, 8)}`;
|
|
39
|
+
}
|
|
7
40
|
/**
|
|
8
41
|
* Creates the flow execution engine that manages flow lifecycle:
|
|
9
42
|
* start, run steps, handle wait/resume, and persist state.
|
|
43
|
+
*
|
|
44
|
+
* Supports lease-based claiming to prevent duplicate step execution
|
|
45
|
+
* across multiple workers sharing the same database.
|
|
10
46
|
*/
|
|
11
47
|
export function createFlowEngine(config) {
|
|
12
|
-
const { db, registry, stepDeps, audit, onFlowError } = config;
|
|
48
|
+
const { db, registry, stepDeps, audit, onFlowError, createEventService } = config;
|
|
49
|
+
const workerId = config.workerId ?? generateWorkerId();
|
|
50
|
+
const leaseDurationMs = config.flowLeaseDurationMs ?? DEFAULT_LEASE_DURATION_MS;
|
|
51
|
+
const heartbeatIntervalMs = config.flowHeartbeatIntervalMs ?? Math.floor(leaseDurationMs / 3);
|
|
52
|
+
const claimBatchSize = config.flowClaimBatchSize ?? DEFAULT_CLAIM_BATCH_SIZE;
|
|
53
|
+
const logger = config.logger;
|
|
54
|
+
/**
|
|
55
|
+
* AbortControllers for currently-executing steps in THIS engine instance,
|
|
56
|
+
* keyed by executionId. `cancel()` uses this to abort the running step
|
|
57
|
+
* synchronously when the cancel request arrives in the same process as
|
|
58
|
+
* the worker. Cross-process cancels still converge via the next heartbeat
|
|
59
|
+
* (which now also fails when status != 'running').
|
|
60
|
+
*/
|
|
61
|
+
const activeStepAborts = new Map();
|
|
13
62
|
/**
|
|
14
63
|
* Start a new flow execution.
|
|
15
64
|
*/
|
|
@@ -24,7 +73,12 @@ export function createFlowEngine(config) {
|
|
|
24
73
|
throw new Error(`Flow "${flowName}": invalid input — ${parseResult.error.message}`);
|
|
25
74
|
}
|
|
26
75
|
const executionId = randomUUID();
|
|
27
|
-
const initialState = flow.state
|
|
76
|
+
const initialState = flow.state
|
|
77
|
+
? (() => {
|
|
78
|
+
const parsed = flow.state.safeParse({});
|
|
79
|
+
return parsed.success ? parsed.data : {};
|
|
80
|
+
})()
|
|
81
|
+
: null;
|
|
28
82
|
await db.insert(flowExecutionsTable).values({
|
|
29
83
|
id: executionId,
|
|
30
84
|
flowName,
|
|
@@ -54,9 +108,125 @@ export function createFlowEngine(config) {
|
|
|
54
108
|
status: FlowStatus.Created,
|
|
55
109
|
};
|
|
56
110
|
}
|
|
111
|
+
/**
|
|
112
|
+
* Atomically claim up to `batchSize` runnable flow executions.
|
|
113
|
+
* Uses FOR UPDATE SKIP LOCKED so concurrent workers each get different rows.
|
|
114
|
+
* Also reclaims rows whose lease has expired (crash recovery).
|
|
115
|
+
*/
|
|
116
|
+
async function claimNext(batchSize) {
|
|
117
|
+
const limit = batchSize ?? claimBatchSize;
|
|
118
|
+
const leaseDurationInterval = `${leaseDurationMs} milliseconds`;
|
|
119
|
+
const result = await db.execute(sql `
|
|
120
|
+
UPDATE flow_executions
|
|
121
|
+
SET status = ${FlowStatus.Running},
|
|
122
|
+
lease_owner = ${workerId},
|
|
123
|
+
lease_expires_at = now() + ${leaseDurationInterval}::interval,
|
|
124
|
+
updated_at = now()
|
|
125
|
+
WHERE id IN (
|
|
126
|
+
SELECT id
|
|
127
|
+
FROM flow_executions
|
|
128
|
+
WHERE
|
|
129
|
+
status = ${FlowStatus.Created}
|
|
130
|
+
OR (status = ${FlowStatus.Running} AND lease_expires_at < now())
|
|
131
|
+
OR (status = ${FlowStatus.Waiting} AND waiting_for_event IS NULL AND wake_at <= now())
|
|
132
|
+
ORDER BY created_at
|
|
133
|
+
FOR UPDATE SKIP LOCKED
|
|
134
|
+
LIMIT ${limit}
|
|
135
|
+
)
|
|
136
|
+
RETURNING *
|
|
137
|
+
`);
|
|
138
|
+
const rows = result;
|
|
139
|
+
for (const row of rows) {
|
|
140
|
+
if (audit) {
|
|
141
|
+
const wasExpired = row.status === FlowStatus.Running;
|
|
142
|
+
if (wasExpired) {
|
|
143
|
+
await audit.record('flow.lease.expired', {
|
|
144
|
+
executionId: row.id,
|
|
145
|
+
flowName: row.flowName,
|
|
146
|
+
workerId,
|
|
147
|
+
previousOwner: row.leaseOwner,
|
|
148
|
+
step: row.currentStep,
|
|
149
|
+
});
|
|
150
|
+
}
|
|
151
|
+
await audit.record('flow.lease.claimed', {
|
|
152
|
+
executionId: row.id,
|
|
153
|
+
flowName: row.flowName,
|
|
154
|
+
workerId,
|
|
155
|
+
step: row.currentStep,
|
|
156
|
+
});
|
|
157
|
+
}
|
|
158
|
+
}
|
|
159
|
+
return rows;
|
|
160
|
+
}
|
|
161
|
+
/**
|
|
162
|
+
* Extend the lease for a flow execution.
|
|
163
|
+
* Returns true if the lease was extended, false if the lease was lost.
|
|
164
|
+
*
|
|
165
|
+
* The WHERE clause also requires `status = 'running'`. If another actor
|
|
166
|
+
* (e.g. `cancel()`) has flipped the status to `cancelled`/`failed`, the
|
|
167
|
+
* UPDATE matches 0 rows and we take the same "lease lost" path — which
|
|
168
|
+
* the heartbeat uses to abort the per-step AbortController so the running
|
|
169
|
+
* capability can cooperatively stop its in-flight AI / HTTP work.
|
|
170
|
+
*/
|
|
171
|
+
async function extendLease(executionId) {
|
|
172
|
+
const leaseDurationInterval = `${leaseDurationMs} milliseconds`;
|
|
173
|
+
const result = await db
|
|
174
|
+
.update(flowExecutionsTable)
|
|
175
|
+
.set({
|
|
176
|
+
leaseExpiresAt: sql `now() + ${leaseDurationInterval}::interval`,
|
|
177
|
+
updatedAt: new Date(),
|
|
178
|
+
})
|
|
179
|
+
.where(and(eq(flowExecutionsTable.id, executionId), eq(flowExecutionsTable.leaseOwner, workerId), eq(flowExecutionsTable.status, FlowStatus.Running)));
|
|
180
|
+
const rowsAffected = getRowsAffected(result);
|
|
181
|
+
if (rowsAffected === 0 && logger) {
|
|
182
|
+
// D1 diagnostic: the heartbeat's status+owner guard didn't match, which
|
|
183
|
+
// means something mutated the row out from under us (status flipped by
|
|
184
|
+
// cancel/complete/failure, or a different worker stole the lease). Read
|
|
185
|
+
// the row back and log its actual shape plus our own identity so we can
|
|
186
|
+
// tell at a glance which dimension diverged. Best-effort: a failure
|
|
187
|
+
// here must not break the heartbeat.
|
|
188
|
+
try {
|
|
189
|
+
const rows = await db
|
|
190
|
+
.select({
|
|
191
|
+
status: flowExecutionsTable.status,
|
|
192
|
+
leaseOwner: flowExecutionsTable.leaseOwner,
|
|
193
|
+
leaseExpiresAt: flowExecutionsTable.leaseExpiresAt,
|
|
194
|
+
updatedAt: flowExecutionsTable.updatedAt,
|
|
195
|
+
currentStep: flowExecutionsTable.currentStep,
|
|
196
|
+
})
|
|
197
|
+
.from(flowExecutionsTable)
|
|
198
|
+
.where(eq(flowExecutionsTable.id, executionId))
|
|
199
|
+
.limit(1);
|
|
200
|
+
const row = rows[0];
|
|
201
|
+
logger.warn('extendLease matched 0 rows', {
|
|
202
|
+
executionId,
|
|
203
|
+
expectedWorkerId: workerId,
|
|
204
|
+
expectedStatus: FlowStatus.Running,
|
|
205
|
+
actual: row
|
|
206
|
+
? {
|
|
207
|
+
status: row.status,
|
|
208
|
+
leaseOwner: row.leaseOwner,
|
|
209
|
+
leaseExpiresAt: row.leaseExpiresAt?.toISOString() ?? null,
|
|
210
|
+
updatedAt: row.updatedAt?.toISOString() ?? null,
|
|
211
|
+
currentStep: row.currentStep,
|
|
212
|
+
}
|
|
213
|
+
: null,
|
|
214
|
+
nowMs: Date.now(),
|
|
215
|
+
});
|
|
216
|
+
}
|
|
217
|
+
catch (diagErr) {
|
|
218
|
+
logger.warn('extendLease diagnostic SELECT failed', {
|
|
219
|
+
executionId,
|
|
220
|
+
error: diagErr instanceof Error ? diagErr.message : String(diagErr),
|
|
221
|
+
});
|
|
222
|
+
}
|
|
223
|
+
}
|
|
224
|
+
return rowsAffected > 0;
|
|
225
|
+
}
|
|
57
226
|
/**
|
|
58
227
|
* Run the next step(s) for a flow execution.
|
|
59
228
|
* This is designed to be called by a worker process.
|
|
229
|
+
* The execution must have been claimed by this worker (via claimNext) first.
|
|
60
230
|
*/
|
|
61
231
|
async function runNext(executionId, ctx) {
|
|
62
232
|
const rows = await db
|
|
@@ -82,13 +252,15 @@ export function createFlowEngine(config) {
|
|
|
82
252
|
if (row.status === FlowStatus.Waiting && row.wakeAt && row.wakeAt.getTime() > Date.now()) {
|
|
83
253
|
return { id: row.id, flowName: row.flowName, status: FlowStatus.Waiting };
|
|
84
254
|
}
|
|
85
|
-
// Transition to running
|
|
255
|
+
// Transition to running (only if not already claimed via claimNext)
|
|
86
256
|
if (row.status === FlowStatus.Created || row.status === FlowStatus.Waiting) {
|
|
87
257
|
assertTransition(row.status, FlowStatus.Running);
|
|
88
|
-
await
|
|
258
|
+
await guardedUpdate(executionId, {
|
|
89
259
|
status: FlowStatus.Running,
|
|
90
260
|
waitingForEvent: null,
|
|
91
261
|
wakeAt: null,
|
|
262
|
+
leaseOwner: workerId,
|
|
263
|
+
leaseExpiresAt: new Date(Date.now() + leaseDurationMs),
|
|
92
264
|
});
|
|
93
265
|
}
|
|
94
266
|
// Find current step
|
|
@@ -111,21 +283,126 @@ export function createFlowEngine(config) {
|
|
|
111
283
|
internal: true,
|
|
112
284
|
};
|
|
113
285
|
const flowData = config.createDataService ? config.createDataService(flowAuth) : ctx.data;
|
|
286
|
+
const flowEvents = createEventService ? createEventService(flowAuth) : ctx.events;
|
|
287
|
+
// Create a heartbeat function for manual use by step handlers
|
|
288
|
+
const heartbeatFn = async () => {
|
|
289
|
+
const extended = await extendLease(executionId);
|
|
290
|
+
if (!extended) {
|
|
291
|
+
throw new LeaseLostError(`Lease lost for flow execution "${executionId}" (worker: ${workerId})`, { executionId, workerId, step: currentStepName });
|
|
292
|
+
}
|
|
293
|
+
};
|
|
294
|
+
// Per-step AbortController: aborted when the lease is lost (either
|
|
295
|
+
// because another worker claimed the execution OR because cancel() flipped
|
|
296
|
+
// the status out of 'running'). Capability handlers can subscribe via
|
|
297
|
+
// `ctx.signal` and pass it to AI/HTTP calls to stop cooperatively rather
|
|
298
|
+
// than continuing to burn budget as a zombie.
|
|
299
|
+
const stepAc = new AbortController();
|
|
300
|
+
activeStepAborts.set(executionId, stepAc);
|
|
301
|
+
// Auto-thread `signal` into AI calls so capability authors don't have to
|
|
302
|
+
// pass it explicitly. An explicit `signal` in the call overrides the
|
|
303
|
+
// step default. See AIService type for the per-method signal parameter.
|
|
304
|
+
const flowAi = wrapAiWithDefaultSignal(ctx.ai, stepAc.signal);
|
|
114
305
|
const flowCtx = {
|
|
115
306
|
...ctx,
|
|
116
307
|
auth: flowAuth,
|
|
117
308
|
data: flowData,
|
|
309
|
+
events: flowEvents,
|
|
310
|
+
ai: flowAi,
|
|
118
311
|
state: row.state,
|
|
119
312
|
step: currentStepName,
|
|
120
313
|
flowId: executionId,
|
|
314
|
+
workerId,
|
|
315
|
+
signal: stepAc.signal,
|
|
316
|
+
flows: {
|
|
317
|
+
...ctx.flows,
|
|
318
|
+
heartbeat: heartbeatFn,
|
|
319
|
+
},
|
|
121
320
|
};
|
|
321
|
+
// Start automatic heartbeat timer
|
|
322
|
+
let leaseLost = false;
|
|
323
|
+
let heartbeatTickCount = 0;
|
|
324
|
+
const heartbeatStartedAtMs = Date.now();
|
|
325
|
+
const heartbeatTimer = setInterval(async () => {
|
|
326
|
+
heartbeatTickCount += 1;
|
|
327
|
+
const tick = heartbeatTickCount;
|
|
328
|
+
const firedAtMs = Date.now();
|
|
329
|
+
const expectedFireAtMs = heartbeatStartedAtMs + tick * heartbeatIntervalMs;
|
|
330
|
+
const driftMs = firedAtMs - expectedFireAtMs;
|
|
331
|
+
// D2 diagnostic: emit one log line per heartbeat tick so we can see
|
|
332
|
+
// whether ticks are firing on schedule (event-loop starvation shows up
|
|
333
|
+
// here as large positive `driftMs`). First tick is logged at `info`
|
|
334
|
+
// because that's typically enough for a post-mortem; subsequent ticks
|
|
335
|
+
// at `debug` to avoid log spam for long-running flows.
|
|
336
|
+
if (logger) {
|
|
337
|
+
const payload = {
|
|
338
|
+
executionId,
|
|
339
|
+
workerId,
|
|
340
|
+
step: currentStepName,
|
|
341
|
+
tick,
|
|
342
|
+
firedAtMs,
|
|
343
|
+
expectedFireAtMs,
|
|
344
|
+
driftMs,
|
|
345
|
+
heartbeatIntervalMs,
|
|
346
|
+
};
|
|
347
|
+
if (tick === 1) {
|
|
348
|
+
logger.info('flow.heartbeat.tick', payload);
|
|
349
|
+
}
|
|
350
|
+
else {
|
|
351
|
+
logger.debug('flow.heartbeat.tick', payload);
|
|
352
|
+
}
|
|
353
|
+
}
|
|
354
|
+
const extended = await extendLease(executionId);
|
|
355
|
+
if (extended) {
|
|
356
|
+
if (audit) {
|
|
357
|
+
await audit.record('flow.lease.extended', {
|
|
358
|
+
executionId,
|
|
359
|
+
flowName: row.flowName,
|
|
360
|
+
workerId,
|
|
361
|
+
step: currentStepName,
|
|
362
|
+
});
|
|
363
|
+
}
|
|
364
|
+
}
|
|
365
|
+
else {
|
|
366
|
+
leaseLost = true;
|
|
367
|
+
if (!stepAc.signal.aborted) {
|
|
368
|
+
stepAc.abort(new LeaseLostError(`Lease lost for flow execution "${executionId}" (worker: ${workerId})`, { executionId, workerId, step: currentStepName }));
|
|
369
|
+
}
|
|
370
|
+
if (audit) {
|
|
371
|
+
await audit.record('flow.lease.lost', {
|
|
372
|
+
executionId,
|
|
373
|
+
flowName: row.flowName,
|
|
374
|
+
workerId,
|
|
375
|
+
step: currentStepName,
|
|
376
|
+
});
|
|
377
|
+
}
|
|
378
|
+
}
|
|
379
|
+
}, heartbeatIntervalMs);
|
|
122
380
|
// Execute the step
|
|
381
|
+
let result;
|
|
123
382
|
const startedAt = new Date();
|
|
124
|
-
|
|
383
|
+
try {
|
|
384
|
+
result = await executeStep(step, flowCtx, row.input, row.state, stepDeps);
|
|
385
|
+
}
|
|
386
|
+
finally {
|
|
387
|
+
clearInterval(heartbeatTimer);
|
|
388
|
+
// Abort any stray detached tasks once the step settles so they observe
|
|
389
|
+
// the abort promptly. If already aborted (lease loss), this is a no-op.
|
|
390
|
+
if (!stepAc.signal.aborted) {
|
|
391
|
+
stepAc.abort();
|
|
392
|
+
}
|
|
393
|
+
if (activeStepAborts.get(executionId) === stepAc) {
|
|
394
|
+
activeStepAborts.delete(executionId);
|
|
395
|
+
}
|
|
396
|
+
}
|
|
125
397
|
const completedAt = new Date();
|
|
398
|
+
// Check if lease was lost during step execution
|
|
399
|
+
if (leaseLost) {
|
|
400
|
+
throw new LeaseLostError(`Lease lost during step "${currentStepName}" of flow execution "${executionId}"`, { executionId, workerId, step: currentStepName });
|
|
401
|
+
}
|
|
126
402
|
const historyEntry = buildHistoryEntry(currentStepName, result, startedAt, completedAt);
|
|
127
403
|
const history = Array.isArray(row.stepHistory) ? row.stepHistory : [];
|
|
128
404
|
history.push(historyEntry);
|
|
405
|
+
const nextState = mergeExecutionState(row.state, result.data);
|
|
129
406
|
if (audit) {
|
|
130
407
|
await audit.record(`flow.step.${result.status}.${currentStepName}`, {
|
|
131
408
|
executionId,
|
|
@@ -141,13 +418,23 @@ export function createFlowEngine(config) {
|
|
|
141
418
|
}
|
|
142
419
|
// Wait step — pause the flow
|
|
143
420
|
if (result.waitEvent) {
|
|
144
|
-
await
|
|
421
|
+
const ok = await guardedUpdate(executionId, {
|
|
145
422
|
status: FlowStatus.Waiting,
|
|
423
|
+
state: nextState,
|
|
146
424
|
stepHistory: history,
|
|
147
425
|
currentStep: currentStepName, // stay on current step until resumed
|
|
148
426
|
waitingForEvent: result.waitEvent,
|
|
149
427
|
wakeAt: null,
|
|
428
|
+
leaseOwner: null,
|
|
429
|
+
leaseExpiresAt: null,
|
|
150
430
|
});
|
|
431
|
+
if (!ok) {
|
|
432
|
+
throw new LeaseLostError(`Lease lost committing wait for flow execution "${executionId}"`, {
|
|
433
|
+
executionId,
|
|
434
|
+
workerId,
|
|
435
|
+
step: currentStepName,
|
|
436
|
+
});
|
|
437
|
+
}
|
|
151
438
|
return { id: executionId, flowName: row.flowName, status: FlowStatus.Waiting };
|
|
152
439
|
}
|
|
153
440
|
// Delay step — schedule next step after duration
|
|
@@ -160,17 +447,21 @@ export function createFlowEngine(config) {
|
|
|
160
447
|
catch (err) {
|
|
161
448
|
return handleStepFailure(executionId, row, flow, history, err instanceof Error ? err.message : String(err));
|
|
162
449
|
}
|
|
163
|
-
await
|
|
450
|
+
await guardedUpdate(executionId, {
|
|
164
451
|
status: FlowStatus.Waiting,
|
|
452
|
+
state: nextState,
|
|
165
453
|
stepHistory: history,
|
|
166
454
|
currentStep: nextStep,
|
|
167
455
|
waitingForEvent: null,
|
|
168
456
|
wakeAt: new Date(Date.now() + delayMs),
|
|
457
|
+
leaseOwner: null,
|
|
458
|
+
leaseExpiresAt: null,
|
|
169
459
|
});
|
|
170
460
|
return { id: executionId, flowName: row.flowName, status: FlowStatus.Waiting };
|
|
171
461
|
}
|
|
172
462
|
// Parallel step — execute all branches concurrently, then advance
|
|
173
463
|
if (result.parallelBranches) {
|
|
464
|
+
let parallelState = nextState;
|
|
174
465
|
const branchSteps = result.parallelBranches
|
|
175
466
|
.map((branchName) => flow.steps.find((s) => s.name === branchName))
|
|
176
467
|
.filter((s) => s != null);
|
|
@@ -185,6 +476,7 @@ export function createFlowEngine(config) {
|
|
|
185
476
|
if (settled.status === 'fulfilled') {
|
|
186
477
|
const { branchStep, branchResult, branchStart, branchEnd } = settled.value;
|
|
187
478
|
history.push(buildHistoryEntry(branchStep.name, branchResult, branchStart, branchEnd));
|
|
479
|
+
parallelState = mergeExecutionState(parallelState, branchResult.data);
|
|
188
480
|
}
|
|
189
481
|
}
|
|
190
482
|
const anyFailed = branchResults.some((r) => r.status === 'rejected' ||
|
|
@@ -193,7 +485,8 @@ export function createFlowEngine(config) {
|
|
|
193
485
|
return handleStepFailure(executionId, row, flow, history, 'One or more parallel branches failed');
|
|
194
486
|
}
|
|
195
487
|
const nextStep = getNextStepName(flow.steps, currentStepName);
|
|
196
|
-
await
|
|
488
|
+
await guardedUpdate(executionId, {
|
|
489
|
+
state: parallelState,
|
|
197
490
|
stepHistory: history,
|
|
198
491
|
currentStep: nextStep,
|
|
199
492
|
});
|
|
@@ -205,7 +498,8 @@ export function createFlowEngine(config) {
|
|
|
205
498
|
}
|
|
206
499
|
// Conditional step — jump to the chosen branch
|
|
207
500
|
if (result.nextStep) {
|
|
208
|
-
await
|
|
501
|
+
await guardedUpdate(executionId, {
|
|
502
|
+
state: nextState,
|
|
209
503
|
stepHistory: history,
|
|
210
504
|
currentStep: result.nextStep,
|
|
211
505
|
});
|
|
@@ -214,11 +508,12 @@ export function createFlowEngine(config) {
|
|
|
214
508
|
// Normal completion — advance to next step
|
|
215
509
|
const nextStep = getNextStepName(flow.steps, currentStepName);
|
|
216
510
|
if (!nextStep) {
|
|
217
|
-
await
|
|
511
|
+
await guardedUpdate(executionId, { state: nextState, stepHistory: history });
|
|
218
512
|
await completeFlow(executionId, row.flowName);
|
|
219
513
|
return { id: executionId, flowName: row.flowName, status: FlowStatus.Completed };
|
|
220
514
|
}
|
|
221
|
-
await
|
|
515
|
+
await guardedUpdate(executionId, {
|
|
516
|
+
state: nextState,
|
|
222
517
|
stepHistory: history,
|
|
223
518
|
currentStep: nextStep,
|
|
224
519
|
});
|
|
@@ -242,27 +537,47 @@ export function createFlowEngine(config) {
|
|
|
242
537
|
const flow = registry.get(row.flowName);
|
|
243
538
|
if (!flow)
|
|
244
539
|
throw new Error(`Flow "${row.flowName}" not registered`);
|
|
245
|
-
// Advance past the wait/delay step
|
|
540
|
+
// Advance past the wait/delay step — set to created so claimNext picks it up
|
|
246
541
|
const nextStep = getNextStepName(flow.steps, row.currentStep ?? '');
|
|
247
542
|
await updateExecution(executionId, {
|
|
248
|
-
status: FlowStatus.
|
|
543
|
+
status: FlowStatus.Created,
|
|
249
544
|
currentStep: nextStep,
|
|
250
545
|
state: signal !== undefined ? signal : row.state,
|
|
251
546
|
waitingForEvent: null,
|
|
252
547
|
wakeAt: null,
|
|
548
|
+
leaseOwner: null,
|
|
549
|
+
leaseExpiresAt: null,
|
|
253
550
|
});
|
|
254
551
|
}
|
|
255
552
|
/**
|
|
256
553
|
* Resume all flows waiting on a specific event type.
|
|
554
|
+
* Uses FOR UPDATE SKIP LOCKED to prevent duplicate resumes across workers.
|
|
257
555
|
* Returns the number of executions resumed.
|
|
258
556
|
*/
|
|
259
557
|
async function resumeWaitingByEvent(eventType, signal) {
|
|
260
|
-
const
|
|
261
|
-
|
|
262
|
-
|
|
263
|
-
|
|
264
|
-
|
|
265
|
-
|
|
558
|
+
const result = await db.execute(sql `
|
|
559
|
+
UPDATE flow_executions
|
|
560
|
+
SET status = ${FlowStatus.Created},
|
|
561
|
+
waiting_for_event = NULL,
|
|
562
|
+
wake_at = NULL,
|
|
563
|
+
lease_owner = NULL,
|
|
564
|
+
lease_expires_at = NULL,
|
|
565
|
+
updated_at = now()
|
|
566
|
+
WHERE id IN (
|
|
567
|
+
SELECT id
|
|
568
|
+
FROM flow_executions
|
|
569
|
+
WHERE status = ${FlowStatus.Waiting}
|
|
570
|
+
AND waiting_for_event = ${eventType}
|
|
571
|
+
FOR UPDATE SKIP LOCKED
|
|
572
|
+
)
|
|
573
|
+
RETURNING id
|
|
574
|
+
`);
|
|
575
|
+
const rows = result;
|
|
576
|
+
// If a signal was provided, update state for each resumed execution
|
|
577
|
+
if (signal !== undefined) {
|
|
578
|
+
for (const row of rows) {
|
|
579
|
+
await updateExecution(row.id, { state: signal });
|
|
580
|
+
}
|
|
266
581
|
}
|
|
267
582
|
return rows.length;
|
|
268
583
|
}
|
|
@@ -298,6 +613,13 @@ export function createFlowEngine(config) {
|
|
|
298
613
|
status: FlowStatus.Cancelled,
|
|
299
614
|
completedAt: new Date(),
|
|
300
615
|
});
|
|
616
|
+
// Synchronously abort the step's AbortController if it is running in THIS
|
|
617
|
+
// engine instance. Cross-process cancellations still converge via the next
|
|
618
|
+
// heartbeat (which fails because status is no longer 'running').
|
|
619
|
+
const activeAc = activeStepAborts.get(executionId);
|
|
620
|
+
if (activeAc && !activeAc.signal.aborted) {
|
|
621
|
+
activeAc.abort(new FlowCancelledError(executionId));
|
|
622
|
+
}
|
|
301
623
|
if (audit) {
|
|
302
624
|
await audit.record(`flow.cancelled.${row.flowName}`, {
|
|
303
625
|
executionId,
|
|
@@ -325,7 +647,7 @@ export function createFlowEngine(config) {
|
|
|
325
647
|
const maxRetries = flow.retry?.attempts ?? 0;
|
|
326
648
|
if (retryCount <= maxRetries) {
|
|
327
649
|
// Retry: keep current step, increment counter
|
|
328
|
-
await
|
|
650
|
+
await guardedUpdate(executionId, {
|
|
329
651
|
stepHistory: history,
|
|
330
652
|
retryCount,
|
|
331
653
|
lastError: error ?? null,
|
|
@@ -337,21 +659,29 @@ export function createFlowEngine(config) {
|
|
|
337
659
|
return { id: executionId, flowName: row.flowName, status: FlowStatus.Failed };
|
|
338
660
|
}
|
|
339
661
|
async function completeFlow(executionId, flowName) {
|
|
340
|
-
await
|
|
662
|
+
const ok = await guardedUpdate(executionId, {
|
|
341
663
|
status: FlowStatus.Completed,
|
|
342
664
|
currentStep: null,
|
|
343
665
|
completedAt: new Date(),
|
|
666
|
+
leaseOwner: null,
|
|
667
|
+
leaseExpiresAt: null,
|
|
344
668
|
});
|
|
669
|
+
if (!ok)
|
|
670
|
+
return; // Lease lost — another worker owns this execution
|
|
345
671
|
if (audit) {
|
|
346
672
|
await audit.record(`flow.completed.${flowName}`, { executionId, flowName });
|
|
347
673
|
}
|
|
348
674
|
}
|
|
349
675
|
async function failFlow(executionId, flowName, error, row) {
|
|
350
|
-
await
|
|
676
|
+
const ok = await guardedUpdate(executionId, {
|
|
351
677
|
status: FlowStatus.Failed,
|
|
352
678
|
lastError: error ?? 'Unknown error',
|
|
353
679
|
completedAt: new Date(),
|
|
680
|
+
leaseOwner: null,
|
|
681
|
+
leaseExpiresAt: null,
|
|
354
682
|
});
|
|
683
|
+
if (!ok)
|
|
684
|
+
return; // Lease lost — another worker owns this execution
|
|
355
685
|
if (audit) {
|
|
356
686
|
await audit.record(`flow.failed.${flowName}`, {
|
|
357
687
|
executionId,
|
|
@@ -368,6 +698,7 @@ export function createFlowEngine(config) {
|
|
|
368
698
|
error: error ?? 'Unknown error',
|
|
369
699
|
tenantId: row?.tenantId,
|
|
370
700
|
actor: row?.actor,
|
|
701
|
+
db,
|
|
371
702
|
});
|
|
372
703
|
}
|
|
373
704
|
catch {
|
|
@@ -381,7 +712,113 @@ export function createFlowEngine(config) {
|
|
|
381
712
|
.set({ ...updates, updatedAt: new Date() })
|
|
382
713
|
.where(eq(flowExecutionsTable.id, id));
|
|
383
714
|
}
|
|
384
|
-
|
|
715
|
+
/**
|
|
716
|
+
* Finalize an execution as `failed` from the flow runner's perspective when
|
|
717
|
+
* `runNext` rejects with an unhandled error (e.g. `LeaseLostError` from a
|
|
718
|
+
* heartbeat). Without this, a zombie row left by a previously-crashed worker
|
|
719
|
+
* would be re-claimed by `claimNext` on every poll cycle (via the
|
|
720
|
+
* `status='running' AND lease_expires_at < now()` crash-recovery branch),
|
|
721
|
+
* retry forever, and pile up log noise.
|
|
722
|
+
*
|
|
723
|
+
* The guard is `status IN ('running','created','waiting')` (a set-guard, NOT
|
|
724
|
+
* a `lease_owner = workerId` check): on `LeaseLostError` the lease is by
|
|
725
|
+
* definition no longer ours, and we still want to stop the loop. A
|
|
726
|
+
* concurrent successful completion or an explicit `cancel()` will have
|
|
727
|
+
* already moved the row to a terminal status, and the `IN (...)` clause
|
|
728
|
+
* turns this call into a no-op in those races.
|
|
729
|
+
*
|
|
730
|
+
* Idempotent: second call on an already-finalized row is a no-op.
|
|
731
|
+
*/
|
|
732
|
+
async function markFailedFromRunner(executionId, error) {
|
|
733
|
+
const message = error instanceof Error ? error.message : typeof error === 'string' ? error : 'Unknown error';
|
|
734
|
+
const result = await db
|
|
735
|
+
.update(flowExecutionsTable)
|
|
736
|
+
.set({
|
|
737
|
+
status: FlowStatus.Failed,
|
|
738
|
+
lastError: message,
|
|
739
|
+
completedAt: new Date(),
|
|
740
|
+
leaseOwner: null,
|
|
741
|
+
leaseExpiresAt: null,
|
|
742
|
+
updatedAt: new Date(),
|
|
743
|
+
})
|
|
744
|
+
.where(and(eq(flowExecutionsTable.id, executionId), inArray(flowExecutionsTable.status, [
|
|
745
|
+
FlowStatus.Running,
|
|
746
|
+
FlowStatus.Created,
|
|
747
|
+
FlowStatus.Waiting,
|
|
748
|
+
])));
|
|
749
|
+
const rowsAffected = getRowsAffected(result);
|
|
750
|
+
if (rowsAffected > 0 && audit) {
|
|
751
|
+
await audit.record('flow.runner.finalized', {
|
|
752
|
+
executionId,
|
|
753
|
+
workerId,
|
|
754
|
+
error: message,
|
|
755
|
+
});
|
|
756
|
+
}
|
|
757
|
+
return rowsAffected > 0;
|
|
758
|
+
}
|
|
759
|
+
/**
|
|
760
|
+
* Update execution with lease ownership guard.
|
|
761
|
+
* Returns true if the update succeeded (we still own the lease),
|
|
762
|
+
* false if the lease was lost (0 rows affected).
|
|
763
|
+
*/
|
|
764
|
+
async function guardedUpdate(id, updates) {
|
|
765
|
+
const result = await db
|
|
766
|
+
.update(flowExecutionsTable)
|
|
767
|
+
.set({ ...updates, updatedAt: new Date() })
|
|
768
|
+
.where(and(eq(flowExecutionsTable.id, id), eq(flowExecutionsTable.leaseOwner, workerId)));
|
|
769
|
+
const rowsAffected = getRowsAffected(result);
|
|
770
|
+
return rowsAffected > 0;
|
|
771
|
+
}
|
|
772
|
+
return {
|
|
773
|
+
start,
|
|
774
|
+
runNext,
|
|
775
|
+
resume,
|
|
776
|
+
resumeWaitingByEvent,
|
|
777
|
+
listRunnable,
|
|
778
|
+
claimNext,
|
|
779
|
+
cancel,
|
|
780
|
+
status,
|
|
781
|
+
markFailedFromRunner,
|
|
782
|
+
get workerId() {
|
|
783
|
+
return workerId;
|
|
784
|
+
},
|
|
785
|
+
};
|
|
786
|
+
}
|
|
787
|
+
/**
|
|
788
|
+
* Wrap an AIService so that every method defaults `signal` to the flow step's
|
|
789
|
+
* AbortSignal when the caller doesn't pass one explicitly. `streamGenerate`
|
|
790
|
+
* is a sync function returning an AsyncIterable and is wrapped accordingly.
|
|
791
|
+
*/
|
|
792
|
+
function wrapAiWithDefaultSignal(ai, defaultSignal) {
|
|
793
|
+
const withSignal = (params) => ({
|
|
794
|
+
...params,
|
|
795
|
+
signal: params.signal ?? defaultSignal,
|
|
796
|
+
});
|
|
797
|
+
return {
|
|
798
|
+
generate: (params) => ai.generate(withSignal(params)),
|
|
799
|
+
generateWithUsage: (params) => ai.generateWithUsage(withSignal(params)),
|
|
800
|
+
streamGenerate: (params) => ai.streamGenerate(withSignal(params)),
|
|
801
|
+
extract: (params) => ai.extract(withSignal(params)),
|
|
802
|
+
classify: (params) => ai.classify(withSignal(params)),
|
|
803
|
+
retrieve: (params) => ai.retrieve(withSignal(params)),
|
|
804
|
+
};
|
|
805
|
+
}
|
|
806
|
+
function mergeExecutionState(currentState, stepData) {
|
|
807
|
+
if (stepData === undefined) {
|
|
808
|
+
return currentState;
|
|
809
|
+
}
|
|
810
|
+
if (currentState &&
|
|
811
|
+
typeof currentState === 'object' &&
|
|
812
|
+
!Array.isArray(currentState) &&
|
|
813
|
+
stepData &&
|
|
814
|
+
typeof stepData === 'object' &&
|
|
815
|
+
!Array.isArray(stepData)) {
|
|
816
|
+
return {
|
|
817
|
+
...currentState,
|
|
818
|
+
...stepData,
|
|
819
|
+
};
|
|
820
|
+
}
|
|
821
|
+
return stepData;
|
|
385
822
|
}
|
|
386
823
|
/**
|
|
387
824
|
* Collect all step names that are branches of parallel steps.
|