@boardwalk-labs/engine 0.1.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (80) hide show
  1. package/LICENSE +202 -0
  2. package/README.md +69 -0
  3. package/bin/boardwalk-server.js +16 -0
  4. package/dist/agent/conversation.d.ts +42 -0
  5. package/dist/agent/conversation.js +4 -0
  6. package/dist/agent/leaf.d.ts +81 -0
  7. package/dist/agent/leaf.js +190 -0
  8. package/dist/agent/providers.d.ts +23 -0
  9. package/dist/agent/providers.js +347 -0
  10. package/dist/agent/rates.d.ts +13 -0
  11. package/dist/agent/rates.js +35 -0
  12. package/dist/agent/redact.d.ts +9 -0
  13. package/dist/agent/redact.js +27 -0
  14. package/dist/agent/resolve.d.ts +58 -0
  15. package/dist/agent/resolve.js +153 -0
  16. package/dist/agent/sse.d.ts +2 -0
  17. package/dist/agent/sse.js +30 -0
  18. package/dist/agent/tools.d.ts +57 -0
  19. package/dist/agent/tools.js +324 -0
  20. package/dist/clock.d.ts +8 -0
  21. package/dist/clock.js +32 -0
  22. package/dist/cron/cron.d.ts +34 -0
  23. package/dist/cron/cron.js +331 -0
  24. package/dist/engine.d.ts +106 -0
  25. package/dist/engine.js +183 -0
  26. package/dist/errors.d.ts +15 -0
  27. package/dist/errors.js +40 -0
  28. package/dist/ids.d.ts +7 -0
  29. package/dist/ids.js +42 -0
  30. package/dist/index.d.ts +6 -0
  31. package/dist/index.js +8 -0
  32. package/dist/json_value.d.ts +7 -0
  33. package/dist/json_value.js +29 -0
  34. package/dist/mcp/client.d.ts +39 -0
  35. package/dist/mcp/client.js +112 -0
  36. package/dist/mcp/jsonrpc.d.ts +57 -0
  37. package/dist/mcp/jsonrpc.js +117 -0
  38. package/dist/mcp/oauth.d.ts +72 -0
  39. package/dist/mcp/oauth.js +337 -0
  40. package/dist/mcp/token_store.d.ts +30 -0
  41. package/dist/mcp/token_store.js +101 -0
  42. package/dist/mcp/transport_http.d.ts +38 -0
  43. package/dist/mcp/transport_http.js +143 -0
  44. package/dist/mcp/transport_stdio.d.ts +27 -0
  45. package/dist/mcp/transport_stdio.js +94 -0
  46. package/dist/run/child.d.ts +1 -0
  47. package/dist/run/child.js +139 -0
  48. package/dist/run/child_host.d.ts +26 -0
  49. package/dist/run/child_host.js +124 -0
  50. package/dist/run/idempotency.d.ts +5 -0
  51. package/dist/run/idempotency.js +31 -0
  52. package/dist/run/ipc.d.ts +159 -0
  53. package/dist/run/ipc.js +150 -0
  54. package/dist/run/run_dir.d.ts +31 -0
  55. package/dist/run/run_dir.js +106 -0
  56. package/dist/run/supervisor.d.ts +107 -0
  57. package/dist/run/supervisor.js +676 -0
  58. package/dist/scheduler/scheduler.d.ts +54 -0
  59. package/dist/scheduler/scheduler.js +215 -0
  60. package/dist/server/http.d.ts +42 -0
  61. package/dist/server/http.js +183 -0
  62. package/dist/server/routes/api.d.ts +17 -0
  63. package/dist/server/routes/api.js +107 -0
  64. package/dist/server/routes/hooks.d.ts +2 -0
  65. package/dist/server/routes/hooks.js +88 -0
  66. package/dist/server/routes/router.d.ts +15 -0
  67. package/dist/server/routes/router.js +75 -0
  68. package/dist/server/routes/stream.d.ts +2 -0
  69. package/dist/server/routes/stream.js +79 -0
  70. package/dist/server/routes/ui.d.ts +2 -0
  71. package/dist/server/routes/ui.js +120 -0
  72. package/dist/server/server.d.ts +25 -0
  73. package/dist/server/server.js +67 -0
  74. package/dist/server_main.d.ts +46 -0
  75. package/dist/server_main.js +203 -0
  76. package/dist/store/migrations.d.ts +21 -0
  77. package/dist/store/migrations.js +159 -0
  78. package/dist/store/store.d.ts +194 -0
  79. package/dist/store/store.js +567 -0
  80. package/package.json +57 -0
