@toist/aja 0.5.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/CHANGELOG.md ADDED
@@ -0,0 +1,69 @@
1
+ # Changelog
2
+
3
+ All notable changes to `@toist/aja` are recorded here.
4
+
5
+ ## 0.5.0 — 2026-05-05
6
+
7
+ Lockstep version bump. No functional changes in this package.
8
+
9
+ ## 0.4.0 — 2026-05-05
10
+
11
+ **Breaking rename:** package was `@toist/run`; it is now `@toist/aja`.
12
+ Update your `package.json` dependency and any `import ... from "@toist/run"`
13
+ statements to `"@toist/aja"`. No API or behaviour changes.
14
+
15
+ ## 0.3.0 — 2026-05-05
16
+
17
+ Lockstep version bump alongside the new `@toist/up` package. No
18
+ functional changes in this package.
19
+
20
+ ## 0.2.2 — 2026-05-05
21
+
22
+ **Fix:** UI assets now ship with explicit `Content-Type` headers. The
23
+ previous version relied on `new Response(Bun.file(path))` to carry the
24
+ MIME type through automatically, which works in some hosts but not
25
+ others — observed in published installs (consumer's `node_modules/@toist/ui`)
26
+ the header arrived empty, tripping the browser's strict MIME check on
27
+ JS module scripts (`Failed to load module script: MIME type ""`). The
28
+ runner now sets `Content-Type` from `BunFile.type` explicitly. SPA
29
+ fallback uses the same path.
30
+
31
+ ## 0.2.1 — 2026-05-05
32
+
33
+ Lockstep version bump alongside `@toist/in@0.2.1`. No functional changes
34
+ in this package.
35
+
36
+ ## 0.2.0 — 2026-05-05
37
+
38
+ Rebrand: `@2121/runner` → `@toist/aja`. Republished to **npmjs.com**
39
+ (public scope). See `@toist/spec` CHANGELOG for the full rationale.
40
+
41
+ No API or behaviour changes from 0.1.0; the rename is the only breaking
42
+ change. Workspace deps now reference `@toist/spec` and `@toist/ui`.
43
+
44
+ ## 0.1.0 — 2026-05-05 — abandoned (@2121/runner on Gitea)
45
+
46
+ Initial release. The pipeline runtime, extracted from the platform
47
+ monorepo per Phase F W2 and shipped to the Gitea npm registry per W3.
48
+
49
+ - Public API: `startRunner(options): Promise<RunnerHandle>`,
50
+ `register(...kinds)`, `getKind(id)`, `manifest()`. See instance-spec at
51
+ https://toist.in for the full contract.
52
+ - HTTP server (Hono on Bun) — mounts API at `/api/*` and serves the
53
+ bundled UI from `@toist/ui`'s `distDir` at `/`.
54
+ - Pipeline dispatcher with HITL-aware suspendable executor (per-node
55
+ memoization for resume-skip; tasks queue with single-use response
56
+ tokens).
57
+ - Kind registry with built-in kinds: `if`, `for`, `try`, `parallel`,
58
+ `human.input`, `js.eval`, `http.request`, `pipeline.invoke`,
59
+ `chat.completion`, and platform-internal helpers. Custom kinds register
60
+ via `register(...)` before `startRunner`.
61
+ - Resource resolution per resource-spec three-tier: ENV > DB > YAML.
62
+ - Persistence: SQLite — `runtime.db` (run ledger, logs, HITL tasks,
63
+ resources), `cache.db` (TTL'd cache), `data.db` (host-owned domain
64
+ state). All under `dataDir`.
65
+ - Migration runner: linear, checksum-tracked SQL files in `migrations/`.
66
+ This release ships a single squashed `001_initial.sql` baseline.
67
+ - StartRunner options: `disableUi`, `disableMcp`, `disableWatch`,
68
+ `corsOrigins`. MCP server surface is reserved (instance-spec §12);
69
+ not yet served from this process.
@@ -0,0 +1,111 @@
1
+ -- 2121 platform migration 001: initial runtime schema (v0.1 baseline).
2
+ --
3
+ -- Squashed from the pre-publish migrations (initial + HITL + resources). For
4
+ -- the first published version of @2121/runner this is the single baseline a
5
+ -- fresh instance applies on first start. Future migrations stack on top.
6
+ --
7
+ -- All statements use IF NOT EXISTS so a surgical re-baseline (DELETE FROM
8
+ -- _migrations on a populated DB) is a safe no-op against existing schema.
9
+ --
10
+ -- Cache lives in its own cache.db (see cache.ts) and is not migration-managed
11
+ -- — its TTL semantics make drop-recreate safe. Host domain data lives in
12
+ -- data.db, schema-owned by the host repo.
13
+
14
+ -- ---------------------------------------------------------------------------
15
+ -- runs — the run ledger. Every pipeline invocation gets a row here, kept for
16
+ -- the lifetime of the runtime DB. current_node + updated_at + trigger support
17
+ -- the suspendable executor (HITL pause/resume).
18
+
19
+ CREATE TABLE IF NOT EXISTS runs (
20
+ id INTEGER PRIMARY KEY AUTOINCREMENT,
21
+ pipeline TEXT NOT NULL,
22
+ status TEXT NOT NULL DEFAULT 'pending',
23
+ payload TEXT,
24
+ result TEXT,
25
+ steps TEXT,
26
+ error TEXT,
27
+ started_at TEXT DEFAULT (datetime('now')),
28
+ finished_at TEXT,
29
+ current_node TEXT,
30
+ updated_at TEXT,
31
+ trigger TEXT NOT NULL DEFAULT 'api'
32
+ );
33
+
34
+ CREATE INDEX IF NOT EXISTS idx_runs_pipeline ON runs(pipeline);
35
+ CREATE INDEX IF NOT EXISTS idx_runs_started ON runs(started_at DESC);
36
+
37
+ -- ---------------------------------------------------------------------------
38
+ -- logs — append-only per-run log lines. Cascades on run deletion.
39
+
40
+ CREATE TABLE IF NOT EXISTS logs (
41
+ id INTEGER PRIMARY KEY AUTOINCREMENT,
42
+ run_id INTEGER REFERENCES runs(id) ON DELETE CASCADE,
43
+ ts TEXT DEFAULT (datetime('now')),
44
+ level TEXT DEFAULT 'info',
45
+ msg TEXT
46
+ );
47
+
48
+ CREATE INDEX IF NOT EXISTS idx_logs_run ON logs(run_id);
49
+
50
+ -- ---------------------------------------------------------------------------
51
+ -- node_outputs — per-node memoization for resume-skip. On resume, the
52
+ -- executor hydrates results from this table and skips any node that already
53
+ -- has an output recorded; only the suspended node and its successors actually
54
+ -- re-execute.
55
+
56
+ CREATE TABLE IF NOT EXISTS node_outputs (
57
+ run_id INTEGER NOT NULL REFERENCES runs(id) ON DELETE CASCADE,
58
+ node_id TEXT NOT NULL,
59
+ output_json TEXT,
60
+ started_at TEXT NOT NULL DEFAULT (datetime('now')),
61
+ finished_at TEXT NOT NULL DEFAULT (datetime('now')),
62
+ PRIMARY KEY (run_id, node_id)
63
+ );
64
+
65
+ CREATE INDEX IF NOT EXISTS idx_node_outputs_run ON node_outputs(run_id);
66
+
67
+ -- ---------------------------------------------------------------------------
68
+ -- tasks — HITL pending-input queue. One row per human.input occurrence in a
69
+ -- run. response_token is single-use (UNIQUE), giving idempotency for retried
70
+ -- resume requests for free. assignee is polymorphic: 'user:<email>' or
71
+ -- 'agent:<id>'.
72
+
73
+ CREATE TABLE IF NOT EXISTS tasks (
74
+ id INTEGER PRIMARY KEY AUTOINCREMENT,
75
+ run_id INTEGER NOT NULL REFERENCES runs(id) ON DELETE CASCADE,
76
+ node_id TEXT NOT NULL,
77
+ kind TEXT NOT NULL, -- e.g. 'human.input'
78
+ prompt TEXT,
79
+ schema_json TEXT,
80
+ assignee TEXT,
81
+ status TEXT NOT NULL DEFAULT 'open', -- open|answered|expired|cancelled
82
+ response_json TEXT,
83
+ response_token TEXT NOT NULL UNIQUE,
84
+ responded_by TEXT,
85
+ responded_at TEXT,
86
+ created_at TEXT NOT NULL DEFAULT (datetime('now')),
87
+ expires_at TEXT
88
+ );
89
+
90
+ CREATE INDEX IF NOT EXISTS idx_tasks_status ON tasks(status);
91
+ CREATE INDEX IF NOT EXISTS idx_tasks_run ON tasks(run_id);
92
+ CREATE INDEX IF NOT EXISTS idx_tasks_assignee ON tasks(assignee);
93
+
94
+ -- ---------------------------------------------------------------------------
95
+ -- resources — UI/API-managed resource instances. YAML-file and ENV-VAR
96
+ -- resources are not stored here; this table holds only the "UI/API mutation"
97
+ -- tier of the three-tier override chain. Priority: ENV > DB (this table) >
98
+ -- YAML files. Sensitive fields are stored clear-text in v1 with a load-time
99
+ -- warning; encryption at rest is deferred (pipeline-spec.md §16.1).
100
+
101
+ CREATE TABLE IF NOT EXISTS resources (
102
+ id INTEGER PRIMARY KEY AUTOINCREMENT,
103
+ name TEXT NOT NULL UNIQUE, -- access key in ctx.resource.<name>
104
+ type TEXT NOT NULL, -- ResourceType name (e.g. "AnthropicApi")
105
+ fields_json TEXT NOT NULL DEFAULT '{}', -- JSON object of field values
106
+ created_at TEXT NOT NULL DEFAULT (datetime('now')),
107
+ updated_at TEXT NOT NULL DEFAULT (datetime('now'))
108
+ );
109
+
110
+ CREATE INDEX IF NOT EXISTS idx_resources_name ON resources(name);
111
+ CREATE INDEX IF NOT EXISTS idx_resources_type ON resources(type);
package/package.json ADDED
@@ -0,0 +1,27 @@
1
+ {
2
+ "name": "@toist/aja",
3
+ "version": "0.5.0",
4
+ "type": "module",
5
+ "main": "./src/index.ts",
6
+ "types": "./src/index.ts",
7
+ "exports": {
8
+ ".": "./src/index.ts"
9
+ },
10
+ "files": [
11
+ "src/",
12
+ "migrations/",
13
+ "CHANGELOG.md"
14
+ ],
15
+ "scripts": {
16
+ "dev": "bun --watch src/server.ts"
17
+ },
18
+ "dependencies": {
19
+ "@toist/spec": "0.5.0",
20
+ "@toist/ui": "0.5.0",
21
+ "hono": "^4.7.7",
22
+ "proper-lockfile": "^4.1.2"
23
+ },
24
+ "devDependencies": {
25
+ "@types/proper-lockfile": "^4.1.4"
26
+ }
27
+ }
@@ -0,0 +1,17 @@
1
+ // 2121
2
+ // Cache database — separate file from runtime.db so its drop-recreate
3
+ // semantics (TTL makes content valueless) do not entangle runtime durability.
4
+ //
5
+ // Schema is owned and bootstrapped by makeCache() in cache.ts. No migrations
6
+ // here: cache table can be dropped or recreated freely.
7
+
8
+ import { Database } from "bun:sqlite"
9
+ import { mkdirSync } from "node:fs"
10
+ import { dirname } from "node:path"
11
+ import { cacheDbPath } from "./config.ts"
12
+
13
+ export function openCacheDb(): Database {
14
+ const path = cacheDbPath()
15
+ mkdirSync(dirname(path), { recursive: true })
16
+ return new Database(path, { create: true })
17
+ }
package/src/cache.ts ADDED
@@ -0,0 +1,67 @@
1
+ // 2121
2
+ // Sqlite-backed cache with TTL. Kindit memoisoivat kalliit fetchit transparentisti
3
+ // pipelinelle: ctx.cache.get/set, key on canonical hash inputeista + paramsista.
4
+ //
5
+ // TTL accepts: "30s" | "5m" | "2h" | "1d" or seconds as number. Default: "1h".
6
+
7
+ import type { Database } from "bun:sqlite"
8
+ import { createHash } from "node:crypto"
9
+ import type { Cache } from "@toist/spec"
10
+
11
+ export type { Cache }
12
+
13
+ const TTL_RE = /^(\d+)(s|m|h|d)$/
14
+ const DEFAULT_TTL_SECONDS = 3600
15
+
16
+ function ttlToSeconds(ttl: string | number | undefined): number {
17
+ if (ttl === undefined) return DEFAULT_TTL_SECONDS
18
+ if (typeof ttl === "number") return ttl
19
+ const m = TTL_RE.exec(ttl)
20
+ if (!m) throw new Error(`Invalid TTL: ${ttl}`)
21
+ const n = Number(m[1])
22
+ const unit = m[2]
23
+ return unit === "s" ? n : unit === "m" ? n * 60 : unit === "h" ? n * 3600 : n * 86400
24
+ }
25
+
26
+ export function makeCache(db: Database): Cache {
27
+ db.exec(`
28
+ CREATE TABLE IF NOT EXISTS cache (
29
+ key TEXT PRIMARY KEY,
30
+ value TEXT NOT NULL,
31
+ expires_at INTEGER NOT NULL
32
+ );
33
+ CREATE INDEX IF NOT EXISTS idx_cache_expires ON cache(expires_at);
34
+ `)
35
+
36
+ const get = db.prepare("SELECT value, expires_at FROM cache WHERE key = ?")
37
+ const set = db.prepare("INSERT OR REPLACE INTO cache (key, value, expires_at) VALUES (?, ?, ?)")
38
+ const del = db.prepare("DELETE FROM cache WHERE key = ?")
39
+ const prune = db.prepare("DELETE FROM cache WHERE expires_at < ?")
40
+
41
+ return {
42
+ key(...parts) {
43
+ const canon = JSON.stringify(parts)
44
+ return createHash("sha1").update(canon).digest("hex")
45
+ },
46
+ get<T>(key: string): T | undefined {
47
+ const row = get.get(key) as { value: string; expires_at: number } | undefined
48
+ if (!row) return undefined
49
+ if (row.expires_at < Date.now()) {
50
+ del.run(key)
51
+ return undefined
52
+ }
53
+ try { return JSON.parse(row.value) as T } catch { return undefined }
54
+ },
55
+ set(key, value, opts) {
56
+ const expires = Date.now() + ttlToSeconds(opts?.ttl) * 1000
57
+ set.run(key, JSON.stringify(value), expires)
58
+ },
59
+ delete(key) {
60
+ del.run(key)
61
+ },
62
+ prune() {
63
+ const r = prune.run(Date.now())
64
+ return Number(r.changes ?? 0)
65
+ },
66
+ }
67
+ }
package/src/config.ts ADDED
@@ -0,0 +1,129 @@
1
+ // 2121
2
+ // Single source of truth for instance-relative paths. Per
3
+ // context/instance-spec.md §3 + §5:
4
+ //
5
+ // <rootDir>/
6
+ // instance.json — optional metadata (§6)
7
+ // pipelines/ — pipeline YAML files
8
+ // resources/ — resource YAML files
9
+ // data/ — runner-managed runtime state (gitignored):
10
+ // data.db — host's domain data (schema-on-write)
11
+ // runtime.db — platform ledger (runs/logs/tasks/node_outputs)
12
+ // cache.db — TTL-driven cache
13
+ // .lock — proper-lockfile holder (single-leader guard)
14
+ //
15
+ // rootDir resolution order:
16
+ // 1. process.env.PLATFORM_ROOT_DIR — explicit override (used by hosts
17
+ // that bootstrap @toist/aja from
18
+ // a non-cwd-aligned location)
19
+ // 2. join(process.cwd(), "..") — fallback matching the historic
20
+ // "server cwd is <root>/server"
21
+ // convention
22
+ //
23
+ // Note: the legacy `<rootDir>/.platform/` directory is platform-internal
24
+ // dev-mode coordination (pm2 port handoff in runtime.json, CLI tooling output
25
+ // in generated/). It is NOT part of the instance contract and is not exposed
26
+ // from this module — start.ts and ecosystem.config.cjs handle it on their own.
27
+
28
+ import { join } from "node:path"
29
+
30
+ let rootDirOverride: string | null = null
31
+ let uiDisabled = false
32
+ let watchDisabled = false
33
+ let corsOriginsConfig: string | string[] | null = null
34
+
35
+ /** Set the rootDir explicitly. Used by `startRunner({rootDir, ...})` to
36
+ * configure paths before the db modules' static imports trigger their
37
+ * module-load init. Once set, the override wins over PLATFORM_ROOT_DIR
38
+ * and the cwd-parent fallback. Calling twice is an error — rootDir is
39
+ * once-set per process. */
40
+ export function setRootDir(dir: string): void {
41
+ if (rootDirOverride !== null && rootDirOverride !== dir) {
42
+ throw new Error(
43
+ `[config] setRootDir(${JSON.stringify(dir)}) called after rootDir ` +
44
+ `was already set to ${JSON.stringify(rootDirOverride)}. ` +
45
+ `rootDir is once-set per process.`,
46
+ )
47
+ }
48
+ rootDirOverride = dir
49
+ }
50
+
51
+ /** Set the disableUi flag. Used by `startRunner({disableUi: true, ...})` to
52
+ * opt out of the bundled static-UI mount before server.ts's outer app
53
+ * construction reads the flag. Default: UI is enabled when the dist/
54
+ * directory in @toist/ui exists. */
55
+ export function setDisableUi(v: boolean): void {
56
+ uiDisabled = v
57
+ }
58
+
59
+ export function isUiDisabled(): boolean {
60
+ return uiDisabled
61
+ }
62
+
63
+ /** Set the disableWatch flag. Used by `startRunner({disableWatch: true, ...})`
64
+ * to opt out of the filesystem watcher on pipelineDir/resourceDir. Default:
65
+ * watcher is enabled. Production deployments may set true to avoid
66
+ * inotify/fsevent overhead and accidental hot-reloads. */
67
+ export function setDisableWatch(v: boolean): void {
68
+ watchDisabled = v
69
+ }
70
+
71
+ export function isWatchDisabled(): boolean {
72
+ return watchDisabled
73
+ }
74
+
75
+ /** Set the CORS allow-origin configuration. `string` for a single origin,
76
+ * `string[]` for an allowlist, `null` for default (permissive `*`).
77
+ * Per Hono's cors() middleware semantics. */
78
+ export function setCorsOrigins(origins: string | string[] | null | undefined): void {
79
+ corsOriginsConfig = origins ?? null
80
+ }
81
+
82
+ export function getCorsOrigins(): string | string[] | null {
83
+ return corsOriginsConfig
84
+ }
85
+
86
+ export function resolveRootDir(): string {
87
+ if (rootDirOverride !== null) return rootDirOverride
88
+ const override = process.env.PLATFORM_ROOT_DIR
89
+ if (override && override.length > 0) return override
90
+ return join(process.cwd(), "..")
91
+ }
92
+
93
+ export function dataDir(): string {
94
+ const override = process.env.PLATFORM_DATA_DIR
95
+ if (override && override.length > 0) return override
96
+ return join(resolveRootDir(), "data")
97
+ }
98
+
99
+ export function dataDbPath(): string {
100
+ return join(dataDir(), "data.db")
101
+ }
102
+
103
+ export function runtimeDbPath(): string {
104
+ return join(dataDir(), "runtime.db")
105
+ }
106
+
107
+ export function cacheDbPath(): string {
108
+ return join(dataDir(), "cache.db")
109
+ }
110
+
111
+ export function lockfilePath(): string {
112
+ return join(dataDir(), ".lock")
113
+ }
114
+
115
+ export function instanceFilePath(): string {
116
+ return join(resolveRootDir(), "instance.json")
117
+ }
118
+
119
+ export function pipelinesDir(): string {
120
+ const override = process.env.PIPELINES_DIR
121
+ if (override && override.length > 0) return override
122
+ return join(resolveRootDir(), "pipelines")
123
+ }
124
+
125
+ export function resourcesDir(): string {
126
+ const override = process.env.RESOURCES_DIR
127
+ if (override && override.length > 0) return override
128
+ return join(resolveRootDir(), "resources")
129
+ }
package/src/data-db.ts ADDED
@@ -0,0 +1,21 @@
1
+ // 2121
2
+ // Domain data store. The instance owns the schema entirely — every table here
3
+ // is created by a pipeline (via db.insert) or by a domain migration the project
4
+ // chooses to add. Platform never writes to this DB directly.
5
+ //
6
+ // Kinds receive this as `ctx.db`. Pipelines write to it (db.insert), read
7
+ // from it (db.query), or any custom domain kind interacts with it as needed.
8
+ //
9
+ // Location: <dataDir>/data.db (per context/instance-spec.md §3 — the host's
10
+ // schema-on-write domain DB lives in data/ alongside runtime.db and cache.db).
11
+
12
+ import { Database } from "bun:sqlite"
13
+ import { mkdirSync } from "node:fs"
14
+ import { dirname } from "node:path"
15
+ import { dataDbPath } from "./config.ts"
16
+
17
+ export function openDataDb(): Database {
18
+ const path = dataDbPath()
19
+ mkdirSync(dirname(path), { recursive: true })
20
+ return new Database(path, { create: true })
21
+ }
@@ -0,0 +1,70 @@
1
+ // 2121
2
+ // Singleton db handle holder + lifecycle orchestration. Decouples the db
3
+ // modules (which are now pure factories per `openX()`) from the consumers
4
+ // (index.ts, pipeline.ts) that need stable handle references throughout the
5
+ // HTTP request lifetime.
6
+ //
7
+ // Lifecycle (matches context/instance-spec.md §7):
8
+ //
9
+ // initDbs() — acquire runner lock, migrate runtime.db, open all three
10
+ // handles. Idempotent: second call is a no-op when handles
11
+ // are already populated.
12
+ // runtimeDb() — getter; throws when init has not run
13
+ // dataDb() — getter; throws when init has not run
14
+ // cacheDb() — getter; throws when init has not run
15
+ // closeDbs() — close all three handles, release the lock. Used by
16
+ // `startRunner`'s graceful stop path.
17
+ //
18
+ // Why getters and not exported instances: the db modules used to do their
19
+ // init at module load via top-level await, which forced lifecycle to happen
20
+ // during the import graph. That works for source-mod single-instance runs
21
+ // but breaks the moment startRunner() needs to set rootDir before init runs.
22
+ // Getters defer the resolution to call time; init runs once, before any
23
+ // HTTP request fires.
24
+
25
+ import type { Database } from "bun:sqlite"
26
+ import { openRuntimeDb } from "./runtime-db.ts"
27
+ import { openDataDb } from "./data-db.ts"
28
+ import { openCacheDb } from "./cache-db.ts"
29
+ import { acquireRunnerLock, releaseRunnerLock } from "./lock.ts"
30
+
31
+ let _runtimeDb: Database | null = null
32
+ let _dataDb: Database | null = null
33
+ let _cacheDb: Database | null = null
34
+
35
+ export async function initDbs(): Promise<void> {
36
+ if (_runtimeDb !== null) return // idempotent
37
+
38
+ await acquireRunnerLock()
39
+ _runtimeDb = openRuntimeDb()
40
+ _dataDb = openDataDb()
41
+ _cacheDb = openCacheDb()
42
+ }
43
+
44
+ export async function closeDbs(): Promise<void> {
45
+ if (_runtimeDb) { _runtimeDb.close(); _runtimeDb = null }
46
+ if (_dataDb) { _dataDb.close(); _dataDb = null }
47
+ if (_cacheDb) { _cacheDb.close(); _cacheDb = null }
48
+ await releaseRunnerLock()
49
+ }
50
+
51
+ export function runtimeDb(): Database {
52
+ if (_runtimeDb === null) {
53
+ throw new Error("[db-handles] runtimeDb() called before initDbs() — lifecycle violation")
54
+ }
55
+ return _runtimeDb
56
+ }
57
+
58
+ export function dataDb(): Database {
59
+ if (_dataDb === null) {
60
+ throw new Error("[db-handles] dataDb() called before initDbs() — lifecycle violation")
61
+ }
62
+ return _dataDb
63
+ }
64
+
65
+ export function cacheDb(): Database {
66
+ if (_cacheDb === null) {
67
+ throw new Error("[db-handles] cacheDb() called before initDbs() — lifecycle violation")
68
+ }
69
+ return _cacheDb
70
+ }