lightflow-engine 0.2.0 → 0.2.2
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/src/compat/api.js +57 -44
- package/dist/src/compat/workflow.js +25 -0
- package/dist/test/compat.js +3 -2
- package/dist/test/dbg3.mjs +23 -0
- package/package.json +1 -1
package/dist/src/compat/api.js
CHANGED
|
@@ -29,51 +29,56 @@ export function getEngine() {
|
|
|
29
29
|
* Register a workflow function under a stable id and start it. Mirrors
|
|
30
30
|
* Vercel's start(workflowFn, args).
|
|
31
31
|
*/
|
|
32
|
-
export async function start(fn, args) {
|
|
33
|
-
const engine = getEngine();
|
|
34
|
-
const id = workflowIdFor(fn);
|
|
35
|
-
registerWorkflow(id, fn);
|
|
36
|
-
const { runId } = await engine.start(id, args);
|
|
37
|
-
return makeRunHandle(runId);
|
|
38
|
-
}
|
|
39
|
-
export function getRun(runId) {
|
|
40
|
-
return makeRunHandle(runId);
|
|
41
|
-
}
|
|
42
32
|
function makeRunHandle(runId) {
|
|
43
33
|
const engine = getEngine();
|
|
44
34
|
const store = sharedStore;
|
|
45
|
-
|
|
46
|
-
const
|
|
47
|
-
|
|
48
|
-
|
|
35
|
+
/** One DB read -> Vercel status vocabulary. */
|
|
36
|
+
const readStatus = async () => {
|
|
37
|
+
const row = await store.getRun(runId);
|
|
38
|
+
if (!row)
|
|
39
|
+
return "pending";
|
|
40
|
+
if (row.status === "completed")
|
|
41
|
+
return "completed";
|
|
42
|
+
if (row.status === "failed") {
|
|
43
|
+
return row.output?.error === "cancelled"
|
|
44
|
+
? "cancelled"
|
|
45
|
+
: "failed";
|
|
46
|
+
}
|
|
47
|
+
return "running";
|
|
48
|
+
};
|
|
49
|
+
/** Promise resolving when the run reaches a terminal state. */
|
|
50
|
+
const terminal = (cache) => {
|
|
49
51
|
const deadline = Date.now() + 10 * 60_000;
|
|
50
|
-
let delay =
|
|
51
|
-
|
|
52
|
-
|
|
53
|
-
|
|
54
|
-
|
|
55
|
-
|
|
56
|
-
|
|
57
|
-
cachedStatus = "completed";
|
|
58
|
-
return cachedStatus;
|
|
59
|
-
}
|
|
60
|
-
if (row.status === "failed") {
|
|
61
|
-
// cancelled runs are 'failed' with error 'cancelled'
|
|
62
|
-
cachedStatus =
|
|
63
|
-
row.output?.error === "cancelled"
|
|
64
|
-
? "cancelled" : "failed";
|
|
65
|
-
return cachedStatus;
|
|
52
|
+
let delay = 25;
|
|
53
|
+
const loop = async () => {
|
|
54
|
+
while (Date.now() < deadline) {
|
|
55
|
+
const st = await readStatus();
|
|
56
|
+
if (st === "completed" || st === "failed" || st === "cancelled") {
|
|
57
|
+
cache.status = st;
|
|
58
|
+
return st;
|
|
66
59
|
}
|
|
60
|
+
await new Promise((r) => setTimeout(r, delay));
|
|
61
|
+
delay = Math.min(delay * 1.4, 250);
|
|
67
62
|
}
|
|
68
|
-
|
|
69
|
-
|
|
70
|
-
|
|
71
|
-
|
|
72
|
-
}
|
|
63
|
+
throw new Error(`run status timeout: ${runId}`);
|
|
64
|
+
};
|
|
65
|
+
return loop();
|
|
66
|
+
};
|
|
67
|
+
const cache = {};
|
|
73
68
|
const handle = {
|
|
74
69
|
runId,
|
|
75
|
-
status
|
|
76
|
-
|
|
70
|
+
get status() {
|
|
71
|
+
// Live snapshot: resolve immediately with the current state; the
|
|
72
|
+
// startStopMonitor poll loop relies on this re-reading each tick.
|
|
73
|
+
return readStatus().then((st) => {
|
|
74
|
+
cache.status = st;
|
|
75
|
+
return st;
|
|
76
|
+
});
|
|
77
|
+
},
|
|
78
|
+
returnValue: (async () => {
|
|
79
|
+
await terminal(cache);
|
|
80
|
+
return (await engine.getRun(runId)).returnValue;
|
|
81
|
+
})(),
|
|
77
82
|
getReadable(opts) {
|
|
78
83
|
const startIndex = opts?.startIndex ?? 0;
|
|
79
84
|
let sent = startIndex;
|
|
@@ -109,19 +114,17 @@ function makeRunHandle(runId) {
|
|
|
109
114
|
}
|
|
110
115
|
const chunks = await collectChunks();
|
|
111
116
|
const hasDone = chunks.some((c) => c.done);
|
|
112
|
-
// chunks carry a monotonic `index`; entries after startIndex
|
|
113
117
|
for (const c of chunks) {
|
|
114
|
-
|
|
115
|
-
if (p.done) {
|
|
118
|
+
if (c.done) {
|
|
116
119
|
closed = true;
|
|
117
120
|
controller.close();
|
|
118
121
|
return;
|
|
119
122
|
}
|
|
120
|
-
if (
|
|
123
|
+
if (c.index < startIndex)
|
|
121
124
|
continue;
|
|
122
|
-
if (
|
|
123
|
-
controller.enqueue(
|
|
124
|
-
sent =
|
|
125
|
+
if (c.index >= sent) {
|
|
126
|
+
controller.enqueue(c.value);
|
|
127
|
+
sent = c.index + 1;
|
|
125
128
|
}
|
|
126
129
|
}
|
|
127
130
|
if (hasDone) {
|
|
@@ -155,3 +158,13 @@ function makeRunHandle(runId) {
|
|
|
155
158
|
};
|
|
156
159
|
return handle;
|
|
157
160
|
}
|
|
161
|
+
export async function start(fn, args) {
|
|
162
|
+
const engine = getEngine();
|
|
163
|
+
const id = workflowIdFor(fn);
|
|
164
|
+
registerWorkflow(id, fn);
|
|
165
|
+
const { runId } = await engine.start(id, args);
|
|
166
|
+
return makeRunHandle(runId);
|
|
167
|
+
}
|
|
168
|
+
export function getRun(runId) {
|
|
169
|
+
return makeRunHandle(runId);
|
|
170
|
+
}
|
|
@@ -8,6 +8,7 @@
|
|
|
8
8
|
* webpack loader (workflow/next) or explicit step() wrappers.
|
|
9
9
|
*/
|
|
10
10
|
import { createHash } from "node:crypto";
|
|
11
|
+
import { step, registerStep, } from "../index.js";
|
|
11
12
|
import { current } from "../index.js";
|
|
12
13
|
export { FatalError, CancelledError, sleep } from "../index.js";
|
|
13
14
|
export { getWorkflowMetadata } from "../compat/metadata.js";
|
|
@@ -69,3 +70,27 @@ export function workflowIdFor(fn) {
|
|
|
69
70
|
return "wf_" + createHash("sha256")
|
|
70
71
|
.update(String(fn).slice(0, 512)).digest("hex").slice(0, 16);
|
|
71
72
|
}
|
|
73
|
+
/**
|
|
74
|
+
* makeStep — turns a plain async function into a durable step callable.
|
|
75
|
+
*
|
|
76
|
+
* Entry's "use step" functions take arguments; lightflow's step() takes a
|
|
77
|
+
* thunk. makeStep wraps both: the returned function is a drop-in for the
|
|
78
|
+
* original (same signature), but each call becomes a memoized, retrying,
|
|
79
|
+
* replayable step keyed by call position. Outside a workflow it degrades
|
|
80
|
+
* to a plain call, so tests and non-durable paths keep working.
|
|
81
|
+
*/
|
|
82
|
+
export function makeStep(fn, opts) {
|
|
83
|
+
const wrapped = async (...args) => step(() => fn(...args));
|
|
84
|
+
Object.defineProperty(wrapped, "__lightflowStepId", {
|
|
85
|
+
value: `step_${workflowIdFor(fn)}`,
|
|
86
|
+
enumerable: false,
|
|
87
|
+
});
|
|
88
|
+
if (opts?.retries !== undefined) {
|
|
89
|
+
Object.defineProperty(wrapped, "__lightflowRetries", {
|
|
90
|
+
value: opts.retries,
|
|
91
|
+
enumerable: false,
|
|
92
|
+
});
|
|
93
|
+
}
|
|
94
|
+
registerStep(wrapped.__lightflowStepId, fn);
|
|
95
|
+
return wrapped;
|
|
96
|
+
}
|
package/dist/test/compat.js
CHANGED
|
@@ -52,7 +52,8 @@ test("compat: vercel-style agent chat workflow end-to-end", async () => {
|
|
|
52
52
|
{ messages: ["hello", "world"], chatId: "c1" },
|
|
53
53
|
]);
|
|
54
54
|
assert.ok(run.runId.startsWith("lrun_"));
|
|
55
|
-
// status is a
|
|
55
|
+
// status is a live getter; await terminal state via returnValue
|
|
56
|
+
await run.returnValue;
|
|
56
57
|
const status = await run.status;
|
|
57
58
|
assert.equal(status, "completed");
|
|
58
59
|
const ret = (await run.returnValue);
|
|
@@ -79,7 +80,7 @@ test("compat: vercel-style agent chat workflow end-to-end", async () => {
|
|
|
79
80
|
// cancel semantics: getRun + status + cancel()
|
|
80
81
|
const dup = await start(runAgentWorkflow, [{ messages: ["x"], chatId: "c2" }]);
|
|
81
82
|
await dup.cancel();
|
|
82
|
-
assert.equal(await dup.status, "cancelled");
|
|
83
|
+
assert.equal(await dup.status, "cancelled"); // live getter reflects cancel
|
|
83
84
|
await pool.end();
|
|
84
85
|
engine.stopWorker();
|
|
85
86
|
await workerPromise;
|
|
@@ -0,0 +1,23 @@
|
|
|
1
|
+
|
|
2
|
+
import { Engine, step, sleep } from "/home/agentuser/lightflow/dist/src/index.js";
|
|
3
|
+
import { createPostgresStore } from "/home/agentuser/lightflow/dist/src/pg-store.js";
|
|
4
|
+
import { initWorkflowApi, start } from "/home/agentuser/lightflow/dist/src/compat/api.js";
|
|
5
|
+
const store = await createPostgresStore(process.env.LIGHTFLOW_PG_URL);
|
|
6
|
+
const engine = new Engine(store, { pollMs: 25 });
|
|
7
|
+
initWorkflowApi(store, engine);
|
|
8
|
+
const wp = engine.startWorker();
|
|
9
|
+
setTimeout(() => engine.stopWorker(), 20000);
|
|
10
|
+
const wf = async () => {
|
|
11
|
+
await sleep(new Date(Date.now() + 300));
|
|
12
|
+
await step(async () => "x");
|
|
13
|
+
return "ok";
|
|
14
|
+
};
|
|
15
|
+
const run = await start(wf, []);
|
|
16
|
+
const s1 = await run.status; // live snapshot (running/pending)
|
|
17
|
+
const s2 = await run.status; // fresh promise each access
|
|
18
|
+
console.log("live status:", s1, s2);
|
|
19
|
+
const t = await run.status && await (async()=>{ // wait terminal
|
|
20
|
+
for(;;){ const st = await run.status; if(st!=="running"&&st!=="pending") return st; await new Promise(r=>setTimeout(r,25)); }
|
|
21
|
+
})();
|
|
22
|
+
console.log("terminal:", t);
|
|
23
|
+
process.exit(0);
|
package/package.json
CHANGED