@plico-ai/runtime 0.0.0-reserved.0 → 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 (187) hide show
  1. package/NOTICE +4 -0
  2. package/README.md +9 -3
  3. package/dist/artifacts.d.ts +10 -0
  4. package/dist/artifacts.js +106 -0
  5. package/dist/engine/context.d.ts +16 -0
  6. package/dist/engine/context.js +77 -0
  7. package/dist/engine/engine.d.ts +12 -0
  8. package/dist/engine/engine.js +235 -0
  9. package/dist/engine/errors.d.ts +12 -0
  10. package/dist/engine/errors.js +19 -0
  11. package/dist/engine/index.d.ts +7 -0
  12. package/dist/engine/index.js +5 -0
  13. package/dist/engine/journal.d.ts +65 -0
  14. package/dist/engine/journal.js +1 -0
  15. package/dist/engine/memory-journal.d.ts +2 -0
  16. package/dist/engine/memory-journal.js +125 -0
  17. package/dist/engine/sqlite-engine.d.ts +7 -0
  18. package/dist/engine/sqlite-engine.js +6 -0
  19. package/dist/engine/sqlite-journal.d.ts +5 -0
  20. package/dist/engine/sqlite-journal.js +136 -0
  21. package/dist/engine/types.d.ts +97 -0
  22. package/dist/engine/types.js +1 -0
  23. package/dist/env.d.ts +7 -0
  24. package/dist/env.js +19 -0
  25. package/dist/errors.d.ts +18 -0
  26. package/dist/errors.js +40 -0
  27. package/dist/evals/discover.d.ts +3 -0
  28. package/dist/evals/discover.js +49 -0
  29. package/dist/evals/index.d.ts +3 -0
  30. package/dist/evals/index.js +3 -0
  31. package/dist/evals/promote.d.ts +7 -0
  32. package/dist/evals/promote.js +32 -0
  33. package/dist/evals/run-evals.d.ts +2 -0
  34. package/dist/evals/run-evals.js +69 -0
  35. package/dist/events.d.ts +28 -0
  36. package/dist/events.js +104 -0
  37. package/dist/guards.d.ts +1 -0
  38. package/dist/guards.js +3 -0
  39. package/dist/index.d.ts +18 -0
  40. package/dist/index.js +17 -0
  41. package/dist/instructions.d.ts +4 -0
  42. package/dist/instructions.js +23 -0
  43. package/dist/policy/approval.d.ts +8 -0
  44. package/dist/policy/approval.js +27 -0
  45. package/dist/providers/ai-sdk.d.ts +10 -0
  46. package/dist/providers/ai-sdk.js +154 -0
  47. package/dist/providers/configured.d.ts +17 -0
  48. package/dist/providers/configured.js +60 -0
  49. package/dist/providers/openai-compatible.d.ts +8 -0
  50. package/dist/providers/openai-compatible.js +14 -0
  51. package/dist/providers/project-module.d.ts +7 -0
  52. package/dist/providers/project-module.js +29 -0
  53. package/dist/providers/registry.d.ts +20 -0
  54. package/dist/providers/registry.js +53 -0
  55. package/dist/readiness/check-readiness.d.ts +2 -0
  56. package/dist/readiness/check-readiness.js +33 -0
  57. package/dist/readiness/checks/approval-policy.d.ts +5 -0
  58. package/dist/readiness/checks/approval-policy.js +19 -0
  59. package/dist/readiness/checks/artifact-access.d.ts +3 -0
  60. package/dist/readiness/checks/artifact-access.js +5 -0
  61. package/dist/readiness/checks/artifacts.d.ts +6 -0
  62. package/dist/readiness/checks/artifacts.js +22 -0
  63. package/dist/readiness/checks/env.d.ts +3 -0
  64. package/dist/readiness/checks/env.js +10 -0
  65. package/dist/readiness/checks/evals.d.ts +2 -0
  66. package/dist/readiness/checks/evals.js +53 -0
  67. package/dist/readiness/checks/persistence.d.ts +3 -0
  68. package/dist/readiness/checks/persistence.js +61 -0
  69. package/dist/readiness/checks/project.d.ts +7 -0
  70. package/dist/readiness/checks/project.js +21 -0
  71. package/dist/readiness/checks/provider.d.ts +2 -0
  72. package/dist/readiness/checks/provider.js +20 -0
  73. package/dist/readiness/checks/server-config.d.ts +2 -0
  74. package/dist/readiness/checks/server-config.js +13 -0
  75. package/dist/readiness/checks/studio.d.ts +2 -0
  76. package/dist/readiness/checks/studio.js +16 -0
  77. package/dist/readiness/checks/thread-store.d.ts +3 -0
  78. package/dist/readiness/checks/thread-store.js +12 -0
  79. package/dist/readiness/checks/tools.d.ts +3 -0
  80. package/dist/readiness/checks/tools.js +42 -0
  81. package/dist/readiness/diagnostic.d.ts +2 -0
  82. package/dist/readiness/diagnostic.js +7 -0
  83. package/dist/readiness/index.d.ts +1 -0
  84. package/dist/readiness/index.js +1 -0
  85. package/dist/readiness/open-database.d.ts +8 -0
  86. package/dist/readiness/open-database.js +15 -0
  87. package/dist/run/artifact-tool-context.d.ts +19 -0
  88. package/dist/run/artifact-tool-context.js +74 -0
  89. package/dist/run/finalize.d.ts +16 -0
  90. package/dist/run/finalize.js +11 -0
  91. package/dist/run/index.d.ts +1 -0
  92. package/dist/run/index.js +1 -0
  93. package/dist/run/pinned-instructions.d.ts +18 -0
  94. package/dist/run/pinned-instructions.js +36 -0
  95. package/dist/run/record-tools.d.ts +8 -0
  96. package/dist/run/record-tools.js +21 -0
  97. package/dist/run/run-project.d.ts +17 -0
  98. package/dist/run/run-project.js +222 -0
  99. package/dist/run/strategies/approval.d.ts +9 -0
  100. package/dist/run/strategies/approval.js +42 -0
  101. package/dist/run/strategies/default-dry.d.ts +3 -0
  102. package/dist/run/strategies/default-dry.js +5 -0
  103. package/dist/run/strategies/provider.d.ts +6 -0
  104. package/dist/run/strategies/provider.js +193 -0
  105. package/dist/run/strategies/scripted.d.ts +5 -0
  106. package/dist/run/strategies/scripted.js +106 -0
  107. package/dist/run/tool-fingerprint.d.ts +2 -0
  108. package/dist/run/tool-fingerprint.js +26 -0
  109. package/dist/run/tool-step.d.ts +4 -0
  110. package/dist/run/tool-step.js +62 -0
  111. package/dist/run/tracing.d.ts +12 -0
  112. package/dist/run/tracing.js +92 -0
  113. package/dist/script.d.ts +4 -0
  114. package/dist/script.js +51 -0
  115. package/dist/storage/index.d.ts +1 -0
  116. package/dist/storage/index.js +1 -0
  117. package/dist/storage/metadata.d.ts +5 -0
  118. package/dist/storage/metadata.js +26 -0
  119. package/dist/storage/sqlite/append-event.d.ts +3 -0
  120. package/dist/storage/sqlite/append-event.js +108 -0
  121. package/dist/storage/sqlite/driver.d.ts +9 -0
  122. package/dist/storage/sqlite/driver.js +13 -0
  123. package/dist/storage/sqlite/index.d.ts +1 -0
  124. package/dist/storage/sqlite/index.js +1 -0
  125. package/dist/storage/sqlite/open.d.ts +3 -0
  126. package/dist/storage/sqlite/open.js +54 -0
  127. package/dist/storage/sqlite/owner-lock.d.ts +5 -0
  128. package/dist/storage/sqlite/owner-lock.js +55 -0
  129. package/dist/storage/sqlite/project-thread.d.ts +4 -0
  130. package/dist/storage/sqlite/project-thread.js +33 -0
  131. package/dist/storage/sqlite/queries.d.ts +9 -0
  132. package/dist/storage/sqlite/queries.js +134 -0
  133. package/dist/storage/sqlite/recover-interrupted.d.ts +2 -0
  134. package/dist/storage/sqlite/recover-interrupted.js +46 -0
  135. package/dist/storage/sqlite/schema.d.ts +10 -0
  136. package/dist/storage/sqlite/schema.js +205 -0
  137. package/dist/storage/sqlite/store.d.ts +2 -0
  138. package/dist/storage/sqlite/store.js +51 -0
  139. package/dist/storage/sqlite/thread-log.d.ts +4 -0
  140. package/dist/storage/sqlite/thread-log.js +9 -0
  141. package/dist/storage/sqlite/thread-title.d.ts +24 -0
  142. package/dist/storage/sqlite/thread-title.js +85 -0
  143. package/dist/storage/sqlite/threads.d.ts +47 -0
  144. package/dist/storage/sqlite/threads.js +156 -0
  145. package/dist/threads/stopwords.d.ts +1 -0
  146. package/dist/threads/stopwords.js +16 -0
  147. package/dist/threads/suggest-title.d.ts +9 -0
  148. package/dist/threads/suggest-title.js +102 -0
  149. package/dist/threads/titler.d.ts +10 -0
  150. package/dist/threads/titler.js +63 -0
  151. package/dist/threads/words.d.ts +12 -0
  152. package/dist/threads/words.js +39 -0
  153. package/dist/tools/artifact-model-tool-operations.d.ts +29 -0
  154. package/dist/tools/artifact-model-tool-operations.js +124 -0
  155. package/dist/tools/artifact-model-tools.d.ts +2 -0
  156. package/dist/tools/artifact-model-tools.js +101 -0
  157. package/dist/tools/discover.d.ts +4 -0
  158. package/dist/tools/discover.js +34 -0
  159. package/dist/tools/index.d.ts +3 -0
  160. package/dist/tools/index.js +3 -0
  161. package/dist/tools/load.d.ts +2 -0
  162. package/dist/tools/load.js +83 -0
  163. package/dist/tools/normalize.d.ts +2 -0
  164. package/dist/tools/normalize.js +48 -0
  165. package/dist/tools/schema.d.ts +7 -0
  166. package/dist/tools/schema.js +101 -0
  167. package/dist/tools/skill-model-tool.d.ts +7 -0
  168. package/dist/tools/skill-model-tool.js +38 -0
  169. package/dist/types/artifacts.d.ts +5 -0
  170. package/dist/types/artifacts.js +1 -0
  171. package/dist/types/evals.d.ts +16 -0
  172. package/dist/types/evals.js +1 -0
  173. package/dist/types/index.d.ts +10 -0
  174. package/dist/types/index.js +1 -0
  175. package/dist/types/provider.d.ts +45 -0
  176. package/dist/types/provider.js +1 -0
  177. package/dist/types/readiness.d.ts +9 -0
  178. package/dist/types/readiness.js +1 -0
  179. package/dist/types/run.d.ts +28 -0
  180. package/dist/types/run.js +1 -0
  181. package/dist/types/script.d.ts +16 -0
  182. package/dist/types/script.js +1 -0
  183. package/dist/types/store.d.ts +24 -0
  184. package/dist/types/store.js +1 -0
  185. package/dist/types/tools.d.ts +15 -0
  186. package/dist/types/tools.js +1 -0
  187. package/package.json +67 -8
