@plico-ai/runtime 0.0.0-stage → 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 (188) hide show
  1. package/LICENSE +202 -0
  2. package/NOTICE +4 -0
  3. package/README.md +11 -2
  4. package/dist/artifacts.d.ts +10 -0
  5. package/dist/artifacts.js +106 -0
  6. package/dist/engine/context.d.ts +16 -0
  7. package/dist/engine/context.js +77 -0
  8. package/dist/engine/engine.d.ts +12 -0
  9. package/dist/engine/engine.js +235 -0
  10. package/dist/engine/errors.d.ts +12 -0
  11. package/dist/engine/errors.js +19 -0
  12. package/dist/engine/index.d.ts +7 -0
  13. package/dist/engine/index.js +5 -0
  14. package/dist/engine/journal.d.ts +65 -0
  15. package/dist/engine/journal.js +1 -0
  16. package/dist/engine/memory-journal.d.ts +2 -0
  17. package/dist/engine/memory-journal.js +125 -0
  18. package/dist/engine/sqlite-engine.d.ts +7 -0
  19. package/dist/engine/sqlite-engine.js +6 -0
  20. package/dist/engine/sqlite-journal.d.ts +5 -0
  21. package/dist/engine/sqlite-journal.js +136 -0
  22. package/dist/engine/types.d.ts +97 -0
  23. package/dist/engine/types.js +1 -0
  24. package/dist/env.d.ts +7 -0
  25. package/dist/env.js +19 -0
  26. package/dist/errors.d.ts +18 -0
  27. package/dist/errors.js +40 -0
  28. package/dist/evals/discover.d.ts +3 -0
  29. package/dist/evals/discover.js +49 -0
  30. package/dist/evals/index.d.ts +3 -0
  31. package/dist/evals/index.js +3 -0
  32. package/dist/evals/promote.d.ts +7 -0
  33. package/dist/evals/promote.js +32 -0
  34. package/dist/evals/run-evals.d.ts +2 -0
  35. package/dist/evals/run-evals.js +69 -0
  36. package/dist/events.d.ts +28 -0
  37. package/dist/events.js +104 -0
  38. package/dist/guards.d.ts +1 -0
  39. package/dist/guards.js +3 -0
  40. package/dist/index.d.ts +18 -0
  41. package/dist/index.js +17 -0
  42. package/dist/instructions.d.ts +4 -0
  43. package/dist/instructions.js +23 -0
  44. package/dist/policy/approval.d.ts +8 -0
  45. package/dist/policy/approval.js +27 -0
  46. package/dist/providers/ai-sdk.d.ts +10 -0
  47. package/dist/providers/ai-sdk.js +154 -0
  48. package/dist/providers/configured.d.ts +17 -0
  49. package/dist/providers/configured.js +60 -0
  50. package/dist/providers/openai-compatible.d.ts +8 -0
  51. package/dist/providers/openai-compatible.js +14 -0
  52. package/dist/providers/project-module.d.ts +7 -0
  53. package/dist/providers/project-module.js +29 -0
  54. package/dist/providers/registry.d.ts +20 -0
  55. package/dist/providers/registry.js +53 -0
  56. package/dist/readiness/check-readiness.d.ts +2 -0
  57. package/dist/readiness/check-readiness.js +33 -0
  58. package/dist/readiness/checks/approval-policy.d.ts +5 -0
  59. package/dist/readiness/checks/approval-policy.js +19 -0
  60. package/dist/readiness/checks/artifact-access.d.ts +3 -0
  61. package/dist/readiness/checks/artifact-access.js +5 -0
  62. package/dist/readiness/checks/artifacts.d.ts +6 -0
  63. package/dist/readiness/checks/artifacts.js +22 -0
  64. package/dist/readiness/checks/env.d.ts +3 -0
  65. package/dist/readiness/checks/env.js +10 -0
  66. package/dist/readiness/checks/evals.d.ts +2 -0
  67. package/dist/readiness/checks/evals.js +53 -0
  68. package/dist/readiness/checks/persistence.d.ts +3 -0
  69. package/dist/readiness/checks/persistence.js +61 -0
  70. package/dist/readiness/checks/project.d.ts +7 -0
  71. package/dist/readiness/checks/project.js +21 -0
  72. package/dist/readiness/checks/provider.d.ts +2 -0
  73. package/dist/readiness/checks/provider.js +20 -0
  74. package/dist/readiness/checks/server-config.d.ts +2 -0
  75. package/dist/readiness/checks/server-config.js +13 -0
  76. package/dist/readiness/checks/studio.d.ts +2 -0
  77. package/dist/readiness/checks/studio.js +16 -0
  78. package/dist/readiness/checks/thread-store.d.ts +3 -0
  79. package/dist/readiness/checks/thread-store.js +12 -0
  80. package/dist/readiness/checks/tools.d.ts +3 -0
  81. package/dist/readiness/checks/tools.js +42 -0
  82. package/dist/readiness/diagnostic.d.ts +2 -0
  83. package/dist/readiness/diagnostic.js +7 -0
  84. package/dist/readiness/index.d.ts +1 -0
  85. package/dist/readiness/index.js +1 -0
  86. package/dist/readiness/open-database.d.ts +8 -0
  87. package/dist/readiness/open-database.js +15 -0
  88. package/dist/run/artifact-tool-context.d.ts +19 -0
  89. package/dist/run/artifact-tool-context.js +74 -0
  90. package/dist/run/finalize.d.ts +16 -0
  91. package/dist/run/finalize.js +11 -0
  92. package/dist/run/index.d.ts +1 -0
  93. package/dist/run/index.js +1 -0
  94. package/dist/run/pinned-instructions.d.ts +18 -0
  95. package/dist/run/pinned-instructions.js +36 -0
  96. package/dist/run/record-tools.d.ts +8 -0
  97. package/dist/run/record-tools.js +21 -0
  98. package/dist/run/run-project.d.ts +17 -0
  99. package/dist/run/run-project.js +222 -0
  100. package/dist/run/strategies/approval.d.ts +9 -0
  101. package/dist/run/strategies/approval.js +42 -0
  102. package/dist/run/strategies/default-dry.d.ts +3 -0
  103. package/dist/run/strategies/default-dry.js +5 -0
  104. package/dist/run/strategies/provider.d.ts +6 -0
  105. package/dist/run/strategies/provider.js +193 -0
  106. package/dist/run/strategies/scripted.d.ts +5 -0
  107. package/dist/run/strategies/scripted.js +106 -0
  108. package/dist/run/tool-fingerprint.d.ts +2 -0
  109. package/dist/run/tool-fingerprint.js +26 -0
  110. package/dist/run/tool-step.d.ts +4 -0
  111. package/dist/run/tool-step.js +62 -0
  112. package/dist/run/tracing.d.ts +12 -0
  113. package/dist/run/tracing.js +92 -0
  114. package/dist/script.d.ts +4 -0
  115. package/dist/script.js +51 -0
  116. package/dist/storage/index.d.ts +1 -0
  117. package/dist/storage/index.js +1 -0
  118. package/dist/storage/metadata.d.ts +5 -0
  119. package/dist/storage/metadata.js +26 -0
  120. package/dist/storage/sqlite/append-event.d.ts +3 -0
  121. package/dist/storage/sqlite/append-event.js +108 -0
  122. package/dist/storage/sqlite/driver.d.ts +9 -0
  123. package/dist/storage/sqlite/driver.js +13 -0
  124. package/dist/storage/sqlite/index.d.ts +1 -0
  125. package/dist/storage/sqlite/index.js +1 -0
  126. package/dist/storage/sqlite/open.d.ts +3 -0
  127. package/dist/storage/sqlite/open.js +54 -0
  128. package/dist/storage/sqlite/owner-lock.d.ts +5 -0
  129. package/dist/storage/sqlite/owner-lock.js +55 -0
  130. package/dist/storage/sqlite/project-thread.d.ts +4 -0
  131. package/dist/storage/sqlite/project-thread.js +33 -0
  132. package/dist/storage/sqlite/queries.d.ts +9 -0
  133. package/dist/storage/sqlite/queries.js +134 -0
  134. package/dist/storage/sqlite/recover-interrupted.d.ts +2 -0
  135. package/dist/storage/sqlite/recover-interrupted.js +46 -0
  136. package/dist/storage/sqlite/schema.d.ts +10 -0
  137. package/dist/storage/sqlite/schema.js +205 -0
  138. package/dist/storage/sqlite/store.d.ts +2 -0
  139. package/dist/storage/sqlite/store.js +51 -0
  140. package/dist/storage/sqlite/thread-log.d.ts +4 -0
  141. package/dist/storage/sqlite/thread-log.js +9 -0
  142. package/dist/storage/sqlite/thread-title.d.ts +24 -0
  143. package/dist/storage/sqlite/thread-title.js +85 -0
  144. package/dist/storage/sqlite/threads.d.ts +47 -0
  145. package/dist/storage/sqlite/threads.js +156 -0
  146. package/dist/threads/stopwords.d.ts +1 -0
  147. package/dist/threads/stopwords.js +16 -0
  148. package/dist/threads/suggest-title.d.ts +9 -0
  149. package/dist/threads/suggest-title.js +102 -0
  150. package/dist/threads/titler.d.ts +10 -0
  151. package/dist/threads/titler.js +63 -0
  152. package/dist/threads/words.d.ts +12 -0
  153. package/dist/threads/words.js +39 -0
  154. package/dist/tools/artifact-model-tool-operations.d.ts +29 -0
  155. package/dist/tools/artifact-model-tool-operations.js +124 -0
  156. package/dist/tools/artifact-model-tools.d.ts +2 -0
  157. package/dist/tools/artifact-model-tools.js +101 -0
  158. package/dist/tools/discover.d.ts +4 -0
  159. package/dist/tools/discover.js +34 -0
  160. package/dist/tools/index.d.ts +3 -0
  161. package/dist/tools/index.js +3 -0
  162. package/dist/tools/load.d.ts +2 -0
  163. package/dist/tools/load.js +83 -0
  164. package/dist/tools/normalize.d.ts +2 -0
  165. package/dist/tools/normalize.js +48 -0
  166. package/dist/tools/schema.d.ts +7 -0
  167. package/dist/tools/schema.js +101 -0
  168. package/dist/tools/skill-model-tool.d.ts +7 -0
  169. package/dist/tools/skill-model-tool.js +38 -0
  170. package/dist/types/artifacts.d.ts +5 -0
  171. package/dist/types/artifacts.js +1 -0
  172. package/dist/types/evals.d.ts +16 -0
  173. package/dist/types/evals.js +1 -0
  174. package/dist/types/index.d.ts +10 -0
  175. package/dist/types/index.js +1 -0
  176. package/dist/types/provider.d.ts +45 -0
  177. package/dist/types/provider.js +1 -0
  178. package/dist/types/readiness.d.ts +9 -0
  179. package/dist/types/readiness.js +1 -0
  180. package/dist/types/run.d.ts +28 -0
  181. package/dist/types/run.js +1 -0
  182. package/dist/types/script.d.ts +16 -0
  183. package/dist/types/script.js +1 -0
  184. package/dist/types/store.d.ts +24 -0
  185. package/dist/types/store.js +1 -0
  186. package/dist/types/tools.d.ts +15 -0
  187. package/dist/types/tools.js +1 -0
  188. package/package.json +77 -3
