@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,106 @@
1
+ import { RunCancelled } from "../../engine/index.js";
2
+ import { RuntimeDiagnosticError } from "../../errors.js";
3
+ import { approvalTimeoutMs, decideApproval } from "../../policy/approval.js";
4
+ import { runToolStep } from "../tool-step.js";
5
+ import { nameToolStep } from "../tracing.js";
6
+ import { awaitApproval } from "./approval.js";
7
+ export async function runScriptedDryRun(ctx, script, tools, events, eventStore, config, mockTools = false) {
8
+ const toolsByName = new Map(tools.map((tool) => [tool.name, tool]));
9
+ let output;
10
+ let workspaceId;
11
+ const artifactPaths = new Set();
12
+ const artifactVersions = new Map();
13
+ for (const [index, step] of script.entries()) {
14
+ if (step.type === "assistant.output") {
15
+ output = step.content;
16
+ await events.append("assistant.output", { content: step.content });
17
+ continue;
18
+ }
19
+ if (step.type === "artifact.write") {
20
+ if (workspaceId === undefined) {
21
+ workspaceId = `${ctx.runId}:workspace`;
22
+ await events.append("artifact.workspace.created", { workspaceId });
23
+ }
24
+ for (const file of step.files) {
25
+ artifactPaths.add(file.path);
26
+ const version = (artifactVersions.get(file.path) ?? 0) + 1;
27
+ artifactVersions.set(file.path, version);
28
+ await events.append("artifact.file.written", {
29
+ path: file.path,
30
+ content: file.content,
31
+ mimeType: file.mimeType ?? "text/plain",
32
+ version,
33
+ });
34
+ }
35
+ const snapshotId = `${ctx.runId}:snapshot:${index}`;
36
+ await events.append("artifact.snapshot.created", {
37
+ snapshotId,
38
+ fileCount: artifactPaths.size,
39
+ });
40
+ output = `Artifact workspace ${workspaceId} created with ${step.files.length} files`;
41
+ continue;
42
+ }
43
+ const tool = toolsByName.get(step.toolName);
44
+ if (!tool) {
45
+ throw new RuntimeDiagnosticError({
46
+ phase: "tool.validation",
47
+ toolName: step.toolName,
48
+ message: `Unknown tool: ${step.toolName}`,
49
+ });
50
+ }
51
+ await events.append("tool.call", {
52
+ toolName: tool.name,
53
+ arguments: step.arguments,
54
+ });
55
+ const parsedInput = await tool.parseInput(step.arguments);
56
+ if (!parsedInput.success) {
57
+ throw new RuntimeDiagnosticError({
58
+ phase: "tool.validation",
59
+ toolName: tool.name,
60
+ message: `Tool ${tool.name} arguments invalid: ${parsedInput.error}`,
61
+ });
62
+ }
63
+ const approval = decideApproval(tool, config);
64
+ if (approval.required) {
65
+ const requestId = `${ctx.runId}:${index}`;
66
+ await events.append("approval.requested", {
67
+ requestId,
68
+ toolName: tool.name,
69
+ arguments: step.arguments,
70
+ approval,
71
+ });
72
+ await events.append("run.waiting", {
73
+ requestId,
74
+ toolName: tool.name,
75
+ ...(output === undefined ? {} : { output }),
76
+ });
77
+ await awaitApproval(ctx, requestId, tool.name, output, events, approvalTimeoutMs(config));
78
+ }
79
+ if (mockTools && step.live !== true) {
80
+ if (!("result" in step)) {
81
+ throw new RuntimeDiagnosticError({
82
+ phase: "tool.validation",
83
+ toolName: tool.name,
84
+ message: `Tool ${tool.name} has no mocked result. Add "result" to the step, or "live": true to run the real handler.`,
85
+ });
86
+ }
87
+ await events.append("tool.result", { toolName: tool.name, result: step.result });
88
+ continue;
89
+ }
90
+ const idempotencyKey = `${ctx.runId}:${index}`;
91
+ nameToolStep(ctx, `tool:${index}`, tool.name);
92
+ try {
93
+ await runToolStep(ctx, events, eventStore, tool, parsedInput.value, `tool:${index}`, idempotencyKey);
94
+ }
95
+ catch (error) {
96
+ if (error instanceof RunCancelled)
97
+ throw error;
98
+ throw new RuntimeDiagnosticError({
99
+ phase: "runtime",
100
+ toolName: tool.name,
101
+ message: error instanceof Error ? error.message : String(error),
102
+ });
103
+ }
104
+ }
105
+ return output;
106
+ }
@@ -0,0 +1,2 @@
1
+ /** Hashes every TypeScript file under `tools/`, tool files and the helpers they import. */
2
+ export declare function fingerprintTools(root: string): Promise<string>;
@@ -0,0 +1,26 @@
1
+ import { createHash } from "node:crypto";
2
+ import { readdir, readFile } from "node:fs/promises";
3
+ import { join } from "node:path";
4
+ import { getProjectPaths } from "@plico-ai/core";
5
+ async function toolFiles(directory) {
6
+ const entries = await readdir(directory, { withFileTypes: true });
7
+ const files = await Promise.all(entries.map(async (entry) => {
8
+ const path = join(directory, entry.name);
9
+ if (entry.isDirectory())
10
+ return entry.name === "node_modules" ? [] : toolFiles(path);
11
+ return entry.name.endsWith(".ts") ? [path] : [];
12
+ }));
13
+ return files.flat().sort();
14
+ }
15
+ /** Hashes every TypeScript file under `tools/`, tool files and the helpers they import. */
16
+ export async function fingerprintTools(root) {
17
+ const directory = getProjectPaths(root).directories.tools;
18
+ const hash = createHash("sha256");
19
+ for (const file of await toolFiles(directory)) {
20
+ hash.update(file.slice(directory.length).replaceAll("\\", "/"));
21
+ hash.update("\0");
22
+ hash.update(await readFile(file));
23
+ hash.update("\0");
24
+ }
25
+ return hash.digest("hex");
26
+ }
@@ -0,0 +1,4 @@
1
+ import type { RunContext } from "../engine/index.js";
2
+ import type { InMemoryEventLog } from "../events.js";
3
+ import type { DeclaredTool, EventStore } from "../types/index.js";
4
+ export declare function runToolStep(ctx: RunContext, events: InMemoryEventLog, eventStore: EventStore | undefined, tool: DeclaredTool, input: unknown, stepName: string, idempotencyKey: string): Promise<unknown>;
@@ -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;