@@ -0,0 +1,55 @@
1
+ import { randomUUID } from "node:crypto";
2
+ import { rmSync, statSync } from "node:fs";
3
+ import { tmpdir } from "node:os";
4
+ import { basename, join } from "node:path";
5
+ import { createDatabase } from "./driver.js";
6
+ let current;
7
+ function lockPath(token) {
8
+ return join(tmpdir(), `plico-run-owner-${token}.sqlite`);
9
+ }
10
+ export function acquireOwner() {
11
+ if (current)
12
+ return current;
13
+ const token = randomUUID();
14
+ const path = lockPath(token);
15
+ const database = createDatabase(path);
16
+ // SQLite releases this process-owned lock on exit, even when a PID is reused.
17
+ database.exec("CREATE TABLE owner_lock (id INTEGER); BEGIN EXCLUSIVE;");
18
+ current = { token, path, database };
19
+ process.once("exit", () => {
20
+ try {
21
+ database.close();
22
+ rmSync(path, { force: true });
23
+ }
24
+ catch {
25
+ /* OS cleanup may happen later. */
26
+ }
27
+ });
28
+ return current;
29
+ }
30
+ export function ownerIsAlive(token, path) {
31
+ if (current?.token === token)
32
+ return true;
33
+ if (!/^[a-f0-9-]{36}$/.test(token) || basename(path) !== `plico-run-owner-${token}.sqlite`)
34
+ return true;
35
+ try {
36
+ statSync(path);
37
+ }
38
+ catch (error) {
39
+ // ENOENT means the owner is gone, though a temp-folder cleaner could remove a live lock file.
40
+ return error.code !== "ENOENT";
41
+ }
42
+ let probe;
43
+ try {
44
+ probe = createDatabase(path, { readOnly: true });
45
+ probe.exec("PRAGMA busy_timeout = 0; BEGIN EXCLUSIVE; ROLLBACK;");
46
+ return false;
47
+ }
48
+ catch {
49
+ // Any probe error counts as alive, so the run stays running rather than failing spuriously.
50
+ return true;
51
+ }
52
+ finally {
53
+ probe?.close();
54
+ }
55
+ }
@@ -0,0 +1,4 @@
1
+ import type { DatabaseSync } from "node:sqlite";
2
+ import type { PlicoEvent } from "@plico-ai/contracts";
3
+ /** Called inside the run event transaction so the transcript cannot lag behind its log. */
4
+ export declare function projectThreadEvent(db: DatabaseSync, event: PlicoEvent): void;
@@ -0,0 +1,33 @@
1
+ import { randomUUID } from "node:crypto";
2
+ import { appendThreadEvent } from "./thread-log.js";
3
+ import { retitleThread } from "./thread-title.js";
4
+ /** Called inside the run event transaction so the transcript cannot lag behind its log. */
5
+ export function projectThreadEvent(db, event) {
6
+ if (event.type === "run.started" && event.payload.threadId) {
7
+ const threadId = event.payload.threadId;
8
+ if (!db.prepare("SELECT 1 FROM threads WHERE thread_id = ?").get(threadId))
9
+ throw new Error("Unknown thread.");
10
+ db.prepare("INSERT OR IGNORE INTO thread_runs (run_id, thread_id) VALUES (?, ?)").run(event.runId, threadId);
11
+ const linked = db
12
+ .prepare("SELECT thread_id FROM thread_runs WHERE run_id = ?")
13
+ .get(event.runId);
14
+ if (linked.thread_id !== threadId)
15
+ throw new Error("Run already belongs to another thread.");
16
+ }
17
+ if (event.type !== "run.completed" || event.payload.output === undefined)
18
+ return;
19
+ const run = db.prepare("SELECT thread_id FROM thread_runs WHERE run_id = ?").get(event.runId);
20
+ if (!run)
21
+ return;
22
+ const message = {
23
+ id: randomUUID(),
24
+ threadId: run.thread_id,
25
+ role: "assistant",
26
+ content: event.payload.output,
27
+ createdAt: event.timestamp,
28
+ runId: event.runId,
29
+ };
30
+ const sequence = appendThreadEvent(db, run.thread_id, "message.created", { message }, event.timestamp);
31
+ db.prepare("INSERT INTO thread_messages (message_id, thread_id, role, content, created_at, sequence, run_id) VALUES (?, ?, 'assistant', ?, ?, ?, ?)").run(message.id, run.thread_id, message.content, event.timestamp, sequence, event.runId);
32
+ retitleThread(db, run.thread_id, event.timestamp);
33
+ }
@@ -0,0 +1,9 @@
1
+ import type { DatabaseSync } from "node:sqlite";
2
+ import type { EvalReport, PlicoEvent, RunSummary } from "@plico-ai/contracts";
3
+ export declare function readRunSummaries(database: DatabaseSync): RunSummary[];
4
+ export declare function readRunSummary(database: DatabaseSync, runId: string): RunSummary | undefined;
5
+ export declare function readEvents(database: DatabaseSync, runId: string, afterSequence: number | undefined): PlicoEvent[];
6
+ export declare function readThreadRunEvents(database: DatabaseSync, threadId: string): PlicoEvent[];
7
+ export declare function readThreadRunEventsThrough(database: DatabaseSync, threadId: string, runId: string): PlicoEvent[];
8
+ export declare function persistEvalReportRecord(database: DatabaseSync, report: EvalReport): void;
9
+ export declare function readLatestEvalReport(database: DatabaseSync): EvalReport | undefined;
@@ -0,0 +1,134 @@
1
+ import { runStreamType } from "../../events.js";
2
+ import { parseMetadata } from "../metadata.js";
3
+ export function readRunSummaries(database) {
4
+ const rows = database
5
+ .prepare(`
6
+ SELECT run_id, project_root, project_name, status, started_at, ended_at, output, metadata_json
7
+ FROM runs
8
+ ORDER BY started_at DESC, run_id DESC
9
+ `)
10
+ .all();
11
+ return rows.map((row) => rowToRunSummary(row));
12
+ }
13
+ export function readRunSummary(database, runId) {
14
+ const row = database
15
+ .prepare(`
16
+ SELECT run_id, project_root, project_name, status, started_at, ended_at, output, metadata_json
17
+ FROM runs
18
+ WHERE run_id = ?
19
+ `)
20
+ .get(runId);
21
+ return row ? rowToRunSummary(row) : undefined;
22
+ }
23
+ export function readEvents(database, runId, afterSequence) {
24
+ const rows = database
25
+ .prepare(`
26
+ SELECT events.stream_id, events.sequence, events.event_id, events.schema_version,
27
+ events.type, events.timestamp, events.payload_json, artifact_content.content AS artifact_content
28
+ FROM events
29
+ LEFT JOIN artifact_content ON artifact_content.content_hash = json_extract(events.payload_json, '$.contentHash')
30
+ WHERE stream_type = ? AND stream_id = ? AND sequence > ?
31
+ ORDER BY sequence ASC
32
+ `)
33
+ .all(runStreamType, runId, afterSequence ?? 0);
34
+ return rows.map((row) => rowToEvent(row));
35
+ }
36
+ export function readThreadRunEvents(database, threadId) {
37
+ const rows = database
38
+ .prepare(`
39
+ SELECT events.stream_id, events.sequence, events.event_id, events.schema_version,
40
+ events.type, events.timestamp, events.payload_json, artifact_content.content AS artifact_content
41
+ FROM events
42
+ JOIN thread_runs ON thread_runs.run_id = events.stream_id
43
+ LEFT JOIN artifact_content ON artifact_content.content_hash = json_extract(events.payload_json, '$.contentHash')
44
+ WHERE events.stream_type = 'run' AND thread_runs.thread_id = ?
45
+ ORDER BY events.position ASC
46
+ `)
47
+ .all(threadId);
48
+ return rows.map((row) => rowToEvent(row));
49
+ }
50
+ export function readThreadRunEventsThrough(database, threadId, runId) {
51
+ const rows = database
52
+ .prepare(`
53
+ WITH selected_run_position AS (
54
+ SELECT MAX(target_events.position) AS position
55
+ FROM events AS target_events
56
+ JOIN thread_runs AS target_runs ON target_runs.run_id = target_events.stream_id
57
+ WHERE target_events.stream_type = 'run'
58
+ AND target_runs.thread_id = ?
59
+ AND target_events.stream_id = ?
60
+ )
61
+ SELECT events.stream_id, events.sequence, events.event_id, events.schema_version,
62
+ events.type, events.timestamp, events.payload_json, artifact_content.content AS artifact_content
63
+ FROM events
64
+ JOIN thread_runs ON thread_runs.run_id = events.stream_id
65
+ LEFT JOIN artifact_content ON artifact_content.content_hash = json_extract(events.payload_json, '$.contentHash')
66
+ WHERE events.stream_type = 'run' AND thread_runs.thread_id = ?
67
+ AND events.position <= (SELECT position FROM selected_run_position)
68
+ ORDER BY events.position ASC
69
+ `)
70
+ .all(threadId, runId, threadId);
71
+ return rows.map((row) => rowToEvent(row));
72
+ }
73
+ export function persistEvalReportRecord(database, report) {
74
+ database
75
+ .prepare("INSERT INTO eval_reports (evaluated_at, report_json) VALUES (?, ?)")
76
+ .run(report.evaluatedAt, JSON.stringify(report));
77
+ }
78
+ export function readLatestEvalReport(database) {
79
+ // Read-only stores skip schema setup, so older databases may lack this table.
80
+ const hasTable = database
81
+ .prepare("SELECT 1 FROM sqlite_master WHERE type = 'table' AND name = 'eval_reports'")
82
+ .get();
83
+ if (!hasTable)
84
+ return undefined;
85
+ const row = database
86
+ .prepare("SELECT report_json FROM eval_reports ORDER BY id DESC LIMIT 1")
87
+ .get();
88
+ if (!row)
89
+ return undefined;
90
+ // A report from another version is not readable here; the next eval run replaces it.
91
+ const report = JSON.parse(row.report_json);
92
+ return report.schemaVersion === 1 ? report : undefined;
93
+ }
94
+ function rowToRunSummary(row) {
95
+ const metadata = parseMetadata(row.metadata_json);
96
+ return {
97
+ runId: String(row.run_id),
98
+ projectRoot: String(row.project_root),
99
+ projectName: String(row.project_name),
100
+ status: row.status,
101
+ startedAt: String(row.started_at),
102
+ endedAt: row.ended_at === null ? null : String(row.ended_at),
103
+ ...(row.output === null || row.output === undefined ? {} : { output: String(row.output) }),
104
+ ...(metadata === undefined ? {} : { metadata }),
105
+ };
106
+ }
107
+ function rowToEvent(row) {
108
+ // Stored rows are trusted, not validated; pre-publication rows may have status and lack requestId and are not migrated.
109
+ const payload = JSON.parse(String(row.payload_json));
110
+ if (row.type === "artifact.file.written" && typeof payload.contentHash === "string") {
111
+ if (!(row.artifact_content instanceof Uint8Array)) {
112
+ // Missing artifact bodies make the canonical event log incomplete, so fail the read instead
113
+ // of returning a workspace that looks complete but silently omits an artifact version.
114
+ throw new Error(`Artifact content ${payload.contentHash} is missing from storage.`);
115
+ }
116
+ const bytes = Buffer.from(row.artifact_content);
117
+ payload.content =
118
+ payload.contentEncoding === "base64"
119
+ ? { encoding: "base64", data: bytes.toString("base64") }
120
+ : bytes.toString("utf8");
121
+ delete payload.contentEncoding;
122
+ }
123
+ return {
124
+ schemaVersion: 1,
125
+ streamType: runStreamType,
126
+ streamId: String(row.stream_id),
127
+ runId: String(row.stream_id),
128
+ id: String(row.event_id),
129
+ sequence: Number(row.sequence),
130
+ timestamp: String(row.timestamp),
131
+ type: row.type,
132
+ payload,
133
+ };
134
+ }
@@ -0,0 +1,2 @@
1
+ import type { DatabaseSync } from "node:sqlite";
2
+ export declare function recoverInterruptedRuns(database: DatabaseSync): void;
@@ -0,0 +1,46 @@
1
+ import { runFailureEvents } from "../../events.js";
2
+ import { ownerIsAlive } from "./owner-lock.js";
3
+ export function recoverInterruptedRuns(database) {
4
+ if (!database
5
+ .prepare("SELECT 1 FROM sqlite_master WHERE type = 'table' AND name = 'run_owners'")
6
+ .get())
7
+ return;
8
+ if (!database.prepare("SELECT 1 FROM runs WHERE status = 'running' LIMIT 1").get())
9
+ return;
10
+ // Runs the engine journals resume from the journal instead of failing; see engine.recover.
11
+ database.exec("BEGIN IMMEDIATE");
12
+ try {
13
+ const rows = database
14
+ .prepare(`
15
+ SELECT runs.run_id, run_owners.owner_token, run_owners.owner_path FROM runs
16
+ LEFT JOIN run_owners ON run_owners.run_id = runs.run_id
17
+ WHERE runs.status = 'running'
18
+ AND runs.run_id NOT IN (SELECT run_id FROM engine_runs)
19
+ `)
20
+ .all();
21
+ // The write transaction serializes recovery with concurrent event appends and other openers.
22
+ for (const row of rows) {
23
+ if (row.owner_token !== null &&
24
+ row.owner_path !== null &&
25
+ ownerIsAlive(row.owner_token, row.owner_path))
26
+ continue;
27
+ // A running row always has run.started because both are written in one transaction.
28
+ const last = database
29
+ .prepare("SELECT sequence, type FROM events WHERE stream_type = 'run' AND stream_id = ? ORDER BY sequence DESC LIMIT 1")
30
+ .get(row.run_id);
31
+ const timestamp = new Date().toISOString();
32
+ const insert = database.prepare("INSERT INTO events (stream_type, stream_id, sequence, event_id, schema_version, type, timestamp, payload_json) VALUES ('run', ?, ?, ?, 1, ?, ?, ?)");
33
+ for (const event of runFailureEvents(row.run_id, last, "Run interrupted by process exit.", timestamp))
34
+ insert.run(event.streamId, event.sequence, event.id, event.type, event.timestamp, JSON.stringify(event.payload));
35
+ database
36
+ .prepare("UPDATE runs SET status = 'failed', ended_at = ? WHERE run_id = ? AND status = 'running'")
37
+ .run(timestamp, row.run_id);
38
+ database.prepare("DELETE FROM run_owners WHERE run_id = ?").run(row.run_id);
39
+ }
40
+ database.exec("COMMIT");
41
+ }
42
+ catch (error) {
43
+ database.exec("ROLLBACK");
44
+ throw error;
45
+ }
46
+ }
@@ -0,0 +1,10 @@
1
+ import type { DatabaseSync } from "node:sqlite";
2
+ declare const migrations: ReadonlyArray<{
3
+ from: number;
4
+ to: number;
5
+ sql: string;
6
+ }>;
7
+ /** Read-only access must not silently accept an incompatible database. */
8
+ export declare function checkReadOnlySchema(database: DatabaseSync): void;
9
+ export declare function initializeSchema(database: DatabaseSync, steps?: typeof migrations): void;
10
+ export {};
@@ -0,0 +1,205 @@
1
+ const baselineVersion = 2;
2
+ const migrations = [
3
+ {
4
+ from: 2,
5
+ to: 3,
6
+ sql: `
7
+ CREATE TABLE runs_new (
8
+ run_id TEXT PRIMARY KEY, project_root TEXT NOT NULL, project_name TEXT NOT NULL,
9
+ status TEXT NOT NULL, started_at TEXT NOT NULL, ended_at TEXT, output TEXT
10
+ );
11
+ INSERT INTO runs_new SELECT run_id, project_root, project_name, status, started_at, ended_at, output FROM runs;
12
+ DROP TABLE runs;
13
+ ALTER TABLE runs_new RENAME TO runs;
14
+ CREATE INDEX runs_started_at_index ON runs(started_at);
15
+ `,
16
+ },
17
+ {
18
+ from: 3,
19
+ to: 4,
20
+ sql: `CREATE TABLE run_owners (run_id TEXT PRIMARY KEY, process_id INTEGER NOT NULL, owner_token TEXT NOT NULL, owner_path TEXT NOT NULL);`,
21
+ },
22
+ {
23
+ // The execution engine's journal. These tables are private to the engine.
24
+ from: 4,
25
+ to: 5,
26
+ sql: `
27
+ CREATE TABLE engine_runs (
28
+ run_id TEXT PRIMARY KEY, state TEXT NOT NULL, input_json TEXT NOT NULL,
29
+ worker_id TEXT NOT NULL, lease_until INTEGER NOT NULL,
30
+ owner_token TEXT NOT NULL, owner_path TEXT NOT NULL
31
+ );
32
+ CREATE TABLE engine_steps (
33
+ run_id TEXT NOT NULL, step_index INTEGER NOT NULL, name TEXT NOT NULL, result_json TEXT NOT NULL,
34
+ PRIMARY KEY (run_id, step_index)
35
+ );
36
+ `,
37
+ },
38
+ {
39
+ from: 5,
40
+ to: 6,
41
+ sql: `
42
+ CREATE TABLE engine_waits (
43
+ run_id TEXT NOT NULL, name TEXT NOT NULL, deadline INTEGER, outcome_json TEXT,
44
+ PRIMARY KEY (run_id, name)
45
+ );
46
+ CREATE TABLE engine_signals (
47
+ run_id TEXT NOT NULL, name TEXT NOT NULL, value_json TEXT NOT NULL,
48
+ PRIMARY KEY (run_id, name)
49
+ );
50
+ `,
51
+ },
52
+ {
53
+ from: 6,
54
+ to: 7,
55
+ sql: `ALTER TABLE engine_runs ADD COLUMN cancel_requested INTEGER NOT NULL DEFAULT 0;`,
56
+ },
57
+ {
58
+ from: 7,
59
+ to: 8,
60
+ sql: `
61
+ CREATE TABLE threads (thread_id TEXT PRIMARY KEY, created_at TEXT NOT NULL);
62
+ CREATE TABLE thread_messages (message_id TEXT PRIMARY KEY, thread_id TEXT NOT NULL, role TEXT NOT NULL, content TEXT NOT NULL, created_at TEXT NOT NULL, sequence INTEGER NOT NULL, FOREIGN KEY (thread_id) REFERENCES threads(thread_id));
63
+ CREATE INDEX thread_messages_thread_sequence ON thread_messages(thread_id, sequence);
64
+ `,
65
+ },
66
+ {
67
+ from: 8,
68
+ to: 9,
69
+ sql: `
70
+ ALTER TABLE thread_messages ADD COLUMN run_id TEXT;
71
+ CREATE TABLE thread_runs (run_id TEXT PRIMARY KEY, thread_id TEXT NOT NULL, provider_key TEXT, project_root TEXT);
72
+ CREATE INDEX thread_runs_thread ON thread_runs(thread_id);
73
+ CREATE INDEX thread_messages_run ON thread_messages(run_id);
74
+ CREATE UNIQUE INDEX thread_assistant_run ON thread_messages(run_id) WHERE role = 'assistant';
75
+ `,
76
+ },
77
+ {
78
+ from: 9,
79
+ to: 10,
80
+ sql: `CREATE TABLE artifact_content (content_hash TEXT PRIMARY KEY, content TEXT NOT NULL);`,
81
+ },
82
+ {
83
+ from: 10,
84
+ to: 11,
85
+ sql: `
86
+ ALTER TABLE artifact_content RENAME TO artifact_content_text;
87
+ CREATE TABLE artifact_content (
88
+ content_hash TEXT PRIMARY KEY,
89
+ content BLOB NOT NULL
90
+ );
91
+ INSERT INTO artifact_content (content_hash, content)
92
+ SELECT content_hash, CAST(content AS BLOB) FROM artifact_content_text;
93
+ DROP TABLE artifact_content_text;
94
+ `,
95
+ },
96
+ {
97
+ from: 11,
98
+ to: 12,
99
+ sql: `ALTER TABLE threads ADD COLUMN title TEXT; ALTER TABLE threads ADD COLUMN title_source TEXT;`,
100
+ },
101
+ {
102
+ from: 12,
103
+ to: 13,
104
+ sql: `ALTER TABLE threads ADD COLUMN metadata_json TEXT; ALTER TABLE runs ADD COLUMN metadata_json TEXT; ALTER TABLE thread_runs ADD COLUMN context_json TEXT;`,
105
+ },
106
+ {
107
+ from: 13,
108
+ to: 14,
109
+ sql: `ALTER TABLE threads ADD COLUMN title_revision INTEGER NOT NULL DEFAULT 0;`,
110
+ },
111
+ ];
112
+ function readSchemaVersion(database, sqliteSchemaVersion) {
113
+ const row = database.prepare("SELECT value FROM plico_meta WHERE key = 'schema_version'").get();
114
+ if (!row)
115
+ return undefined;
116
+ const version = Number(row.value);
117
+ if (!Number.isSafeInteger(version) || version < 1 || String(version) !== row.value) {
118
+ throw new Error(`Database has an invalid schema version (${row.value}).`);
119
+ }
120
+ if (version > sqliteSchemaVersion) {
121
+ throw new Error(`Database uses a newer schema (${version}, supported ${sqliteSchemaVersion}).`);
122
+ }
123
+ return version;
124
+ }
125
+ /** Read-only access must not silently accept an incompatible database. */
126
+ export function checkReadOnlySchema(database) {
127
+ const latest = migrations.at(-1)?.to ?? baselineVersion;
128
+ const table = database
129
+ .prepare("SELECT name FROM sqlite_master WHERE type = 'table' AND name = 'plico_meta'")
130
+ .get();
131
+ if (!table)
132
+ throw new Error("Database has no Plico schema version.");
133
+ const version = readSchemaVersion(database, latest);
134
+ if (version === undefined)
135
+ throw new Error("Database has no Plico schema version.");
136
+ if (version < latest)
137
+ throw new Error(`Database uses an older schema (${version}, supported ${latest}); open it writable to migrate.`);
138
+ }
139
+ export function initializeSchema(database, steps = migrations) {
140
+ const ordered = [...steps].sort((a, b) => a.from - b.from);
141
+ const sqliteSchemaVersion = ordered.at(-1)?.to ?? baselineVersion;
142
+ database.exec("BEGIN IMMEDIATE");
143
+ try {
144
+ database.exec(`
145
+ CREATE TABLE IF NOT EXISTS plico_meta (
146
+ key TEXT PRIMARY KEY,
147
+ value TEXT NOT NULL
148
+ );
149
+ `);
150
+ let current = readSchemaVersion(database, sqliteSchemaVersion);
151
+ if (current === undefined) {
152
+ // `position` is the log-wide order from ADR 0007. Nothing reads it yet; it exists now so that
153
+ // thread streams need no second migration. It stays internal: public cursors are per stream.
154
+ database.exec(`
155
+ CREATE TABLE IF NOT EXISTS runs (
156
+ run_id TEXT PRIMARY KEY,
157
+ project_root TEXT NOT NULL,
158
+ project_name TEXT NOT NULL,
159
+ status TEXT NOT NULL,
160
+ started_at TEXT NOT NULL,
161
+ ended_at TEXT NOT NULL,
162
+ output TEXT
163
+ );
164
+ CREATE TABLE IF NOT EXISTS events (
165
+ position INTEGER PRIMARY KEY AUTOINCREMENT,
166
+ stream_type TEXT NOT NULL,
167
+ stream_id TEXT NOT NULL,
168
+ sequence INTEGER NOT NULL,
169
+ event_id TEXT NOT NULL,
170
+ schema_version INTEGER NOT NULL,
171
+ type TEXT NOT NULL,
172
+ timestamp TEXT NOT NULL,
173
+ payload_json TEXT NOT NULL,
174
+ UNIQUE (stream_type, stream_id, sequence)
175
+ );
176
+ CREATE TABLE IF NOT EXISTS eval_reports (
177
+ id INTEGER PRIMARY KEY AUTOINCREMENT,
178
+ evaluated_at TEXT NOT NULL,
179
+ report_json TEXT NOT NULL
180
+ );
181
+ CREATE INDEX IF NOT EXISTS runs_started_at_index ON runs(started_at);
182
+ `);
183
+ database
184
+ .prepare("INSERT INTO plico_meta(key, value) VALUES ('schema_version', ?)")
185
+ .run(String(baselineVersion));
186
+ current = baselineVersion;
187
+ }
188
+ while (current < sqliteSchemaVersion) {
189
+ const migration = ordered.find((item) => item.from === current);
190
+ if (!migration || migration.to <= current) {
191
+ throw new Error(`Database uses an older schema (${current}) with no supported migration. Delete it or choose another path with --db.`);
192
+ }
193
+ database.exec(migration.sql);
194
+ current = migration.to;
195
+ database
196
+ .prepare("UPDATE plico_meta SET value = ? WHERE key = 'schema_version'")
197
+ .run(String(current));
198
+ }
199
+ database.exec("COMMIT");
200
+ }
201
+ catch (error) {
202
+ database.exec("ROLLBACK");
203
+ throw error;
204
+ }
205
+ }
@@ -0,0 +1,2 @@
1
+ import type { CreateSqliteEventStoreOptions, EventStore } from "../../types/index.js";
2
+ export declare function createSqliteEventStore({ databasePath, readOnly, }: CreateSqliteEventStoreOptions): EventStore;
@@ -0,0 +1,51 @@
1
+ import { resolve } from "node:path";
2
+ import { matchesMetadata } from "../metadata.js";
3
+ import { appendEventRecord } from "./append-event.js";
4
+ import { openDatabase } from "./open.js";
5
+ import { persistEvalReportRecord, readEvents, readLatestEvalReport, readRunSummaries, readRunSummary, readThreadRunEvents, readThreadRunEventsThrough, } from "./queries.js";
6
+ import { recoverInterruptedRuns } from "./recover-interrupted.js";
7
+ export function createSqliteEventStore({ databasePath, readOnly = false, }) {
8
+ const database = openDatabase(databasePath, readOnly);
9
+ if (!readOnly) {
10
+ try {
11
+ recoverInterruptedRuns(database);
12
+ }
13
+ catch (error) {
14
+ database.close();
15
+ throw error;
16
+ }
17
+ }
18
+ return {
19
+ ...(databasePath === ":memory:"
20
+ ? {}
21
+ : {
22
+ storageIdentity: process.platform === "win32"
23
+ ? resolve(databasePath).toLowerCase()
24
+ : resolve(databasePath),
25
+ }),
26
+ async appendEvent(event) {
27
+ appendEventRecord(database, event);
28
+ },
29
+ async listRuns(filter) {
30
+ return readRunSummaries(database).filter((run) => matchesMetadata(run.metadata, filter?.metadata));
31
+ },
32
+ async getRun(runId) {
33
+ return readRunSummary(database, runId);
34
+ },
35
+ async getEvents(runId, options = {}) {
36
+ return readEvents(database, runId, options.afterSequence);
37
+ },
38
+ async getThreadRunEvents(threadId) {
39
+ return readThreadRunEvents(database, threadId);
40
+ },
41
+ async getThreadRunEventsThrough(threadId, runId) {
42
+ return readThreadRunEventsThrough(database, threadId, runId);
43
+ },
44
+ async persistEvalReport(report) {
45
+ persistEvalReportRecord(database, report);
46
+ },
47
+ async getLatestEvalReport() {
48
+ return readLatestEvalReport(database);
49
+ },
50
+ };
51
+ }
@@ -0,0 +1,4 @@
1
+ import type { DatabaseSync } from "node:sqlite";
2
+ import type { ThreadEvent } from "@plico-ai/contracts";
3
+ /** Appends to a thread's own event stream and returns the new sequence number. */
4
+ export declare function appendThreadEvent(db: DatabaseSync, threadId: string, type: ThreadEvent["type"], payload: unknown, timestamp: string): number;
@@ -0,0 +1,9 @@
1
+ import { randomUUID } from "node:crypto";
2
+ /** Appends to a thread's own event stream and returns the new sequence number. */
3
+ export function appendThreadEvent(db, threadId, type, payload, timestamp) {
4
+ const row = db
5
+ .prepare("SELECT COALESCE(MAX(sequence), 0) + 1 AS next FROM events WHERE stream_type = 'thread' AND stream_id = ?")
6
+ .get(threadId);
7
+ db.prepare("INSERT INTO events (stream_type, stream_id, sequence, event_id, schema_version, type, timestamp, payload_json) VALUES ('thread', ?, ?, ?, 1, ?, ?, ?)").run(threadId, row.next, randomUUID(), type, timestamp, JSON.stringify(payload));
8
+ return row.next;
9
+ }
@@ -0,0 +1,24 @@
1
+ import type { DatabaseSync } from "node:sqlite";
2
+ /** Sets the title a user chose. It is never replaced by a generated one afterwards. */
3
+ export declare function setUserTitle(db: DatabaseSync, threadId: string, title: string, timestamp: string): void;
4
+ /** The transcript a titler should name, and the revision a title written from it belongs to. */
5
+ export interface TitleRequest {
6
+ revision: number;
7
+ messages: Array<{
8
+ role: "user" | "assistant";
9
+ content: string;
10
+ }>;
11
+ }
12
+ /** The transcript to title now, or nothing when the thread is user-titled or past its exchanges. */
13
+ export declare function getTitleRequest(db: DatabaseSync, threadId: string): TitleRequest | undefined;
14
+ /**
15
+ * Stores a title a titler wrote after a reply. It never replaces a user's title, stops after the
16
+ * first exchanges like the keyword titler, and rejects a title written from an older transcript
17
+ * than the one behind the stored title. Returns whether the title changed.
18
+ */
19
+ export declare function setGeneratedTitle(db: DatabaseSync, threadId: string, title: string, revision: number, timestamp: string): boolean;
20
+ /**
21
+ * Names the thread from its transcript. Called inside the transaction that stores a reply, where
22
+ * a title is only a convenience: if naming fails, the reply is still stored and the run completes.
23
+ */
24
+ export declare function retitleThread(db: DatabaseSync, threadId: string, timestamp: string): void;