@@ -0,0 +1,62 @@
1
+ import { encodeArtifactContent } from "../artifacts.js";
2
+ import { artifactWriteEventPayload, createStagedArtifactContext, } from "./artifact-tool-context.js";
3
+ export async function runToolStep(ctx, events, eventStore, tool, input, stepName, idempotencyKey) {
4
+ const started = events.all().find((event) => event.type === "run.started");
5
+ const threadId = started?.payload.threadId;
6
+ const context = started?.payload.context;
7
+ let staged;
8
+ const workspaceId = threadId ? `${threadId}:workspace` : `${ctx.runId}:workspace`;
9
+ const ensureStaged = async () => {
10
+ if (staged)
11
+ return staged;
12
+ const threadHistory = threadId && eventStore?.getThreadRunEvents
13
+ ? (await eventStore.getThreadRunEvents(threadId)).filter((event) => event.runId !== ctx.runId)
14
+ : [];
15
+ staged = createStagedArtifactContext(ctx.runId, threadId, [...threadHistory, ...events.all()]);
16
+ return staged;
17
+ };
18
+ const artifacts = {
19
+ async read(path) {
20
+ return (await ensureStaged()).context.read(path);
21
+ },
22
+ async write(path, content, options) {
23
+ return (await ensureStaged()).context.write(path, content, options);
24
+ },
25
+ async list() {
26
+ return (await ensureStaged()).context.list();
27
+ },
28
+ };
29
+ const recorded = await ctx.step(stepName, async ({ signal }) => {
30
+ const result = await tool.handler(input, {
31
+ signal,
32
+ idempotencyKey,
33
+ artifacts,
34
+ ...(context === undefined ? {} : { context }),
35
+ });
36
+ return {
37
+ result: normalizeToolResult(result),
38
+ writes: staged?.writes() ?? [],
39
+ createWorkspace: staged?.workspaceCreated() ?? false,
40
+ workspaceId: staged?.workspaceId ?? workspaceId,
41
+ };
42
+ });
43
+ await events.append("tool.result", { toolName: tool.name, result: recorded.result });
44
+ if (recorded.createWorkspace) {
45
+ await events.append("artifact.workspace.created", { workspaceId: recorded.workspaceId });
46
+ }
47
+ for (const write of recorded.writes) {
48
+ await events.append("artifact.file.written", artifactWriteEventPayload(write));
49
+ }
50
+ return recorded.result;
51
+ }
52
+ function normalizeToolResult(value) {
53
+ if (value instanceof Uint8Array)
54
+ return encodeArtifactContent(value);
55
+ if (Array.isArray(value))
56
+ return value.map(normalizeToolResult);
57
+ if (value !== null &&
58
+ typeof value === "object" &&
59
+ Object.getPrototypeOf(value) === Object.prototype)
60
+ return Object.fromEntries(Object.entries(value).map(([key, entry]) => [key, normalizeToolResult(entry)]));
61
+ return value;
62
+ }
@@ -0,0 +1,12 @@
1
+ import { type Span, type SpanContext } from "@opentelemetry/api";
2
+ import type { RunContext } from "../engine/index.js";
3
+ export type StoredSpanContext = Pick<SpanContext, "traceId" | "spanId" | "traceFlags"> & {
4
+ traceState?: string;
5
+ };
6
+ export declare function storeSpanContext(span: Span): StoredSpanContext;
7
+ export declare function nameToolStep(ctx: RunContext, step: string, tool: string): void;
8
+ export declare function startRunSpan(runId: string, stored?: StoredSpanContext): Span;
9
+ export declare function tracedContext(ctx: RunContext, runSpan: Span, model?: {
10
+ name: string;
11
+ provider: string;
12
+ }): RunContext;
@@ -0,0 +1,92 @@
1
+ import { context, createTraceState, ROOT_CONTEXT, SpanStatusCode, trace, } from "@opentelemetry/api";
2
+ const getTracer = () => trace.getTracer("@plico-ai/runtime");
3
+ const toolNames = new WeakMap();
4
+ export function storeSpanContext(span) {
5
+ const { traceId, spanId, traceFlags, traceState } = span.spanContext();
6
+ return {
7
+ traceId,
8
+ spanId,
9
+ traceFlags,
10
+ ...(traceState ? { traceState: traceState.serialize() } : {}),
11
+ };
12
+ }
13
+ export function nameToolStep(ctx, step, tool) {
14
+ toolNames.get(ctx)?.set(step, tool);
15
+ }
16
+ export function startRunSpan(runId, stored) {
17
+ const previous = stored && {
18
+ traceId: stored.traceId,
19
+ spanId: stored.spanId,
20
+ traceFlags: stored.traceFlags,
21
+ ...(typeof stored.traceState === "string"
22
+ ? { traceState: createTraceState(stored.traceState) }
23
+ : {}),
24
+ };
25
+ return getTracer().startSpan("plico.run", {
26
+ attributes: { "plico.run.id": runId },
27
+ ...(previous && trace.isSpanContextValid(previous) ? { links: [{ context: previous }] } : {}),
28
+ });
29
+ }
30
+ export function tracedContext(ctx, runSpan, model) {
31
+ const names = new Map();
32
+ const wrapped = {
33
+ get runId() {
34
+ return ctx.runId;
35
+ },
36
+ get signal() {
37
+ return ctx.signal;
38
+ },
39
+ wait: (name, options) => ctx.wait(name, options),
40
+ step: (name, fn, options) => {
41
+ const step = getTracer().startSpan("plico.step", { attributes: { "plico.step.name": name } }, trace.setSpan(ROOT_CONTEXT, runSpan));
42
+ return ctx
43
+ .step(name, async (attempt) => {
44
+ const tool = names.get(name);
45
+ const modelCall = name.startsWith("model:");
46
+ const child = getTracer().startSpan(modelCall
47
+ ? `chat ${model?.name ?? "model"}`
48
+ : tool
49
+ ? `execute_tool ${tool}`
50
+ : "plico.attempt", {
51
+ attributes: {
52
+ "plico.step.attempt": attempt.attempt,
53
+ ...(tool
54
+ ? { "gen_ai.operation.name": "execute_tool", "gen_ai.tool.name": tool }
55
+ : {}),
56
+ ...(modelCall
57
+ ? {
58
+ "gen_ai.operation.name": "chat",
59
+ ...(model
60
+ ? {
61
+ "gen_ai.request.model": model.name,
62
+ "gen_ai.provider.name": model.provider,
63
+ }
64
+ : {}),
65
+ }
66
+ : {}),
67
+ },
68
+ }, trace.setSpan(ROOT_CONTEXT, step));
69
+ try {
70
+ return await context.with(trace.setSpan(context.active(), child), () => fn(attempt));
71
+ }
72
+ catch (error) {
73
+ child.setStatus({ code: SpanStatusCode.ERROR });
74
+ throw error;
75
+ }
76
+ finally {
77
+ child.end();
78
+ }
79
+ }, options)
80
+ .then((value) => {
81
+ step.end();
82
+ return value;
83
+ }, (error) => {
84
+ step.setStatus({ code: SpanStatusCode.ERROR });
85
+ step.end();
86
+ throw error;
87
+ });
88
+ },
89
+ };
90
+ toolNames.set(wrapped, names);
91
+ return wrapped;
92
+ }
@@ -0,0 +1,4 @@
1
+ import type { DryRunScriptStep } from "./types/index.js";
2
+ export declare class ScriptValidationError extends Error {
3
+ }
4
+ export declare function normalizeScript(value: unknown): DryRunScriptStep[];
package/dist/script.js ADDED
@@ -0,0 +1,51 @@
1
+ export class ScriptValidationError extends Error {
2
+ }
3
+ export function normalizeScript(value) {
4
+ if (!Array.isArray(value))
5
+ throw new ScriptValidationError("script must be an array.");
6
+ return value.map(normalizeStep);
7
+ }
8
+ function normalizeStep(step, index) {
9
+ const prefix = `script step ${index + 1}`;
10
+ if (!isRecord(step) || typeof step.type !== "string") {
11
+ throw new ScriptValidationError(`${prefix} must include a type.`);
12
+ }
13
+ if (step.type === "assistant.output") {
14
+ if (typeof step.content !== "string") {
15
+ throw new ScriptValidationError(`${prefix} must include content.`);
16
+ }
17
+ return { type: "assistant.output", content: step.content };
18
+ }
19
+ if (step.type === "tool.call") {
20
+ if (typeof step.toolName !== "string") {
21
+ throw new ScriptValidationError(`${prefix} must include a toolName.`);
22
+ }
23
+ return {
24
+ type: "tool.call",
25
+ toolName: step.toolName,
26
+ arguments: step.arguments,
27
+ ...("result" in step ? { result: step.result } : {}),
28
+ ...(step.live === true ? { live: true } : {}),
29
+ };
30
+ }
31
+ if (step.type === "artifact.write") {
32
+ if (!Array.isArray(step.files)) {
33
+ throw new ScriptValidationError(`${prefix} must include a files array.`);
34
+ }
35
+ const files = step.files.map((file, fileIndex) => {
36
+ if (!isRecord(file) || typeof file.path !== "string" || typeof file.content !== "string") {
37
+ throw new ScriptValidationError(`${prefix} file ${fileIndex + 1} must have path and content strings.`);
38
+ }
39
+ return {
40
+ path: file.path,
41
+ content: file.content,
42
+ ...(typeof file.mimeType === "string" ? { mimeType: file.mimeType } : {}),
43
+ };
44
+ });
45
+ return { type: "artifact.write", files };
46
+ }
47
+ throw new ScriptValidationError(`${prefix} has an unsupported type.`);
48
+ }
49
+ function isRecord(value) {
50
+ return typeof value === "object" && value !== null;
51
+ }
@@ -0,0 +1 @@
1
+ export { createSqliteEventStore } from "./sqlite/index.js";
@@ -0,0 +1 @@
1
+ export { createSqliteEventStore } from "./sqlite/index.js";
@@ -0,0 +1,5 @@
1
+ import type { JsonValue } from "@plico-ai/contracts";
2
+ export type Metadata = Record<string, JsonValue>;
3
+ /** True when every top-level key of `filter` equals the same key of `metadata`, compared as JSON. */
4
+ export declare function matchesMetadata(metadata: Metadata | undefined, filter: Metadata | undefined): boolean;
5
+ export declare function parseMetadata(json: unknown): Metadata | undefined;
@@ -0,0 +1,26 @@
1
+ /** True when every top-level key of `filter` equals the same key of `metadata`, compared as JSON. */
2
+ export function matchesMetadata(metadata, filter) {
3
+ if (filter === undefined)
4
+ return true;
5
+ return Object.entries(filter).every(([key, value]) => metadata !== undefined &&
6
+ Object.hasOwn(metadata, key) &&
7
+ sameJson(metadata[key], value));
8
+ }
9
+ function sameJson(left, right) {
10
+ if (left === right)
11
+ return true;
12
+ if (Array.isArray(left) || Array.isArray(right)) {
13
+ return (Array.isArray(left) &&
14
+ Array.isArray(right) &&
15
+ left.length === right.length &&
16
+ left.every((item, index) => sameJson(item, right[index])));
17
+ }
18
+ if (left === null || right === null || typeof left !== "object" || typeof right !== "object")
19
+ return false;
20
+ const keys = Object.keys(left);
21
+ return (keys.length === Object.keys(right).length &&
22
+ keys.every((key) => Object.hasOwn(right, key) && sameJson(left[key], right[key])));
23
+ }
24
+ export function parseMetadata(json) {
25
+ return typeof json === "string" ? JSON.parse(json) : undefined;
26
+ }
@@ -0,0 +1,3 @@
1
+ import type { DatabaseSync } from "node:sqlite";
2
+ import type { PlicoEvent } from "@plico-ai/contracts";
3
+ export declare function appendEventRecord(database: DatabaseSync, event: PlicoEvent): void;
@@ -0,0 +1,108 @@
1
+ import { artifactContentBytes, hashArtifactContent } from "../../artifacts.js";
2
+ import { acquireOwner } from "./owner-lock.js";
3
+ import { projectThreadEvent } from "./project-thread.js";
4
+ export function appendEventRecord(database, event) {
5
+ if (event.streamType !== "run" || event.streamId !== event.runId) {
6
+ throw new Error("Event must belong to its run stream.");
7
+ }
8
+ database.exec("BEGIN IMMEDIATE");
9
+ try {
10
+ const payload = compactArtifactPayload(database, event);
11
+ const existing = database
12
+ .prepare("SELECT event_id, type, timestamp, payload_json FROM events WHERE stream_type = ? AND stream_id = ? AND sequence = ?")
13
+ .get(event.streamType, event.streamId, event.sequence);
14
+ if (existing) {
15
+ if (existing.event_id !== event.id ||
16
+ existing.type !== event.type ||
17
+ existing.timestamp !== event.timestamp ||
18
+ existing.payload_json !== JSON.stringify(payload)) {
19
+ throw new Error(`Conflicting event at sequence ${event.sequence}.`);
20
+ }
21
+ database.exec("COMMIT");
22
+ return;
23
+ }
24
+ const row = database.prepare("SELECT status FROM runs WHERE run_id = ?").get(event.runId);
25
+ const last = database
26
+ .prepare("SELECT sequence, type FROM events WHERE stream_type = ? AND stream_id = ? ORDER BY sequence DESC LIMIT 1")
27
+ .get(event.streamType, event.streamId);
28
+ if (event.sequence !== (last?.sequence ?? 0) + 1)
29
+ throw new Error("Events must be appended in sequence.");
30
+ if (last?.type === "run.error" && event.type !== "run.failed") {
31
+ throw new Error("run.error must be followed by run.failed.");
32
+ }
33
+ if (!row) {
34
+ if (event.type !== "run.started" || event.sequence !== 1)
35
+ throw new Error("The first event must be run.started.");
36
+ database
37
+ .prepare("INSERT INTO runs (run_id, project_root, project_name, status, started_at, ended_at, metadata_json) VALUES (?, ?, ?, 'running', ?, NULL, ?)")
38
+ .run(event.runId, String(event.payload.projectRoot ?? ""), String(event.payload.projectName ?? ""), event.timestamp, event.payload.metadata === undefined ? null : JSON.stringify(event.payload.metadata));
39
+ const owner = acquireOwner();
40
+ database
41
+ .prepare("INSERT INTO run_owners (run_id, process_id, owner_token, owner_path) VALUES (?, ?, ?, ?)")
42
+ .run(event.runId, process.pid, owner.token, owner.path);
43
+ }
44
+ else {
45
+ if (row.status === "waiting") {
46
+ // The first event after run.waiting is the run continuing, so the log alone says it is running.
47
+ database
48
+ .prepare("UPDATE runs SET status = 'running', ended_at = NULL WHERE run_id = ?")
49
+ .run(event.runId);
50
+ const owner = acquireOwner();
51
+ database
52
+ .prepare("INSERT OR REPLACE INTO run_owners (run_id, process_id, owner_token, owner_path) VALUES (?, ?, ?, ?)")
53
+ .run(event.runId, process.pid, owner.token, owner.path);
54
+ }
55
+ else if (row.status !== "running") {
56
+ throw new Error(`Cannot append to run with status ${row.status}.`);
57
+ }
58
+ if (event.type === "run.started")
59
+ throw new Error("Run has already started.");
60
+ const status = event.type === "run.waiting"
61
+ ? "waiting"
62
+ : event.type === "run.completed"
63
+ ? "completed"
64
+ : event.type === "run.failed"
65
+ ? "failed"
66
+ : event.type === "run.cancelled"
67
+ ? "cancelled"
68
+ : undefined;
69
+ if (status) {
70
+ const endedAt = status === "waiting" ? null : event.timestamp;
71
+ const output = (event.type === "run.completed" || event.type === "run.waiting") &&
72
+ typeof event.payload.output === "string"
73
+ ? event.payload.output
74
+ : null;
75
+ database
76
+ .prepare("UPDATE runs SET status = ?, ended_at = ?, output = ? WHERE run_id = ?")
77
+ .run(status, endedAt, output, event.runId);
78
+ // Drop the owner once the run leaves running, including waiting; recovery only inspects running runs.
79
+ database.prepare("DELETE FROM run_owners WHERE run_id = ?").run(event.runId);
80
+ }
81
+ }
82
+ database
83
+ .prepare("INSERT INTO events (stream_type, stream_id, sequence, event_id, schema_version, type, timestamp, payload_json) VALUES (?, ?, ?, ?, ?, ?, ?, ?)")
84
+ .run(event.streamType, event.streamId, event.sequence, event.id, event.schemaVersion, event.type, event.timestamp, JSON.stringify(payload));
85
+ projectThreadEvent(database, event);
86
+ database.exec("COMMIT");
87
+ }
88
+ catch (error) {
89
+ database.exec("ROLLBACK");
90
+ throw error;
91
+ }
92
+ }
93
+ function compactArtifactPayload(database, event) {
94
+ if (event.type !== "artifact.file.written")
95
+ return event.payload;
96
+ const { content, contentHash: declaredHash, ...receipt } = event.payload;
97
+ const actualHash = hashArtifactContent(content);
98
+ if (declaredHash !== undefined && actualHash !== declaredHash)
99
+ throw new Error("Artifact content hash does not match its content.");
100
+ database
101
+ .prepare("INSERT OR IGNORE INTO artifact_content (content_hash, content) VALUES (?, ?)")
102
+ .run(actualHash, Buffer.from(artifactContentBytes(content)));
103
+ return {
104
+ ...receipt,
105
+ ...(typeof content === "string" ? {} : { contentEncoding: "base64" }),
106
+ contentHash: actualHash,
107
+ };
108
+ }
@@ -0,0 +1,9 @@
1
+ import type { DatabaseSync } from "node:sqlite";
2
+ type SqliteSource = ConstructorParameters<typeof DatabaseSync>[0];
3
+ type SqliteOptions = ConstructorParameters<typeof DatabaseSync>[1];
4
+ /**
5
+ * Opens a SQLite database, loading `node:sqlite` on first use. Importing it eagerly would print
6
+ * Node's experimental warning for commands that never touch a database, such as `--version`.
7
+ */
8
+ export declare function createDatabase(path: SqliteSource, options?: SqliteOptions): DatabaseSync;
9
+ export {};
@@ -0,0 +1,13 @@
1
+ import { createRequire } from "node:module";
2
+ const require = createRequire(import.meta.url);
3
+ let sqlite;
4
+ /**
5
+ * Opens a SQLite database, loading `node:sqlite` on first use. Importing it eagerly would print
6
+ * Node's experimental warning for commands that never touch a database, such as `--version`.
7
+ */
8
+ export function createDatabase(path, options) {
9
+ sqlite ??= require("node:sqlite");
10
+ return options === undefined
11
+ ? new sqlite.DatabaseSync(path)
12
+ : new sqlite.DatabaseSync(path, options);
13
+ }
@@ -0,0 +1 @@
1
+ export { createSqliteEventStore } from "./store.js";
@@ -0,0 +1 @@
1
+ export { createSqliteEventStore } from "./store.js";
@@ -0,0 +1,3 @@
1
+ import type { DatabaseSync } from "node:sqlite";
2
+ /** Opens the database with the pragma order and migrator every Plico store shares. */
3
+ export declare function openDatabase(databasePath: string, readOnly?: boolean): DatabaseSync;
@@ -0,0 +1,54 @@
1
+ import { existsSync, mkdirSync } from "node:fs";
2
+ import { dirname } from "node:path";
3
+ import { pathToFileURL } from "node:url";
4
+ import { createDatabase } from "./driver.js";
5
+ import { checkReadOnlySchema, initializeSchema } from "./schema.js";
6
+ /** Opens the database with the pragma order and migrator every Plico store shares. */
7
+ export function openDatabase(databasePath, readOnly = false) {
8
+ if (!readOnly)
9
+ mkdirSync(dirname(databasePath), { recursive: true });
10
+ const database = createDatabase(readOnly ? readOnlySource(databasePath) : databasePath, {
11
+ readOnly,
12
+ });
13
+ try {
14
+ database.exec("PRAGMA busy_timeout = 5000;");
15
+ if (!readOnly) {
16
+ setWalMode(database);
17
+ initializeSchema(database);
18
+ }
19
+ else {
20
+ checkReadOnlySchema(database);
21
+ }
22
+ }
23
+ catch (error) {
24
+ database.close();
25
+ throw error;
26
+ }
27
+ return database;
28
+ }
29
+ function setWalMode(database) {
30
+ // Changing journal mode may return SQLITE_BUSY immediately even with busy_timeout set.
31
+ const deadline = Date.now() + 5000;
32
+ const pause = new Int32Array(new SharedArrayBuffer(4));
33
+ while (true) {
34
+ try {
35
+ database.exec("PRAGMA journal_mode = WAL;");
36
+ return;
37
+ }
38
+ catch (error) {
39
+ const code = error.errcode;
40
+ if (code === undefined || (code & 0xff) !== 5 || Date.now() >= deadline)
41
+ throw error;
42
+ Atomics.wait(pause, 0, 0, Math.min(25, deadline - Date.now()));
43
+ }
44
+ }
45
+ }
46
+ // A plain read-only open of a WAL database creates -shm and -wal files. Immutable mode
47
+ // avoids them but ignores WAL content, so it is only safe when no -wal file exists.
48
+ function readOnlySource(databasePath) {
49
+ if (existsSync(`${databasePath}-wal`))
50
+ return databasePath;
51
+ const url = pathToFileURL(databasePath);
52
+ url.searchParams.set("immutable", "1");
53
+ return url;
54
+ }
@@ -0,0 +1,5 @@
1
+ export declare function acquireOwner(): {
2
+ token: string;
3
+ path: string;
4
+ };
5
+ export declare function ownerIsAlive(token: string, path: string): boolean;
@@ -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;