@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.
- package/NOTICE +4 -0
- package/README.md +9 -3
- package/dist/artifacts.d.ts +10 -0
- package/dist/artifacts.js +106 -0
- package/dist/engine/context.d.ts +16 -0
- package/dist/engine/context.js +77 -0
- package/dist/engine/engine.d.ts +12 -0
- package/dist/engine/engine.js +235 -0
- package/dist/engine/errors.d.ts +12 -0
- package/dist/engine/errors.js +19 -0
- package/dist/engine/index.d.ts +7 -0
- package/dist/engine/index.js +5 -0
- package/dist/engine/journal.d.ts +65 -0
- package/dist/engine/journal.js +1 -0
- package/dist/engine/memory-journal.d.ts +2 -0
- package/dist/engine/memory-journal.js +125 -0
- package/dist/engine/sqlite-engine.d.ts +7 -0
- package/dist/engine/sqlite-engine.js +6 -0
- package/dist/engine/sqlite-journal.d.ts +5 -0
- package/dist/engine/sqlite-journal.js +136 -0
- package/dist/engine/types.d.ts +97 -0
- package/dist/engine/types.js +1 -0
- package/dist/env.d.ts +7 -0
- package/dist/env.js +19 -0
- package/dist/errors.d.ts +18 -0
- package/dist/errors.js +40 -0
- package/dist/evals/discover.d.ts +3 -0
- package/dist/evals/discover.js +49 -0
- package/dist/evals/index.d.ts +3 -0
- package/dist/evals/index.js +3 -0
- package/dist/evals/promote.d.ts +7 -0
- package/dist/evals/promote.js +32 -0
- package/dist/evals/run-evals.d.ts +2 -0
- package/dist/evals/run-evals.js +69 -0
- package/dist/events.d.ts +28 -0
- package/dist/events.js +104 -0
- package/dist/guards.d.ts +1 -0
- package/dist/guards.js +3 -0
- package/dist/index.d.ts +18 -0
- package/dist/index.js +17 -0
- package/dist/instructions.d.ts +4 -0
- package/dist/instructions.js +23 -0
- package/dist/policy/approval.d.ts +8 -0
- package/dist/policy/approval.js +27 -0
- package/dist/providers/ai-sdk.d.ts +10 -0
- package/dist/providers/ai-sdk.js +154 -0
- package/dist/providers/configured.d.ts +17 -0
- package/dist/providers/configured.js +60 -0
- package/dist/providers/openai-compatible.d.ts +8 -0
- package/dist/providers/openai-compatible.js +14 -0
- package/dist/providers/project-module.d.ts +7 -0
- package/dist/providers/project-module.js +29 -0
- package/dist/providers/registry.d.ts +20 -0
- package/dist/providers/registry.js +53 -0
- package/dist/readiness/check-readiness.d.ts +2 -0
- package/dist/readiness/check-readiness.js +33 -0
- package/dist/readiness/checks/approval-policy.d.ts +5 -0
- package/dist/readiness/checks/approval-policy.js +19 -0
- package/dist/readiness/checks/artifact-access.d.ts +3 -0
- package/dist/readiness/checks/artifact-access.js +5 -0
- package/dist/readiness/checks/artifacts.d.ts +6 -0
- package/dist/readiness/checks/artifacts.js +22 -0
- package/dist/readiness/checks/env.d.ts +3 -0
- package/dist/readiness/checks/env.js +10 -0
- package/dist/readiness/checks/evals.d.ts +2 -0
- package/dist/readiness/checks/evals.js +53 -0
- package/dist/readiness/checks/persistence.d.ts +3 -0
- package/dist/readiness/checks/persistence.js +61 -0
- package/dist/readiness/checks/project.d.ts +7 -0
- package/dist/readiness/checks/project.js +21 -0
- package/dist/readiness/checks/provider.d.ts +2 -0
- package/dist/readiness/checks/provider.js +20 -0
- package/dist/readiness/checks/server-config.d.ts +2 -0
- package/dist/readiness/checks/server-config.js +13 -0
- package/dist/readiness/checks/studio.d.ts +2 -0
- package/dist/readiness/checks/studio.js +16 -0
- package/dist/readiness/checks/thread-store.d.ts +3 -0
- package/dist/readiness/checks/thread-store.js +12 -0
- package/dist/readiness/checks/tools.d.ts +3 -0
- package/dist/readiness/checks/tools.js +42 -0
- package/dist/readiness/diagnostic.d.ts +2 -0
- package/dist/readiness/diagnostic.js +7 -0
- package/dist/readiness/index.d.ts +1 -0
- package/dist/readiness/index.js +1 -0
- package/dist/readiness/open-database.d.ts +8 -0
- package/dist/readiness/open-database.js +15 -0
- package/dist/run/artifact-tool-context.d.ts +19 -0
- package/dist/run/artifact-tool-context.js +74 -0
- package/dist/run/finalize.d.ts +16 -0
- package/dist/run/finalize.js +11 -0
- package/dist/run/index.d.ts +1 -0
- package/dist/run/index.js +1 -0
- package/dist/run/pinned-instructions.d.ts +18 -0
- package/dist/run/pinned-instructions.js +36 -0
- package/dist/run/record-tools.d.ts +8 -0
- package/dist/run/record-tools.js +21 -0
- package/dist/run/run-project.d.ts +17 -0
- package/dist/run/run-project.js +222 -0
- package/dist/run/strategies/approval.d.ts +9 -0
- package/dist/run/strategies/approval.js +42 -0
- package/dist/run/strategies/default-dry.d.ts +3 -0
- package/dist/run/strategies/default-dry.js +5 -0
- package/dist/run/strategies/provider.d.ts +6 -0
- package/dist/run/strategies/provider.js +193 -0
- package/dist/run/strategies/scripted.d.ts +5 -0
- package/dist/run/strategies/scripted.js +106 -0
- package/dist/run/tool-fingerprint.d.ts +2 -0
- package/dist/run/tool-fingerprint.js +26 -0
- package/dist/run/tool-step.d.ts +4 -0
- package/dist/run/tool-step.js +62 -0
- package/dist/run/tracing.d.ts +12 -0
- package/dist/run/tracing.js +92 -0
- package/dist/script.d.ts +4 -0
- package/dist/script.js +51 -0
- package/dist/storage/index.d.ts +1 -0
- package/dist/storage/index.js +1 -0
- package/dist/storage/metadata.d.ts +5 -0
- package/dist/storage/metadata.js +26 -0
- package/dist/storage/sqlite/append-event.d.ts +3 -0
- package/dist/storage/sqlite/append-event.js +108 -0
- package/dist/storage/sqlite/driver.d.ts +9 -0
- package/dist/storage/sqlite/driver.js +13 -0
- package/dist/storage/sqlite/index.d.ts +1 -0
- package/dist/storage/sqlite/index.js +1 -0
- package/dist/storage/sqlite/open.d.ts +3 -0
- package/dist/storage/sqlite/open.js +54 -0
- package/dist/storage/sqlite/owner-lock.d.ts +5 -0
- package/dist/storage/sqlite/owner-lock.js +55 -0
- package/dist/storage/sqlite/project-thread.d.ts +4 -0
- package/dist/storage/sqlite/project-thread.js +33 -0
- package/dist/storage/sqlite/queries.d.ts +9 -0
- package/dist/storage/sqlite/queries.js +134 -0
- package/dist/storage/sqlite/recover-interrupted.d.ts +2 -0
- package/dist/storage/sqlite/recover-interrupted.js +46 -0
- package/dist/storage/sqlite/schema.d.ts +10 -0
- package/dist/storage/sqlite/schema.js +205 -0
- package/dist/storage/sqlite/store.d.ts +2 -0
- package/dist/storage/sqlite/store.js +51 -0
- package/dist/storage/sqlite/thread-log.d.ts +4 -0
- package/dist/storage/sqlite/thread-log.js +9 -0
- package/dist/storage/sqlite/thread-title.d.ts +24 -0
- package/dist/storage/sqlite/thread-title.js +85 -0
- package/dist/storage/sqlite/threads.d.ts +47 -0
- package/dist/storage/sqlite/threads.js +156 -0
- package/dist/threads/stopwords.d.ts +1 -0
- package/dist/threads/stopwords.js +16 -0
- package/dist/threads/suggest-title.d.ts +9 -0
- package/dist/threads/suggest-title.js +102 -0
- package/dist/threads/titler.d.ts +10 -0
- package/dist/threads/titler.js +63 -0
- package/dist/threads/words.d.ts +12 -0
- package/dist/threads/words.js +39 -0
- package/dist/tools/artifact-model-tool-operations.d.ts +29 -0
- package/dist/tools/artifact-model-tool-operations.js +124 -0
- package/dist/tools/artifact-model-tools.d.ts +2 -0
- package/dist/tools/artifact-model-tools.js +101 -0
- package/dist/tools/discover.d.ts +4 -0
- package/dist/tools/discover.js +34 -0
- package/dist/tools/index.d.ts +3 -0
- package/dist/tools/index.js +3 -0
- package/dist/tools/load.d.ts +2 -0
- package/dist/tools/load.js +83 -0
- package/dist/tools/normalize.d.ts +2 -0
- package/dist/tools/normalize.js +48 -0
- package/dist/tools/schema.d.ts +7 -0
- package/dist/tools/schema.js +101 -0
- package/dist/tools/skill-model-tool.d.ts +7 -0
- package/dist/tools/skill-model-tool.js +38 -0
- package/dist/types/artifacts.d.ts +5 -0
- package/dist/types/artifacts.js +1 -0
- package/dist/types/evals.d.ts +16 -0
- package/dist/types/evals.js +1 -0
- package/dist/types/index.d.ts +10 -0
- package/dist/types/index.js +1 -0
- package/dist/types/provider.d.ts +45 -0
- package/dist/types/provider.js +1 -0
- package/dist/types/readiness.d.ts +9 -0
- package/dist/types/readiness.js +1 -0
- package/dist/types/run.d.ts +28 -0
- package/dist/types/run.js +1 -0
- package/dist/types/script.d.ts +16 -0
- package/dist/types/script.js +1 -0
- package/dist/types/store.d.ts +24 -0
- package/dist/types/store.js +1 -0
- package/dist/types/tools.d.ts +15 -0
- package/dist/types/tools.js +1 -0
- package/package.json +67 -8
|
@@ -0,0 +1,125 @@
|
|
|
1
|
+
export function createMemoryJournal() {
|
|
2
|
+
const runs = new Map();
|
|
3
|
+
const steps = new Map();
|
|
4
|
+
const waits = new Map();
|
|
5
|
+
const signals = new Map();
|
|
6
|
+
return {
|
|
7
|
+
async begin(runId, inputJson, lease) {
|
|
8
|
+
if (runs.has(runId))
|
|
9
|
+
return false;
|
|
10
|
+
runs.set(runId, { runId, state: "running", inputJson, cancelRequested: false, lease });
|
|
11
|
+
return true;
|
|
12
|
+
},
|
|
13
|
+
async getRun(runId) {
|
|
14
|
+
const run = runs.get(runId);
|
|
15
|
+
return (run && {
|
|
16
|
+
runId,
|
|
17
|
+
state: run.state,
|
|
18
|
+
inputJson: run.inputJson,
|
|
19
|
+
cancelRequested: run.cancelRequested,
|
|
20
|
+
});
|
|
21
|
+
},
|
|
22
|
+
async setState(runId, state) {
|
|
23
|
+
const run = runs.get(runId);
|
|
24
|
+
if (run)
|
|
25
|
+
run.state = state;
|
|
26
|
+
},
|
|
27
|
+
async renew(runId, lease) {
|
|
28
|
+
const run = runs.get(runId);
|
|
29
|
+
if (run?.lease.workerId === lease.workerId)
|
|
30
|
+
run.lease = lease;
|
|
31
|
+
},
|
|
32
|
+
async claim(runId, lease, expected) {
|
|
33
|
+
const run = runs.get(runId);
|
|
34
|
+
if (!run)
|
|
35
|
+
return false;
|
|
36
|
+
if (expected &&
|
|
37
|
+
(run.lease.workerId !== expected.workerId || run.lease.until !== expected.until)) {
|
|
38
|
+
return false;
|
|
39
|
+
}
|
|
40
|
+
run.lease = lease;
|
|
41
|
+
run.state = "running";
|
|
42
|
+
return true;
|
|
43
|
+
},
|
|
44
|
+
async claimWaiting(runId, lease, expected) {
|
|
45
|
+
const run = runs.get(runId);
|
|
46
|
+
if (!run || run.state !== "waiting")
|
|
47
|
+
return false;
|
|
48
|
+
if (expected &&
|
|
49
|
+
(run.lease.workerId !== expected.workerId || run.lease.until !== expected.until))
|
|
50
|
+
return false;
|
|
51
|
+
run.lease = lease;
|
|
52
|
+
run.state = "running";
|
|
53
|
+
return true;
|
|
54
|
+
},
|
|
55
|
+
async listStale(workerId, now) {
|
|
56
|
+
return [...runs.values()]
|
|
57
|
+
.filter((run) => run.state === "running" && run.lease.workerId !== workerId && run.lease.until < now)
|
|
58
|
+
.map((run) => ({ runId: run.runId, held: run.lease }));
|
|
59
|
+
},
|
|
60
|
+
async listReady(now) {
|
|
61
|
+
return [...runs.values()]
|
|
62
|
+
.filter((run) => {
|
|
63
|
+
if (run.state !== "waiting")
|
|
64
|
+
return false;
|
|
65
|
+
if (run.cancelRequested)
|
|
66
|
+
return true;
|
|
67
|
+
return [...waits.entries()].some(([key, wait]) => {
|
|
68
|
+
if (!key.startsWith(`${run.runId}\u0000`) || wait.outcomeJson !== null)
|
|
69
|
+
return false;
|
|
70
|
+
return signals.has(key) || (wait.deadline !== null && wait.deadline <= now);
|
|
71
|
+
});
|
|
72
|
+
})
|
|
73
|
+
.map((run) => ({ runId: run.runId, held: run.lease }));
|
|
74
|
+
},
|
|
75
|
+
async requestCancel(runId) {
|
|
76
|
+
const run = runs.get(runId);
|
|
77
|
+
if (!run || run.cancelRequested || ["completed", "failed", "cancelled"].includes(run.state)) {
|
|
78
|
+
return false;
|
|
79
|
+
}
|
|
80
|
+
run.cancelRequested = true;
|
|
81
|
+
return true;
|
|
82
|
+
},
|
|
83
|
+
async listWaiting() {
|
|
84
|
+
return [...runs.values()]
|
|
85
|
+
.filter((run) => run.state === "waiting")
|
|
86
|
+
.flatMap((run) => [...waits.entries()]
|
|
87
|
+
.filter(([key, wait]) => key.startsWith(`${run.runId}\0`) && wait.outcomeJson === null)
|
|
88
|
+
.map(([, wait]) => ({ runId: run.runId, deadline: wait.deadline })));
|
|
89
|
+
},
|
|
90
|
+
async getWait(runId, name) {
|
|
91
|
+
const wait = waits.get(`${runId}\u0000${name}`);
|
|
92
|
+
return wait && { ...wait };
|
|
93
|
+
},
|
|
94
|
+
async openWait(runId, name, deadline) {
|
|
95
|
+
const key = `${runId}\u0000${name}`;
|
|
96
|
+
if (!waits.has(key))
|
|
97
|
+
waits.set(key, { deadline, outcomeJson: null });
|
|
98
|
+
},
|
|
99
|
+
async resolveWait(runId, name, outcomeJson) {
|
|
100
|
+
const wait = waits.get(`${runId}\u0000${name}`);
|
|
101
|
+
if (!wait || wait.outcomeJson !== null)
|
|
102
|
+
return false;
|
|
103
|
+
wait.outcomeJson = outcomeJson;
|
|
104
|
+
return true;
|
|
105
|
+
},
|
|
106
|
+
async putSignal(runId, name, valueJson) {
|
|
107
|
+
const key = `${runId}\u0000${name}`;
|
|
108
|
+
if (signals.has(key))
|
|
109
|
+
return false;
|
|
110
|
+
signals.set(key, valueJson);
|
|
111
|
+
return true;
|
|
112
|
+
},
|
|
113
|
+
async getSignal(runId, name) {
|
|
114
|
+
return signals.get(`${runId}\u0000${name}`);
|
|
115
|
+
},
|
|
116
|
+
async getStep(runId, index) {
|
|
117
|
+
return steps.get(`${runId}:${index}`);
|
|
118
|
+
},
|
|
119
|
+
async putStep(runId, index, step) {
|
|
120
|
+
const key = `${runId}:${index}`;
|
|
121
|
+
if (!steps.has(key))
|
|
122
|
+
steps.set(key, step);
|
|
123
|
+
},
|
|
124
|
+
};
|
|
125
|
+
}
|
|
@@ -0,0 +1,7 @@
|
|
|
1
|
+
import { type EngineOptions } from "./engine.js";
|
|
2
|
+
import type { Engine } from "./types.js";
|
|
3
|
+
export interface SqliteEngineOptions extends Omit<EngineOptions, "journal"> {
|
|
4
|
+
databasePath: string;
|
|
5
|
+
}
|
|
6
|
+
/** The local engine: runs survive a restart because the journal lives in the project's SQLite file. */
|
|
7
|
+
export declare function createSqliteEngine({ databasePath, ...options }: SqliteEngineOptions): Engine;
|
|
@@ -0,0 +1,6 @@
|
|
|
1
|
+
import { createEngine } from "./engine.js";
|
|
2
|
+
import { createSqliteJournal } from "./sqlite-journal.js";
|
|
3
|
+
/** The local engine: runs survive a restart because the journal lives in the project's SQLite file. */
|
|
4
|
+
export function createSqliteEngine({ databasePath, ...options }) {
|
|
5
|
+
return createEngine({ ...options, journal: createSqliteJournal({ databasePath }) });
|
|
6
|
+
}
|
|
@@ -0,0 +1,136 @@
|
|
|
1
|
+
import { openDatabase } from "../storage/sqlite/open.js";
|
|
2
|
+
import { acquireOwner, ownerIsAlive } from "../storage/sqlite/owner-lock.js";
|
|
3
|
+
function leaseOf(row) {
|
|
4
|
+
return { workerId: row.worker_id, until: row.lease_until };
|
|
5
|
+
}
|
|
6
|
+
/** Keeps the journal in the project's SQLite file, in tables private to the engine. */
|
|
7
|
+
export function createSqliteJournal({ databasePath }) {
|
|
8
|
+
const database = openDatabase(databasePath);
|
|
9
|
+
function readRun(runId) {
|
|
10
|
+
return database.prepare("SELECT * FROM engine_runs WHERE run_id = ?").get(runId);
|
|
11
|
+
}
|
|
12
|
+
return {
|
|
13
|
+
async begin(runId, inputJson, lease) {
|
|
14
|
+
const owner = acquireOwner();
|
|
15
|
+
const result = database
|
|
16
|
+
.prepare("INSERT OR IGNORE INTO engine_runs (run_id, state, input_json, worker_id, lease_until, owner_token, owner_path) VALUES (?, 'running', ?, ?, ?, ?, ?)")
|
|
17
|
+
.run(runId, inputJson, lease.workerId, lease.until, owner.token, owner.path);
|
|
18
|
+
return result.changes === 1;
|
|
19
|
+
},
|
|
20
|
+
async getRun(runId) {
|
|
21
|
+
const row = readRun(runId);
|
|
22
|
+
return (row && {
|
|
23
|
+
runId,
|
|
24
|
+
state: row.state,
|
|
25
|
+
inputJson: row.input_json,
|
|
26
|
+
cancelRequested: row.cancel_requested === 1,
|
|
27
|
+
});
|
|
28
|
+
},
|
|
29
|
+
async setState(runId, state) {
|
|
30
|
+
database.prepare("UPDATE engine_runs SET state = ? WHERE run_id = ?").run(state, runId);
|
|
31
|
+
},
|
|
32
|
+
async renew(runId, lease) {
|
|
33
|
+
database
|
|
34
|
+
.prepare("UPDATE engine_runs SET lease_until = ? WHERE run_id = ? AND worker_id = ?")
|
|
35
|
+
.run(lease.until, runId, lease.workerId);
|
|
36
|
+
},
|
|
37
|
+
async claim(runId, lease, expected) {
|
|
38
|
+
const owner = acquireOwner();
|
|
39
|
+
const result = expected
|
|
40
|
+
? database
|
|
41
|
+
.prepare("UPDATE engine_runs SET state = 'running', worker_id = ?, lease_until = ?, owner_token = ?, owner_path = ? WHERE run_id = ? AND worker_id = ? AND lease_until = ?")
|
|
42
|
+
.run(lease.workerId, lease.until, owner.token, owner.path, runId, expected.workerId, expected.until)
|
|
43
|
+
: database
|
|
44
|
+
.prepare("UPDATE engine_runs SET state = 'running', worker_id = ?, lease_until = ?, owner_token = ?, owner_path = ? WHERE run_id = ?")
|
|
45
|
+
.run(lease.workerId, lease.until, owner.token, owner.path, runId);
|
|
46
|
+
return result.changes === 1;
|
|
47
|
+
},
|
|
48
|
+
async claimWaiting(runId, lease, expected) {
|
|
49
|
+
const owner = acquireOwner();
|
|
50
|
+
const sql = `UPDATE engine_runs SET state = 'running', worker_id = ?, lease_until = ?, owner_token = ?, owner_path = ? WHERE run_id = ? AND state = 'waiting'`;
|
|
51
|
+
const result = expected
|
|
52
|
+
? database
|
|
53
|
+
.prepare(`${sql} AND worker_id = ? AND lease_until = ?`)
|
|
54
|
+
.run(lease.workerId, lease.until, owner.token, owner.path, runId, expected.workerId, expected.until)
|
|
55
|
+
: database.prepare(sql).run(lease.workerId, lease.until, owner.token, owner.path, runId);
|
|
56
|
+
return result.changes === 1;
|
|
57
|
+
},
|
|
58
|
+
async listStale(workerId, now) {
|
|
59
|
+
const rows = database
|
|
60
|
+
.prepare("SELECT * FROM engine_runs WHERE state = 'running' AND worker_id != ?")
|
|
61
|
+
.all(workerId);
|
|
62
|
+
// A dead process is recovered at the next start; a live one only after its lease runs out.
|
|
63
|
+
return rows
|
|
64
|
+
.filter((row) => row.lease_until < now || !ownerIsAlive(row.owner_token, row.owner_path))
|
|
65
|
+
.map((row) => ({ runId: row.run_id, held: leaseOf(row) }));
|
|
66
|
+
},
|
|
67
|
+
async listReady(now) {
|
|
68
|
+
const rows = database
|
|
69
|
+
.prepare(`
|
|
70
|
+
SELECT DISTINCT r.* FROM engine_runs r
|
|
71
|
+
JOIN engine_waits w ON w.run_id = r.run_id AND w.outcome_json IS NULL
|
|
72
|
+
WHERE r.state = 'waiting' AND (
|
|
73
|
+
r.cancel_requested = 1 OR w.deadline <= ? OR EXISTS (
|
|
74
|
+
SELECT 1 FROM engine_signals s WHERE s.run_id = w.run_id AND s.name = w.name
|
|
75
|
+
)
|
|
76
|
+
)
|
|
77
|
+
`)
|
|
78
|
+
.all(now);
|
|
79
|
+
return rows.map((row) => ({ runId: row.run_id, held: leaseOf(row) }));
|
|
80
|
+
},
|
|
81
|
+
async listWaiting() {
|
|
82
|
+
const rows = database
|
|
83
|
+
.prepare(`SELECT r.run_id, w.deadline FROM engine_runs r
|
|
84
|
+
JOIN engine_waits w ON w.run_id = r.run_id AND w.outcome_json IS NULL
|
|
85
|
+
WHERE r.state = 'waiting'`)
|
|
86
|
+
.all();
|
|
87
|
+
return rows.map((row) => ({ runId: row.run_id, deadline: row.deadline }));
|
|
88
|
+
},
|
|
89
|
+
async requestCancel(runId) {
|
|
90
|
+
const result = database
|
|
91
|
+
.prepare("UPDATE engine_runs SET cancel_requested = 1 WHERE run_id = ? AND cancel_requested = 0 AND state IN ('running', 'waiting')")
|
|
92
|
+
.run(runId);
|
|
93
|
+
return result.changes === 1;
|
|
94
|
+
},
|
|
95
|
+
async getWait(runId, name) {
|
|
96
|
+
const row = database
|
|
97
|
+
.prepare("SELECT deadline, outcome_json FROM engine_waits WHERE run_id = ? AND name = ?")
|
|
98
|
+
.get(runId, name);
|
|
99
|
+
return row && { deadline: row.deadline, outcomeJson: row.outcome_json };
|
|
100
|
+
},
|
|
101
|
+
async openWait(runId, name, deadline) {
|
|
102
|
+
database
|
|
103
|
+
.prepare("INSERT OR IGNORE INTO engine_waits (run_id, name, deadline) VALUES (?, ?, ?)")
|
|
104
|
+
.run(runId, name, deadline);
|
|
105
|
+
},
|
|
106
|
+
async resolveWait(runId, name, outcomeJson) {
|
|
107
|
+
const result = database
|
|
108
|
+
.prepare("UPDATE engine_waits SET outcome_json = ? WHERE run_id = ? AND name = ? AND outcome_json IS NULL")
|
|
109
|
+
.run(outcomeJson, runId, name);
|
|
110
|
+
return result.changes === 1;
|
|
111
|
+
},
|
|
112
|
+
async putSignal(runId, name, valueJson) {
|
|
113
|
+
const result = database
|
|
114
|
+
.prepare("INSERT OR IGNORE INTO engine_signals (run_id, name, value_json) VALUES (?, ?, ?)")
|
|
115
|
+
.run(runId, name, valueJson);
|
|
116
|
+
return result.changes === 1;
|
|
117
|
+
},
|
|
118
|
+
async getSignal(runId, name) {
|
|
119
|
+
const row = database
|
|
120
|
+
.prepare("SELECT value_json FROM engine_signals WHERE run_id = ? AND name = ?")
|
|
121
|
+
.get(runId, name);
|
|
122
|
+
return row?.value_json;
|
|
123
|
+
},
|
|
124
|
+
async getStep(runId, index) {
|
|
125
|
+
const row = database
|
|
126
|
+
.prepare("SELECT name, result_json FROM engine_steps WHERE run_id = ? AND step_index = ?")
|
|
127
|
+
.get(runId, index);
|
|
128
|
+
return row && { name: row.name, resultJson: row.result_json };
|
|
129
|
+
},
|
|
130
|
+
async putStep(runId, index, step) {
|
|
131
|
+
database
|
|
132
|
+
.prepare("INSERT OR IGNORE INTO engine_steps (run_id, step_index, name, result_json) VALUES (?, ?, ?, ?)")
|
|
133
|
+
.run(runId, index, step.name, step.resultJson);
|
|
134
|
+
},
|
|
135
|
+
};
|
|
136
|
+
}
|
|
@@ -0,0 +1,97 @@
|
|
|
1
|
+
export interface StepOptions {
|
|
2
|
+
/** Extra attempts inside one step. Defaults to 0: the step runs once unless the process dies inside it. */
|
|
3
|
+
retries?: number;
|
|
4
|
+
/** When supplied, only errors accepted by this predicate are retried. */
|
|
5
|
+
shouldRetry?: (error: unknown) => boolean;
|
|
6
|
+
}
|
|
7
|
+
export interface StepAttempt {
|
|
8
|
+
/** Zero-based attempt number within this step. */
|
|
9
|
+
attempt: number;
|
|
10
|
+
/** Aborts when the run is cancelled, so a long step can stop early. */
|
|
11
|
+
signal: AbortSignal;
|
|
12
|
+
}
|
|
13
|
+
export interface RunContext {
|
|
14
|
+
readonly runId: string;
|
|
15
|
+
/** Aborts when the run is cancelled. */
|
|
16
|
+
readonly signal: AbortSignal;
|
|
17
|
+
/**
|
|
18
|
+
* Runs `fn` and records its result. When the run is replayed, the recorded result is returned
|
|
19
|
+
* and `fn` does not run. Results are stored as JSON, so `undefined` properties are dropped.
|
|
20
|
+
* A step runs at least once: a process that dies inside it runs it again on recovery.
|
|
21
|
+
*/
|
|
22
|
+
step<T>(name: string, fn: (attempt: StepAttempt) => T | Promise<T>, options?: StepOptions): Promise<T>;
|
|
23
|
+
/**
|
|
24
|
+
* Holds the run until a signal named `name` arrives or the timeout passes. A signal that arrived
|
|
25
|
+
* earlier is used at once. Nothing executes while the run waits: the first time there is nothing
|
|
26
|
+
* to return, this throws RunSuspended. Let it reach the engine, or catch it and return; the engine
|
|
27
|
+
* replays the workflow from its recorded steps once the wait can end. Use a unique name per wait.
|
|
28
|
+
*/
|
|
29
|
+
wait<T = unknown>(name: string, options?: WaitOptions): Promise<WaitOutcome<T>>;
|
|
30
|
+
}
|
|
31
|
+
/** How a run ended for now. A waiting run may carry what the workflow returned before it stopped. */
|
|
32
|
+
export type RunOutcome<O> = {
|
|
33
|
+
state: "completed";
|
|
34
|
+
output: O;
|
|
35
|
+
} | {
|
|
36
|
+
state: "waiting";
|
|
37
|
+
output?: O;
|
|
38
|
+
} | {
|
|
39
|
+
state: "cancelled";
|
|
40
|
+
output?: O;
|
|
41
|
+
};
|
|
42
|
+
/** `ignored`: the run had already finished or been cancelled. */
|
|
43
|
+
export type CancelResult = "accepted" | "ignored" | "unknown";
|
|
44
|
+
/** `ignored`: a decision arrived late, twice, or for a finished run. The first one wins. */
|
|
45
|
+
export type SignalResult = "accepted" | "ignored" | "unknown";
|
|
46
|
+
export interface WaitOptions {
|
|
47
|
+
/** Ends the wait as a timeout this long after it is first reached. No timeout by default. */
|
|
48
|
+
timeoutMs?: number;
|
|
49
|
+
}
|
|
50
|
+
/** A timeout is its own outcome, so a caller can tell it from a decision. */
|
|
51
|
+
export type WaitOutcome<T> = {
|
|
52
|
+
kind: "signal";
|
|
53
|
+
value: T;
|
|
54
|
+
} | {
|
|
55
|
+
kind: "timeout";
|
|
56
|
+
};
|
|
57
|
+
export type Workflow<I, O> = (ctx: RunContext, input: I) => Promise<O>;
|
|
58
|
+
export interface RunHandle<O> {
|
|
59
|
+
readonly runId: string;
|
|
60
|
+
result(): Promise<RunOutcome<O>>;
|
|
61
|
+
}
|
|
62
|
+
export type ExecutionState = "running" | "waiting" | "completed" | "failed" | "cancelled";
|
|
63
|
+
export interface ExecutionStatus {
|
|
64
|
+
state: ExecutionState;
|
|
65
|
+
}
|
|
66
|
+
export interface RecoveredRun<O = unknown> {
|
|
67
|
+
runId: string;
|
|
68
|
+
state: "completed" | "waiting" | "failed" | "cancelled";
|
|
69
|
+
error?: string;
|
|
70
|
+
output?: O;
|
|
71
|
+
}
|
|
72
|
+
export interface Engine {
|
|
73
|
+
/** Registers the run and begins executing it in the background. */
|
|
74
|
+
start<I, O>(runId: string, workflow: Workflow<I, O>, input: I): Promise<RunHandle<O>>;
|
|
75
|
+
/** Starts the run and waits until it completes or stops to wait. */
|
|
76
|
+
run<I, O>(runId: string, workflow: Workflow<I, O>, input: I): Promise<RunOutcome<O>>;
|
|
77
|
+
/** Continues an unfinished run from its recorded steps, whoever held it. */
|
|
78
|
+
resume<I, O>(runId: string, workflow: Workflow<I, O>): Promise<RunOutcome<O>>;
|
|
79
|
+
/**
|
|
80
|
+
* Delivers a decision to the wait named `name`. The first signal for a name wins; one that comes
|
|
81
|
+
* after the wait ended changes nothing. A run waiting in this process continues at once; any other
|
|
82
|
+
* continues when `recover` next runs.
|
|
83
|
+
*/
|
|
84
|
+
signal(runId: string, name: string, value: unknown): Promise<SignalResult>;
|
|
85
|
+
/**
|
|
86
|
+
* Continues every run whose worker died or let its lease run out, and every waiting run whose
|
|
87
|
+
* wait can now end. `resolve` picks the workflow for a run; return undefined to leave it alone.
|
|
88
|
+
*/
|
|
89
|
+
recover<I, O>(resolve: (runId: string, input: I) => Workflow<I, O> | undefined): Promise<RecoveredRun<O>[]>;
|
|
90
|
+
/**
|
|
91
|
+
* Cancels a run. A step that is running gets its abort signal fired; a waiting run ends at once
|
|
92
|
+
* where it holds the run, or when `recover` next runs. The workflow sees RunCancelled at its next
|
|
93
|
+
* step or wait and returns.
|
|
94
|
+
*/
|
|
95
|
+
cancel(runId: string): Promise<CancelResult>;
|
|
96
|
+
status(runId: string): Promise<ExecutionStatus | undefined>;
|
|
97
|
+
}
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
export {};
|
package/dist/env.d.ts
ADDED
|
@@ -0,0 +1,7 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Loads `<root>/.env` into `process.env` without replacing variables that are already set, and
|
|
3
|
+
* returns the file path, or `undefined` when the project has none. Values are never logged.
|
|
4
|
+
*/
|
|
5
|
+
export declare function loadProjectEnv(root: string): string | undefined;
|
|
6
|
+
/** The `.env` files this process loaded through {@link loadProjectEnv}. */
|
|
7
|
+
export declare function loadedEnvFiles(): string[];
|
package/dist/env.js
ADDED
|
@@ -0,0 +1,19 @@
|
|
|
1
|
+
import { existsSync } from "node:fs";
|
|
2
|
+
import { join } from "node:path";
|
|
3
|
+
const loaded = new Set();
|
|
4
|
+
/**
|
|
5
|
+
* Loads `<root>/.env` into `process.env` without replacing variables that are already set, and
|
|
6
|
+
* returns the file path, or `undefined` when the project has none. Values are never logged.
|
|
7
|
+
*/
|
|
8
|
+
export function loadProjectEnv(root) {
|
|
9
|
+
const file = join(root, ".env");
|
|
10
|
+
if (!existsSync(file))
|
|
11
|
+
return undefined;
|
|
12
|
+
process.loadEnvFile(file);
|
|
13
|
+
loaded.add(file);
|
|
14
|
+
return file;
|
|
15
|
+
}
|
|
16
|
+
/** The `.env` files this process loaded through {@link loadProjectEnv}. */
|
|
17
|
+
export function loadedEnvFiles() {
|
|
18
|
+
return [...loaded];
|
|
19
|
+
}
|
package/dist/errors.d.ts
ADDED
|
@@ -0,0 +1,18 @@
|
|
|
1
|
+
import type { PlicoEventPayloads } from "@plico-ai/contracts";
|
|
2
|
+
export declare class RuntimeDiagnosticError extends Error {
|
|
3
|
+
readonly payload: PlicoEventPayloads["run.error"];
|
|
4
|
+
constructor(payload: PlicoEventPayloads["run.error"]);
|
|
5
|
+
}
|
|
6
|
+
export declare class RunWaitingError extends Error {
|
|
7
|
+
readonly output: string | undefined;
|
|
8
|
+
constructor(output: string | undefined);
|
|
9
|
+
}
|
|
10
|
+
export declare function errorMessage(error: unknown): string;
|
|
11
|
+
export declare function runtimeErrorPayload(error: unknown): PlicoEventPayloads["run.error"];
|
|
12
|
+
export type ModelErrorKind = "rate_limit" | "overloaded" | "auth" | "bad_request" | "context_length" | "aborted" | "network";
|
|
13
|
+
/** Provider adapters normalize errors to these provider-neutral kinds. */
|
|
14
|
+
export declare class ModelProviderError extends Error {
|
|
15
|
+
readonly kind: ModelErrorKind;
|
|
16
|
+
readonly retryable: boolean;
|
|
17
|
+
constructor(kind: ModelErrorKind, message: string);
|
|
18
|
+
}
|
package/dist/errors.js
ADDED
|
@@ -0,0 +1,40 @@
|
|
|
1
|
+
export class RuntimeDiagnosticError extends Error {
|
|
2
|
+
payload;
|
|
3
|
+
constructor(payload) {
|
|
4
|
+
super(typeof payload.message === "string" ? payload.message : "Runtime diagnostic error");
|
|
5
|
+
this.payload = payload;
|
|
6
|
+
}
|
|
7
|
+
}
|
|
8
|
+
export class RunWaitingError extends Error {
|
|
9
|
+
output;
|
|
10
|
+
constructor(output) {
|
|
11
|
+
super("Run waiting for approval");
|
|
12
|
+
this.output = output;
|
|
13
|
+
}
|
|
14
|
+
}
|
|
15
|
+
export function errorMessage(error) {
|
|
16
|
+
if (error instanceof Error && error.message) {
|
|
17
|
+
return error.message;
|
|
18
|
+
}
|
|
19
|
+
return "Unknown runtime error";
|
|
20
|
+
}
|
|
21
|
+
export function runtimeErrorPayload(error) {
|
|
22
|
+
if (error instanceof RuntimeDiagnosticError) {
|
|
23
|
+
return error.payload;
|
|
24
|
+
}
|
|
25
|
+
return {
|
|
26
|
+
phase: "runtime",
|
|
27
|
+
message: errorMessage(error),
|
|
28
|
+
};
|
|
29
|
+
}
|
|
30
|
+
/** Provider adapters normalize errors to these provider-neutral kinds. */
|
|
31
|
+
export class ModelProviderError extends Error {
|
|
32
|
+
kind;
|
|
33
|
+
retryable;
|
|
34
|
+
constructor(kind, message) {
|
|
35
|
+
super(message);
|
|
36
|
+
this.kind = kind;
|
|
37
|
+
this.name = "ModelProviderError";
|
|
38
|
+
this.retryable = kind === "rate_limit" || kind === "overloaded" || kind === "network";
|
|
39
|
+
}
|
|
40
|
+
}
|
|
@@ -0,0 +1,49 @@
|
|
|
1
|
+
import { createHash } from "node:crypto";
|
|
2
|
+
import { readdir, readFile } from "node:fs/promises";
|
|
3
|
+
import { join } from "node:path";
|
|
4
|
+
const fixtureSuffix = ".script.json";
|
|
5
|
+
function toEvalCase(file, parsed) {
|
|
6
|
+
const fixture = (Array.isArray(parsed) ? { script: parsed } : parsed);
|
|
7
|
+
if (!Array.isArray(fixture?.script)) {
|
|
8
|
+
throw new Error(`Invalid eval fixture ${file}: expected a step array or a "script" array.`);
|
|
9
|
+
}
|
|
10
|
+
const entry = {
|
|
11
|
+
name: typeof fixture.name === "string" ? fixture.name : file.slice(0, -fixtureSuffix.length),
|
|
12
|
+
script: fixture.script,
|
|
13
|
+
};
|
|
14
|
+
if (typeof fixture.description === "string")
|
|
15
|
+
entry.description = fixture.description;
|
|
16
|
+
if (fixture.expect !== undefined)
|
|
17
|
+
entry.expect = fixture.expect;
|
|
18
|
+
return entry;
|
|
19
|
+
}
|
|
20
|
+
async function readFixtures(evalsDir) {
|
|
21
|
+
let entries;
|
|
22
|
+
try {
|
|
23
|
+
entries = await readdir(evalsDir);
|
|
24
|
+
}
|
|
25
|
+
catch {
|
|
26
|
+
return [];
|
|
27
|
+
}
|
|
28
|
+
const scriptFiles = entries.filter((e) => e.endsWith(fixtureSuffix)).sort();
|
|
29
|
+
return Promise.all(scriptFiles.map(async (file) => ({ file, raw: await readFile(join(evalsDir, file), "utf8") })));
|
|
30
|
+
}
|
|
31
|
+
function parseFixture(file, raw) {
|
|
32
|
+
try {
|
|
33
|
+
return JSON.parse(raw);
|
|
34
|
+
}
|
|
35
|
+
catch (error) {
|
|
36
|
+
throw new Error(`Invalid eval fixture ${file}: ${error.message}`);
|
|
37
|
+
}
|
|
38
|
+
}
|
|
39
|
+
export async function discoverEvalCases(evalsDir) {
|
|
40
|
+
const fixtures = await readFixtures(evalsDir);
|
|
41
|
+
return fixtures.map(({ file, raw }) => toEvalCase(file, parseFixture(file, raw)));
|
|
42
|
+
}
|
|
43
|
+
export async function digestEvalFixtures(evalsDir) {
|
|
44
|
+
const hash = createHash("sha256");
|
|
45
|
+
for (const { file, raw } of await readFixtures(evalsDir)) {
|
|
46
|
+
hash.update(`${file}\0${raw}\0`);
|
|
47
|
+
}
|
|
48
|
+
return hash.digest("hex");
|
|
49
|
+
}
|
|
@@ -0,0 +1,7 @@
|
|
|
1
|
+
import type { PlicoEvent, RunStatus } from "@plico-ai/contracts";
|
|
2
|
+
import type { EvalCase } from "../types/index.js";
|
|
3
|
+
/**
|
|
4
|
+
* Turns a stored run into an eval case. Each tool call keeps the output it produced as the mocked
|
|
5
|
+
* `result`, so the case replays without running any handler. Files a tool wrote are not carried over.
|
|
6
|
+
*/
|
|
7
|
+
export declare function evalCaseFromRun(name: string, status: RunStatus, events: PlicoEvent[]): EvalCase;
|
|
@@ -0,0 +1,32 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Turns a stored run into an eval case. Each tool call keeps the output it produced as the mocked
|
|
3
|
+
* `result`, so the case replays without running any handler. Files a tool wrote are not carried over.
|
|
4
|
+
*/
|
|
5
|
+
export function evalCaseFromRun(name, status, events) {
|
|
6
|
+
const script = [];
|
|
7
|
+
let output;
|
|
8
|
+
let pending;
|
|
9
|
+
for (const event of events) {
|
|
10
|
+
if (event.type === "assistant.output") {
|
|
11
|
+
script.push({ type: "assistant.output", content: event.payload.content });
|
|
12
|
+
output = event.payload.content;
|
|
13
|
+
}
|
|
14
|
+
else if (event.type === "tool.call") {
|
|
15
|
+
pending = {
|
|
16
|
+
type: "tool.call",
|
|
17
|
+
toolName: event.payload.toolName,
|
|
18
|
+
arguments: event.payload.arguments,
|
|
19
|
+
};
|
|
20
|
+
script.push(pending);
|
|
21
|
+
}
|
|
22
|
+
else if (event.type === "tool.result" && pending) {
|
|
23
|
+
pending.result = event.payload.result;
|
|
24
|
+
pending = undefined;
|
|
25
|
+
}
|
|
26
|
+
}
|
|
27
|
+
return {
|
|
28
|
+
name,
|
|
29
|
+
script,
|
|
30
|
+
expect: { status, ...(output === undefined ? {} : { outputContains: output }) },
|
|
31
|
+
};
|
|
32
|
+
}
|
|
@@ -0,0 +1,69 @@
|
|
|
1
|
+
import { getProjectPaths } from "@plico-ai/core";
|
|
2
|
+
import { runProject } from "../run/index.js";
|
|
3
|
+
import { digestEvalFixtures, discoverEvalCases } from "./discover.js";
|
|
4
|
+
export async function runEvals(projectRoot, options = {}) {
|
|
5
|
+
const evalsDir = getProjectPaths(projectRoot).directories.evals;
|
|
6
|
+
const cases = await discoverEvalCases(evalsDir);
|
|
7
|
+
const fixturesDigest = await digestEvalFixtures(evalsDir);
|
|
8
|
+
const results = [];
|
|
9
|
+
for (const evalCase of cases) {
|
|
10
|
+
const runId = `eval-${evalCase.name.replace(/[^a-z0-9_-]/gi, "_")}-${crypto.randomUUID().slice(0, 8)}`;
|
|
11
|
+
const runOpts = {
|
|
12
|
+
script: evalCase.script,
|
|
13
|
+
mockTools: true,
|
|
14
|
+
runId,
|
|
15
|
+
};
|
|
16
|
+
if (options.eventStore !== undefined)
|
|
17
|
+
runOpts.eventStore = options.eventStore;
|
|
18
|
+
const result = await runProject(projectRoot, runOpts);
|
|
19
|
+
const diagnostics = [];
|
|
20
|
+
let passed = true;
|
|
21
|
+
if (evalCase.expect?.status && result.status !== evalCase.expect.status) {
|
|
22
|
+
passed = false;
|
|
23
|
+
diagnostics.push(`Expected status "${evalCase.expect.status}" but got "${result.status}"`);
|
|
24
|
+
}
|
|
25
|
+
if (!evalCase.expect?.status && result.status !== "completed") {
|
|
26
|
+
passed = false;
|
|
27
|
+
diagnostics.push(`Run ended with status "${result.status}"`);
|
|
28
|
+
}
|
|
29
|
+
if (evalCase.expect?.outputContains) {
|
|
30
|
+
const output = result.output ?? "";
|
|
31
|
+
if (!output.includes(evalCase.expect.outputContains)) {
|
|
32
|
+
passed = false;
|
|
33
|
+
diagnostics.push(`Expected output to contain "${evalCase.expect.outputContains}" but got "${output}"`);
|
|
34
|
+
}
|
|
35
|
+
}
|
|
36
|
+
for (const event of result.events) {
|
|
37
|
+
if (event.type === "run.error" && typeof event.payload.message === "string")
|
|
38
|
+
diagnostics.push(event.payload.message);
|
|
39
|
+
}
|
|
40
|
+
if (result.errors?.length) {
|
|
41
|
+
for (const err of result.errors) {
|
|
42
|
+
diagnostics.push(`${err.path}: ${err.message}`);
|
|
43
|
+
}
|
|
44
|
+
}
|
|
45
|
+
const entry = {
|
|
46
|
+
name: evalCase.name,
|
|
47
|
+
status: passed ? "pass" : "fail",
|
|
48
|
+
runId: result.runId,
|
|
49
|
+
runStatus: result.status,
|
|
50
|
+
};
|
|
51
|
+
if (evalCase.description !== undefined)
|
|
52
|
+
entry.description = evalCase.description;
|
|
53
|
+
if (result.output !== undefined)
|
|
54
|
+
entry.output = result.output;
|
|
55
|
+
if (diagnostics.length > 0)
|
|
56
|
+
entry.diagnostics = diagnostics;
|
|
57
|
+
results.push(entry);
|
|
58
|
+
}
|
|
59
|
+
const passed = results.filter((r) => r.status === "pass").length;
|
|
60
|
+
const failed = results.filter((r) => r.status === "fail").length;
|
|
61
|
+
const summary = { total: results.length, passed, failed, results };
|
|
62
|
+
await options.eventStore?.persistEvalReport({
|
|
63
|
+
schemaVersion: 1,
|
|
64
|
+
...summary,
|
|
65
|
+
evaluatedAt: new Date().toISOString(),
|
|
66
|
+
fixturesDigest,
|
|
67
|
+
});
|
|
68
|
+
return summary;
|
|
69
|
+
}
|