@yaag/runtime 0.2.0 → 0.3.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/package.json +1 -1
- package/src/cassette-schema.ts +1 -0
- package/src/cassette.ts +3 -1
- package/src/program-hash.ts +9 -0
- package/src/resume-identity.ts +47 -0
- package/src/run-checkpoint.ts +17 -3
- package/src/run.ts +42 -7
package/package.json
CHANGED
package/src/cassette-schema.ts
CHANGED
package/src/cassette.ts
CHANGED
|
@@ -22,8 +22,10 @@ export interface CassetteRun {
|
|
|
22
22
|
readonly outcome: RunOutcome;
|
|
23
23
|
/** Invocation identity: what a resume is about to re-execute. */
|
|
24
24
|
readonly programFile?: string;
|
|
25
|
+
/** Inline Program source text; identity for a Run with no program file (ADR-0033). */
|
|
26
|
+
readonly programSource?: string;
|
|
25
27
|
readonly args?: unknown;
|
|
26
|
-
/** Advisory content hash of the program file; SHA-256 hex. */
|
|
28
|
+
/** Advisory content hash of the program file, or of the inline source; SHA-256 hex. */
|
|
27
29
|
readonly programHash?: string;
|
|
28
30
|
}
|
|
29
31
|
|
|
@@ -0,0 +1,9 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* One advisory content hash, shared by the modules that record a program's
|
|
3
|
+
* identity and the modules that compare it (ADR-0033).
|
|
4
|
+
*/
|
|
5
|
+
|
|
6
|
+
/** Advisory content hash of a program's bytes or inline source; SHA-256 hex. */
|
|
7
|
+
export function sha256Hex(input: string | ArrayBuffer): string {
|
|
8
|
+
return new Bun.CryptoHasher("sha256").update(input).digest("hex");
|
|
9
|
+
}
|
|
@@ -0,0 +1,47 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* The advisory comparison between a loaded Cassette's recorded program identity
|
|
3
|
+
* and the program this Run executes (ADR-0033).
|
|
4
|
+
*/
|
|
5
|
+
import type { Cassette } from "./cassette.ts";
|
|
6
|
+
import { sha256Hex } from "./program-hash.ts";
|
|
7
|
+
import type { ProgramIdentity } from "./run-checkpoint.ts";
|
|
8
|
+
|
|
9
|
+
/**
|
|
10
|
+
* Warns when a Cassette's recorded program identity does not match the program
|
|
11
|
+
* this Run executes. Advisory only: the caller always supplies the program and
|
|
12
|
+
* the Cassette supplies the history, so a mismatch never stops the Run
|
|
13
|
+
* (ADR-0033). Returns null when nothing inline is involved, so file-to-file
|
|
14
|
+
* resume behaviour is unchanged.
|
|
15
|
+
*/
|
|
16
|
+
export function programIdentityWarning(
|
|
17
|
+
cassette: Cassette,
|
|
18
|
+
path: string,
|
|
19
|
+
program: ProgramIdentity,
|
|
20
|
+
): string | null {
|
|
21
|
+
const recorded = cassette.run.programSource;
|
|
22
|
+
const given = program.programSource;
|
|
23
|
+
if (recorded === undefined && given === undefined) return null;
|
|
24
|
+
if (recorded !== undefined && given !== undefined) {
|
|
25
|
+
if (recorded === given) return null;
|
|
26
|
+
const recordedHash = cassette.run.programHash ?? sha256Hex(recorded);
|
|
27
|
+
return (
|
|
28
|
+
`cassette "${path}" recorded a different inline program: ` +
|
|
29
|
+
`recorded hash ${recordedHash}, given hash ${sha256Hex(given)}; ` +
|
|
30
|
+
"the Cassette gives history only, and this Run continues with the source you gave (ADR-0033)"
|
|
31
|
+
);
|
|
32
|
+
}
|
|
33
|
+
if (recorded !== undefined) {
|
|
34
|
+
const file = program.programFile;
|
|
35
|
+
if (file === undefined) return null;
|
|
36
|
+
return (
|
|
37
|
+
`cassette "${path}" recorded an inline program, and this Run runs the program file "${file}"; ` +
|
|
38
|
+
"the Cassette gives history only, and this Run continues with the program you gave"
|
|
39
|
+
);
|
|
40
|
+
}
|
|
41
|
+
const recordedFile = cassette.run.programFile;
|
|
42
|
+
if (recordedFile === undefined) return null;
|
|
43
|
+
return (
|
|
44
|
+
`cassette "${path}" recorded the program file "${recordedFile}", and this Run runs an inline program; ` +
|
|
45
|
+
"the Cassette gives history only, and this Run continues with the program you gave"
|
|
46
|
+
);
|
|
47
|
+
}
|
package/src/run-checkpoint.ts
CHANGED
|
@@ -3,6 +3,7 @@ import { dirname } from "node:path";
|
|
|
3
3
|
import type { CassetteCollector, CassetteRun } from "./cassette.ts";
|
|
4
4
|
import { publishCassette } from "./cassette-publish.ts";
|
|
5
5
|
import type { RunOutcome } from "./events.ts";
|
|
6
|
+
import { sha256Hex } from "./program-hash.ts";
|
|
6
7
|
|
|
7
8
|
/** What a resume is about to re-execute; resolved once, at Run start. */
|
|
8
9
|
export type RunIdentity = Omit<CassetteRun, "outcome">;
|
|
@@ -92,11 +93,25 @@ export async function ensureCheckpointDirectory(
|
|
|
92
93
|
await mkdir(dirname(destination), { recursive: true, mode: 0o700 });
|
|
93
94
|
}
|
|
94
95
|
|
|
96
|
+
/** Which program a Run executes: a file on disk, or inline source text (ADR-0033). */
|
|
97
|
+
export interface ProgramIdentity {
|
|
98
|
+
readonly programFile?: string;
|
|
99
|
+
/** Inline source, exactly as the caller gave it; it excludes any loader prelude. */
|
|
100
|
+
readonly programSource?: string;
|
|
101
|
+
}
|
|
102
|
+
|
|
95
103
|
/** Resolves what a resume re-executes. The program hash is advisory only. */
|
|
96
104
|
export async function resolveRunIdentity(
|
|
97
|
-
|
|
105
|
+
program: ProgramIdentity,
|
|
98
106
|
args: unknown,
|
|
99
107
|
): Promise<RunIdentity> {
|
|
108
|
+
// An Inline Program's file is a temporary path, and no artifact may name it,
|
|
109
|
+
// so the inline arm drops `programFile` even when the caller supplies one.
|
|
110
|
+
if (program.programSource !== undefined) {
|
|
111
|
+
const programSource = program.programSource;
|
|
112
|
+
return { programSource, args, programHash: sha256Hex(programSource) };
|
|
113
|
+
}
|
|
114
|
+
const programFile = program.programFile;
|
|
100
115
|
if (programFile === undefined) return {};
|
|
101
116
|
const programHash = await hashProgram(programFile);
|
|
102
117
|
return { programFile, args, ...(programHash === null ? {} : { programHash }) };
|
|
@@ -128,8 +143,7 @@ function isMissingFile(error: unknown): boolean {
|
|
|
128
143
|
/** Advisory only: an unreadable program file omits the hash and never fails the Run. */
|
|
129
144
|
async function hashProgram(programFile: string): Promise<string | null> {
|
|
130
145
|
try {
|
|
131
|
-
|
|
132
|
-
return new Bun.CryptoHasher("sha256").update(bytes).digest("hex");
|
|
146
|
+
return sha256Hex(await Bun.file(programFile).arrayBuffer());
|
|
133
147
|
} catch {
|
|
134
148
|
return null;
|
|
135
149
|
}
|
package/src/run.ts
CHANGED
|
@@ -16,8 +16,10 @@ import type { EventSink, LifecycleEvent, RunOutcome, StampedEventSink } from "./
|
|
|
16
16
|
import { liveTransport } from "./live-transport.ts";
|
|
17
17
|
import { recordingTransport } from "./recording-transport.ts";
|
|
18
18
|
import { replayTransport } from "./replay-transport.ts";
|
|
19
|
+
import { programIdentityWarning } from "./resume-identity.ts";
|
|
19
20
|
import { resumeTransport } from "./resume-transport.ts";
|
|
20
21
|
import {
|
|
22
|
+
type ProgramIdentity,
|
|
21
23
|
publishRunCheckpoint,
|
|
22
24
|
type RunCheckpointResult,
|
|
23
25
|
resolveRunIdentity,
|
|
@@ -44,6 +46,11 @@ export interface RunOptions {
|
|
|
44
46
|
* extension declarations. This is Run context, never an Agent working directory.
|
|
45
47
|
*/
|
|
46
48
|
readonly programFile?: string;
|
|
49
|
+
/**
|
|
50
|
+
* Inline Program source text, recorded as the Run's identity when there is no
|
|
51
|
+
* program file (ADR-0033). It never resolves extension paths.
|
|
52
|
+
*/
|
|
53
|
+
readonly programSource?: string;
|
|
47
54
|
/** Resolves pi's enabled skills for live restriction requests. */
|
|
48
55
|
readonly skillProbe?: SkillProbeFactory;
|
|
49
56
|
/** Write every public transport-seam frame once the Run settles. */
|
|
@@ -83,8 +90,14 @@ export async function executeRun<Args, Result>(
|
|
|
83
90
|
} catch (error) {
|
|
84
91
|
writeRecordingDiagnostic(error);
|
|
85
92
|
}
|
|
86
|
-
|
|
87
|
-
|
|
93
|
+
// Built once, so the identity a resume compares and the identity an artifact
|
|
94
|
+
// records cannot drift apart.
|
|
95
|
+
const programIdentity: ProgramIdentity = {
|
|
96
|
+
...(options.programFile === undefined ? {} : { programFile: options.programFile }),
|
|
97
|
+
...(options.programSource === undefined ? {} : { programSource: options.programSource }),
|
|
98
|
+
};
|
|
99
|
+
const replay = await loadReplay(options.replay, programIdentity);
|
|
100
|
+
const resume = await loadResume(options.resume, programIdentity);
|
|
88
101
|
const definition = programDefinition(program);
|
|
89
102
|
// Collection is universal so every Run can checkpoint, and every Run flushes
|
|
90
103
|
// that collection at each Ask boundary (ADR-0031). Only a stopped Run and a
|
|
@@ -96,7 +109,7 @@ export async function executeRun<Args, Result>(
|
|
|
96
109
|
// Fixed here, so every Ask-boundary flush and the settlement write one file,
|
|
97
110
|
// and so run_start can name it (ADR-0031).
|
|
98
111
|
const destination = options.record ?? join(checkpointDir, checkpointFileName());
|
|
99
|
-
const identity = await resolveRunIdentity(
|
|
112
|
+
const identity = await resolveRunIdentity(programIdentity, args);
|
|
100
113
|
const checkpoint = { collector, record: options.record, destination, identity };
|
|
101
114
|
try {
|
|
102
115
|
assertArgs<Args>(definition.args, args);
|
|
@@ -169,23 +182,45 @@ export async function executeRun<Args, Result>(
|
|
|
169
182
|
return settled.value;
|
|
170
183
|
}
|
|
171
184
|
|
|
172
|
-
/**
|
|
173
|
-
|
|
185
|
+
/**
|
|
186
|
+
* Loads a strict-replay source, which must be a settled Cassette (ADR-0031). A
|
|
187
|
+
* replay of an Inline Program follows the same rule as a resume: a recorded
|
|
188
|
+
* identity that differs from the given program warns once, and the Run goes on.
|
|
189
|
+
*/
|
|
190
|
+
async function loadReplay(
|
|
191
|
+
path: string | undefined,
|
|
192
|
+
program: ProgramIdentity,
|
|
193
|
+
): Promise<Cassette | null> {
|
|
174
194
|
if (path === undefined) return null;
|
|
175
195
|
const cassette = await loadCassette(path);
|
|
176
196
|
assertReplayable(cassette, path);
|
|
197
|
+
warnOnIdentity(cassette, path, program);
|
|
177
198
|
return cassette;
|
|
178
199
|
}
|
|
179
200
|
|
|
180
|
-
/**
|
|
181
|
-
|
|
201
|
+
/**
|
|
202
|
+
* Loads a resume source, warning once when it came from a hard death (ADR-0031),
|
|
203
|
+
* and once more when its recorded program identity differs from this Run's
|
|
204
|
+
* program (ADR-0033).
|
|
205
|
+
*/
|
|
206
|
+
async function loadResume(
|
|
207
|
+
path: string | undefined,
|
|
208
|
+
program: ProgramIdentity,
|
|
209
|
+
): Promise<Cassette | null> {
|
|
182
210
|
if (path === undefined) return null;
|
|
183
211
|
const cassette = await loadCassette(path);
|
|
184
212
|
const warning = interruptedResumeWarning(cassette, path);
|
|
185
213
|
if (warning !== null) writeRecordingDiagnostic(warning);
|
|
214
|
+
warnOnIdentity(cassette, path, program);
|
|
186
215
|
return cassette;
|
|
187
216
|
}
|
|
188
217
|
|
|
218
|
+
/** Advisory only: a Cassette gives history, and never the program to run (ADR-0033). */
|
|
219
|
+
function warnOnIdentity(cassette: Cassette, path: string, program: ProgramIdentity): void {
|
|
220
|
+
const warning = programIdentityWarning(cassette, path, program);
|
|
221
|
+
if (warning !== null) writeRecordingDiagnostic(warning);
|
|
222
|
+
}
|
|
223
|
+
|
|
189
224
|
/** What the Orchestration Program body produced, before the Run acknowledges it. */
|
|
190
225
|
type Settlement<Result> =
|
|
191
226
|
| { readonly ok: true; readonly value: Result }
|