workplane 0.4.1 → 0.4.3
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 +31 -1
- package/dist/cli.js +164 -8
- package/dist/cli.js.map +1 -1
- package/dist/migrate.js +243 -9
- package/dist/migrate.js.map +1 -1
- package/dist/migrations/pg/0001_loud_otto_octavius.sql +45 -0
- package/dist/migrations/pg/meta/0001_snapshot.json +900 -0
- package/dist/migrations/pg/meta/_journal.json +7 -0
- package/dist/migrations/sqlite/0001_curvy_anita_blake.sql +45 -0
- package/dist/migrations/sqlite/meta/0001_snapshot.json +841 -0
- package/dist/migrations/sqlite/meta/_journal.json +7 -0
- package/dist/server.js +1194 -62
- package/dist/server.js.map +1 -1
- package/dist/setup.js +252 -12
- package/dist/setup.js.map +1 -1
- package/package.json +2 -1
package/dist/server.js
CHANGED
|
@@ -9,13 +9,137 @@ var __export = (target, all) => {
|
|
|
9
9
|
__defProp(target, name, { get: all[name], enumerable: true });
|
|
10
10
|
};
|
|
11
11
|
|
|
12
|
+
// ../workplans/src/providers/anthropic.ts
|
|
13
|
+
var anthropic_exports = {};
|
|
14
|
+
__export(anthropic_exports, {
|
|
15
|
+
runAnthropicStep: () => runAnthropicStep
|
|
16
|
+
});
|
|
17
|
+
import Anthropic from "@anthropic-ai/sdk";
|
|
18
|
+
async function runAnthropicStep(opts) {
|
|
19
|
+
const client = new Anthropic({ apiKey: opts.apiKey ?? process.env.ANTHROPIC_API_KEY });
|
|
20
|
+
const message = await client.messages.create({
|
|
21
|
+
model: opts.model,
|
|
22
|
+
max_tokens: 4096,
|
|
23
|
+
messages: [{ role: "user", content: opts.prompt }]
|
|
24
|
+
});
|
|
25
|
+
const block = message.content[0];
|
|
26
|
+
return block.type === "text" ? block.text : "";
|
|
27
|
+
}
|
|
28
|
+
var init_anthropic = __esm({
|
|
29
|
+
"../workplans/src/providers/anthropic.ts"() {
|
|
30
|
+
"use strict";
|
|
31
|
+
}
|
|
32
|
+
});
|
|
33
|
+
|
|
34
|
+
// ../workplans/src/providers/openai.ts
|
|
35
|
+
var openai_exports = {};
|
|
36
|
+
__export(openai_exports, {
|
|
37
|
+
runOpenAIStep: () => runOpenAIStep
|
|
38
|
+
});
|
|
39
|
+
import OpenAI from "openai";
|
|
40
|
+
async function runOpenAIStep(opts) {
|
|
41
|
+
const client = new OpenAI({ apiKey: opts.apiKey ?? process.env.OPENAI_API_KEY });
|
|
42
|
+
const completion = await client.chat.completions.create({
|
|
43
|
+
model: opts.model,
|
|
44
|
+
messages: [{ role: "user", content: opts.prompt }]
|
|
45
|
+
});
|
|
46
|
+
return completion.choices[0]?.message.content ?? "";
|
|
47
|
+
}
|
|
48
|
+
var init_openai = __esm({
|
|
49
|
+
"../workplans/src/providers/openai.ts"() {
|
|
50
|
+
"use strict";
|
|
51
|
+
}
|
|
52
|
+
});
|
|
53
|
+
|
|
54
|
+
// ../workplans/src/providers/ollama.ts
|
|
55
|
+
var ollama_exports = {};
|
|
56
|
+
__export(ollama_exports, {
|
|
57
|
+
runOllamaStep: () => runOllamaStep
|
|
58
|
+
});
|
|
59
|
+
import { Ollama } from "ollama";
|
|
60
|
+
async function runOllamaStep(opts) {
|
|
61
|
+
const host = opts.host ?? process.env.OLLAMA_HOST ?? "http://localhost:11434";
|
|
62
|
+
const client = new Ollama({ host });
|
|
63
|
+
const response = await client.generate({ model: opts.model, prompt: opts.prompt });
|
|
64
|
+
return response.response;
|
|
65
|
+
}
|
|
66
|
+
var init_ollama = __esm({
|
|
67
|
+
"../workplans/src/providers/ollama.ts"() {
|
|
68
|
+
"use strict";
|
|
69
|
+
}
|
|
70
|
+
});
|
|
71
|
+
|
|
72
|
+
// ../workplans/src/providers/shell.ts
|
|
73
|
+
var shell_exports = {};
|
|
74
|
+
__export(shell_exports, {
|
|
75
|
+
runShellStep: () => runShellStep
|
|
76
|
+
});
|
|
77
|
+
import { exec } from "child_process";
|
|
78
|
+
import { promisify } from "util";
|
|
79
|
+
async function runShellStep(opts) {
|
|
80
|
+
const { stdout } = await execAsync(opts.command, {
|
|
81
|
+
cwd: opts.cwd,
|
|
82
|
+
maxBuffer: 10 * 1024 * 1024
|
|
83
|
+
});
|
|
84
|
+
return stdout;
|
|
85
|
+
}
|
|
86
|
+
var execAsync;
|
|
87
|
+
var init_shell = __esm({
|
|
88
|
+
"../workplans/src/providers/shell.ts"() {
|
|
89
|
+
"use strict";
|
|
90
|
+
execAsync = promisify(exec);
|
|
91
|
+
}
|
|
92
|
+
});
|
|
93
|
+
|
|
94
|
+
// ../workplans/src/providers/file.ts
|
|
95
|
+
var file_exports = {};
|
|
96
|
+
__export(file_exports, {
|
|
97
|
+
runFileStep: () => runFileStep
|
|
98
|
+
});
|
|
99
|
+
import { readFile } from "fs/promises";
|
|
100
|
+
async function runFileStep(opts) {
|
|
101
|
+
return readFile(opts.path, opts.encoding ?? "utf8");
|
|
102
|
+
}
|
|
103
|
+
var init_file = __esm({
|
|
104
|
+
"../workplans/src/providers/file.ts"() {
|
|
105
|
+
"use strict";
|
|
106
|
+
}
|
|
107
|
+
});
|
|
108
|
+
|
|
109
|
+
// ../dbos/src/scheduler.ts
|
|
110
|
+
var scheduler_exports = {};
|
|
111
|
+
__export(scheduler_exports, {
|
|
112
|
+
registerDbosSchedulerTick: () => registerDbosSchedulerTick
|
|
113
|
+
});
|
|
114
|
+
import { DBOS } from "@dbos-inc/dbos-sdk";
|
|
115
|
+
function registerDbosSchedulerTick(scheduler) {
|
|
116
|
+
if (registered) {
|
|
117
|
+
return;
|
|
118
|
+
}
|
|
119
|
+
DBOS.registerScheduled(
|
|
120
|
+
async () => {
|
|
121
|
+
await scheduler.tick();
|
|
122
|
+
},
|
|
123
|
+
{ crontab: "* * * * *" }
|
|
124
|
+
);
|
|
125
|
+
registered = true;
|
|
126
|
+
}
|
|
127
|
+
var registered;
|
|
128
|
+
var init_scheduler = __esm({
|
|
129
|
+
"../dbos/src/scheduler.ts"() {
|
|
130
|
+
"use strict";
|
|
131
|
+
registered = false;
|
|
132
|
+
}
|
|
133
|
+
});
|
|
134
|
+
|
|
12
135
|
// ../dbos/src/index.ts
|
|
13
136
|
var src_exports = {};
|
|
14
137
|
__export(src_exports, {
|
|
15
138
|
DbosWorkflows: () => DbosWorkflows,
|
|
16
|
-
createDbosWorkflows: () => createDbosWorkflows
|
|
139
|
+
createDbosWorkflows: () => createDbosWorkflows,
|
|
140
|
+
registerDbosSchedulerTick: () => registerDbosSchedulerTick
|
|
17
141
|
});
|
|
18
|
-
import { DBOS } from "@dbos-inc/dbos-sdk";
|
|
142
|
+
import { DBOS as DBOS2 } from "@dbos-inc/dbos-sdk";
|
|
19
143
|
function createDbosWorkflows(store) {
|
|
20
144
|
return new DbosWorkflows(store);
|
|
21
145
|
}
|
|
@@ -23,39 +147,40 @@ var DbosWorkflows;
|
|
|
23
147
|
var init_src = __esm({
|
|
24
148
|
"../dbos/src/index.ts"() {
|
|
25
149
|
"use strict";
|
|
150
|
+
init_scheduler();
|
|
26
151
|
DbosWorkflows = class {
|
|
27
152
|
registered;
|
|
28
153
|
constructor(store) {
|
|
29
|
-
const createTask =
|
|
30
|
-
async (input) =>
|
|
154
|
+
const createTask = DBOS2.registerWorkflow(
|
|
155
|
+
async (input) => DBOS2.runStep(() => store.createTask(input), { name: "store-create-task" }),
|
|
31
156
|
{ name: "createTaskWorkflow" }
|
|
32
157
|
);
|
|
33
|
-
const retryTask =
|
|
34
|
-
async (taskId) =>
|
|
158
|
+
const retryTask = DBOS2.registerWorkflow(
|
|
159
|
+
async (taskId) => DBOS2.runStep(() => store.retryTask(taskId), { name: "store-retry-task" }),
|
|
35
160
|
{ name: "retryTaskWorkflow" }
|
|
36
161
|
);
|
|
37
|
-
const cancelTask =
|
|
38
|
-
async (taskId) =>
|
|
162
|
+
const cancelTask = DBOS2.registerWorkflow(
|
|
163
|
+
async (taskId) => DBOS2.runStep(() => store.cancelTask(taskId), { name: "store-cancel-task" }),
|
|
39
164
|
{ name: "cancelTaskWorkflow" }
|
|
40
165
|
);
|
|
41
|
-
const updateRunStatus =
|
|
42
|
-
async (runId, status, error) =>
|
|
166
|
+
const updateRunStatus = DBOS2.registerWorkflow(
|
|
167
|
+
async (runId, status, error) => DBOS2.runStep(() => store.updateRunStatus(runId, status, error), { name: "store-update-run-status" }),
|
|
43
168
|
{ name: "updateRunStatusWorkflow" }
|
|
44
169
|
);
|
|
45
|
-
const appendRunLogs =
|
|
46
|
-
async (runId, logs) =>
|
|
170
|
+
const appendRunLogs = DBOS2.registerWorkflow(
|
|
171
|
+
async (runId, logs) => DBOS2.runStep(() => store.appendRunLogs(runId, logs), { name: "store-append-run-logs" }),
|
|
47
172
|
{ name: "appendRunLogsWorkflow" }
|
|
48
173
|
);
|
|
49
|
-
const createArtifact =
|
|
50
|
-
async (runId, input) =>
|
|
174
|
+
const createArtifact = DBOS2.registerWorkflow(
|
|
175
|
+
async (runId, input) => DBOS2.runStep(() => store.createArtifact(runId, input), { name: "store-create-artifact" }),
|
|
51
176
|
{ name: "createArtifactWorkflow" }
|
|
52
177
|
);
|
|
53
|
-
const appendInputEvent =
|
|
54
|
-
async (runId, input) =>
|
|
178
|
+
const appendInputEvent = DBOS2.registerWorkflow(
|
|
179
|
+
async (runId, input) => DBOS2.runStep(() => store.appendInputEvent(runId, input), { name: "store-append-input-event" }),
|
|
55
180
|
{ name: "appendInputEventWorkflow" }
|
|
56
181
|
);
|
|
57
|
-
const markInputDelivered =
|
|
58
|
-
async (runId, sequence) =>
|
|
182
|
+
const markInputDelivered = DBOS2.registerWorkflow(
|
|
183
|
+
async (runId, sequence) => DBOS2.runStep(() => store.markInputDelivered(runId, sequence), { name: "store-mark-input-delivered" }),
|
|
59
184
|
{ name: "markInputDeliveredWorkflow" }
|
|
60
185
|
);
|
|
61
186
|
this.registered = {
|
|
@@ -70,48 +195,48 @@ var init_src = __esm({
|
|
|
70
195
|
};
|
|
71
196
|
}
|
|
72
197
|
async launch(options) {
|
|
73
|
-
|
|
198
|
+
DBOS2.setConfig({
|
|
74
199
|
name: options.appName,
|
|
75
200
|
systemDatabaseUrl: options.systemDatabaseUrl
|
|
76
201
|
});
|
|
77
|
-
await
|
|
202
|
+
await DBOS2.launch({
|
|
78
203
|
conductorKey: options.conductorKey,
|
|
79
204
|
conductorURL: options.conductorUrl
|
|
80
205
|
});
|
|
81
206
|
}
|
|
82
207
|
async shutdown() {
|
|
83
|
-
await
|
|
208
|
+
await DBOS2.shutdown();
|
|
84
209
|
}
|
|
85
210
|
async createTask(input) {
|
|
86
|
-
const handle = await
|
|
211
|
+
const handle = await DBOS2.startWorkflow(this.registered.createTask)(input);
|
|
87
212
|
return handle.getResult();
|
|
88
213
|
}
|
|
89
214
|
async retryTask(taskId) {
|
|
90
|
-
const handle = await
|
|
215
|
+
const handle = await DBOS2.startWorkflow(this.registered.retryTask)(taskId);
|
|
91
216
|
return handle.getResult();
|
|
92
217
|
}
|
|
93
218
|
async cancelTask(taskId) {
|
|
94
|
-
const handle = await
|
|
219
|
+
const handle = await DBOS2.startWorkflow(this.registered.cancelTask)(taskId);
|
|
95
220
|
return handle.getResult();
|
|
96
221
|
}
|
|
97
222
|
async updateRunStatus(runId, status, error) {
|
|
98
|
-
const handle = await
|
|
223
|
+
const handle = await DBOS2.startWorkflow(this.registered.updateRunStatus)(runId, status, error);
|
|
99
224
|
return handle.getResult();
|
|
100
225
|
}
|
|
101
226
|
async appendRunLogs(runId, logs) {
|
|
102
|
-
const handle = await
|
|
227
|
+
const handle = await DBOS2.startWorkflow(this.registered.appendRunLogs)(runId, logs);
|
|
103
228
|
return handle.getResult();
|
|
104
229
|
}
|
|
105
230
|
async createArtifact(runId, input) {
|
|
106
|
-
const handle = await
|
|
231
|
+
const handle = await DBOS2.startWorkflow(this.registered.createArtifact)(runId, input);
|
|
107
232
|
return handle.getResult();
|
|
108
233
|
}
|
|
109
234
|
async appendInputEvent(runId, input) {
|
|
110
|
-
const handle = await
|
|
235
|
+
const handle = await DBOS2.startWorkflow(this.registered.appendInputEvent)(runId, input);
|
|
111
236
|
return handle.getResult();
|
|
112
237
|
}
|
|
113
238
|
async markInputDelivered(runId, sequence) {
|
|
114
|
-
const handle = await
|
|
239
|
+
const handle = await DBOS2.startWorkflow(this.registered.markInputDelivered)(runId, sequence);
|
|
115
240
|
await handle.getResult();
|
|
116
241
|
}
|
|
117
242
|
};
|
|
@@ -183,7 +308,7 @@ import Database from "better-sqlite3";
|
|
|
183
308
|
import { Pool } from "pg";
|
|
184
309
|
|
|
185
310
|
// ../db/src/pg-store.ts
|
|
186
|
-
import { and, asc, desc, eq, gt, sql as sql2 } from "drizzle-orm";
|
|
311
|
+
import { and, arrayContains, asc, desc, eq, gt, isNotNull, lte, sql as sql2 } from "drizzle-orm";
|
|
187
312
|
|
|
188
313
|
// ../core/src/ids.ts
|
|
189
314
|
import { randomUUID } from "crypto";
|
|
@@ -199,10 +324,13 @@ __export(pg_exports, {
|
|
|
199
324
|
runInputEvents: () => runInputEvents,
|
|
200
325
|
runLogs: () => runLogs,
|
|
201
326
|
runs: () => runs,
|
|
202
|
-
tasks: () => tasks
|
|
327
|
+
tasks: () => tasks,
|
|
328
|
+
workplanRuns: () => workplanRuns,
|
|
329
|
+
workplanSchedules: () => workplanSchedules,
|
|
330
|
+
workplanStepResults: () => workplanStepResults
|
|
203
331
|
});
|
|
204
332
|
import { isNull, sql } from "drizzle-orm";
|
|
205
|
-
import { bigint, bigserial, index, integer, jsonb, pgTable, text, timestamp, uniqueIndex } from "drizzle-orm/pg-core";
|
|
333
|
+
import { bigint, bigserial, boolean, index, integer, jsonb, pgTable, text, timestamp, uniqueIndex } from "drizzle-orm/pg-core";
|
|
206
334
|
var tasks = pgTable("tasks", {
|
|
207
335
|
id: text("id").primaryKey(),
|
|
208
336
|
kind: text("kind").notNull(),
|
|
@@ -260,6 +388,49 @@ var artifacts = pgTable("artifacts", {
|
|
|
260
388
|
}, (table) => ({
|
|
261
389
|
runIdIdx: index("idx_artifacts_run_id").on(table.runId)
|
|
262
390
|
}));
|
|
391
|
+
var workplanSchedules = pgTable("workplan_schedules", {
|
|
392
|
+
id: text("id").primaryKey(),
|
|
393
|
+
planId: text("plan_id").notNull(),
|
|
394
|
+
name: text("name").notNull(),
|
|
395
|
+
cronExpression: text("cron_expression").notNull(),
|
|
396
|
+
timezone: text("timezone").notNull(),
|
|
397
|
+
inputs: jsonb("inputs").$type().notNull().default({}),
|
|
398
|
+
enabled: boolean("enabled").notNull().default(true),
|
|
399
|
+
lastRunAt: timestamp("last_run_at", { withTimezone: true }),
|
|
400
|
+
nextRunAt: timestamp("next_run_at", { withTimezone: true }),
|
|
401
|
+
createdBy: text("created_by"),
|
|
402
|
+
createdAt: timestamp("created_at", { withTimezone: true }).notNull().defaultNow(),
|
|
403
|
+
updatedAt: timestamp("updated_at", { withTimezone: true }).notNull().defaultNow()
|
|
404
|
+
}, (table) => ({
|
|
405
|
+
dueIdx: index("idx_workplan_schedules_due").on(table.nextRunAt)
|
|
406
|
+
}));
|
|
407
|
+
var workplanRuns = pgTable("workplan_runs", {
|
|
408
|
+
id: text("id").primaryKey(),
|
|
409
|
+
scheduleId: text("schedule_id").references(() => workplanSchedules.id),
|
|
410
|
+
planId: text("plan_id").notNull(),
|
|
411
|
+
planName: text("plan_name").notNull(),
|
|
412
|
+
status: text("status").notNull().default("running"),
|
|
413
|
+
idempotencyKey: text("idempotency_key"),
|
|
414
|
+
createdAt: timestamp("created_at", { withTimezone: true }).notNull().defaultNow(),
|
|
415
|
+
endedAt: timestamp("ended_at", { withTimezone: true }),
|
|
416
|
+
error: text("error")
|
|
417
|
+
}, (table) => ({
|
|
418
|
+
scheduleIdx: index("idx_workplan_runs_schedule_id").on(table.scheduleId),
|
|
419
|
+
idempotencyUnique: uniqueIndex("idx_workplan_runs_idempotency").on(table.idempotencyKey)
|
|
420
|
+
}));
|
|
421
|
+
var workplanStepResults = pgTable("workplan_step_results", {
|
|
422
|
+
id: text("id").primaryKey(),
|
|
423
|
+
workplanRunId: text("workplan_run_id").notNull().references(() => workplanRuns.id),
|
|
424
|
+
stepId: text("step_id").notNull(),
|
|
425
|
+
stepName: text("step_name").notNull(),
|
|
426
|
+
output: text("output"),
|
|
427
|
+
exitCode: integer("exit_code"),
|
|
428
|
+
durationMs: integer("duration_ms"),
|
|
429
|
+
metadata: jsonb("metadata").$type(),
|
|
430
|
+
createdAt: timestamp("created_at", { withTimezone: true }).notNull().defaultNow()
|
|
431
|
+
}, (table) => ({
|
|
432
|
+
runIdx: index("idx_workplan_step_results_run_id").on(table.workplanRunId)
|
|
433
|
+
}));
|
|
263
434
|
var runInputEvents = pgTable("run_input_events", {
|
|
264
435
|
id: bigserial("id", { mode: "number" }).primaryKey(),
|
|
265
436
|
runId: text("run_id").notNull().references(() => runs.id),
|
|
@@ -339,16 +510,46 @@ function toInputEventRecord(row) {
|
|
|
339
510
|
deliveredAt: row.deliveredAt?.toISOString() ?? null
|
|
340
511
|
};
|
|
341
512
|
}
|
|
342
|
-
function
|
|
513
|
+
function toScheduleRecord(row) {
|
|
343
514
|
return {
|
|
344
|
-
id:
|
|
345
|
-
|
|
346
|
-
|
|
347
|
-
|
|
348
|
-
|
|
515
|
+
id: row.id,
|
|
516
|
+
planId: row.planId,
|
|
517
|
+
name: row.name,
|
|
518
|
+
cronExpression: row.cronExpression,
|
|
519
|
+
timezone: row.timezone,
|
|
520
|
+
inputs: row.inputs ?? {},
|
|
521
|
+
enabled: row.enabled,
|
|
522
|
+
lastRunAt: row.lastRunAt?.toISOString() ?? null,
|
|
523
|
+
nextRunAt: row.nextRunAt?.toISOString() ?? null,
|
|
524
|
+
createdBy: row.createdBy ?? null,
|
|
525
|
+
createdAt: row.createdAt.toISOString(),
|
|
526
|
+
updatedAt: row.updatedAt.toISOString()
|
|
527
|
+
};
|
|
528
|
+
}
|
|
529
|
+
function toWorkplanRunRecord(row) {
|
|
530
|
+
return {
|
|
531
|
+
id: row.id,
|
|
532
|
+
scheduleId: row.scheduleId ?? null,
|
|
533
|
+
planId: row.planId,
|
|
534
|
+
planName: row.planName,
|
|
349
535
|
status: row.status,
|
|
350
|
-
|
|
351
|
-
|
|
536
|
+
idempotencyKey: row.idempotencyKey ?? null,
|
|
537
|
+
createdAt: row.createdAt.toISOString(),
|
|
538
|
+
endedAt: row.endedAt?.toISOString() ?? null,
|
|
539
|
+
error: row.error ?? null
|
|
540
|
+
};
|
|
541
|
+
}
|
|
542
|
+
function toWorkplanStepResultRecord(row) {
|
|
543
|
+
return {
|
|
544
|
+
id: row.id,
|
|
545
|
+
workplanRunId: row.workplanRunId,
|
|
546
|
+
stepId: row.stepId,
|
|
547
|
+
stepName: row.stepName,
|
|
548
|
+
output: row.output ?? null,
|
|
549
|
+
exitCode: row.exitCode ?? null,
|
|
550
|
+
durationMs: row.durationMs ?? null,
|
|
551
|
+
metadata: row.metadata ?? null,
|
|
552
|
+
createdAt: row.createdAt.toISOString()
|
|
352
553
|
};
|
|
353
554
|
}
|
|
354
555
|
function mapRawRun(row) {
|
|
@@ -421,22 +622,15 @@ var PgStore = class {
|
|
|
421
622
|
}
|
|
422
623
|
async pollNode(nodeId, capabilities) {
|
|
423
624
|
return await this.db.transaction(async (tx) => {
|
|
424
|
-
await tx.
|
|
425
|
-
|
|
426
|
-
|
|
427
|
-
|
|
428
|
-
|
|
429
|
-
|
|
430
|
-
const
|
|
431
|
-
|
|
432
|
-
|
|
433
|
-
ORDER BY created_at ASC
|
|
434
|
-
LIMIT 1
|
|
435
|
-
FOR UPDATE SKIP LOCKED
|
|
436
|
-
`);
|
|
437
|
-
if (taskResult.rows.length === 0) return null;
|
|
438
|
-
const taskRow = taskResult.rows[0];
|
|
439
|
-
const taskId = String(taskRow.id);
|
|
625
|
+
await tx.update(nodes).set({
|
|
626
|
+
capabilities,
|
|
627
|
+
status: "online",
|
|
628
|
+
lastHeartbeatAt: sql2`now()`,
|
|
629
|
+
updatedAt: sql2`now()`
|
|
630
|
+
}).where(eq(nodes.id, nodeId));
|
|
631
|
+
const [taskRow] = await tx.select().from(tasks).where(and(eq(tasks.status, "queued"), arrayContains(tasks.requires, capabilities))).orderBy(asc(tasks.createdAt)).limit(1).for("update", { skipLocked: true });
|
|
632
|
+
if (!taskRow) return null;
|
|
633
|
+
const taskId = taskRow.id;
|
|
440
634
|
const attemptResult = await tx.execute(sql2`
|
|
441
635
|
SELECT COALESCE(MAX(attempt), 0) AS max_attempt FROM runs WHERE task_id = ${taskId}
|
|
442
636
|
`);
|
|
@@ -451,7 +645,7 @@ var PgStore = class {
|
|
|
451
645
|
UPDATE tasks SET status = 'assigned', updated_at = now() WHERE id = ${taskId}
|
|
452
646
|
`);
|
|
453
647
|
return {
|
|
454
|
-
task:
|
|
648
|
+
task: toTaskRecord(taskRow),
|
|
455
649
|
run: mapRawRun(runResult.rows[0])
|
|
456
650
|
};
|
|
457
651
|
});
|
|
@@ -564,10 +758,122 @@ var PgStore = class {
|
|
|
564
758
|
async markInputDelivered(runId, sequence) {
|
|
565
759
|
await this.db.update(runInputEvents).set({ deliveredAt: sql2`now()` }).where(and(eq(runInputEvents.runId, runId), eq(runInputEvents.sequence, sequence)));
|
|
566
760
|
}
|
|
761
|
+
async createWorkplanSchedule(input) {
|
|
762
|
+
const [row] = await this.db.insert(workplanSchedules).values({
|
|
763
|
+
id: makeId("sched"),
|
|
764
|
+
planId: input.planId,
|
|
765
|
+
name: input.name,
|
|
766
|
+
cronExpression: input.cronExpression,
|
|
767
|
+
timezone: input.timezone,
|
|
768
|
+
inputs: input.inputs ?? {},
|
|
769
|
+
enabled: input.enabled ?? true,
|
|
770
|
+
nextRunAt: input.enabled === false ? null : input.nextRunAt ? new Date(input.nextRunAt) : null,
|
|
771
|
+
createdBy: input.createdBy ?? null
|
|
772
|
+
}).returning();
|
|
773
|
+
return toScheduleRecord(row);
|
|
774
|
+
}
|
|
775
|
+
async listWorkplanSchedules(enabled) {
|
|
776
|
+
const rows = await this.db.select().from(workplanSchedules).where(enabled === void 0 ? void 0 : eq(workplanSchedules.enabled, enabled)).orderBy(desc(workplanSchedules.createdAt));
|
|
777
|
+
return rows.map(toScheduleRecord);
|
|
778
|
+
}
|
|
779
|
+
async getWorkplanSchedule(scheduleId) {
|
|
780
|
+
const [row] = await this.db.select().from(workplanSchedules).where(eq(workplanSchedules.id, scheduleId));
|
|
781
|
+
return row ? toScheduleRecord(row) : null;
|
|
782
|
+
}
|
|
783
|
+
async updateWorkplanSchedule(scheduleId, input) {
|
|
784
|
+
const patch = {};
|
|
785
|
+
if (input.name !== void 0) patch.name = input.name;
|
|
786
|
+
if (input.cronExpression !== void 0) patch.cronExpression = input.cronExpression;
|
|
787
|
+
if (input.timezone !== void 0) patch.timezone = input.timezone;
|
|
788
|
+
if (input.inputs !== void 0) patch.inputs = input.inputs;
|
|
789
|
+
if (input.enabled !== void 0) patch.enabled = input.enabled;
|
|
790
|
+
if (input.nextRunAt !== void 0) patch.nextRunAt = input.nextRunAt ? new Date(input.nextRunAt) : null;
|
|
791
|
+
patch.updatedAt = /* @__PURE__ */ new Date();
|
|
792
|
+
const [row] = await this.db.update(workplanSchedules).set(patch).where(eq(workplanSchedules.id, scheduleId)).returning();
|
|
793
|
+
return row ? toScheduleRecord(row) : null;
|
|
794
|
+
}
|
|
795
|
+
async deleteWorkplanSchedule(scheduleId) {
|
|
796
|
+
return await this.db.transaction(async (tx) => {
|
|
797
|
+
await tx.update(workplanRuns).set({ scheduleId: null }).where(eq(workplanRuns.scheduleId, scheduleId));
|
|
798
|
+
const deleted = await tx.delete(workplanSchedules).where(eq(workplanSchedules.id, scheduleId)).returning({ id: workplanSchedules.id });
|
|
799
|
+
return deleted.length > 0;
|
|
800
|
+
});
|
|
801
|
+
}
|
|
802
|
+
async listDueWorkplanSchedules(asOf) {
|
|
803
|
+
const asOfDate = new Date(asOf);
|
|
804
|
+
const rows = await this.db.select().from(workplanSchedules).where(and(
|
|
805
|
+
eq(workplanSchedules.enabled, true),
|
|
806
|
+
isNotNull(workplanSchedules.nextRunAt),
|
|
807
|
+
lte(workplanSchedules.nextRunAt, asOfDate)
|
|
808
|
+
)).orderBy(asc(workplanSchedules.nextRunAt));
|
|
809
|
+
return rows.map(toScheduleRecord);
|
|
810
|
+
}
|
|
811
|
+
async markWorkplanScheduleRan(scheduleId, lastRunAt, nextRunAt) {
|
|
812
|
+
await this.db.update(workplanSchedules).set({
|
|
813
|
+
lastRunAt: new Date(lastRunAt),
|
|
814
|
+
nextRunAt: new Date(nextRunAt),
|
|
815
|
+
updatedAt: sql2`now()`
|
|
816
|
+
}).where(eq(workplanSchedules.id, scheduleId));
|
|
817
|
+
}
|
|
818
|
+
async tryCreateWorkplanRun(input) {
|
|
819
|
+
try {
|
|
820
|
+
const [row] = await this.db.insert(workplanRuns).values({
|
|
821
|
+
id: makeId("wprun"),
|
|
822
|
+
scheduleId: input.scheduleId ?? null,
|
|
823
|
+
planId: input.planId,
|
|
824
|
+
planName: input.planName,
|
|
825
|
+
status: "running",
|
|
826
|
+
idempotencyKey: input.idempotencyKey ?? null
|
|
827
|
+
}).returning();
|
|
828
|
+
return toWorkplanRunRecord(row);
|
|
829
|
+
} catch (err) {
|
|
830
|
+
const code = err.code;
|
|
831
|
+
if (input.idempotencyKey && code === "23505") {
|
|
832
|
+
return null;
|
|
833
|
+
}
|
|
834
|
+
throw err;
|
|
835
|
+
}
|
|
836
|
+
}
|
|
837
|
+
async updateWorkplanRun(runId, status, error, endedAt) {
|
|
838
|
+
const [row] = await this.db.update(workplanRuns).set({
|
|
839
|
+
status,
|
|
840
|
+
error: error ?? null,
|
|
841
|
+
endedAt: endedAt ? new Date(endedAt) : status !== "running" ? /* @__PURE__ */ new Date() : null
|
|
842
|
+
}).where(eq(workplanRuns.id, runId)).returning();
|
|
843
|
+
return row ? toWorkplanRunRecord(row) : null;
|
|
844
|
+
}
|
|
845
|
+
async getWorkplanRun(runId) {
|
|
846
|
+
const [row] = await this.db.select().from(workplanRuns).where(eq(workplanRuns.id, runId));
|
|
847
|
+
return row ? toWorkplanRunRecord(row) : null;
|
|
848
|
+
}
|
|
849
|
+
async listWorkplanRuns(filters) {
|
|
850
|
+
const rows = await this.db.select().from(workplanRuns).where(filters?.scheduleId ? eq(workplanRuns.scheduleId, filters.scheduleId) : void 0).orderBy(desc(workplanRuns.createdAt)).limit(100);
|
|
851
|
+
return rows.map(toWorkplanRunRecord);
|
|
852
|
+
}
|
|
853
|
+
async appendWorkplanStepResults(runId, steps) {
|
|
854
|
+
if (steps.length === 0) return [];
|
|
855
|
+
const rows = await this.db.insert(workplanStepResults).values(
|
|
856
|
+
steps.map((step) => ({
|
|
857
|
+
id: makeId("wpstep"),
|
|
858
|
+
workplanRunId: runId,
|
|
859
|
+
stepId: step.stepId,
|
|
860
|
+
stepName: step.stepName,
|
|
861
|
+
output: step.output ?? null,
|
|
862
|
+
exitCode: step.exitCode ?? null,
|
|
863
|
+
durationMs: step.durationMs ?? null,
|
|
864
|
+
metadata: step.metadata ?? null
|
|
865
|
+
}))
|
|
866
|
+
).returning();
|
|
867
|
+
return rows.map(toWorkplanStepResultRecord);
|
|
868
|
+
}
|
|
869
|
+
async listWorkplanStepResults(runId) {
|
|
870
|
+
const rows = await this.db.select().from(workplanStepResults).where(eq(workplanStepResults.workplanRunId, runId)).orderBy(asc(workplanStepResults.createdAt));
|
|
871
|
+
return rows.map(toWorkplanStepResultRecord);
|
|
872
|
+
}
|
|
567
873
|
};
|
|
568
874
|
|
|
569
875
|
// ../db/src/sqlite-store.ts
|
|
570
|
-
import { and as and2, asc as asc2, desc as desc2, eq as eq2, gt as gt2 } from "drizzle-orm";
|
|
876
|
+
import { and as and2, asc as asc2, desc as desc2, eq as eq2, gt as gt2, lte as lte2, sql as sql4 } from "drizzle-orm";
|
|
571
877
|
|
|
572
878
|
// ../db/src/schema/sqlite.ts
|
|
573
879
|
var sqlite_exports = {};
|
|
@@ -577,7 +883,10 @@ __export(sqlite_exports, {
|
|
|
577
883
|
runInputEvents: () => runInputEvents2,
|
|
578
884
|
runLogs: () => runLogs2,
|
|
579
885
|
runs: () => runs2,
|
|
580
|
-
tasks: () => tasks2
|
|
886
|
+
tasks: () => tasks2,
|
|
887
|
+
workplanRuns: () => workplanRuns2,
|
|
888
|
+
workplanSchedules: () => workplanSchedules2,
|
|
889
|
+
workplanStepResults: () => workplanStepResults2
|
|
581
890
|
});
|
|
582
891
|
import { isNull as isNull2, sql as sql3 } from "drizzle-orm";
|
|
583
892
|
import { index as index2, integer as integer2, sqliteTable, text as text2, uniqueIndex as uniqueIndex2 } from "drizzle-orm/sqlite-core";
|
|
@@ -639,6 +948,49 @@ var artifacts2 = sqliteTable("artifacts", {
|
|
|
639
948
|
}, (table) => ({
|
|
640
949
|
runIdIdx: index2("idx_artifacts_run_id").on(table.runId)
|
|
641
950
|
}));
|
|
951
|
+
var workplanSchedules2 = sqliteTable("workplan_schedules", {
|
|
952
|
+
id: text2("id").primaryKey(),
|
|
953
|
+
planId: text2("plan_id").notNull(),
|
|
954
|
+
name: text2("name").notNull(),
|
|
955
|
+
cronExpression: text2("cron_expression").notNull(),
|
|
956
|
+
timezone: text2("timezone").notNull(),
|
|
957
|
+
inputs: text2("inputs", { mode: "json" }).$type().notNull().default({}),
|
|
958
|
+
enabled: integer2("enabled", { mode: "boolean" }).notNull().default(true),
|
|
959
|
+
lastRunAt: text2("last_run_at"),
|
|
960
|
+
nextRunAt: text2("next_run_at"),
|
|
961
|
+
createdBy: text2("created_by"),
|
|
962
|
+
createdAt: text2("created_at").notNull().default(nowDefault),
|
|
963
|
+
updatedAt: text2("updated_at").notNull().default(nowDefault)
|
|
964
|
+
}, (table) => ({
|
|
965
|
+
dueIdx: index2("idx_workplan_schedules_due").on(table.nextRunAt)
|
|
966
|
+
}));
|
|
967
|
+
var workplanRuns2 = sqliteTable("workplan_runs", {
|
|
968
|
+
id: text2("id").primaryKey(),
|
|
969
|
+
scheduleId: text2("schedule_id").references(() => workplanSchedules2.id),
|
|
970
|
+
planId: text2("plan_id").notNull(),
|
|
971
|
+
planName: text2("plan_name").notNull(),
|
|
972
|
+
status: text2("status").notNull().default("running"),
|
|
973
|
+
idempotencyKey: text2("idempotency_key"),
|
|
974
|
+
createdAt: text2("created_at").notNull().default(nowDefault),
|
|
975
|
+
endedAt: text2("ended_at"),
|
|
976
|
+
error: text2("error")
|
|
977
|
+
}, (table) => ({
|
|
978
|
+
scheduleIdx: index2("idx_workplan_runs_schedule_id").on(table.scheduleId),
|
|
979
|
+
idempotencyUnique: uniqueIndex2("idx_workplan_runs_idempotency").on(table.idempotencyKey)
|
|
980
|
+
}));
|
|
981
|
+
var workplanStepResults2 = sqliteTable("workplan_step_results", {
|
|
982
|
+
id: text2("id").primaryKey(),
|
|
983
|
+
workplanRunId: text2("workplan_run_id").notNull().references(() => workplanRuns2.id),
|
|
984
|
+
stepId: text2("step_id").notNull(),
|
|
985
|
+
stepName: text2("step_name").notNull(),
|
|
986
|
+
output: text2("output"),
|
|
987
|
+
exitCode: integer2("exit_code"),
|
|
988
|
+
durationMs: integer2("duration_ms"),
|
|
989
|
+
metadata: text2("metadata", { mode: "json" }).$type(),
|
|
990
|
+
createdAt: text2("created_at").notNull().default(nowDefault)
|
|
991
|
+
}, (table) => ({
|
|
992
|
+
runIdx: index2("idx_workplan_step_results_run_id").on(table.workplanRunId)
|
|
993
|
+
}));
|
|
642
994
|
var runInputEvents2 = sqliteTable("run_input_events", {
|
|
643
995
|
id: integer2("id").primaryKey({ autoIncrement: true }),
|
|
644
996
|
runId: text2("run_id").notNull().references(() => runs2.id),
|
|
@@ -721,7 +1073,49 @@ function toInputEventRecord2(row) {
|
|
|
721
1073
|
deliveredAt: row.deliveredAt ?? null
|
|
722
1074
|
};
|
|
723
1075
|
}
|
|
724
|
-
function
|
|
1076
|
+
function toScheduleRecord2(row) {
|
|
1077
|
+
return {
|
|
1078
|
+
id: row.id,
|
|
1079
|
+
planId: row.planId,
|
|
1080
|
+
name: row.name,
|
|
1081
|
+
cronExpression: row.cronExpression,
|
|
1082
|
+
timezone: row.timezone,
|
|
1083
|
+
inputs: row.inputs ?? {},
|
|
1084
|
+
enabled: row.enabled,
|
|
1085
|
+
lastRunAt: row.lastRunAt ?? null,
|
|
1086
|
+
nextRunAt: row.nextRunAt ?? null,
|
|
1087
|
+
createdBy: row.createdBy ?? null,
|
|
1088
|
+
createdAt: row.createdAt,
|
|
1089
|
+
updatedAt: row.updatedAt
|
|
1090
|
+
};
|
|
1091
|
+
}
|
|
1092
|
+
function toWorkplanRunRecord2(row) {
|
|
1093
|
+
return {
|
|
1094
|
+
id: row.id,
|
|
1095
|
+
scheduleId: row.scheduleId ?? null,
|
|
1096
|
+
planId: row.planId,
|
|
1097
|
+
planName: row.planName,
|
|
1098
|
+
status: row.status,
|
|
1099
|
+
idempotencyKey: row.idempotencyKey ?? null,
|
|
1100
|
+
createdAt: row.createdAt,
|
|
1101
|
+
endedAt: row.endedAt ?? null,
|
|
1102
|
+
error: row.error ?? null
|
|
1103
|
+
};
|
|
1104
|
+
}
|
|
1105
|
+
function toWorkplanStepResultRecord2(row) {
|
|
1106
|
+
return {
|
|
1107
|
+
id: row.id,
|
|
1108
|
+
workplanRunId: row.workplanRunId,
|
|
1109
|
+
stepId: row.stepId,
|
|
1110
|
+
stepName: row.stepName,
|
|
1111
|
+
output: row.output ?? null,
|
|
1112
|
+
exitCode: row.exitCode ?? null,
|
|
1113
|
+
durationMs: row.durationMs ?? null,
|
|
1114
|
+
metadata: row.metadata ?? null,
|
|
1115
|
+
createdAt: row.createdAt
|
|
1116
|
+
};
|
|
1117
|
+
}
|
|
1118
|
+
function mapRawTask(row) {
|
|
725
1119
|
return {
|
|
726
1120
|
id: row.id,
|
|
727
1121
|
kind: row.kind,
|
|
@@ -829,7 +1223,7 @@ var SqliteStore = class {
|
|
|
829
1223
|
).run(runId, task.id, nodeId, max_attempt + 1, now, now, now);
|
|
830
1224
|
raw.prepare("UPDATE tasks SET status = 'assigned', updated_at = ? WHERE id = ?").run(now, task.id);
|
|
831
1225
|
return {
|
|
832
|
-
task:
|
|
1226
|
+
task: mapRawTask(task),
|
|
833
1227
|
run: mapRawRun2(raw.prepare("SELECT * FROM runs WHERE id = ?").get(runId))
|
|
834
1228
|
};
|
|
835
1229
|
})();
|
|
@@ -925,7 +1319,7 @@ var SqliteStore = class {
|
|
|
925
1319
|
}
|
|
926
1320
|
}
|
|
927
1321
|
raw.prepare("UPDATE tasks SET status = 'cancelled', updated_at = ? WHERE id = ?").run(now, taskId);
|
|
928
|
-
return
|
|
1322
|
+
return mapRawTask(raw.prepare("SELECT * FROM tasks WHERE id = ?").get(taskId));
|
|
929
1323
|
})();
|
|
930
1324
|
}
|
|
931
1325
|
async appendInputEvent(runId, input) {
|
|
@@ -959,6 +1353,119 @@ var SqliteStore = class {
|
|
|
959
1353
|
async markInputDelivered(runId, sequence) {
|
|
960
1354
|
await this.db.update(runInputEvents2).set({ deliveredAt: nowIso() }).where(and2(eq2(runInputEvents2.runId, runId), eq2(runInputEvents2.sequence, sequence)));
|
|
961
1355
|
}
|
|
1356
|
+
async createWorkplanSchedule(input) {
|
|
1357
|
+
const now = nowIso();
|
|
1358
|
+
const [row] = await this.db.insert(workplanSchedules2).values({
|
|
1359
|
+
id: makeId("sched"),
|
|
1360
|
+
planId: input.planId,
|
|
1361
|
+
name: input.name,
|
|
1362
|
+
cronExpression: input.cronExpression,
|
|
1363
|
+
timezone: input.timezone,
|
|
1364
|
+
inputs: input.inputs ?? {},
|
|
1365
|
+
enabled: input.enabled ?? true,
|
|
1366
|
+
nextRunAt: input.enabled === false ? null : input.nextRunAt ?? null,
|
|
1367
|
+
createdBy: input.createdBy ?? null,
|
|
1368
|
+
createdAt: now,
|
|
1369
|
+
updatedAt: now
|
|
1370
|
+
}).returning();
|
|
1371
|
+
return toScheduleRecord2(row);
|
|
1372
|
+
}
|
|
1373
|
+
async listWorkplanSchedules(enabled) {
|
|
1374
|
+
const rows = await this.db.select().from(workplanSchedules2).where(enabled === void 0 ? void 0 : eq2(workplanSchedules2.enabled, enabled)).orderBy(desc2(workplanSchedules2.createdAt));
|
|
1375
|
+
return rows.map(toScheduleRecord2);
|
|
1376
|
+
}
|
|
1377
|
+
async getWorkplanSchedule(scheduleId) {
|
|
1378
|
+
const [row] = await this.db.select().from(workplanSchedules2).where(eq2(workplanSchedules2.id, scheduleId));
|
|
1379
|
+
return row ? toScheduleRecord2(row) : null;
|
|
1380
|
+
}
|
|
1381
|
+
async updateWorkplanSchedule(scheduleId, input) {
|
|
1382
|
+
const patch = { updatedAt: nowIso() };
|
|
1383
|
+
if (input.name !== void 0) patch.name = input.name;
|
|
1384
|
+
if (input.cronExpression !== void 0) patch.cronExpression = input.cronExpression;
|
|
1385
|
+
if (input.timezone !== void 0) patch.timezone = input.timezone;
|
|
1386
|
+
if (input.inputs !== void 0) patch.inputs = input.inputs;
|
|
1387
|
+
if (input.enabled !== void 0) patch.enabled = input.enabled;
|
|
1388
|
+
if (input.nextRunAt !== void 0) patch.nextRunAt = input.nextRunAt;
|
|
1389
|
+
const [row] = await this.db.update(workplanSchedules2).set(patch).where(eq2(workplanSchedules2.id, scheduleId)).returning();
|
|
1390
|
+
return row ? toScheduleRecord2(row) : null;
|
|
1391
|
+
}
|
|
1392
|
+
async deleteWorkplanSchedule(scheduleId) {
|
|
1393
|
+
const raw = this.db.$client;
|
|
1394
|
+
return raw.transaction(() => {
|
|
1395
|
+
raw.prepare("UPDATE workplan_runs SET schedule_id = NULL WHERE schedule_id = ?").run(scheduleId);
|
|
1396
|
+
const result = raw.prepare("DELETE FROM workplan_schedules WHERE id = ?").run(scheduleId);
|
|
1397
|
+
return (result.changes ?? 0) > 0;
|
|
1398
|
+
})();
|
|
1399
|
+
}
|
|
1400
|
+
async listDueWorkplanSchedules(asOf) {
|
|
1401
|
+
const rows = await this.db.select().from(workplanSchedules2).where(and2(
|
|
1402
|
+
eq2(workplanSchedules2.enabled, true),
|
|
1403
|
+
sql4`${workplanSchedules2.nextRunAt} IS NOT NULL`,
|
|
1404
|
+
lte2(workplanSchedules2.nextRunAt, asOf)
|
|
1405
|
+
)).orderBy(asc2(workplanSchedules2.nextRunAt));
|
|
1406
|
+
return rows.map(toScheduleRecord2);
|
|
1407
|
+
}
|
|
1408
|
+
async markWorkplanScheduleRan(scheduleId, lastRunAt, nextRunAt) {
|
|
1409
|
+
await this.db.update(workplanSchedules2).set({ lastRunAt, nextRunAt, updatedAt: nowIso() }).where(eq2(workplanSchedules2.id, scheduleId));
|
|
1410
|
+
}
|
|
1411
|
+
async tryCreateWorkplanRun(input) {
|
|
1412
|
+
const now = nowIso();
|
|
1413
|
+
try {
|
|
1414
|
+
const [row] = await this.db.insert(workplanRuns2).values({
|
|
1415
|
+
id: makeId("wprun"),
|
|
1416
|
+
scheduleId: input.scheduleId ?? null,
|
|
1417
|
+
planId: input.planId,
|
|
1418
|
+
planName: input.planName,
|
|
1419
|
+
status: "running",
|
|
1420
|
+
idempotencyKey: input.idempotencyKey ?? null,
|
|
1421
|
+
createdAt: now
|
|
1422
|
+
}).returning();
|
|
1423
|
+
return toWorkplanRunRecord2(row);
|
|
1424
|
+
} catch (err) {
|
|
1425
|
+
if (input.idempotencyKey && err instanceof Error && err.message.includes("UNIQUE constraint failed")) {
|
|
1426
|
+
return null;
|
|
1427
|
+
}
|
|
1428
|
+
throw err;
|
|
1429
|
+
}
|
|
1430
|
+
}
|
|
1431
|
+
async updateWorkplanRun(runId, status, error, endedAt) {
|
|
1432
|
+
const [row] = await this.db.update(workplanRuns2).set({
|
|
1433
|
+
status,
|
|
1434
|
+
error: error ?? null,
|
|
1435
|
+
endedAt: endedAt ?? (status !== "running" ? nowIso() : null)
|
|
1436
|
+
}).where(eq2(workplanRuns2.id, runId)).returning();
|
|
1437
|
+
return row ? toWorkplanRunRecord2(row) : null;
|
|
1438
|
+
}
|
|
1439
|
+
async getWorkplanRun(runId) {
|
|
1440
|
+
const [row] = await this.db.select().from(workplanRuns2).where(eq2(workplanRuns2.id, runId));
|
|
1441
|
+
return row ? toWorkplanRunRecord2(row) : null;
|
|
1442
|
+
}
|
|
1443
|
+
async listWorkplanRuns(filters) {
|
|
1444
|
+
const rows = await this.db.select().from(workplanRuns2).where(filters?.scheduleId ? eq2(workplanRuns2.scheduleId, filters.scheduleId) : void 0).orderBy(desc2(workplanRuns2.createdAt)).limit(100);
|
|
1445
|
+
return rows.map(toWorkplanRunRecord2);
|
|
1446
|
+
}
|
|
1447
|
+
async appendWorkplanStepResults(runId, steps) {
|
|
1448
|
+
if (steps.length === 0) return [];
|
|
1449
|
+
const now = nowIso();
|
|
1450
|
+
const rows = await this.db.insert(workplanStepResults2).values(
|
|
1451
|
+
steps.map((step) => ({
|
|
1452
|
+
id: makeId("wpstep"),
|
|
1453
|
+
workplanRunId: runId,
|
|
1454
|
+
stepId: step.stepId,
|
|
1455
|
+
stepName: step.stepName,
|
|
1456
|
+
output: step.output ?? null,
|
|
1457
|
+
exitCode: step.exitCode ?? null,
|
|
1458
|
+
durationMs: step.durationMs ?? null,
|
|
1459
|
+
metadata: step.metadata ?? null,
|
|
1460
|
+
createdAt: now
|
|
1461
|
+
}))
|
|
1462
|
+
).returning();
|
|
1463
|
+
return rows.map(toWorkplanStepResultRecord2);
|
|
1464
|
+
}
|
|
1465
|
+
async listWorkplanStepResults(runId) {
|
|
1466
|
+
const rows = await this.db.select().from(workplanStepResults2).where(eq2(workplanStepResults2.workplanRunId, runId)).orderBy(asc2(workplanStepResults2.createdAt));
|
|
1467
|
+
return rows.map(toWorkplanStepResultRecord2);
|
|
1468
|
+
}
|
|
962
1469
|
};
|
|
963
1470
|
|
|
964
1471
|
// ../db/src/client.ts
|
|
@@ -1125,6 +1632,497 @@ function validateInputEvent(input) {
|
|
|
1125
1632
|
return inputEventSchema.parse(input);
|
|
1126
1633
|
}
|
|
1127
1634
|
|
|
1635
|
+
// ../server/src/schedule-validation.ts
|
|
1636
|
+
import { z as z3 } from "zod";
|
|
1637
|
+
var createScheduleSchema = z3.object({
|
|
1638
|
+
planId: z3.string().min(1),
|
|
1639
|
+
name: z3.string().min(1),
|
|
1640
|
+
cronExpression: z3.string().min(1),
|
|
1641
|
+
timezone: z3.string().min(1),
|
|
1642
|
+
inputs: z3.record(z3.unknown()).optional(),
|
|
1643
|
+
enabled: z3.boolean().optional()
|
|
1644
|
+
});
|
|
1645
|
+
var updateScheduleSchema = z3.object({
|
|
1646
|
+
name: z3.string().min(1).optional(),
|
|
1647
|
+
cronExpression: z3.string().min(1).optional(),
|
|
1648
|
+
timezone: z3.string().min(1).optional(),
|
|
1649
|
+
inputs: z3.record(z3.unknown()).optional(),
|
|
1650
|
+
enabled: z3.boolean().optional()
|
|
1651
|
+
});
|
|
1652
|
+
function validateCreateSchedule(body) {
|
|
1653
|
+
return createScheduleSchema.parse(body);
|
|
1654
|
+
}
|
|
1655
|
+
function validateUpdateSchedule(body) {
|
|
1656
|
+
return updateScheduleSchema.parse(body);
|
|
1657
|
+
}
|
|
1658
|
+
|
|
1659
|
+
// ../agent-skills/src/skills/code-review.ts
|
|
1660
|
+
function codeReviewPlan(opts = {}) {
|
|
1661
|
+
const { repoPath = ".", model = "claude-haiku-4-5-20251001", branch } = opts;
|
|
1662
|
+
const diffCmd = branch ? `git diff ${branch}..HEAD` : "git diff HEAD~1";
|
|
1663
|
+
return {
|
|
1664
|
+
id: "code-review",
|
|
1665
|
+
name: "Code Review",
|
|
1666
|
+
steps: [
|
|
1667
|
+
{
|
|
1668
|
+
id: "diff",
|
|
1669
|
+
name: "Git Diff",
|
|
1670
|
+
adapter: "shell",
|
|
1671
|
+
provider: "shell",
|
|
1672
|
+
payload: { command: diffCmd, cwd: repoPath },
|
|
1673
|
+
output: { dest: "next" }
|
|
1674
|
+
},
|
|
1675
|
+
{
|
|
1676
|
+
id: "summarize",
|
|
1677
|
+
name: "Summarize",
|
|
1678
|
+
adapter: "ollama",
|
|
1679
|
+
provider: "ollama",
|
|
1680
|
+
model: "llama3",
|
|
1681
|
+
payload: { prompt: "Summarize these code changes concisely:\n{{prevOutput}}" },
|
|
1682
|
+
output: { dest: "next" }
|
|
1683
|
+
},
|
|
1684
|
+
{
|
|
1685
|
+
id: "critique",
|
|
1686
|
+
name: "Critique",
|
|
1687
|
+
adapter: "anthropic",
|
|
1688
|
+
provider: "anthropic",
|
|
1689
|
+
model,
|
|
1690
|
+
payload: { prompt: "Review this summary for correctness, security issues, and code quality:\n{{prevOutput}}" }
|
|
1691
|
+
}
|
|
1692
|
+
]
|
|
1693
|
+
};
|
|
1694
|
+
}
|
|
1695
|
+
|
|
1696
|
+
// ../agent-skills/src/skills/hello.ts
|
|
1697
|
+
function helloPlan(opts = {}) {
|
|
1698
|
+
const message = opts.message ?? "hello from workplane";
|
|
1699
|
+
return {
|
|
1700
|
+
id: "hello",
|
|
1701
|
+
name: "Hello",
|
|
1702
|
+
description: "Single shell echo \u2014 useful for scheduler smoke tests",
|
|
1703
|
+
steps: [
|
|
1704
|
+
{
|
|
1705
|
+
id: "echo",
|
|
1706
|
+
name: "Echo",
|
|
1707
|
+
adapter: "shell",
|
|
1708
|
+
provider: "shell",
|
|
1709
|
+
payload: { command: `echo ${JSON.stringify(message)}` }
|
|
1710
|
+
}
|
|
1711
|
+
]
|
|
1712
|
+
};
|
|
1713
|
+
}
|
|
1714
|
+
|
|
1715
|
+
// ../agent-skills/src/skills/summarize-file.ts
|
|
1716
|
+
function summarizeFilePlan(opts) {
|
|
1717
|
+
const { filePath, provider = "ollama", model = "llama3" } = opts;
|
|
1718
|
+
return {
|
|
1719
|
+
id: "summarize-file",
|
|
1720
|
+
name: "Summarize File",
|
|
1721
|
+
steps: [
|
|
1722
|
+
{
|
|
1723
|
+
id: "read",
|
|
1724
|
+
name: "Read File",
|
|
1725
|
+
adapter: "file",
|
|
1726
|
+
provider: "file",
|
|
1727
|
+
payload: { path: filePath },
|
|
1728
|
+
output: { dest: "next" }
|
|
1729
|
+
},
|
|
1730
|
+
{
|
|
1731
|
+
id: "summarize",
|
|
1732
|
+
name: "Summarize",
|
|
1733
|
+
adapter: provider,
|
|
1734
|
+
provider,
|
|
1735
|
+
model,
|
|
1736
|
+
payload: { prompt: "Summarize the following file content concisely:\n{{prevOutput}}" }
|
|
1737
|
+
}
|
|
1738
|
+
]
|
|
1739
|
+
};
|
|
1740
|
+
}
|
|
1741
|
+
|
|
1742
|
+
// ../agent-skills/src/skills/index.ts
|
|
1743
|
+
var SkillRegistry = class {
|
|
1744
|
+
skills = /* @__PURE__ */ new Map();
|
|
1745
|
+
register(entry) {
|
|
1746
|
+
this.skills.set(entry.name, entry);
|
|
1747
|
+
}
|
|
1748
|
+
get(name) {
|
|
1749
|
+
return this.skills.get(name);
|
|
1750
|
+
}
|
|
1751
|
+
list() {
|
|
1752
|
+
return Array.from(this.skills.values());
|
|
1753
|
+
}
|
|
1754
|
+
};
|
|
1755
|
+
function createDefaultRegistry() {
|
|
1756
|
+
const registry = new SkillRegistry();
|
|
1757
|
+
registry.register({
|
|
1758
|
+
name: "hello",
|
|
1759
|
+
description: "Shell echo (scheduler smoke test)",
|
|
1760
|
+
buildPlan: (opts) => helloPlan({
|
|
1761
|
+
message: typeof opts.message === "string" ? opts.message : void 0
|
|
1762
|
+
})
|
|
1763
|
+
});
|
|
1764
|
+
registry.register({
|
|
1765
|
+
name: "code-review",
|
|
1766
|
+
description: "Git diff \u2192 local summarize (ollama) \u2192 frontier critique (anthropic)",
|
|
1767
|
+
buildPlan: (opts) => codeReviewPlan({
|
|
1768
|
+
repoPath: typeof opts.repo === "string" ? opts.repo : void 0,
|
|
1769
|
+
model: typeof opts.model === "string" ? opts.model : void 0,
|
|
1770
|
+
branch: typeof opts.branch === "string" ? opts.branch : void 0
|
|
1771
|
+
})
|
|
1772
|
+
});
|
|
1773
|
+
registry.register({
|
|
1774
|
+
name: "summarize-file",
|
|
1775
|
+
description: "Read file \u2192 summarize with configurable provider (default: ollama)",
|
|
1776
|
+
buildPlan: (opts) => {
|
|
1777
|
+
const filePath = typeof opts.file === "string" ? opts.file : "";
|
|
1778
|
+
if (!filePath) throw new Error("--file is required for summarize-file");
|
|
1779
|
+
return summarizeFilePlan({
|
|
1780
|
+
filePath,
|
|
1781
|
+
provider: typeof opts.provider === "string" ? opts.provider : void 0,
|
|
1782
|
+
model: typeof opts.model === "string" ? opts.model : void 0
|
|
1783
|
+
});
|
|
1784
|
+
}
|
|
1785
|
+
});
|
|
1786
|
+
return registry;
|
|
1787
|
+
}
|
|
1788
|
+
|
|
1789
|
+
// ../workplans/src/sinks/stdout.ts
|
|
1790
|
+
function printStepResult(stepName, output) {
|
|
1791
|
+
process.stdout.write(`=== step: ${stepName} ===
|
|
1792
|
+
${output}
|
|
1793
|
+
|
|
1794
|
+
`);
|
|
1795
|
+
}
|
|
1796
|
+
|
|
1797
|
+
// ../workplans/src/context.ts
|
|
1798
|
+
var WorkplaneNodeHandle = class {
|
|
1799
|
+
constructor(serverUrl, nodeToken) {
|
|
1800
|
+
this.serverUrl = serverUrl;
|
|
1801
|
+
this.nodeToken = nodeToken;
|
|
1802
|
+
}
|
|
1803
|
+
serverUrl;
|
|
1804
|
+
nodeToken;
|
|
1805
|
+
async execute(step) {
|
|
1806
|
+
const start = Date.now();
|
|
1807
|
+
const taskRes = await fetch(`${this.serverUrl}/tasks`, {
|
|
1808
|
+
method: "POST",
|
|
1809
|
+
headers: {
|
|
1810
|
+
"content-type": "application/json",
|
|
1811
|
+
authorization: `Bearer ${this.nodeToken}`
|
|
1812
|
+
},
|
|
1813
|
+
body: JSON.stringify({
|
|
1814
|
+
kind: "workplan.step",
|
|
1815
|
+
adapter: step.adapter,
|
|
1816
|
+
payload: step.payload,
|
|
1817
|
+
requires: step.requires ?? []
|
|
1818
|
+
})
|
|
1819
|
+
});
|
|
1820
|
+
if (!taskRes.ok) {
|
|
1821
|
+
throw new Error(`failed to submit step ${step.id}: ${taskRes.status}`);
|
|
1822
|
+
}
|
|
1823
|
+
const task = await taskRes.json();
|
|
1824
|
+
const output = await this.pollUntilDone(task.id);
|
|
1825
|
+
return { stepId: step.id, output, exitCode: 0, durationMs: Date.now() - start };
|
|
1826
|
+
}
|
|
1827
|
+
async pollUntilDone(taskId) {
|
|
1828
|
+
const terminalStatuses = /* @__PURE__ */ new Set(["succeeded", "failed", "cancelled"]);
|
|
1829
|
+
for (; ; ) {
|
|
1830
|
+
await new Promise((r) => setTimeout(r, 1e3));
|
|
1831
|
+
const res = await fetch(`${this.serverUrl}/tasks/${taskId}`, {
|
|
1832
|
+
headers: { authorization: `Bearer ${this.nodeToken}` }
|
|
1833
|
+
});
|
|
1834
|
+
const task = await res.json();
|
|
1835
|
+
if (terminalStatuses.has(task.status)) {
|
|
1836
|
+
if (task.status !== "succeeded") {
|
|
1837
|
+
throw new Error(`step task ${taskId} ended with status: ${task.status}`);
|
|
1838
|
+
}
|
|
1839
|
+
return typeof task.payload?.output === "string" ? task.payload.output : "";
|
|
1840
|
+
}
|
|
1841
|
+
}
|
|
1842
|
+
}
|
|
1843
|
+
};
|
|
1844
|
+
var LocalWorkplanContext = class {
|
|
1845
|
+
serverUrl;
|
|
1846
|
+
nodeToken;
|
|
1847
|
+
constructor(opts = {}) {
|
|
1848
|
+
this.serverUrl = opts.serverUrl ?? process.env.WORKPLANE_SERVER_URL ?? "http://localhost:8787";
|
|
1849
|
+
this.nodeToken = opts.nodeToken ?? process.env.WORKPLANE_NODE_TOKEN ?? "";
|
|
1850
|
+
}
|
|
1851
|
+
async resolveNode(_requires, _provider) {
|
|
1852
|
+
return new WorkplaneNodeHandle(this.serverUrl, this.nodeToken);
|
|
1853
|
+
}
|
|
1854
|
+
emitResult(_stepId, stepName, output) {
|
|
1855
|
+
printStepResult(stepName, output);
|
|
1856
|
+
}
|
|
1857
|
+
};
|
|
1858
|
+
|
|
1859
|
+
// ../workplans/src/template.ts
|
|
1860
|
+
function applyTemplate(payload, prevOutput) {
|
|
1861
|
+
return Object.fromEntries(
|
|
1862
|
+
Object.entries(payload).map(([k, v]) => [
|
|
1863
|
+
k,
|
|
1864
|
+
typeof v === "string" ? v.replaceAll("{{prevOutput}}", prevOutput) : v
|
|
1865
|
+
])
|
|
1866
|
+
);
|
|
1867
|
+
}
|
|
1868
|
+
|
|
1869
|
+
// ../workplans/src/runner.ts
|
|
1870
|
+
var INLINE_PROVIDERS = /* @__PURE__ */ new Set(["anthropic", "openai", "ollama", "shell", "file"]);
|
|
1871
|
+
async function runInlineStep(step, prompt) {
|
|
1872
|
+
const provider = step.provider;
|
|
1873
|
+
if (provider === "anthropic") {
|
|
1874
|
+
const { runAnthropicStep: runAnthropicStep2 } = await Promise.resolve().then(() => (init_anthropic(), anthropic_exports));
|
|
1875
|
+
return runAnthropicStep2({ model: step.model ?? "claude-haiku-4-5-20251001", prompt });
|
|
1876
|
+
}
|
|
1877
|
+
if (provider === "openai") {
|
|
1878
|
+
const { runOpenAIStep: runOpenAIStep2 } = await Promise.resolve().then(() => (init_openai(), openai_exports));
|
|
1879
|
+
return runOpenAIStep2({ model: step.model ?? "gpt-4o-mini", prompt });
|
|
1880
|
+
}
|
|
1881
|
+
if (provider === "ollama") {
|
|
1882
|
+
const { runOllamaStep: runOllamaStep2 } = await Promise.resolve().then(() => (init_ollama(), ollama_exports));
|
|
1883
|
+
return runOllamaStep2({ model: step.model ?? "llama3", prompt });
|
|
1884
|
+
}
|
|
1885
|
+
if (provider === "shell") {
|
|
1886
|
+
const { runShellStep: runShellStep2 } = await Promise.resolve().then(() => (init_shell(), shell_exports));
|
|
1887
|
+
return runShellStep2({
|
|
1888
|
+
command: String(step.payload.command ?? ""),
|
|
1889
|
+
cwd: typeof step.payload.cwd === "string" ? step.payload.cwd : void 0
|
|
1890
|
+
});
|
|
1891
|
+
}
|
|
1892
|
+
if (provider === "file") {
|
|
1893
|
+
const { runFileStep: runFileStep2 } = await Promise.resolve().then(() => (init_file(), file_exports));
|
|
1894
|
+
return runFileStep2({ path: String(step.payload.path ?? "") });
|
|
1895
|
+
}
|
|
1896
|
+
throw new Error(`unknown inline provider: ${provider}`);
|
|
1897
|
+
}
|
|
1898
|
+
var SequentialWorkplanRunner = class {
|
|
1899
|
+
async run(plan, ctx) {
|
|
1900
|
+
const results = [];
|
|
1901
|
+
let prevOutput = "";
|
|
1902
|
+
let planSucceeded = true;
|
|
1903
|
+
for (const step of plan.steps) {
|
|
1904
|
+
const payload = prevOutput ? applyTemplate(step.payload, prevOutput) : step.payload;
|
|
1905
|
+
const prompt = typeof payload.prompt === "string" ? payload.prompt : "";
|
|
1906
|
+
const start = Date.now();
|
|
1907
|
+
let output = "";
|
|
1908
|
+
let exitCode = 0;
|
|
1909
|
+
try {
|
|
1910
|
+
if (step.provider && INLINE_PROVIDERS.has(step.provider)) {
|
|
1911
|
+
output = await runInlineStep({ ...step, payload }, prompt);
|
|
1912
|
+
} else {
|
|
1913
|
+
const node = await ctx.resolveNode(step.requires ?? [], step.provider);
|
|
1914
|
+
const result = await node.execute({ ...step, payload });
|
|
1915
|
+
output = result.output;
|
|
1916
|
+
exitCode = result.exitCode;
|
|
1917
|
+
}
|
|
1918
|
+
} catch (err) {
|
|
1919
|
+
exitCode = 1;
|
|
1920
|
+
output = err instanceof Error ? err.message : String(err);
|
|
1921
|
+
planSucceeded = false;
|
|
1922
|
+
const stepResult2 = {
|
|
1923
|
+
stepId: step.id,
|
|
1924
|
+
output,
|
|
1925
|
+
exitCode,
|
|
1926
|
+
durationMs: Date.now() - start
|
|
1927
|
+
};
|
|
1928
|
+
results.push(stepResult2);
|
|
1929
|
+
ctx.emitResult(step.id, step.name, output);
|
|
1930
|
+
if (!step.continueOnError) {
|
|
1931
|
+
return {
|
|
1932
|
+
planId: plan.id,
|
|
1933
|
+
status: "step_failed",
|
|
1934
|
+
steps: results,
|
|
1935
|
+
succeeded: false
|
|
1936
|
+
};
|
|
1937
|
+
}
|
|
1938
|
+
prevOutput = "";
|
|
1939
|
+
continue;
|
|
1940
|
+
}
|
|
1941
|
+
const dest = step.output?.dest ?? "stdout";
|
|
1942
|
+
const stepResult = {
|
|
1943
|
+
stepId: step.id,
|
|
1944
|
+
output,
|
|
1945
|
+
exitCode,
|
|
1946
|
+
durationMs: Date.now() - start
|
|
1947
|
+
};
|
|
1948
|
+
results.push(stepResult);
|
|
1949
|
+
if (dest === "stdout" || dest === "artifact") {
|
|
1950
|
+
ctx.emitResult(step.id, step.name, output);
|
|
1951
|
+
prevOutput = "";
|
|
1952
|
+
} else if (dest === "next") {
|
|
1953
|
+
prevOutput = output;
|
|
1954
|
+
}
|
|
1955
|
+
}
|
|
1956
|
+
return {
|
|
1957
|
+
planId: plan.id,
|
|
1958
|
+
status: planSucceeded ? "completed" : "failed",
|
|
1959
|
+
steps: results,
|
|
1960
|
+
succeeded: planSucceeded
|
|
1961
|
+
};
|
|
1962
|
+
}
|
|
1963
|
+
};
|
|
1964
|
+
|
|
1965
|
+
// ../workplans/src/schedule-builder.ts
|
|
1966
|
+
import { CronExpressionParser } from "cron-parser";
|
|
1967
|
+
var ScheduleBuilder = class _ScheduleBuilder {
|
|
1968
|
+
static nextRunAt(cronExpression, timezone) {
|
|
1969
|
+
const expr = CronExpressionParser.parse(cronExpression, { tz: timezone });
|
|
1970
|
+
return expr.next().toISOString() ?? (/* @__PURE__ */ new Date()).toISOString();
|
|
1971
|
+
}
|
|
1972
|
+
static withNextRunAt(schedule) {
|
|
1973
|
+
return {
|
|
1974
|
+
...schedule,
|
|
1975
|
+
nextRunAt: _ScheduleBuilder.nextRunAt(schedule.cronExpression, schedule.timezone)
|
|
1976
|
+
};
|
|
1977
|
+
}
|
|
1978
|
+
};
|
|
1979
|
+
|
|
1980
|
+
// ../workplans/src/scheduler.ts
|
|
1981
|
+
var WorkplanScheduler = class {
|
|
1982
|
+
constructor(store, resolver, contextFactory, runner = new SequentialWorkplanRunner()) {
|
|
1983
|
+
this.store = store;
|
|
1984
|
+
this.resolver = resolver;
|
|
1985
|
+
this.contextFactory = contextFactory;
|
|
1986
|
+
this.runner = runner;
|
|
1987
|
+
}
|
|
1988
|
+
store;
|
|
1989
|
+
resolver;
|
|
1990
|
+
contextFactory;
|
|
1991
|
+
runner;
|
|
1992
|
+
async tick(now = /* @__PURE__ */ new Date()) {
|
|
1993
|
+
const due = await this.store.listDueWorkplanSchedules(now.toISOString());
|
|
1994
|
+
const launched = [];
|
|
1995
|
+
for (const schedule of due) {
|
|
1996
|
+
if (!schedule.nextRunAt) {
|
|
1997
|
+
continue;
|
|
1998
|
+
}
|
|
1999
|
+
const run = await this.executeSchedule(schedule, {
|
|
2000
|
+
scheduledTime: schedule.nextRunAt,
|
|
2001
|
+
advanceSchedule: true
|
|
2002
|
+
});
|
|
2003
|
+
if (run) {
|
|
2004
|
+
launched.push(run);
|
|
2005
|
+
}
|
|
2006
|
+
}
|
|
2007
|
+
return launched;
|
|
2008
|
+
}
|
|
2009
|
+
async runNow(scheduleId) {
|
|
2010
|
+
const schedule = await this.store.getWorkplanSchedule(scheduleId);
|
|
2011
|
+
if (!schedule) {
|
|
2012
|
+
return null;
|
|
2013
|
+
}
|
|
2014
|
+
return this.executeSchedule(schedule, {
|
|
2015
|
+
scheduledTime: (/* @__PURE__ */ new Date()).toISOString(),
|
|
2016
|
+
advanceSchedule: false
|
|
2017
|
+
});
|
|
2018
|
+
}
|
|
2019
|
+
async executeSchedule(schedule, options) {
|
|
2020
|
+
const idempotencyKey = options.idempotencyKey ?? (options.advanceSchedule ? `${schedule.id}:${options.scheduledTime}` : void 0);
|
|
2021
|
+
const run = await this.store.tryCreateWorkplanRun({
|
|
2022
|
+
scheduleId: schedule.id,
|
|
2023
|
+
planId: schedule.planId,
|
|
2024
|
+
planName: schedule.name,
|
|
2025
|
+
idempotencyKey
|
|
2026
|
+
});
|
|
2027
|
+
if (!run) {
|
|
2028
|
+
return null;
|
|
2029
|
+
}
|
|
2030
|
+
if (options.advanceSchedule) {
|
|
2031
|
+
const nextRunAt = ScheduleBuilder.nextRunAt(schedule.cronExpression, schedule.timezone);
|
|
2032
|
+
await this.store.markWorkplanScheduleRan(schedule.id, options.scheduledTime, nextRunAt);
|
|
2033
|
+
}
|
|
2034
|
+
try {
|
|
2035
|
+
const plan = this.resolver.resolve(schedule.planId, schedule.inputs);
|
|
2036
|
+
const result = await this.runner.run(plan, this.contextFactory());
|
|
2037
|
+
const stepNames = new Map(plan.steps.map((step) => [step.id, step.name]));
|
|
2038
|
+
const stepInputs = result.steps.map((step) => ({
|
|
2039
|
+
stepId: step.stepId,
|
|
2040
|
+
stepName: stepNames.get(step.stepId) ?? step.stepId,
|
|
2041
|
+
output: step.output,
|
|
2042
|
+
exitCode: step.exitCode,
|
|
2043
|
+
durationMs: step.durationMs,
|
|
2044
|
+
metadata: step.metadata
|
|
2045
|
+
}));
|
|
2046
|
+
await this.store.appendWorkplanStepResults(run.id, stepInputs);
|
|
2047
|
+
const error = result.succeeded ? void 0 : result.steps.at(-1)?.output;
|
|
2048
|
+
return await this.store.updateWorkplanRun(run.id, result.status, error);
|
|
2049
|
+
} catch (err) {
|
|
2050
|
+
const message = err instanceof Error ? err.message : String(err);
|
|
2051
|
+
return await this.store.updateWorkplanRun(run.id, "failed", message);
|
|
2052
|
+
}
|
|
2053
|
+
}
|
|
2054
|
+
};
|
|
2055
|
+
|
|
2056
|
+
// ../server/src/schedule-service.ts
|
|
2057
|
+
var skillRegistry = createDefaultRegistry();
|
|
2058
|
+
function createWorkplanScheduler(store) {
|
|
2059
|
+
return new WorkplanScheduler(
|
|
2060
|
+
store,
|
|
2061
|
+
{
|
|
2062
|
+
resolve(planId, inputs) {
|
|
2063
|
+
const skill = skillRegistry.get(planId);
|
|
2064
|
+
if (!skill) {
|
|
2065
|
+
throw new Error(`unknown workplan skill: ${planId}`);
|
|
2066
|
+
}
|
|
2067
|
+
return skill.buildPlan(inputs);
|
|
2068
|
+
}
|
|
2069
|
+
},
|
|
2070
|
+
() => new LocalWorkplanContext()
|
|
2071
|
+
);
|
|
2072
|
+
}
|
|
2073
|
+
function withScheduleTiming(input) {
|
|
2074
|
+
if (input.enabled === false) {
|
|
2075
|
+
return { ...input, nextRunAt: null };
|
|
2076
|
+
}
|
|
2077
|
+
return {
|
|
2078
|
+
...input,
|
|
2079
|
+
nextRunAt: ScheduleBuilder.nextRunAt(input.cronExpression, input.timezone)
|
|
2080
|
+
};
|
|
2081
|
+
}
|
|
2082
|
+
function withUpdatedScheduleTiming(current, input) {
|
|
2083
|
+
const cronExpression = input.cronExpression ?? current.cronExpression;
|
|
2084
|
+
const timezone = input.timezone ?? current.timezone;
|
|
2085
|
+
const enabled = input.enabled ?? current.enabled;
|
|
2086
|
+
if (!enabled) {
|
|
2087
|
+
return { ...input, nextRunAt: null };
|
|
2088
|
+
}
|
|
2089
|
+
if (input.cronExpression !== void 0 || input.timezone !== void 0 || input.enabled === true) {
|
|
2090
|
+
return {
|
|
2091
|
+
...input,
|
|
2092
|
+
nextRunAt: ScheduleBuilder.nextRunAt(cronExpression, timezone)
|
|
2093
|
+
};
|
|
2094
|
+
}
|
|
2095
|
+
return input;
|
|
2096
|
+
}
|
|
2097
|
+
function isSchedulerEnabled() {
|
|
2098
|
+
return process.env.WORKPLANE_SCHEDULER_ENABLED !== "false";
|
|
2099
|
+
}
|
|
2100
|
+
function schedulerIntervalMs() {
|
|
2101
|
+
const raw = process.env.WORKPLANE_SCHEDULER_INTERVAL_MS;
|
|
2102
|
+
const parsed = raw ? Number(raw) : 6e4;
|
|
2103
|
+
return Number.isFinite(parsed) && parsed > 0 ? parsed : 6e4;
|
|
2104
|
+
}
|
|
2105
|
+
function startSchedulerLoop(scheduler) {
|
|
2106
|
+
const intervalMs = schedulerIntervalMs();
|
|
2107
|
+
const timer = setInterval(() => {
|
|
2108
|
+
void scheduler.tick().catch((error) => {
|
|
2109
|
+
process.stderr.write(`workplan scheduler tick failed: ${String(error)}
|
|
2110
|
+
`);
|
|
2111
|
+
});
|
|
2112
|
+
}, intervalMs);
|
|
2113
|
+
timer.unref();
|
|
2114
|
+
void scheduler.tick().catch((error) => {
|
|
2115
|
+
process.stderr.write(`workplan scheduler initial tick failed: ${String(error)}
|
|
2116
|
+
`);
|
|
2117
|
+
});
|
|
2118
|
+
process.stdout.write(`workplan scheduler enabled (interval ${intervalMs}ms)
|
|
2119
|
+
`);
|
|
2120
|
+
return () => clearInterval(timer);
|
|
2121
|
+
}
|
|
2122
|
+
function listKnownPlanIds() {
|
|
2123
|
+
return skillRegistry.list().map((skill) => skill.name);
|
|
2124
|
+
}
|
|
2125
|
+
|
|
1128
2126
|
// ../core/src/auth.ts
|
|
1129
2127
|
function parseBearerToken(authorizationHeader) {
|
|
1130
2128
|
if (!authorizationHeader) {
|
|
@@ -1211,6 +2209,21 @@ function routeAuthFor(method, pathname) {
|
|
|
1211
2209
|
if (method === "POST" && /^\/tasks\/[^/]+\/cancel$/.test(pathname)) {
|
|
1212
2210
|
return "operator";
|
|
1213
2211
|
}
|
|
2212
|
+
if (method === "POST" && pathname === "/schedules") {
|
|
2213
|
+
return "operator";
|
|
2214
|
+
}
|
|
2215
|
+
if (method === "PATCH" && /^\/schedules\/[^/]+$/.test(pathname)) {
|
|
2216
|
+
return "operator";
|
|
2217
|
+
}
|
|
2218
|
+
if (method === "DELETE" && /^\/schedules\/[^/]+$/.test(pathname)) {
|
|
2219
|
+
return "operator";
|
|
2220
|
+
}
|
|
2221
|
+
if (method === "POST" && /^\/schedules\/[^/]+\/run$/.test(pathname)) {
|
|
2222
|
+
return "operator";
|
|
2223
|
+
}
|
|
2224
|
+
if (method === "POST" && pathname === "/schedules/tick") {
|
|
2225
|
+
return "operator";
|
|
2226
|
+
}
|
|
1214
2227
|
return "public";
|
|
1215
2228
|
}
|
|
1216
2229
|
function requiresConfiguredToken(auth, config) {
|
|
@@ -1266,7 +2279,17 @@ async function buildWorkflows(store, config) {
|
|
|
1266
2279
|
async function main() {
|
|
1267
2280
|
const config = loadServerConfig();
|
|
1268
2281
|
const { store, close: closeStore } = createStore(config.databaseUrl);
|
|
2282
|
+
const scheduler = createWorkplanScheduler(store);
|
|
2283
|
+
if (process.env.WORKPLANE_USE_DBOS === "true" && isSchedulerEnabled()) {
|
|
2284
|
+
const { registerDbosSchedulerTick: registerDbosSchedulerTick2 } = await Promise.resolve().then(() => (init_scheduler(), scheduler_exports));
|
|
2285
|
+
registerDbosSchedulerTick2(scheduler);
|
|
2286
|
+
}
|
|
1269
2287
|
const { workflows, shutdown: shutdownWorkflows } = await buildWorkflows(store, config);
|
|
2288
|
+
let stopSchedulerLoop = () => {
|
|
2289
|
+
};
|
|
2290
|
+
if (process.env.WORKPLANE_USE_DBOS !== "true" && isSchedulerEnabled()) {
|
|
2291
|
+
stopSchedulerLoop = startSchedulerLoop(scheduler);
|
|
2292
|
+
}
|
|
1270
2293
|
const server = createServer(async (req, res) => {
|
|
1271
2294
|
try {
|
|
1272
2295
|
if (!req.url || !req.method) {
|
|
@@ -1430,6 +2453,114 @@ async function main() {
|
|
|
1430
2453
|
writeJson(res, 200, { ok: true });
|
|
1431
2454
|
return;
|
|
1432
2455
|
}
|
|
2456
|
+
if (req.method === "GET" && url.pathname === "/plans") {
|
|
2457
|
+
writeJson(res, 200, { plans: listKnownPlanIds() });
|
|
2458
|
+
return;
|
|
2459
|
+
}
|
|
2460
|
+
if (req.method === "POST" && url.pathname === "/schedules") {
|
|
2461
|
+
const body = await readJson(req);
|
|
2462
|
+
const validated = validateCreateSchedule(body);
|
|
2463
|
+
if (!listKnownPlanIds().includes(validated.planId)) {
|
|
2464
|
+
writeJson(res, 400, {
|
|
2465
|
+
error: `unknown planId: ${validated.planId}`,
|
|
2466
|
+
availablePlans: listKnownPlanIds()
|
|
2467
|
+
});
|
|
2468
|
+
return;
|
|
2469
|
+
}
|
|
2470
|
+
const schedule = await store.createWorkplanSchedule(withScheduleTiming(validated));
|
|
2471
|
+
writeJson(res, 201, schedule);
|
|
2472
|
+
return;
|
|
2473
|
+
}
|
|
2474
|
+
if (req.method === "GET" && url.pathname === "/schedules") {
|
|
2475
|
+
const enabledParam = url.searchParams.get("enabled");
|
|
2476
|
+
const enabled = enabledParam === null ? void 0 : enabledParam === "true";
|
|
2477
|
+
const schedules = await store.listWorkplanSchedules(enabled);
|
|
2478
|
+
writeJson(res, 200, { schedules });
|
|
2479
|
+
return;
|
|
2480
|
+
}
|
|
2481
|
+
if (req.method === "GET" && /^\/schedules\/[^/]+$/.test(url.pathname)) {
|
|
2482
|
+
const scheduleId = url.pathname.split("/")[2];
|
|
2483
|
+
const schedule = await store.getWorkplanSchedule(scheduleId);
|
|
2484
|
+
if (!schedule) {
|
|
2485
|
+
writeJson(res, 404, { error: "schedule not found" });
|
|
2486
|
+
return;
|
|
2487
|
+
}
|
|
2488
|
+
writeJson(res, 200, schedule);
|
|
2489
|
+
return;
|
|
2490
|
+
}
|
|
2491
|
+
if (req.method === "PATCH" && /^\/schedules\/[^/]+$/.test(url.pathname)) {
|
|
2492
|
+
const scheduleId = url.pathname.split("/")[2];
|
|
2493
|
+
const existing = await store.getWorkplanSchedule(scheduleId);
|
|
2494
|
+
if (!existing) {
|
|
2495
|
+
writeJson(res, 404, { error: "schedule not found" });
|
|
2496
|
+
return;
|
|
2497
|
+
}
|
|
2498
|
+
const body = await readJson(req);
|
|
2499
|
+
const validated = validateUpdateSchedule(body);
|
|
2500
|
+
const schedule = await store.updateWorkplanSchedule(
|
|
2501
|
+
scheduleId,
|
|
2502
|
+
withUpdatedScheduleTiming(existing, validated)
|
|
2503
|
+
);
|
|
2504
|
+
writeJson(res, 200, schedule);
|
|
2505
|
+
return;
|
|
2506
|
+
}
|
|
2507
|
+
if (req.method === "DELETE" && /^\/schedules\/[^/]+$/.test(url.pathname)) {
|
|
2508
|
+
const scheduleId = url.pathname.split("/")[2];
|
|
2509
|
+
const deleted = await store.deleteWorkplanSchedule(scheduleId);
|
|
2510
|
+
if (!deleted) {
|
|
2511
|
+
writeJson(res, 404, { error: "schedule not found" });
|
|
2512
|
+
return;
|
|
2513
|
+
}
|
|
2514
|
+
writeJson(res, 200, { ok: true });
|
|
2515
|
+
return;
|
|
2516
|
+
}
|
|
2517
|
+
if (req.method === "POST" && /^\/schedules\/[^/]+\/run$/.test(url.pathname)) {
|
|
2518
|
+
const scheduleId = url.pathname.split("/")[2];
|
|
2519
|
+
const schedule = await store.getWorkplanSchedule(scheduleId);
|
|
2520
|
+
if (!schedule) {
|
|
2521
|
+
writeJson(res, 404, { error: "schedule not found" });
|
|
2522
|
+
return;
|
|
2523
|
+
}
|
|
2524
|
+
const run = await scheduler.runNow(scheduleId);
|
|
2525
|
+
if (!run) {
|
|
2526
|
+
writeJson(res, 500, { error: "failed to start workplan run" });
|
|
2527
|
+
return;
|
|
2528
|
+
}
|
|
2529
|
+
writeJson(res, 201, run);
|
|
2530
|
+
return;
|
|
2531
|
+
}
|
|
2532
|
+
if (req.method === "POST" && url.pathname === "/schedules/tick") {
|
|
2533
|
+
const runs3 = await scheduler.tick();
|
|
2534
|
+
writeJson(res, 200, { runs: runs3 });
|
|
2535
|
+
return;
|
|
2536
|
+
}
|
|
2537
|
+
if (req.method === "GET" && url.pathname === "/workplan-runs") {
|
|
2538
|
+
const scheduleId = url.searchParams.get("scheduleId") ?? void 0;
|
|
2539
|
+
const runs3 = await store.listWorkplanRuns(scheduleId ? { scheduleId } : void 0);
|
|
2540
|
+
writeJson(res, 200, { runs: runs3 });
|
|
2541
|
+
return;
|
|
2542
|
+
}
|
|
2543
|
+
if (req.method === "GET" && /^\/workplan-runs\/[^/]+\/steps$/.test(url.pathname)) {
|
|
2544
|
+
const runId = url.pathname.split("/")[2];
|
|
2545
|
+
const run = await store.getWorkplanRun(runId);
|
|
2546
|
+
if (!run) {
|
|
2547
|
+
writeJson(res, 404, { error: "workplan run not found" });
|
|
2548
|
+
return;
|
|
2549
|
+
}
|
|
2550
|
+
const steps = await store.listWorkplanStepResults(runId);
|
|
2551
|
+
writeJson(res, 200, { steps });
|
|
2552
|
+
return;
|
|
2553
|
+
}
|
|
2554
|
+
if (req.method === "GET" && /^\/workplan-runs\/[^/]+$/.test(url.pathname)) {
|
|
2555
|
+
const runId = url.pathname.split("/")[2];
|
|
2556
|
+
const run = await store.getWorkplanRun(runId);
|
|
2557
|
+
if (!run) {
|
|
2558
|
+
writeJson(res, 404, { error: "workplan run not found" });
|
|
2559
|
+
return;
|
|
2560
|
+
}
|
|
2561
|
+
writeJson(res, 200, run);
|
|
2562
|
+
return;
|
|
2563
|
+
}
|
|
1433
2564
|
writeJson(res, 404, { error: "not found" });
|
|
1434
2565
|
} catch (error) {
|
|
1435
2566
|
if (error instanceof ZodError) {
|
|
@@ -1449,6 +2580,7 @@ async function main() {
|
|
|
1449
2580
|
`);
|
|
1450
2581
|
});
|
|
1451
2582
|
const shutdown = async () => {
|
|
2583
|
+
stopSchedulerLoop();
|
|
1452
2584
|
await shutdownWorkflows();
|
|
1453
2585
|
await closeStore();
|
|
1454
2586
|
server.close();
|