@yaag/runtime 0.2.1 → 0.4.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 CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@yaag/runtime",
3
- "version": "0.2.1",
3
+ "version": "0.4.0",
4
4
  "publishConfig": {
5
5
  "access": "public"
6
6
  },
@@ -139,6 +139,7 @@ const CassetteRunSchema = Type.Object({
139
139
  ),
140
140
  ),
141
141
  programFile: Type.Optional(Type.String()),
142
+ programSource: Type.Optional(Type.String()),
142
143
  args: Type.Optional(Type.Unknown()),
143
144
  programHash: Type.Optional(Type.String()),
144
145
  });
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
+ }
@@ -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
- programFile: string | undefined,
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
- const bytes = await Bun.file(programFile).arrayBuffer();
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
- const replay = await loadReplay(options.replay);
87
- const resume = await loadResume(options.resume);
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(options.programFile, args);
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
- /** Loads a strict-replay source, which must be a settled Cassette (ADR-0031). */
173
- async function loadReplay(path: string | undefined): Promise<Cassette | null> {
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
- /** Loads a resume source, warning once when it came from a hard death (ADR-0031). */
181
- async function loadResume(path: string | undefined): Promise<Cassette | null> {
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 }