@@ -0,0 +1,150 @@
1
+ // The supervisor ⇄ run-process IPC protocol.
2
+ //
3
+ // One run = one spawned Node process (SPEC §2.2). The child executes the user's program and
4
+ // brokers its SDK hook calls back to the supervisor over Node's built-in IPC channel. Every
5
+ // message is Zod-validated on receipt — the child runs user code, so everything it sends is a
6
+ // trust boundary (CODE_QUALITY §2.1).
7
+ //
8
+ // Envelope authority: the child sends event BODIES (no runId/turnId/seq/t); the supervisor is
9
+ // the single place envelopes are stamped and cursors allocated, so cursor monotonicity holds
10
+ // across crash-restarts without the child knowing about them.
11
+ import { z } from "zod";
12
+ const errorShapeSchema = z.strictObject({
13
+ code: z.string(),
14
+ message: z.string(),
15
+ hint: z.string().optional(),
16
+ });
17
+ // The child validates only the discriminator + the fields it dereferences; the manifest was
18
+ // already validated by the store and `unknown` payloads are narrowed at their use sites.
19
+ export const parentToChildSchema = z.union([
20
+ z.object({
21
+ type: z.literal("init"),
22
+ runId: z.string().min(1),
23
+ programPath: z.string().min(1),
24
+ workspaceDir: z.string().min(1),
25
+ skillsDir: z.string().min(1).nullable(),
26
+ input: z.unknown(),
27
+ config: z.record(z.string(), z.unknown()),
28
+ manifest: z.record(z.string(), z.unknown()),
29
+ }),
30
+ z.object({
31
+ type: z.literal("host_result"),
32
+ callId: z.number().int().nonnegative(),
33
+ result: z.union([
34
+ z.object({ ok: z.literal(true), value: z.unknown() }),
35
+ z.object({ ok: z.literal(false), error: errorShapeSchema }),
36
+ ]),
37
+ }),
38
+ ]);
39
+ // ----------------------------------------------------------------------------
40
+ // child → parent
41
+ // ----------------------------------------------------------------------------
42
+ /** Host methods the child brokers to the supervisor (everything that touches engine state). */
43
+ export const HOST_METHODS = [
44
+ "get_secret",
45
+ "call_workflow",
46
+ "run_workflow",
47
+ "write_artifact",
48
+ "resolve_model",
49
+ "mcp_token",
50
+ ];
51
+ const tokenUsageShape = z.strictObject({
52
+ inputTokens: z.number().int().nonnegative().optional(),
53
+ outputTokens: z.number().int().nonnegative().optional(),
54
+ totalTokens: z.number().int().nonnegative().optional(),
55
+ });
56
+ export const childToParentSchema = z.union([
57
+ z.object({
58
+ type: z.literal("host_call"),
59
+ callId: z.number().int().nonnegative(),
60
+ method: z.enum(HOST_METHODS),
61
+ args: z.record(z.string(), z.unknown()),
62
+ }),
63
+ z.object({
64
+ type: z.literal("emit"),
65
+ // Stamped + fully validated against runEventSchema by the supervisor; here we only
66
+ // require an event-body shape with a kind. `turnId` scopes agent-leaf frames to their
67
+ // turn; absent means a run-level frame (turnId = runId).
68
+ body: z.looseObject({ kind: z.string().min(1) }),
69
+ turnId: z.string().min(1).optional(),
70
+ }),
71
+ z.object({
72
+ // Opens a new turn block: the supervisor bumps its cursor stride and emits turn_started.
73
+ // Carries the leaf's identity so the stamped turn_started names which agent is starting.
74
+ type: z.literal("turn_started"),
75
+ turnId: z.string().min(1),
76
+ agentId: z.string().min(1),
77
+ agentName: z.string().min(1).optional(),
78
+ }),
79
+ z.object({
80
+ // Leaf usage report — the supervisor's budget authority consumes this (tokens + max_usd).
81
+ type: z.literal("report_usage"),
82
+ modelRef: z.string().min(1),
83
+ usage: tokenUsageShape,
84
+ }),
85
+ z.object({
86
+ // An agent() call is using a memory dir — the supervisor auto-persists it at success.
87
+ type: z.literal("memory_used"),
88
+ dir: z.string().min(1),
89
+ }),
90
+ z.object({
91
+ type: z.literal("done"),
92
+ output: z.unknown(),
93
+ outputDeclared: z.boolean(),
94
+ }),
95
+ z.object({
96
+ type: z.literal("failed"),
97
+ error: errorShapeSchema,
98
+ // Output declared (output()) BEFORE the program threw still counts — a watch/check often
99
+ // output()s its verdict and then throws to mark the run failed. Mirrors `done`.
100
+ output: z.unknown(),
101
+ outputDeclared: z.boolean(),
102
+ }),
103
+ ]);
104
+ // Host-call argument schemas, validated supervisor-side before acting.
105
+ export const getSecretArgsSchema = z.strictObject({ name: z.string().min(1) });
106
+ export const callWorkflowArgsSchema = z.strictObject({
107
+ slug: z.string().min(1),
108
+ input: z.unknown(),
109
+ idempotencyKey: z.string().min(1).optional(),
110
+ });
111
+ export const writeArtifactArgsSchema = z.strictObject({
112
+ name: z.string().min(1),
113
+ contentType: z.string().min(1),
114
+ /** Body crosses IPC as base64 — Uint8Array does not survive JSON serialization. */
115
+ bodyBase64: z.string(),
116
+ metadata: z.record(z.string(), z.unknown()).optional(),
117
+ });
118
+ export const resolveModelArgsSchema = z.strictObject({
119
+ model: z.string().min(1).optional(),
120
+ provider: z.string().min(1).optional(),
121
+ });
122
+ /** The supervisor's resolve_model response, re-validated child-side before use. */
123
+ export const resolvedModelSchema = z.strictObject({
124
+ provider: z.string().min(1),
125
+ /** Opaque — passed verbatim to the provider; never parsed. */
126
+ model: z.string().min(1),
127
+ protocol: z.enum(["anthropic", "openai"]),
128
+ baseUrl: z.string().min(1),
129
+ apiKey: z.string().nullable(),
130
+ /** Extra request headers, resolved supervisor-side. */
131
+ headers: z.record(z.string(), z.string()),
132
+ /** Header names whose values are env-sourced — the leaf redacts them like the API key. */
133
+ secretHeaderNames: z.array(z.string()),
134
+ });
135
+ /**
136
+ * mcp_token: the child asks the engine for an OAuth bearer token for an MCP server (token
137
+ * state is PARENT-owned — the run process never sees refresh tokens or the store).
138
+ * `invalidateToken` names a token the server just rejected, so the supervisor refreshes
139
+ * instead of handing the same dead value back.
140
+ */
141
+ export const mcpTokenArgsSchema = z.strictObject({
142
+ serverUrl: z.string().min(1),
143
+ invalidateToken: z.string().min(1).optional(),
144
+ });
145
+ /** The supervisor's mcp_token response. null accessToken ⇒ interaction would be required —
146
+ * the hint names the `engine.authorizeMcpServer(...)` call that fixes it. */
147
+ export const mcpTokenResultSchema = z.strictObject({
148
+ accessToken: z.string().nullable(),
149
+ hint: z.string().optional(),
150
+ });
@@ -0,0 +1,31 @@
1
+ export interface RunDirs {
2
+ root: string;
3
+ programPath: string;
4
+ workspaceDir: string;
5
+ artifactsDir: string;
6
+ }
7
+ /** Lay out (or re-lay-out, on restart) the run directory for a program bundle. Idempotent. */
8
+ export declare function prepareRunDir(dataDir: string, runId: string, program: string): RunDirs;
9
+ /** Remove a run directory (terminal-run cleanup; never called on active runs). */
10
+ export declare function removeRunDir(dataDir: string, runId: string): void;
11
+ export type PersistSelection = boolean | readonly string[] | undefined;
12
+ /** The workflow's durable persistence root. */
13
+ export declare function persistRoot(dataDir: string, workflowId: string): string;
14
+ /**
15
+ * Copy persisted state into a fresh run workspace (first attempt only — see above). The whole
16
+ * durable root is hydrated: it only ever contains what a previous successful run persisted
17
+ * (declared dirs + memory dirs), so all of it belongs in the workspace.
18
+ */
19
+ export declare function hydrateWorkspace(root: string, workspaceDir: string): void;
20
+ /**
21
+ * Replace the durable store with the run's final state (successful runs only). `memoryDirs`
22
+ * are the per-agent memory directories used this run — persisted in addition to the
23
+ * manifest's selection (deduplicated; a memory dir inside `persist: true` costs nothing).
24
+ */
25
+ export declare function persistWorkspace(root: string, persist: PersistSelection, memoryDirs: ReadonlySet<string>, workspaceDir: string): void;
26
+ /**
27
+ * The engine's installed `@boardwalk-labs/workflow` package root. Resolved from the package's main
28
+ * entry and walked up to its package.json — the exports map doesn't expose "./package.json",
29
+ * so `require.resolve("@boardwalk-labs/workflow/package.json")` would throw.
30
+ */
31
+ export declare function sdkPackageDir(): string;
@@ -0,0 +1,106 @@
1
+ // Per-run on-disk layout + the SDK-sharing symlink.
2
+ //
3
+ // <dataDir>/runs/<runId>/
4
+ // program/index.mjs — the deployed bundle (SDK left external)
5
+ // program/node_modules/@boardwalk-labs/workflow — symlink to the ENGINE's installed SDK
6
+ // workspace/ — the run's cwd (isolated per run)
7
+ // artifacts/ — artifacts.write targets
8
+ //
9
+ // Why the symlink: the SDK's host state is a module-level singleton, so the program and the
10
+ // child entry (engine code) must load the SAME module instance for installHost() to be visible
11
+ // to the program's hooks. The program bundle imports `@boardwalk-labs/workflow` bare; this symlink
12
+ // makes that specifier resolve — and Node's default symlink realpathing collapses it onto the
13
+ // engine's own copy, giving one shared instance with no bundler in the engine at all.
14
+ import { cpSync, existsSync, mkdirSync, readFileSync, rmSync, symlinkSync, writeFileSync, } from "node:fs";
15
+ import { createRequire } from "node:module";
16
+ import { dirname, join } from "node:path";
17
+ import { z } from "zod";
18
+ import { EngineError } from "../errors.js";
19
+ // A file from disk is a trust boundary — parse, don't cast (CODE_QUALITY §2.1).
20
+ const packageNameSchema = z.looseObject({ name: z.string().optional() });
21
+ /** Lay out (or re-lay-out, on restart) the run directory for a program bundle. Idempotent. */
22
+ export function prepareRunDir(dataDir, runId, program) {
23
+ const root = join(dataDir, "runs", runId);
24
+ const programDir = join(root, "program");
25
+ const workspaceDir = join(root, "workspace");
26
+ const artifactsDir = join(root, "artifacts");
27
+ mkdirSync(programDir, { recursive: true });
28
+ mkdirSync(workspaceDir, { recursive: true });
29
+ mkdirSync(artifactsDir, { recursive: true });
30
+ const programPath = join(programDir, "index.mjs");
31
+ writeFileSync(programPath, program, "utf8");
32
+ const linkParent = join(programDir, "node_modules", "@boardwalk-labs");
33
+ const linkPath = join(linkParent, "workflow");
34
+ if (!existsSync(linkPath)) {
35
+ mkdirSync(linkParent, { recursive: true });
36
+ symlinkSync(sdkPackageDir(), linkPath, "dir");
37
+ }
38
+ return { root, programPath, workspaceDir, artifactsDir };
39
+ }
40
+ /** Remove a run directory (terminal-run cleanup; never called on active runs). */
41
+ export function removeRunDir(dataDir, runId) {
42
+ rmSync(join(dataDir, "runs", runId), { recursive: true, force: true });
43
+ }
44
+ /** The workflow's durable persistence root. */
45
+ export function persistRoot(dataDir, workflowId) {
46
+ return join(dataDir, "persist", workflowId);
47
+ }
48
+ /**
49
+ * Copy persisted state into a fresh run workspace (first attempt only — see above). The whole
50
+ * durable root is hydrated: it only ever contains what a previous successful run persisted
51
+ * (declared dirs + memory dirs), so all of it belongs in the workspace.
52
+ */
53
+ export function hydrateWorkspace(root, workspaceDir) {
54
+ if (existsSync(root))
55
+ cpSync(root, workspaceDir, { recursive: true });
56
+ }
57
+ /**
58
+ * Replace the durable store with the run's final state (successful runs only). `memoryDirs`
59
+ * are the per-agent memory directories used this run — persisted in addition to the
60
+ * manifest's selection (deduplicated; a memory dir inside `persist: true` costs nothing).
61
+ */
62
+ export function persistWorkspace(root, persist, memoryDirs, workspaceDir) {
63
+ if (persist === true) {
64
+ rmSync(root, { recursive: true, force: true });
65
+ cpSync(workspaceDir, root, { recursive: true });
66
+ return;
67
+ }
68
+ const declared = persist === undefined || persist === false ? [] : persist;
69
+ const dirs = new Set([...declared, ...memoryDirs]);
70
+ for (const dir of dirs) {
71
+ const source = join(workspaceDir, dir);
72
+ const target = join(root, dir);
73
+ rmSync(target, { recursive: true, force: true });
74
+ if (existsSync(source)) {
75
+ mkdirSync(dirname(target), { recursive: true });
76
+ cpSync(source, target, { recursive: true });
77
+ }
78
+ }
79
+ }
80
+ let cachedSdkDir = null;
81
+ /**
82
+ * The engine's installed `@boardwalk-labs/workflow` package root. Resolved from the package's main
83
+ * entry and walked up to its package.json — the exports map doesn't expose "./package.json",
84
+ * so `require.resolve("@boardwalk-labs/workflow/package.json")` would throw.
85
+ */
86
+ export function sdkPackageDir() {
87
+ if (cachedSdkDir !== null)
88
+ return cachedSdkDir;
89
+ const require = createRequire(import.meta.url);
90
+ let dir = dirname(require.resolve("@boardwalk-labs/workflow"));
91
+ for (let depth = 0; depth < 10; depth++) {
92
+ const pkgPath = join(dir, "package.json");
93
+ if (existsSync(pkgPath)) {
94
+ const pkg = packageNameSchema.safeParse(JSON.parse(readFileSync(pkgPath, "utf8")));
95
+ if (pkg.success && pkg.data.name === "@boardwalk-labs/workflow") {
96
+ cachedSdkDir = dir;
97
+ return dir;
98
+ }
99
+ }
100
+ const parent = dirname(dir);
101
+ if (parent === dir)
102
+ break;
103
+ dir = parent;
104
+ }
105
+ throw new EngineError("INTERNAL", "Could not locate the installed @boardwalk-labs/workflow package root.");
106
+ }
@@ -0,0 +1,107 @@
1
+ import type { RunStatus } from "../store/store.js";
2
+ import { type InferenceConfig } from "../agent/resolve.js";
3
+ import { type Clock } from "../clock.js";
4
+ import type { EventRow, RunRow, Store } from "../store/store.js";
5
+ export interface SupervisorOptions {
6
+ store: Store;
7
+ /** Engine data directory; run dirs live under `<dataDir>/runs/<runId>`. */
8
+ dataDir: string;
9
+ /** Absolute path to the compiled child entry (dist/run/child.js). */
10
+ childEntryPath: string;
11
+ /** The local secret/env source (.env contents); process.env is the fallback. */
12
+ env: ReadonlyMap<string, string>;
13
+ /** Where secrets come from, for actionable error messages (never values). */
14
+ envLabel: string;
15
+ clock?: Clock;
16
+ /** Crash restarts per run before `failed`/CRASHED. Default 2 (three attempts total). */
17
+ maxRestarts?: number;
18
+ /** Cooperative-cancellation window before SIGKILL. Default 10s. */
19
+ cancelGraceMs?: number;
20
+ /** Default model + provider table for agent() leaves. Default: built-ins only, no default model. */
21
+ inference?: InferenceConfig;
22
+ }
23
+ /** True when a run can no longer change state. */
24
+ export declare function isTerminal(status: RunStatus): boolean;
25
+ export declare class RunSupervisor {
26
+ private readonly store;
27
+ private readonly dataDir;
28
+ private readonly childEntryPath;
29
+ private readonly env;
30
+ private readonly envLabel;
31
+ private readonly clock;
32
+ private readonly maxRestarts;
33
+ private readonly cancelGraceMs;
34
+ private readonly inference;
35
+ private readonly mcpTokens;
36
+ private readonly active;
37
+ private readonly listeners;
38
+ constructor(opts: SupervisorOptions);
39
+ /** Subscribe to every stamped run event (the local feed for SSE/log UIs). */
40
+ onEvent(listener: (row: EventRow) => void): () => void;
41
+ /**
42
+ * Drive a run to terminal status; idempotent per run id (a second call while active returns
43
+ * the same promise; a call on a terminal run resolves immediately). Never rejects for run
44
+ * failures — failure is a status, not an exception. Rejects only on caller bugs (unknown id).
45
+ */
46
+ supervise(runId: string): Promise<RunRow>;
47
+ /** Emit the `queued` lifecycle event for a freshly created run (the creator calls this once). */
48
+ emitQueued(runId: string): void;
49
+ /**
50
+ * Cancel a run: cooperative SIGTERM, SIGKILL after the grace window. A queued/unsupervised
51
+ * run is cancelled directly; a terminal run is a no-op.
52
+ */
53
+ cancel(runId: string): Promise<void>;
54
+ /**
55
+ * Boot recovery sweep (SPEC §2.2): runs a dead engine left active are re-dispatched
56
+ * (restart-from-the-top — the child died with the engine); interrupted cancellations are
57
+ * finalized (the orphan child exits on IPC disconnect, so the kill already happened).
58
+ * Engine restarts do not consume the run's crash-restart budget.
59
+ */
60
+ recoverOnBoot(): {
61
+ resumed: string[];
62
+ cancelled: string[];
63
+ };
64
+ /** SIGTERM all children and stop. In-flight runs are recovered by the next boot's sweep. */
65
+ shutdown(): void;
66
+ private execute;
67
+ private spawnOnce;
68
+ /**
69
+ * Accumulate a leaf's usage into the run row and enforce token/USD budgets — the supervisor
70
+ * is the single budget authority, so a multi-leaf run can't out-run its caps by parallelism.
71
+ */
72
+ private recordUsage;
73
+ private handleHostCall;
74
+ /** Find-or-create the durable child run for workflows.call/run (idempotent re-attach). */
75
+ private startChildRun;
76
+ /**
77
+ * The engine side of MCP OAuth: hand the child a usable access token, refreshing SILENTLY
78
+ * when the stored one is expired (clock + skew) or the child reports the server rejected it
79
+ * (`invalidateToken` — the child retries at most once, so a second rejection lands back here
80
+ * as a failure). When only a human could fix it, answer null + a hint naming
81
+ * engine.authorizeMcpServer — a headless run must fail loudly, never prompt.
82
+ */
83
+ private resolveMcpToken;
84
+ private resolveSecret;
85
+ /**
86
+ * The child's environment: the parent env plus manifest.env with whole-value
87
+ * `${{ secrets.NAME }}` interpolation resolved (fail-closed against meta.secrets).
88
+ */
89
+ private childEnv;
90
+ private setStatus;
91
+ /** Terminal transition: persist status + output/error, emit the lifecycle event, return the row. */
92
+ private finishRun;
93
+ /** Stamp a child-emitted body. A malformed body is dropped with a diagnostic, never fatal. */
94
+ private emitBody;
95
+ /**
96
+ * The single envelope-stamping path: allocate cursor, validate, persist, fan out. The body
97
+ * is typed loosely because child-emitted bodies are untrusted — runEventSchema.parse below
98
+ * is the validation, not the type. Run-level frames carry the run id as turnId; agent-leaf
99
+ * frames carry their turn's id.
100
+ */
101
+ private stampAndStore;
102
+ /** Resume the envelope past everything already persisted (crash/boot safe). */
103
+ private resumeEnvelope;
104
+ private mustGetRun;
105
+ /** Deployed skills live at <dataDir>/skills/<workflowId>/<name>.md (written at deploy). */
106
+ private skillsDirFor;
107
+ }