@twelvehart/orcats 0.0.0-seed.0 → 0.2.3
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/LICENSE +48 -0
- package/NOTICE +5 -0
- package/README.md +399 -0
- package/bin/orcats +4 -0
- package/dist/backends/acp-client.d.ts +74 -0
- package/dist/backends/acp-client.d.ts.map +1 -0
- package/dist/backends/acp-run.d.ts +31 -0
- package/dist/backends/acp-run.d.ts.map +1 -0
- package/dist/backends/claude-run.d.ts +35 -0
- package/dist/backends/claude-run.d.ts.map +1 -0
- package/dist/backends/claude-stream-json.d.ts +15 -0
- package/dist/backends/claude-stream-json.d.ts.map +1 -0
- package/dist/backends/codex-jsonl.d.ts +34 -0
- package/dist/backends/codex-jsonl.d.ts.map +1 -0
- package/dist/backends/codex-run.d.ts +40 -0
- package/dist/backends/codex-run.d.ts.map +1 -0
- package/dist/backends/codex.d.ts +5 -0
- package/dist/backends/codex.d.ts.map +1 -0
- package/dist/backends/conversation-config.d.ts +12 -0
- package/dist/backends/conversation-config.d.ts.map +1 -0
- package/dist/backends/index.d.ts +14 -0
- package/dist/backends/index.d.ts.map +1 -0
- package/dist/backends/opencode-run.d.ts +48 -0
- package/dist/backends/opencode-run.d.ts.map +1 -0
- package/dist/backends/opencode-sse.d.ts +35 -0
- package/dist/backends/opencode-sse.d.ts.map +1 -0
- package/dist/backends/pi-rpc.d.ts +21 -0
- package/dist/backends/pi-rpc.d.ts.map +1 -0
- package/dist/backends/pi-run.d.ts +21 -0
- package/dist/backends/pi-run.d.ts.map +1 -0
- package/dist/backends/select.d.ts +17 -0
- package/dist/backends/select.d.ts.map +1 -0
- package/dist/backends/subprocess-run.d.ts +68 -0
- package/dist/backends/subprocess-run.d.ts.map +1 -0
- package/dist/backends/types.d.ts +17 -0
- package/dist/backends/types.d.ts.map +1 -0
- package/dist/backends/unsupported.d.ts +9 -0
- package/dist/backends/unsupported.d.ts.map +1 -0
- package/dist/baseline/index.d.ts +56 -0
- package/dist/baseline/index.d.ts.map +1 -0
- package/dist/cli/args.d.ts +22 -0
- package/dist/cli/args.d.ts.map +1 -0
- package/dist/cli/embedded.d.ts +7 -0
- package/dist/cli/embedded.d.ts.map +1 -0
- package/dist/cli/main.d.ts +6 -0
- package/dist/cli/main.d.ts.map +1 -0
- package/dist/cli/version.d.ts +2 -0
- package/dist/cli/version.d.ts.map +1 -0
- package/dist/conversation/ask-user.d.ts +17 -0
- package/dist/conversation/ask-user.d.ts.map +1 -0
- package/dist/conversation/conversation.d.ts +46 -0
- package/dist/conversation/conversation.d.ts.map +1 -0
- package/dist/conversation/harness.d.ts +14 -0
- package/dist/conversation/harness.d.ts.map +1 -0
- package/dist/conversation/index.d.ts +5 -0
- package/dist/conversation/index.d.ts.map +1 -0
- package/dist/conversation/queue.d.ts +15 -0
- package/dist/conversation/queue.d.ts.map +1 -0
- package/dist/flow/accessors.d.ts +13 -0
- package/dist/flow/accessors.d.ts.map +1 -0
- package/dist/flow/approvals.d.ts +6 -0
- package/dist/flow/approvals.d.ts.map +1 -0
- package/dist/flow/args.d.ts +4 -0
- package/dist/flow/args.d.ts.map +1 -0
- package/dist/flow/context.d.ts +36 -0
- package/dist/flow/context.d.ts.map +1 -0
- package/dist/flow/index.d.ts +5 -0
- package/dist/flow/index.d.ts.map +1 -0
- package/dist/index.d.ts +16 -0
- package/dist/index.d.ts.map +1 -0
- package/dist/loop/builder/index.d.ts +17 -0
- package/dist/loop/builder/index.d.ts.map +1 -0
- package/dist/loop/builder/types.d.ts +97 -0
- package/dist/loop/builder/types.d.ts.map +1 -0
- package/dist/loop/context/compaction.d.ts +38 -0
- package/dist/loop/context/compaction.d.ts.map +1 -0
- package/dist/loop/context/index.d.ts +4 -0
- package/dist/loop/context/index.d.ts.map +1 -0
- package/dist/loop/context/offload.d.ts +34 -0
- package/dist/loop/context/offload.d.ts.map +1 -0
- package/dist/loop/context/types.d.ts +13 -0
- package/dist/loop/context/types.d.ts.map +1 -0
- package/dist/loop/engine/bridge.d.ts +21 -0
- package/dist/loop/engine/bridge.d.ts.map +1 -0
- package/dist/loop/engine/index.d.ts +73 -0
- package/dist/loop/engine/index.d.ts.map +1 -0
- package/dist/loop/execution.d.ts +68 -0
- package/dist/loop/execution.d.ts.map +1 -0
- package/dist/loop/fanout.d.ts +148 -0
- package/dist/loop/fanout.d.ts.map +1 -0
- package/dist/loop/firing.d.ts +54 -0
- package/dist/loop/firing.d.ts.map +1 -0
- package/dist/loop/graph/analyze.d.ts +8 -0
- package/dist/loop/graph/analyze.d.ts.map +1 -0
- package/dist/loop/graph/index.d.ts +3 -0
- package/dist/loop/graph/index.d.ts.map +1 -0
- package/dist/loop/graph/types.d.ts +35 -0
- package/dist/loop/graph/types.d.ts.map +1 -0
- package/dist/loop/index.d.ts +7 -0
- package/dist/loop/index.d.ts.map +1 -0
- package/dist/loop/io/index.d.ts +4 -0
- package/dist/loop/io/index.d.ts.map +1 -0
- package/dist/loop/io/linear.d.ts +142 -0
- package/dist/loop/io/linear.d.ts.map +1 -0
- package/dist/loop/io/sink.d.ts +47 -0
- package/dist/loop/io/sink.d.ts.map +1 -0
- package/dist/loop/io/source.d.ts +64 -0
- package/dist/loop/io/source.d.ts.map +1 -0
- package/dist/loop/serve.d.ts +97 -0
- package/dist/loop/serve.d.ts.map +1 -0
- package/dist/loop/state/index.d.ts +5 -0
- package/dist/loop/state/index.d.ts.map +1 -0
- package/dist/loop/state/manifest.d.ts +36 -0
- package/dist/loop/state/manifest.d.ts.map +1 -0
- package/dist/loop/state/port.d.ts +33 -0
- package/dist/loop/state/port.d.ts.map +1 -0
- package/dist/loop/state/snapshot.d.ts +15 -0
- package/dist/loop/state/snapshot.d.ts.map +1 -0
- package/dist/loop/state/sqlite.d.ts +31 -0
- package/dist/loop/state/sqlite.d.ts.map +1 -0
- package/dist/loop/termination-contract.d.ts +28 -0
- package/dist/loop/termination-contract.d.ts.map +1 -0
- package/dist/loop/termination.d.ts +33 -0
- package/dist/loop/termination.d.ts.map +1 -0
- package/dist/model/backend-config.d.ts +30 -0
- package/dist/model/backend-config.d.ts.map +1 -0
- package/dist/model/brand.d.ts +9 -0
- package/dist/model/brand.d.ts.map +1 -0
- package/dist/model/errors.d.ts +11 -0
- package/dist/model/errors.d.ts.map +1 -0
- package/dist/model/index.d.ts +8 -0
- package/dist/model/index.d.ts.map +1 -0
- package/dist/model/json-schema.d.ts +4 -0
- package/dist/model/json-schema.d.ts.map +1 -0
- package/dist/model/result.d.ts +3 -0
- package/dist/model/result.d.ts.map +1 -0
- package/dist/model/schemas.d.ts +435 -0
- package/dist/model/schemas.d.ts.map +1 -0
- package/dist/model/structured.d.ts +9 -0
- package/dist/model/structured.d.ts.map +1 -0
- package/dist/monitor/index.d.ts +139 -0
- package/dist/monitor/index.d.ts.map +1 -0
- package/dist/plan/index.d.ts +3 -0
- package/dist/plan/index.d.ts.map +1 -0
- package/dist/plan/persistent.d.ts +23 -0
- package/dist/plan/persistent.d.ts.map +1 -0
- package/dist/plan/verdict.d.ts +9 -0
- package/dist/plan/verdict.d.ts.map +1 -0
- package/dist/review/index.d.ts +4 -0
- package/dist/review/index.d.ts.map +1 -0
- package/dist/review/loop.d.ts +117 -0
- package/dist/review/loop.d.ts.map +1 -0
- package/dist/review/reviewers.d.ts +13 -0
- package/dist/review/reviewers.d.ts.map +1 -0
- package/dist/review/tool.d.ts +20 -0
- package/dist/review/tool.d.ts.map +1 -0
- package/dist/run-output/index.d.ts +112 -0
- package/dist/run-output/index.d.ts.map +1 -0
- package/dist/runner/index.d.ts +2 -0
- package/dist/runner/index.d.ts.map +1 -0
- package/dist/runner/typecheck.d.ts +23 -0
- package/dist/runner/typecheck.d.ts.map +1 -0
- package/dist/test-utils/fakes.d.ts +76 -0
- package/dist/test-utils/fakes.d.ts.map +1 -0
- package/dist/test-utils/fixtures.d.ts +4 -0
- package/dist/test-utils/fixtures.d.ts.map +1 -0
- package/dist/test-utils/index.d.ts +4 -0
- package/dist/test-utils/index.d.ts.map +1 -0
- package/dist/test-utils/tier2.d.ts +8 -0
- package/dist/test-utils/tier2.d.ts.map +1 -0
- package/dist/tools/fs.d.ts +11 -0
- package/dist/tools/fs.d.ts.map +1 -0
- package/dist/tools/git.d.ts +10 -0
- package/dist/tools/git.d.ts.map +1 -0
- package/dist/tools/github.d.ts +13 -0
- package/dist/tools/github.d.ts.map +1 -0
- package/dist/tools/index.d.ts +7 -0
- package/dist/tools/index.d.ts.map +1 -0
- package/dist/tools/linear.d.ts +117 -0
- package/dist/tools/linear.d.ts.map +1 -0
- package/dist/tools/process.d.ts +40 -0
- package/dist/tools/process.d.ts.map +1 -0
- package/dist/tools/terminal.d.ts +23 -0
- package/dist/tools/terminal.d.ts.map +1 -0
- package/package.json +89 -9
- package/src/backends/acp-client.ts +324 -0
- package/src/backends/acp-run.ts +636 -0
- package/src/backends/claude-run.ts +226 -0
- package/src/backends/claude-stream-json.ts +273 -0
- package/src/backends/codex-jsonl.ts +339 -0
- package/src/backends/codex-run.ts +177 -0
- package/src/backends/codex.ts +84 -0
- package/src/backends/conversation-config.ts +26 -0
- package/src/backends/index.ts +13 -0
- package/src/backends/opencode-run.ts +432 -0
- package/src/backends/opencode-sse.ts +360 -0
- package/src/backends/pi-rpc.ts +242 -0
- package/src/backends/pi-run.ts +154 -0
- package/src/backends/select.ts +77 -0
- package/src/backends/subprocess-run.ts +305 -0
- package/src/backends/types.ts +29 -0
- package/src/backends/unsupported.ts +48 -0
- package/src/baseline/index.ts +437 -0
- package/src/cli/args.ts +109 -0
- package/src/cli/embedded.ts +206 -0
- package/src/cli/main.ts +332 -0
- package/src/cli/version.ts +1 -0
- package/src/conversation/ask-user.ts +105 -0
- package/src/conversation/conversation.ts +109 -0
- package/src/conversation/harness.ts +39 -0
- package/src/conversation/index.ts +4 -0
- package/src/conversation/queue.ts +86 -0
- package/src/flow/accessors.ts +43 -0
- package/src/flow/approvals.ts +10 -0
- package/src/flow/args.ts +26 -0
- package/src/flow/context.ts +106 -0
- package/src/flow/index.ts +4 -0
- package/src/index.ts +17 -0
- package/src/loop/builder/index.ts +346 -0
- package/src/loop/builder/types.ts +131 -0
- package/src/loop/context/compaction.ts +175 -0
- package/src/loop/context/index.ts +3 -0
- package/src/loop/context/offload.ts +79 -0
- package/src/loop/context/types.ts +17 -0
- package/src/loop/engine/bridge.ts +61 -0
- package/src/loop/engine/index.ts +166 -0
- package/src/loop/execution.ts +453 -0
- package/src/loop/fanout.ts +444 -0
- package/src/loop/firing.ts +188 -0
- package/src/loop/graph/analyze.ts +128 -0
- package/src/loop/graph/index.ts +2 -0
- package/src/loop/graph/types.ts +45 -0
- package/src/loop/index.ts +11 -0
- package/src/loop/io/index.ts +3 -0
- package/src/loop/io/linear.ts +714 -0
- package/src/loop/io/sink.ts +170 -0
- package/src/loop/io/source.ts +255 -0
- package/src/loop/serve.ts +307 -0
- package/src/loop/state/index.ts +4 -0
- package/src/loop/state/manifest.ts +56 -0
- package/src/loop/state/port.ts +43 -0
- package/src/loop/state/snapshot.ts +129 -0
- package/src/loop/state/sqlite.ts +265 -0
- package/src/loop/termination-contract.ts +89 -0
- package/src/loop/termination.ts +156 -0
- package/src/model/backend-config.ts +34 -0
- package/src/model/brand.ts +11 -0
- package/src/model/errors.ts +28 -0
- package/src/model/index.ts +7 -0
- package/src/model/json-schema.ts +14 -0
- package/src/model/result.ts +28 -0
- package/src/model/schemas.ts +218 -0
- package/src/model/structured.ts +26 -0
- package/src/monitor/index.ts +367 -0
- package/src/plan/index.ts +2 -0
- package/src/plan/persistent.ts +98 -0
- package/src/plan/verdict.ts +3 -0
- package/src/review/index.ts +3 -0
- package/src/review/loop.ts +365 -0
- package/src/review/reviewers.ts +92 -0
- package/src/review/tool.ts +41 -0
- package/src/run-output/index.ts +319 -0
- package/src/runner/index.ts +1 -0
- package/src/runner/typecheck.ts +72 -0
- package/src/test-utils/fakes.ts +260 -0
- package/src/test-utils/fixtures.ts +33 -0
- package/src/test-utils/index.ts +3 -0
- package/src/test-utils/tier2.ts +12 -0
- package/src/tools/fs.ts +44 -0
- package/src/tools/git.ts +34 -0
- package/src/tools/github.ts +25 -0
- package/src/tools/index.ts +6 -0
- package/src/tools/linear.ts +428 -0
- package/src/tools/process.ts +156 -0
- package/src/tools/terminal.ts +116 -0
|
@@ -0,0 +1,265 @@
|
|
|
1
|
+
import { Database } from "bun:sqlite";
|
|
2
|
+
import { createHash, randomUUID } from "node:crypto";
|
|
3
|
+
import { err, ok, type Result } from "neverthrow";
|
|
4
|
+
import type { RuntimeError } from "../../model/index.ts";
|
|
5
|
+
import { parseManifest, type TaskManifest } from "./manifest.ts";
|
|
6
|
+
import { stateHash } from "./snapshot.ts";
|
|
7
|
+
import type { BranchWritableStateStore, StateHash, StateReducer } from "./port.ts";
|
|
8
|
+
|
|
9
|
+
// `sqlite` is the escalate-for-durability adapter (design D4): per-step checkpoints
|
|
10
|
+
// to a single WAL file plus lease-based crash recovery — finer resume than
|
|
11
|
+
// `snapshot`, still no service and no install (bun:sqlite is built into Bun).
|
|
12
|
+
// LangGraph's `better-sqlite3` saver fails on Bun (native ABI mismatch), so this is
|
|
13
|
+
// hand-rolled directly on `bun:sqlite`. It implements the SAME `StateStore` port as
|
|
14
|
+
// `snapshot`: loop code targets the port, so selecting this adapter needs no change
|
|
15
|
+
// to a loop definition. State lives in three tables:
|
|
16
|
+
// - `snapshots(hash, manifest)` — content-addressed manifest JSON (checkpoints AND
|
|
17
|
+
// branch copies/results). Reuses `stateHash` so a manifest hashes identically
|
|
18
|
+
// across adapters; `INSERT OR REPLACE` keeps equal manifests deduplicated.
|
|
19
|
+
// - `history(seq, hash, created_at)` — the ordered cycle stream (time-travel). Only
|
|
20
|
+
// `checkpoint` appends; `branch`/`saveBranch` copies do NOT, matching `snapshot` history.
|
|
21
|
+
// - `lease(owner, heartbeat)` — a single advisory row for crash recovery, NOT a
|
|
22
|
+
// write lock: content-addressed snapshots + append-only history make concurrent
|
|
23
|
+
// writers safe-but-interleaved. A live foreign lease is refused at open; a stale
|
|
24
|
+
// one (holder crashed) is reclaimed, and the per-step-committed history is resumed.
|
|
25
|
+
//
|
|
26
|
+
// Durability boundary: every `checkpoint` commits in its own transaction and folds
|
|
27
|
+
// the WAL into the main db (`wal_checkpoint`), so an interrupt loses at most the
|
|
28
|
+
// in-flight step — a fresh store on the same file recovers `history()`/`load()`.
|
|
29
|
+
|
|
30
|
+
const DEFAULT_LEASE_TTL_MS = 30_000;
|
|
31
|
+
|
|
32
|
+
const SCHEMA: readonly string[] = [
|
|
33
|
+
"CREATE TABLE IF NOT EXISTS snapshots (hash TEXT PRIMARY KEY, manifest TEXT NOT NULL)",
|
|
34
|
+
"CREATE TABLE IF NOT EXISTS history (seq INTEGER PRIMARY KEY AUTOINCREMENT, hash TEXT NOT NULL, created_at INTEGER NOT NULL)",
|
|
35
|
+
"CREATE TABLE IF NOT EXISTS lease (id INTEGER PRIMARY KEY CHECK (id = 1), owner TEXT NOT NULL, heartbeat INTEGER NOT NULL)"
|
|
36
|
+
];
|
|
37
|
+
|
|
38
|
+
export interface SqliteStoreOptions {
|
|
39
|
+
/** Database file path; durability and crash recovery require a real file
|
|
40
|
+
* (`:memory:` works but is lost on exit). */
|
|
41
|
+
readonly path: string;
|
|
42
|
+
/** Lease owner id for this runner; defaults to a random per-process id. */
|
|
43
|
+
readonly owner?: string;
|
|
44
|
+
/** A lease whose heartbeat is older than this many ms is treated as a crashed
|
|
45
|
+
* holder and reclaimed on open. */
|
|
46
|
+
readonly leaseTtlMs?: number;
|
|
47
|
+
/** Injectable clock (ms epoch) for deterministic lease tests; defaults to `Date.now`. */
|
|
48
|
+
readonly now?: () => number;
|
|
49
|
+
}
|
|
50
|
+
|
|
51
|
+
/**
|
|
52
|
+
* The `sqlite` adapter: a {@link StateStore} plus lease lifecycle controls. The
|
|
53
|
+
* extra members are NOT part of the port — loop code targets `StateStore` and never
|
|
54
|
+
* sees them, so swapping `snapshot` <-> `sqlite` needs no loop-definition change.
|
|
55
|
+
*/
|
|
56
|
+
export interface SqliteStore extends BranchWritableStateStore {
|
|
57
|
+
/** This runner's lease owner id. */
|
|
58
|
+
readonly owner: string;
|
|
59
|
+
/** Refresh the lease heartbeat mid-step so a long step keeps its lease;
|
|
60
|
+
* `err` once another runner has reclaimed a stale lease. */
|
|
61
|
+
heartbeat(): Result<void, RuntimeError>;
|
|
62
|
+
/** Release the lease and close the database (clean shutdown — not a crash). */
|
|
63
|
+
close(): void;
|
|
64
|
+
}
|
|
65
|
+
|
|
66
|
+
interface ManifestRow {
|
|
67
|
+
manifest: string;
|
|
68
|
+
}
|
|
69
|
+
interface HashRow {
|
|
70
|
+
hash: string;
|
|
71
|
+
}
|
|
72
|
+
interface LeaseRow {
|
|
73
|
+
owner: string;
|
|
74
|
+
heartbeat: number;
|
|
75
|
+
}
|
|
76
|
+
|
|
77
|
+
function fsErr(path: string, message: string): RuntimeError {
|
|
78
|
+
return { _tag: "FileSystemError", path, message };
|
|
79
|
+
}
|
|
80
|
+
|
|
81
|
+
export function createSqliteStore(options: SqliteStoreOptions): Result<SqliteStore, RuntimeError> {
|
|
82
|
+
const { path } = options;
|
|
83
|
+
const owner = options.owner ?? randomUUID();
|
|
84
|
+
const leaseTtlMs = options.leaseTtlMs ?? DEFAULT_LEASE_TTL_MS;
|
|
85
|
+
const now = options.now ?? Date.now;
|
|
86
|
+
|
|
87
|
+
let db: Database;
|
|
88
|
+
try {
|
|
89
|
+
db = new Database(path, { create: true });
|
|
90
|
+
db.run("PRAGMA journal_mode = WAL");
|
|
91
|
+
db.run("PRAGMA busy_timeout = 5000");
|
|
92
|
+
for (const statement of SCHEMA) {
|
|
93
|
+
db.run(statement);
|
|
94
|
+
}
|
|
95
|
+
} catch (error) {
|
|
96
|
+
return err(fsErr(path, `failed to open sqlite state store: ${String(error)}`));
|
|
97
|
+
}
|
|
98
|
+
|
|
99
|
+
const readSnapshot = (hash: StateHash): Result<TaskManifest, RuntimeError> => {
|
|
100
|
+
let row: ManifestRow | null;
|
|
101
|
+
try {
|
|
102
|
+
row = db.query<ManifestRow, [string]>("SELECT manifest FROM snapshots WHERE hash = ?").get(hash);
|
|
103
|
+
} catch (error) {
|
|
104
|
+
return err(fsErr(path, `failed to read snapshot ${hash}: ${String(error)}`));
|
|
105
|
+
}
|
|
106
|
+
if (row === null) {
|
|
107
|
+
return err(fsErr(path, `no snapshot for hash ${hash}`));
|
|
108
|
+
}
|
|
109
|
+
let raw: unknown;
|
|
110
|
+
try {
|
|
111
|
+
raw = JSON.parse(row.manifest);
|
|
112
|
+
} catch (error) {
|
|
113
|
+
return err(fsErr(path, `corrupt snapshot ${hash}: ${String(error)}`));
|
|
114
|
+
}
|
|
115
|
+
return parseManifest(raw);
|
|
116
|
+
};
|
|
117
|
+
|
|
118
|
+
const writeSnapshot = (hash: StateHash, manifest: TaskManifest): Result<void, RuntimeError> => {
|
|
119
|
+
try {
|
|
120
|
+
db.run("INSERT OR REPLACE INTO snapshots (hash, manifest) VALUES (?, ?)", [hash, JSON.stringify(manifest)]);
|
|
121
|
+
return ok(undefined);
|
|
122
|
+
} catch (error) {
|
|
123
|
+
return err(fsErr(path, `failed to write snapshot ${hash}: ${String(error)}`));
|
|
124
|
+
}
|
|
125
|
+
};
|
|
126
|
+
|
|
127
|
+
// Acquire the lease = the crash-recovery boundary. A fresh foreign lease means
|
|
128
|
+
// another runner is live (refuse); a stale or self-owned lease is reclaimed.
|
|
129
|
+
try {
|
|
130
|
+
const current = db.query<LeaseRow, []>("SELECT owner, heartbeat FROM lease WHERE id = 1").get();
|
|
131
|
+
const ts = now();
|
|
132
|
+
if (current === null) {
|
|
133
|
+
db.run("INSERT INTO lease (id, owner, heartbeat) VALUES (1, ?, ?)", [owner, ts]);
|
|
134
|
+
} else if (current.owner !== owner && ts - current.heartbeat <= leaseTtlMs) {
|
|
135
|
+
db.close();
|
|
136
|
+
return err(fsErr(path, `state store leased by "${current.owner}"; another runner is active`));
|
|
137
|
+
} else {
|
|
138
|
+
db.run("UPDATE lease SET owner = ?, heartbeat = ? WHERE id = 1", [owner, ts]);
|
|
139
|
+
}
|
|
140
|
+
} catch (error) {
|
|
141
|
+
db.close();
|
|
142
|
+
return err(fsErr(path, `lease acquisition failed: ${String(error)}`));
|
|
143
|
+
}
|
|
144
|
+
|
|
145
|
+
const store: SqliteStore = {
|
|
146
|
+
owner,
|
|
147
|
+
|
|
148
|
+
load(hash) {
|
|
149
|
+
if (hash !== undefined) {
|
|
150
|
+
return Promise.resolve(readSnapshot(hash));
|
|
151
|
+
}
|
|
152
|
+
let row: HashRow | null;
|
|
153
|
+
try {
|
|
154
|
+
row = db.query<HashRow, []>("SELECT hash FROM history ORDER BY seq DESC LIMIT 1").get();
|
|
155
|
+
} catch (error) {
|
|
156
|
+
return Promise.resolve(err(fsErr(path, `failed to read history: ${String(error)}`)));
|
|
157
|
+
}
|
|
158
|
+
if (row === null) {
|
|
159
|
+
return Promise.resolve(err(fsErr(path, "no checkpoint to load")));
|
|
160
|
+
}
|
|
161
|
+
return Promise.resolve(readSnapshot(row.hash));
|
|
162
|
+
},
|
|
163
|
+
|
|
164
|
+
checkpoint(state) {
|
|
165
|
+
const validated = parseManifest(state);
|
|
166
|
+
if (validated.isErr()) {
|
|
167
|
+
return Promise.resolve(err(validated.error));
|
|
168
|
+
}
|
|
169
|
+
const manifest = validated.value;
|
|
170
|
+
const hash = stateHash(manifest);
|
|
171
|
+
try {
|
|
172
|
+
// One transaction per step: snapshot + history row + lease heartbeat commit
|
|
173
|
+
// atomically, so an interrupt never leaves a half-written cycle.
|
|
174
|
+
const commit = db.transaction((m: TaskManifest, h: StateHash, ts: number) => {
|
|
175
|
+
db.run("INSERT OR REPLACE INTO snapshots (hash, manifest) VALUES (?, ?)", [h, JSON.stringify(m)]);
|
|
176
|
+
db.run("INSERT INTO history (hash, created_at) VALUES (?, ?)", [h, ts]);
|
|
177
|
+
db.run("UPDATE lease SET heartbeat = ? WHERE id = 1 AND owner = ?", [ts, owner]);
|
|
178
|
+
});
|
|
179
|
+
commit(manifest, hash, now());
|
|
180
|
+
db.run("PRAGMA wal_checkpoint(PASSIVE)");
|
|
181
|
+
} catch (error) {
|
|
182
|
+
return Promise.resolve(err(fsErr(path, `checkpoint failed: ${String(error)}`)));
|
|
183
|
+
}
|
|
184
|
+
return Promise.resolve(ok(hash));
|
|
185
|
+
},
|
|
186
|
+
|
|
187
|
+
branch(from) {
|
|
188
|
+
const source = readSnapshot(from);
|
|
189
|
+
if (source.isErr()) {
|
|
190
|
+
return Promise.resolve(err(source.error));
|
|
191
|
+
}
|
|
192
|
+
// Synthetic (non-content) hash so the isolated branch copy gets its own identity
|
|
193
|
+
// instead of colliding with `from` under content addressing.
|
|
194
|
+
const hash = createHash("sha256").update(`${from}:${randomUUID()}`).digest("hex").slice(0, 12);
|
|
195
|
+
const written = writeSnapshot(hash, source.value);
|
|
196
|
+
if (written.isErr()) {
|
|
197
|
+
return Promise.resolve(err(written.error));
|
|
198
|
+
}
|
|
199
|
+
return Promise.resolve(ok(hash));
|
|
200
|
+
},
|
|
201
|
+
|
|
202
|
+
saveBranch(branch, state) {
|
|
203
|
+
const source = readSnapshot(branch);
|
|
204
|
+
if (source.isErr()) {
|
|
205
|
+
return Promise.resolve(err(source.error));
|
|
206
|
+
}
|
|
207
|
+
const validated = parseManifest(state);
|
|
208
|
+
if (validated.isErr()) {
|
|
209
|
+
return Promise.resolve(err(validated.error));
|
|
210
|
+
}
|
|
211
|
+
const manifest = validated.value;
|
|
212
|
+
const hash = stateHash(manifest);
|
|
213
|
+
const written = writeSnapshot(hash, manifest);
|
|
214
|
+
if (written.isErr()) {
|
|
215
|
+
return Promise.resolve(err(written.error));
|
|
216
|
+
}
|
|
217
|
+
return Promise.resolve(ok(hash));
|
|
218
|
+
},
|
|
219
|
+
|
|
220
|
+
merge(branches, reducer: StateReducer<TaskManifest>) {
|
|
221
|
+
const states: TaskManifest[] = [];
|
|
222
|
+
for (const hash of branches) {
|
|
223
|
+
const loaded = readSnapshot(hash);
|
|
224
|
+
if (loaded.isErr()) {
|
|
225
|
+
return Promise.resolve(err(loaded.error));
|
|
226
|
+
}
|
|
227
|
+
states.push(loaded.value);
|
|
228
|
+
}
|
|
229
|
+
return Promise.resolve(parseManifest(reducer(states)));
|
|
230
|
+
},
|
|
231
|
+
|
|
232
|
+
history() {
|
|
233
|
+
try {
|
|
234
|
+
const rows = db.query<HashRow, []>("SELECT hash FROM history ORDER BY seq").all();
|
|
235
|
+
return Promise.resolve(ok(rows.map((row) => row.hash)));
|
|
236
|
+
} catch (error) {
|
|
237
|
+
return Promise.resolve(err(fsErr(path, `failed to read history: ${String(error)}`)));
|
|
238
|
+
}
|
|
239
|
+
},
|
|
240
|
+
|
|
241
|
+
heartbeat() {
|
|
242
|
+
try {
|
|
243
|
+
const result = db.run("UPDATE lease SET heartbeat = ? WHERE id = 1 AND owner = ?", [now(), owner]);
|
|
244
|
+
if (result.changes === 0) {
|
|
245
|
+
return err(fsErr(path, `lease lost by "${owner}"; another runner reclaimed the store`));
|
|
246
|
+
}
|
|
247
|
+
return ok(undefined);
|
|
248
|
+
} catch (error) {
|
|
249
|
+
return err(fsErr(path, `heartbeat failed: ${String(error)}`));
|
|
250
|
+
}
|
|
251
|
+
},
|
|
252
|
+
|
|
253
|
+
close() {
|
|
254
|
+
try {
|
|
255
|
+
db.run("DELETE FROM lease WHERE id = 1 AND owner = ?", [owner]);
|
|
256
|
+
db.run("PRAGMA wal_checkpoint(TRUNCATE)");
|
|
257
|
+
} catch {
|
|
258
|
+
// Best-effort lease release / WAL fold; closing the handle is what matters.
|
|
259
|
+
}
|
|
260
|
+
db.close();
|
|
261
|
+
}
|
|
262
|
+
};
|
|
263
|
+
|
|
264
|
+
return ok(store);
|
|
265
|
+
}
|
|
@@ -0,0 +1,89 @@
|
|
|
1
|
+
// Termination by construction (design D3 / tasks 4.5): enforced at build/lint time, before
|
|
2
|
+
// any loop runs. A beneficial cycle provably terminates only if its back-edge carries a loop
|
|
3
|
+
// variant — a measure bounded below that strictly decreases. The contract here rejects, with
|
|
4
|
+
// the offending cycle NAMED, two ways a loop graph violates that:
|
|
5
|
+
//
|
|
6
|
+
// 1. An ACCIDENTAL cycle — a back-edge the author never declared. Undeclared cycles have no
|
|
7
|
+
// variant by definition and are reported.
|
|
8
|
+
// 2. An UNGUARDED declared back-edge — a declared loop whose head carries neither a preset
|
|
9
|
+
// archetype nor a `.measure()` override.
|
|
10
|
+
//
|
|
11
|
+
// This module owns the SHAPE of the contract; it does not know about presets. It asks a
|
|
12
|
+
// caller-supplied `HasVariant` predicate whether a given loop-head node has a variant. L06
|
|
13
|
+
// satisfies that predicate by wiring preset archetypes (untilGatesGreen, untilManifestComplete,
|
|
14
|
+
// …) and `.measure()` overrides onto nodes. Effect-free (neverthrow only); kept internal — the
|
|
15
|
+
// builder surfaces the result, this file is not re-exported from ../index.ts.
|
|
16
|
+
|
|
17
|
+
import { err, ok, type Result } from "neverthrow";
|
|
18
|
+
|
|
19
|
+
import type { BackEdge, CycleReport, NodeId } from "./graph/index.ts";
|
|
20
|
+
|
|
21
|
+
/**
|
|
22
|
+
* Predicate the contract queries per loop. L06 implements it by checking whether the back-edge's
|
|
23
|
+
* head node has a bound preset archetype or `.measure()` variant. Queried with the back-edge
|
|
24
|
+
* head (`BackEdge.to`) — the node a loop jumps back to.
|
|
25
|
+
*/
|
|
26
|
+
export type HasVariant = (node: NodeId) => boolean;
|
|
27
|
+
|
|
28
|
+
export type CycleViolationKind = "unguarded-back-edge" | "accidental-cycle";
|
|
29
|
+
|
|
30
|
+
export interface CycleViolation {
|
|
31
|
+
readonly kind: CycleViolationKind;
|
|
32
|
+
readonly backEdge: BackEdge;
|
|
33
|
+
/** The cycle's node path, used to name it in the message. */
|
|
34
|
+
readonly cycle: readonly NodeId[];
|
|
35
|
+
}
|
|
36
|
+
|
|
37
|
+
export interface TerminationContractError {
|
|
38
|
+
readonly _tag: "TerminationContractViolated";
|
|
39
|
+
readonly violations: readonly CycleViolation[];
|
|
40
|
+
/** Human-readable summary naming every offending cycle. */
|
|
41
|
+
readonly message: string;
|
|
42
|
+
}
|
|
43
|
+
|
|
44
|
+
/** Render a cycle path as `a → b → c → a` so the loop is named, not just counted. */
|
|
45
|
+
function nameCycle(cycle: readonly NodeId[]): string {
|
|
46
|
+
const first = cycle[0];
|
|
47
|
+
if (first === undefined) return "(empty)";
|
|
48
|
+
if (cycle.length === 1) return `${first} → ${first}`;
|
|
49
|
+
const last = cycle[cycle.length - 1];
|
|
50
|
+
const closed = last === first ? cycle : [...cycle, first];
|
|
51
|
+
return closed.join(" → ");
|
|
52
|
+
}
|
|
53
|
+
|
|
54
|
+
function describe(v: CycleViolation): string {
|
|
55
|
+
const named = nameCycle(v.cycle);
|
|
56
|
+
return v.kind === "accidental-cycle"
|
|
57
|
+
? `accidental cycle ${named} — declare it as a loop back-edge or remove an edge`
|
|
58
|
+
: `unguarded back-edge ${v.backEdge.from} → ${v.backEdge.to} closing cycle ${named} — ` +
|
|
59
|
+
`add a preset archetype or .measure() to node "${v.backEdge.to}"`;
|
|
60
|
+
}
|
|
61
|
+
|
|
62
|
+
/**
|
|
63
|
+
* Enforce the termination contract over an analyzed loop graph. Returns `ok` when every cycle is
|
|
64
|
+
* a declared loop back-edge whose head carries a variant; otherwise `err` naming each offending
|
|
65
|
+
* cycle. Collects ALL violations in one pass so the author sees every problem at once.
|
|
66
|
+
*/
|
|
67
|
+
export function enforceTerminationContract(
|
|
68
|
+
report: CycleReport,
|
|
69
|
+
hasVariant: HasVariant
|
|
70
|
+
): Result<void, TerminationContractError> {
|
|
71
|
+
const violations: CycleViolation[] = [];
|
|
72
|
+
|
|
73
|
+
for (const backEdge of report.backEdges) {
|
|
74
|
+
if (!backEdge.declared) {
|
|
75
|
+
violations.push({ kind: "accidental-cycle", backEdge, cycle: backEdge.cycle });
|
|
76
|
+
} else if (!hasVariant(backEdge.to)) {
|
|
77
|
+
violations.push({ kind: "unguarded-back-edge", backEdge, cycle: backEdge.cycle });
|
|
78
|
+
}
|
|
79
|
+
}
|
|
80
|
+
|
|
81
|
+
if (violations.length === 0) return ok(undefined);
|
|
82
|
+
|
|
83
|
+
const count = violations.length;
|
|
84
|
+
const message =
|
|
85
|
+
`Loop graph violates termination by construction (${String(count)} ` +
|
|
86
|
+
`${count === 1 ? "cycle" : "cycles"}): ${violations.map(describe).join("; ")}.`;
|
|
87
|
+
|
|
88
|
+
return err({ _tag: "TerminationContractViolated", violations, message });
|
|
89
|
+
}
|
|
@@ -0,0 +1,156 @@
|
|
|
1
|
+
import type { Usage } from "../model/index.ts";
|
|
2
|
+
|
|
3
|
+
export interface FingerprintAction {
|
|
4
|
+
readonly identity: string;
|
|
5
|
+
readonly inputs: unknown;
|
|
6
|
+
}
|
|
7
|
+
|
|
8
|
+
export interface FingerprintDetectorOptions<T> {
|
|
9
|
+
readonly project: (value: T) => FingerprintAction | undefined;
|
|
10
|
+
/** Number of recent fingerprints retained for repeat detection. */
|
|
11
|
+
readonly windowSize?: number;
|
|
12
|
+
/** Repeated occurrences within the window required to declare stuck. */
|
|
13
|
+
readonly repeatThreshold?: number;
|
|
14
|
+
}
|
|
15
|
+
|
|
16
|
+
export type TokenUsageSummary =
|
|
17
|
+
| { readonly kind: "known"; readonly total: number; readonly budget?: number }
|
|
18
|
+
| { readonly kind: "unknown"; readonly knownTotal: number; readonly budget?: number };
|
|
19
|
+
|
|
20
|
+
export function usageTokens(usage: Usage): number {
|
|
21
|
+
return usage.input + usage.output + (usage.reasoning ?? 0);
|
|
22
|
+
}
|
|
23
|
+
|
|
24
|
+
export class TokenBudgetCounter {
|
|
25
|
+
readonly #budget: number | undefined;
|
|
26
|
+
#knownTotal = 0;
|
|
27
|
+
#unknown = false;
|
|
28
|
+
#knownRecords = 0;
|
|
29
|
+
|
|
30
|
+
constructor(budget?: number) {
|
|
31
|
+
this.#budget = budget;
|
|
32
|
+
}
|
|
33
|
+
|
|
34
|
+
record(usage: Usage | undefined): void {
|
|
35
|
+
if (usage === undefined) {
|
|
36
|
+
this.#unknown = true;
|
|
37
|
+
return;
|
|
38
|
+
}
|
|
39
|
+
this.#knownRecords++;
|
|
40
|
+
this.#knownTotal += usageTokens(usage);
|
|
41
|
+
}
|
|
42
|
+
|
|
43
|
+
get shouldReport(): boolean {
|
|
44
|
+
return this.#budget !== undefined || this.#knownRecords > 0;
|
|
45
|
+
}
|
|
46
|
+
|
|
47
|
+
get exhausted(): boolean {
|
|
48
|
+
return this.#budget !== undefined && !this.#unknown && this.#knownTotal > this.#budget;
|
|
49
|
+
}
|
|
50
|
+
|
|
51
|
+
summary(): TokenUsageSummary {
|
|
52
|
+
if (this.#unknown) {
|
|
53
|
+
return withBudget({ kind: "unknown", knownTotal: this.#knownTotal }, this.#budget);
|
|
54
|
+
}
|
|
55
|
+
return withBudget({ kind: "known", total: this.#knownTotal }, this.#budget);
|
|
56
|
+
}
|
|
57
|
+
}
|
|
58
|
+
|
|
59
|
+
export function makeFingerprintStuckDetector<T>(
|
|
60
|
+
options: FingerprintDetectorOptions<T>
|
|
61
|
+
): (value: T) => boolean {
|
|
62
|
+
const repeatThreshold = options.repeatThreshold ?? 2;
|
|
63
|
+
const windowSize = Math.max(options.windowSize ?? 3, repeatThreshold);
|
|
64
|
+
const window: string[] = [];
|
|
65
|
+
const counts = new Map<string, number>();
|
|
66
|
+
|
|
67
|
+
return (value: T): boolean => {
|
|
68
|
+
const projected = options.project(value);
|
|
69
|
+
if (projected === undefined) {
|
|
70
|
+
return false;
|
|
71
|
+
}
|
|
72
|
+
|
|
73
|
+
const fingerprint = fingerprintAction(projected);
|
|
74
|
+
window.push(fingerprint);
|
|
75
|
+
counts.set(fingerprint, (counts.get(fingerprint) ?? 0) + 1);
|
|
76
|
+
|
|
77
|
+
if (window.length > windowSize) {
|
|
78
|
+
const expired = window.shift();
|
|
79
|
+
if (expired !== undefined) {
|
|
80
|
+
const remaining = (counts.get(expired) ?? 1) - 1;
|
|
81
|
+
if (remaining <= 0) {
|
|
82
|
+
counts.delete(expired);
|
|
83
|
+
} else {
|
|
84
|
+
counts.set(expired, remaining);
|
|
85
|
+
}
|
|
86
|
+
}
|
|
87
|
+
}
|
|
88
|
+
|
|
89
|
+
return (counts.get(fingerprint) ?? 0) >= repeatThreshold;
|
|
90
|
+
};
|
|
91
|
+
}
|
|
92
|
+
|
|
93
|
+
export function fingerprintAction(action: FingerprintAction): string {
|
|
94
|
+
return fnv1a32(`${action.identity}\u0000${stableStringify(action.inputs)}`);
|
|
95
|
+
}
|
|
96
|
+
|
|
97
|
+
function withBudget<T extends { readonly kind: "known" | "unknown" }>(
|
|
98
|
+
summary: T,
|
|
99
|
+
budget: number | undefined
|
|
100
|
+
): T | (T & { readonly budget: number }) {
|
|
101
|
+
if (budget === undefined) {
|
|
102
|
+
return summary;
|
|
103
|
+
}
|
|
104
|
+
return { ...summary, budget };
|
|
105
|
+
}
|
|
106
|
+
|
|
107
|
+
function stableStringify(value: unknown, seen = new WeakSet<object>()): string {
|
|
108
|
+
if (value === null) {
|
|
109
|
+
return "null";
|
|
110
|
+
}
|
|
111
|
+
|
|
112
|
+
switch (typeof value) {
|
|
113
|
+
case "string":
|
|
114
|
+
return JSON.stringify(value);
|
|
115
|
+
case "number":
|
|
116
|
+
case "boolean":
|
|
117
|
+
return String(value);
|
|
118
|
+
case "bigint":
|
|
119
|
+
return `${value.toString()}n`;
|
|
120
|
+
case "undefined":
|
|
121
|
+
return "undefined";
|
|
122
|
+
case "symbol":
|
|
123
|
+
case "function":
|
|
124
|
+
return String(value);
|
|
125
|
+
case "object":
|
|
126
|
+
break;
|
|
127
|
+
}
|
|
128
|
+
|
|
129
|
+
const objectValue = value;
|
|
130
|
+
if (seen.has(objectValue)) {
|
|
131
|
+
return "[Circular]";
|
|
132
|
+
}
|
|
133
|
+
seen.add(objectValue);
|
|
134
|
+
|
|
135
|
+
if (Array.isArray(objectValue)) {
|
|
136
|
+
const serialized = `[${objectValue.map((item) => stableStringify(item, seen)).join(",")}]`;
|
|
137
|
+
seen.delete(objectValue);
|
|
138
|
+
return serialized;
|
|
139
|
+
}
|
|
140
|
+
|
|
141
|
+
const entries = Object.entries(objectValue).sort(([left], [right]) => left.localeCompare(right));
|
|
142
|
+
const serialized = `{${entries
|
|
143
|
+
.map(([key, entryValue]) => `${JSON.stringify(key)}:${stableStringify(entryValue, seen)}`)
|
|
144
|
+
.join(",")}}`;
|
|
145
|
+
seen.delete(objectValue);
|
|
146
|
+
return serialized;
|
|
147
|
+
}
|
|
148
|
+
|
|
149
|
+
function fnv1a32(value: string): string {
|
|
150
|
+
let hash = 0x811c9dc5;
|
|
151
|
+
for (let index = 0; index < value.length; index++) {
|
|
152
|
+
hash ^= value.charCodeAt(index);
|
|
153
|
+
hash = Math.imul(hash, 0x01000193);
|
|
154
|
+
}
|
|
155
|
+
return (hash >>> 0).toString(16).padStart(8, "0");
|
|
156
|
+
}
|
|
@@ -0,0 +1,34 @@
|
|
|
1
|
+
import type { z } from "zod";
|
|
2
|
+
import type { BackendTag, LlmResult } from "./schemas.ts";
|
|
3
|
+
import type { SessionId } from "./brand.ts";
|
|
4
|
+
|
|
5
|
+
export type BackendApprovalPolicy = "never" | "on-request" | "on-failure" | "granular" | "untrusted";
|
|
6
|
+
export type BackendSandboxMode = "read-only" | "workspace-write" | "danger-full-access";
|
|
7
|
+
|
|
8
|
+
export interface BackendRetryConfig {
|
|
9
|
+
readonly attempts: number;
|
|
10
|
+
}
|
|
11
|
+
|
|
12
|
+
export interface StructuredOutputConfig<Output = unknown> {
|
|
13
|
+
readonly schema: z.ZodType<Output>;
|
|
14
|
+
readonly name?: string;
|
|
15
|
+
readonly description?: string;
|
|
16
|
+
}
|
|
17
|
+
|
|
18
|
+
export interface BackendConfig<B extends BackendTag = BackendTag, Output = unknown> {
|
|
19
|
+
readonly model?: string;
|
|
20
|
+
readonly systemPrompt?: string;
|
|
21
|
+
readonly approvalPolicy?: BackendApprovalPolicy;
|
|
22
|
+
readonly readOnly?: boolean;
|
|
23
|
+
readonly sandbox?: BackendSandboxMode;
|
|
24
|
+
readonly selfManagedGit?: boolean;
|
|
25
|
+
readonly retry?: BackendRetryConfig;
|
|
26
|
+
readonly structuredOutput?: StructuredOutputConfig<Output>;
|
|
27
|
+
readonly resumeSessionId?: SessionId<B>;
|
|
28
|
+
readonly interactive?: boolean;
|
|
29
|
+
}
|
|
30
|
+
|
|
31
|
+
export type BackendResult<B extends BackendTag> = Omit<LlmResult, "backend" | "sessionId"> & {
|
|
32
|
+
readonly backend: B;
|
|
33
|
+
readonly sessionId: SessionId<B>;
|
|
34
|
+
};
|
|
@@ -0,0 +1,11 @@
|
|
|
1
|
+
import type { BackendTag } from "./schemas.ts";
|
|
2
|
+
|
|
3
|
+
declare const brand: unique symbol;
|
|
4
|
+
|
|
5
|
+
export type Brand<T, Name extends string> = T & { readonly [brand]: Name };
|
|
6
|
+
|
|
7
|
+
export type SessionId<B extends BackendTag> = Brand<string, `SessionId:${B}`>;
|
|
8
|
+
|
|
9
|
+
export function sessionId<B extends BackendTag>(backend: B, value: string): SessionId<B> {
|
|
10
|
+
return value as SessionId<B>;
|
|
11
|
+
}
|
|
@@ -0,0 +1,28 @@
|
|
|
1
|
+
import type { BackendTag, RuntimeError } from "./schemas.ts";
|
|
2
|
+
|
|
3
|
+
type RuntimeErrorArgs<Tag extends RuntimeError["_tag"]> = Omit<
|
|
4
|
+
Extract<RuntimeError, { _tag: Tag }>,
|
|
5
|
+
"_tag"
|
|
6
|
+
>;
|
|
7
|
+
|
|
8
|
+
export function unsupportedFeature(feature: string, reason: string): RuntimeError {
|
|
9
|
+
return { _tag: "UnsupportedFeature", feature, reason };
|
|
10
|
+
}
|
|
11
|
+
|
|
12
|
+
export function backendFailed(backend: BackendTag, message: string): RuntimeError {
|
|
13
|
+
return { _tag: "BackendFailed", backend, message };
|
|
14
|
+
}
|
|
15
|
+
|
|
16
|
+
export function commandFailed(args: RuntimeErrorArgs<"CommandFailed">): RuntimeError {
|
|
17
|
+
return { _tag: "CommandFailed", ...args };
|
|
18
|
+
}
|
|
19
|
+
|
|
20
|
+
export function structuredOutputValidationFailed(
|
|
21
|
+
args: RuntimeErrorArgs<"StructuredOutputValidationFailed">
|
|
22
|
+
): RuntimeError {
|
|
23
|
+
return { _tag: "StructuredOutputValidationFailed", ...args };
|
|
24
|
+
}
|
|
25
|
+
|
|
26
|
+
export function ioFailed(seam: "source" | "sink" | "tool", kind: string, message: string): RuntimeError {
|
|
27
|
+
return { _tag: "IoFailed", seam, kind, message };
|
|
28
|
+
}
|
|
@@ -0,0 +1,14 @@
|
|
|
1
|
+
import { z } from "zod";
|
|
2
|
+
import { CanonicalSchemas } from "./schemas.ts";
|
|
3
|
+
|
|
4
|
+
export function jsonSchemaFromZod(schema: z.ZodType): unknown {
|
|
5
|
+
return z.toJSONSchema(schema, { target: "draft-7" });
|
|
6
|
+
}
|
|
7
|
+
|
|
8
|
+
export function canonicalJsonSchemas(): Record<string, unknown> {
|
|
9
|
+
const schemas: Record<string, unknown> = {};
|
|
10
|
+
for (const [name, schema] of Object.entries(CanonicalSchemas)) {
|
|
11
|
+
schemas[name] = jsonSchemaFromZod(schema);
|
|
12
|
+
}
|
|
13
|
+
return schemas;
|
|
14
|
+
}
|
|
@@ -0,0 +1,28 @@
|
|
|
1
|
+
import type { Result } from "neverthrow";
|
|
2
|
+
|
|
3
|
+
export function orThrow<T, E>(result: Result<T, E>): T {
|
|
4
|
+
if (result.isOk()) {
|
|
5
|
+
return result.value;
|
|
6
|
+
}
|
|
7
|
+
|
|
8
|
+
const error = result.error;
|
|
9
|
+
if (error instanceof Error) {
|
|
10
|
+
throw error;
|
|
11
|
+
}
|
|
12
|
+
throw new Error(errorMessage(error), { cause: error });
|
|
13
|
+
}
|
|
14
|
+
|
|
15
|
+
function errorMessage(error: unknown): string {
|
|
16
|
+
if (
|
|
17
|
+
typeof error === "object" &&
|
|
18
|
+
error !== null &&
|
|
19
|
+
"_tag" in error &&
|
|
20
|
+
typeof error._tag === "string"
|
|
21
|
+
) {
|
|
22
|
+
return error._tag;
|
|
23
|
+
}
|
|
24
|
+
if (typeof error === "string") {
|
|
25
|
+
return error;
|
|
26
|
+
}
|
|
27
|
+
return "Result failed";
|
|
28
|
+
}
|