workplane 0.4.0 → 0.4.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/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 = DBOS.registerWorkflow(
30
- async (input) => DBOS.runStep(() => store.createTask(input), { name: "store-create-task" }),
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 = DBOS.registerWorkflow(
34
- async (taskId) => DBOS.runStep(() => store.retryTask(taskId), { name: "store-retry-task" }),
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 = DBOS.registerWorkflow(
38
- async (taskId) => DBOS.runStep(() => store.cancelTask(taskId), { name: "store-cancel-task" }),
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 = DBOS.registerWorkflow(
42
- async (runId, status, error) => DBOS.runStep(() => store.updateRunStatus(runId, status, error), { name: "store-update-run-status" }),
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 = DBOS.registerWorkflow(
46
- async (runId, logs) => DBOS.runStep(() => store.appendRunLogs(runId, logs), { name: "store-append-run-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 = DBOS.registerWorkflow(
50
- async (runId, input) => DBOS.runStep(() => store.createArtifact(runId, input), { name: "store-create-artifact" }),
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 = DBOS.registerWorkflow(
54
- async (runId, input) => DBOS.runStep(() => store.appendInputEvent(runId, input), { name: "store-append-input-event" }),
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 = DBOS.registerWorkflow(
58
- async (runId, sequence) => DBOS.runStep(() => store.markInputDelivered(runId, sequence), { name: "store-mark-input-delivered" }),
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
- DBOS.setConfig({
198
+ DBOS2.setConfig({
74
199
  name: options.appName,
75
200
  systemDatabaseUrl: options.systemDatabaseUrl
76
201
  });
77
- await DBOS.launch({
202
+ await DBOS2.launch({
78
203
  conductorKey: options.conductorKey,
79
204
  conductorURL: options.conductorUrl
80
205
  });
81
206
  }
82
207
  async shutdown() {
83
- await DBOS.shutdown();
208
+ await DBOS2.shutdown();
84
209
  }
85
210
  async createTask(input) {
86
- const handle = await DBOS.startWorkflow(this.registered.createTask)(input);
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 DBOS.startWorkflow(this.registered.retryTask)(taskId);
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 DBOS.startWorkflow(this.registered.cancelTask)(taskId);
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 DBOS.startWorkflow(this.registered.updateRunStatus)(runId, status, error);
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 DBOS.startWorkflow(this.registered.appendRunLogs)(runId, logs);
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 DBOS.startWorkflow(this.registered.createArtifact)(runId, input);
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 DBOS.startWorkflow(this.registered.appendInputEvent)(runId, input);
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 DBOS.startWorkflow(this.registered.markInputDelivered)(runId, sequence);
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 mapRawTask(row) {
513
+ function toScheduleRecord(row) {
343
514
  return {
344
- id: String(row.id),
345
- kind: String(row.kind),
346
- adapter: String(row.adapter),
347
- payload: row.payload ?? {},
348
- requires: row.requires ?? [],
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
- createdAt: new Date(String(row.created_at)).toISOString(),
351
- updatedAt: new Date(String(row.updated_at)).toISOString()
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.execute(sql2`
425
- UPDATE nodes
426
- SET capabilities = ${capabilities}::text[], status = 'online',
427
- last_heartbeat_at = now(), updated_at = now()
428
- WHERE id = ${nodeId}
429
- `);
430
- const taskResult = await tx.execute(sql2`
431
- SELECT * FROM tasks
432
- WHERE status = 'queued' AND requires <@ ${capabilities}::text[]
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: mapRawTask(taskRow),
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 mapRawTask2(row) {
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: mapRawTask2(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 mapRawTask2(raw.prepare("SELECT * FROM tasks WHERE id = ?").get(taskId));
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();