@noetaris/harness 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/dist/index.js ADDED
@@ -0,0 +1,1141 @@
1
+ // src/harness/state-field.ts
2
+ function field(options) {
3
+ return { ...options };
4
+ }
5
+
6
+ // src/harness/ctx-markers.ts
7
+ var REQUIRED_TAG = "__noetaris_required__";
8
+ var RUNTIME_TAG = "__noetaris_runtime__";
9
+ function required() {
10
+ return { _tag: REQUIRED_TAG };
11
+ }
12
+ function runtime() {
13
+ return { _tag: RUNTIME_TAG };
14
+ }
15
+ function isRequiredMarker(value) {
16
+ return value !== null && typeof value === "object" && "_tag" in value && value._tag === REQUIRED_TAG;
17
+ }
18
+ function isRuntimeMarker(value) {
19
+ return value !== null && typeof value === "object" && "_tag" in value && value._tag === RUNTIME_TAG;
20
+ }
21
+
22
+ // src/loop/loop-dsl.ts
23
+ var InvalidLoopBuilderError = class extends Error {
24
+ constructor() {
25
+ super("argument is not a LoopBuilder instance \u2014 was it created by createLoopBuilder?");
26
+ this.name = "InvalidLoopBuilderError";
27
+ }
28
+ };
29
+ var _builderState = /* @__PURE__ */ Symbol("builderState");
30
+ function deepFreeze(obj) {
31
+ Object.freeze(obj);
32
+ for (const value of Object.values(obj)) {
33
+ if (value !== null && typeof value === "object" && !Object.isFrozen(value)) {
34
+ deepFreeze(value);
35
+ }
36
+ }
37
+ return obj;
38
+ }
39
+ function createLoopBuilder() {
40
+ const state = {
41
+ startCalled: false,
42
+ entryStep: void 0,
43
+ steps: [],
44
+ onError: void 0
45
+ };
46
+ const builder = {
47
+ start() {
48
+ state.startCalled = true;
49
+ return this;
50
+ },
51
+ step(name, options) {
52
+ const stepDef = {
53
+ name,
54
+ run: options.run,
55
+ // as: contravariance in state parameter
56
+ route: options.route,
57
+ // as: contravariance in state parameter
58
+ transitions: [],
59
+ next: void 0,
60
+ errorAware: options.optin === "$error"
61
+ };
62
+ state.steps.push(stepDef);
63
+ if (state.startCalled && state.entryStep === void 0) {
64
+ state.entryStep = name;
65
+ }
66
+ return this;
67
+ },
68
+ on(signal) {
69
+ if (state.steps.length === 0) {
70
+ return {
71
+ to: () => this,
72
+ end: () => this
73
+ };
74
+ }
75
+ const currentStep = state.steps.at(-1);
76
+ return {
77
+ to: (step) => {
78
+ currentStep?.transitions.push({
79
+ signal,
80
+ target: { kind: "step", name: step }
81
+ });
82
+ return this;
83
+ },
84
+ end: () => {
85
+ currentStep?.transitions.push({
86
+ signal,
87
+ target: { kind: "end" }
88
+ });
89
+ return this;
90
+ }
91
+ };
92
+ },
93
+ next(name) {
94
+ if (state.steps.length > 0) {
95
+ const currentStep = state.steps.at(-1);
96
+ if (currentStep !== void 0) {
97
+ currentStep.next = name;
98
+ }
99
+ }
100
+ return this;
101
+ },
102
+ onError(step) {
103
+ state.onError = step;
104
+ return this;
105
+ }
106
+ };
107
+ builder[_builderState] = state;
108
+ return builder;
109
+ }
110
+ function extractLoopDefinition(builder) {
111
+ if (builder === null || builder === void 0 || !(_builderState in builder)) {
112
+ throw new InvalidLoopBuilderError();
113
+ }
114
+ const state = builder[_builderState];
115
+ const immutableSteps = state.steps.map((step) => {
116
+ const transitions = Object.freeze([...step.transitions]);
117
+ for (const transition of transitions) {
118
+ Object.freeze(transition);
119
+ Object.freeze(transition.target);
120
+ }
121
+ const stepDef = {
122
+ name: step.name,
123
+ run: step.run,
124
+ route: step.route,
125
+ transitions,
126
+ next: step.next,
127
+ errorAware: step.errorAware
128
+ };
129
+ return Object.freeze(stepDef);
130
+ });
131
+ const definition = {
132
+ startCalled: state.startCalled,
133
+ entryStep: state.entryStep,
134
+ steps: Object.freeze(immutableSteps),
135
+ onError: state.onError
136
+ };
137
+ return deepFreeze(definition);
138
+ }
139
+
140
+ // src/loop/loop-validator.ts
141
+ var LoopValidationError = class extends Error {
142
+ violations;
143
+ constructor(violations) {
144
+ super(`loop validation failed:
145
+ ${violations.map((v) => ` - ${v}`).join("\n")}`);
146
+ this.name = "LoopValidationError";
147
+ this.violations = violations;
148
+ }
149
+ };
150
+ function validateLoop(def) {
151
+ const violations = [];
152
+ const names = new Set(def.steps.map((s) => s.name));
153
+ for (const step of def.steps) {
154
+ if (step.run === void 0 && step.route === void 0) {
155
+ violations.push(`step "${step.name}" must have at least one of run or route`);
156
+ }
157
+ }
158
+ for (const step of def.steps) {
159
+ if (step.route !== void 0 && step.transitions.length === 0) {
160
+ violations.push(`step "${step.name}" has route but no .on() transitions \u2014 every signal must be handled`);
161
+ }
162
+ }
163
+ for (const [i, step] of def.steps.entries()) {
164
+ if (step === void 0) continue;
165
+ if (step.route === void 0) {
166
+ const isLast = i === def.steps.length - 1;
167
+ if (isLast && step.next === void 0) {
168
+ violations.push(
169
+ `step "${step.name}" has no route, no explicit .next(), and no following step \u2014 no exit path`
170
+ );
171
+ }
172
+ }
173
+ }
174
+ for (const step of def.steps) {
175
+ if (step.route !== void 0 && step.next !== void 0) {
176
+ violations.push(`step "${step.name}" has both route and .next() \u2014 these are mutually exclusive`);
177
+ }
178
+ }
179
+ const seen = /* @__PURE__ */ new Set();
180
+ for (const step of def.steps) {
181
+ if (seen.has(step.name)) {
182
+ violations.push(`duplicate step name "${step.name}"`);
183
+ }
184
+ seen.add(step.name);
185
+ }
186
+ for (const step of def.steps) {
187
+ for (const transition of step.transitions) {
188
+ if (transition.target.kind === "step") {
189
+ if (!names.has(transition.target.name)) {
190
+ violations.push(
191
+ `step "${step.name}" has .on("${transition.signal}").to("${transition.target.name}") but step "${transition.target.name}" is not declared`
192
+ );
193
+ }
194
+ }
195
+ }
196
+ if (step.next !== void 0) {
197
+ if (!names.has(step.next)) {
198
+ violations.push(
199
+ `step "${step.name}" has .next("${step.next}") but step "${step.next}" is not declared`
200
+ );
201
+ }
202
+ }
203
+ }
204
+ if (def.onError !== void 0) {
205
+ if (!names.has(def.onError)) {
206
+ violations.push(`onError target "${def.onError}" is not a declared step`);
207
+ }
208
+ }
209
+ if (def.startCalled === false) {
210
+ violations.push(`loop is missing .start() \u2014 call l.start() before declaring steps`);
211
+ }
212
+ if (def.startCalled && def.entryStep === void 0) {
213
+ violations.push(`l.start() was called but no step was declared after it \u2014 declare at least one step`);
214
+ }
215
+ let hasExit = false;
216
+ for (const step of def.steps) {
217
+ for (const transition of step.transitions) {
218
+ if (transition.target.kind === "end") {
219
+ hasExit = true;
220
+ break;
221
+ }
222
+ }
223
+ if (hasExit) break;
224
+ }
225
+ if (!hasExit) {
226
+ violations.push(`loop has no exit \u2014 add at least one .on("<signal>").end()`);
227
+ }
228
+ if (def.entryStep !== void 0) {
229
+ const reachable = /* @__PURE__ */ new Set([def.entryStep]);
230
+ const queue = [def.entryStep];
231
+ if (def.onError !== void 0) {
232
+ reachable.add(def.onError);
233
+ if (!queue.includes(def.onError)) {
234
+ queue.push(def.onError);
235
+ }
236
+ }
237
+ while (queue.length > 0) {
238
+ const currentName = queue.shift();
239
+ const currentStep = def.steps.find((s) => s.name === currentName);
240
+ if (!currentStep) continue;
241
+ for (const transition of currentStep.transitions) {
242
+ if (transition.target.kind === "step") {
243
+ if (!reachable.has(transition.target.name)) {
244
+ reachable.add(transition.target.name);
245
+ queue.push(transition.target.name);
246
+ }
247
+ }
248
+ }
249
+ if (currentStep.route !== void 0) {
250
+ if (currentStep.next !== void 0 && !reachable.has(currentStep.next)) {
251
+ reachable.add(currentStep.next);
252
+ queue.push(currentStep.next);
253
+ }
254
+ } else {
255
+ if (currentStep.next !== void 0) {
256
+ if (!reachable.has(currentStep.next)) {
257
+ reachable.add(currentStep.next);
258
+ queue.push(currentStep.next);
259
+ }
260
+ } else {
261
+ const currentIndex = def.steps.findIndex((s) => s.name === currentName);
262
+ if (currentIndex !== -1 && currentIndex < def.steps.length - 1) {
263
+ const nextStep = def.steps[currentIndex + 1];
264
+ if (nextStep !== void 0) {
265
+ const nextStepName = nextStep.name;
266
+ if (!reachable.has(nextStepName)) {
267
+ reachable.add(nextStepName);
268
+ queue.push(nextStepName);
269
+ }
270
+ }
271
+ }
272
+ }
273
+ }
274
+ }
275
+ for (const step of def.steps) {
276
+ if (!reachable.has(step.name)) {
277
+ violations.push(`step "${step.name}" is unreachable from the entry step "${def.entryStep}"`);
278
+ }
279
+ }
280
+ }
281
+ for (const step of def.steps) {
282
+ if (step.route === void 0 && step.transitions.length > 0) {
283
+ violations.push(
284
+ `step "${step.name}" has .on() transitions but no route \u2014 .on() is only valid when route is set`
285
+ );
286
+ }
287
+ }
288
+ if (violations.length > 0) {
289
+ throw new LoopValidationError(violations);
290
+ }
291
+ }
292
+
293
+ // src/harness/harness-builder.ts
294
+ var _internals = /* @__PURE__ */ Symbol("_internals");
295
+ function getInternals(harness) {
296
+ if (harness === null || typeof harness !== "object" || !(_internals in harness)) {
297
+ throw new HarnessInternalsError();
298
+ }
299
+ return harness[_internals];
300
+ }
301
+ var HarnessInternalsError = class extends Error {
302
+ constructor() {
303
+ super("value is not a HarnessBuilder instance \u2014 was it created by createHarness?");
304
+ this.name = "HarnessInternalsError";
305
+ }
306
+ };
307
+ function createHarness() {
308
+ return (stateSchema) => {
309
+ const internals = {
310
+ stateSchema,
311
+ // as: S extends object; cast to internal schema representation
312
+ providers: Object.freeze([]),
313
+ loopDef: void 0,
314
+ _req: void 0,
315
+ _run: void 0,
316
+ _state: void 0
317
+ };
318
+ return createBuilderInstance(internals);
319
+ };
320
+ }
321
+ function createBuilderInstance(internals) {
322
+ const builder = {
323
+ // any: TypeScript cannot infer overloaded call signatures from object literal method definitions
324
+ provide(key, value) {
325
+ const newProviders = Object.freeze([
326
+ ...internals.providers,
327
+ { kind: "provide", key: String(key), value }
328
+ ]);
329
+ const newInternals = {
330
+ // any: same reason as return type annotation above
331
+ stateSchema: internals.stateSchema,
332
+ providers: newProviders,
333
+ loopDef: internals.loopDef,
334
+ _req: void 0,
335
+ _run: void 0,
336
+ _state: void 0
337
+ };
338
+ return createBuilderInstance(newInternals);
339
+ },
340
+ store(stores) {
341
+ const newProviders = Object.freeze([
342
+ ...internals.providers,
343
+ { kind: "store", key: "__store__", value: stores }
344
+ ]);
345
+ const newInternals = {
346
+ stateSchema: internals.stateSchema,
347
+ providers: newProviders,
348
+ loopDef: internals.loopDef,
349
+ _req: void 0,
350
+ _run: void 0,
351
+ _state: void 0
352
+ };
353
+ return createBuilderInstance(newInternals);
354
+ },
355
+ loop(builderFn) {
356
+ const lb = createLoopBuilder();
357
+ builderFn(lb);
358
+ const def = extractLoopDefinition(lb);
359
+ validateLoop(def);
360
+ const newInternals = {
361
+ stateSchema: internals.stateSchema,
362
+ providers: internals.providers,
363
+ loopDef: def,
364
+ _req: void 0,
365
+ _run: void 0,
366
+ _state: void 0
367
+ };
368
+ return createBuilderInstance(newInternals);
369
+ },
370
+ [_internals]: internals
371
+ };
372
+ return builder;
373
+ }
374
+
375
+ // src/agent/session-lifecycle.ts
376
+ import { randomUUID } from "crypto";
377
+
378
+ // src/agent/ctx-interrupt.ts
379
+ var InterruptPause = class extends Error {
380
+ interruptId;
381
+ prompt;
382
+ constructor(interruptId, prompt) {
383
+ super(`interrupt requested for id "${interruptId}"`);
384
+ this.name = "InterruptPause";
385
+ this.interruptId = interruptId;
386
+ this.prompt = prompt;
387
+ }
388
+ };
389
+ function isInterruptPause(error) {
390
+ return error instanceof InterruptPause;
391
+ }
392
+ function createInterruptFn(state, callCountRef) {
393
+ return async (prompt, id) => {
394
+ const isAutoId = id === void 0;
395
+ const effectiveId = isAutoId ? `$auto:${callCountRef.current}` : id;
396
+ if (isAutoId) {
397
+ callCountRef.current++;
398
+ }
399
+ const responses = state.$interruptResponses;
400
+ if (effectiveId in responses) {
401
+ return responses[effectiveId];
402
+ }
403
+ state.$interrupt = { interruptId: effectiveId, prompt };
404
+ throw new InterruptPause(effectiveId, prompt);
405
+ };
406
+ }
407
+
408
+ // src/agent/ctx-emit.ts
409
+ function createEmitFn(listeners) {
410
+ return (name, payload) => {
411
+ listeners[name]?.(payload);
412
+ };
413
+ }
414
+ function extractRunListeners(resources) {
415
+ const raw = resources["listeners"];
416
+ if (raw === null || typeof raw !== "object" || Array.isArray(raw)) {
417
+ return {};
418
+ }
419
+ return raw;
420
+ }
421
+
422
+ // src/loop/loop-executor.ts
423
+ var UnknownSignalError = class extends Error {
424
+ step;
425
+ signal;
426
+ constructor(step, signal) {
427
+ super(`step "${step}" emitted signal "${signal}" with no matching .on() transition`);
428
+ this.name = "UnknownSignalError";
429
+ this.step = step;
430
+ this.signal = signal;
431
+ }
432
+ };
433
+ var NoNextStepError = class extends Error {
434
+ step;
435
+ constructor(step) {
436
+ super(
437
+ `step "${step}" has no route, no explicit next, and is the last declared step \u2014 execution cannot continue`
438
+ );
439
+ this.name = "NoNextStepError";
440
+ this.step = step;
441
+ }
442
+ };
443
+ function buildImplicitNextMap(graph) {
444
+ const map = /* @__PURE__ */ new Map();
445
+ for (let i = 0; i < graph.steps.length; i++) {
446
+ const step = graph.steps[i];
447
+ const next = i + 1 < graph.steps.length ? graph.steps[i + 1].name : null;
448
+ map.set(step.name, next);
449
+ }
450
+ return map;
451
+ }
452
+ function applyUpdate(state, update, schema) {
453
+ for (const [key, value] of Object.entries(update)) {
454
+ if (key === "$error" || key === "$interrupt" || key === "$interruptResponses") continue;
455
+ const reducer = schema?.[key]?.reduce;
456
+ if (reducer !== void 0) {
457
+ state[key] = reducer(state[key], value);
458
+ } else {
459
+ state[key] = value;
460
+ }
461
+ }
462
+ }
463
+ async function runLoop(graph, state, ctx, schema, shouldStop, startCursor, callbacks) {
464
+ const { onBeforeStep, onAfterStep, onError, onComplete, onInterrupt } = callbacks ?? {};
465
+ const implicitNextMap = buildImplicitNextMap(graph);
466
+ if (!("$error" in state)) state.$error = null;
467
+ if (!("$interrupt" in state)) state.$interrupt = null;
468
+ if (!("$interruptResponses" in state)) state.$interruptResponses = {};
469
+ const callCountRef = { current: 0 };
470
+ ctx["interrupt"] = createInterruptFn(state, callCountRef);
471
+ ctx["emit"] = createEmitFn(callbacks?.listeners ?? {});
472
+ let cursor = startCursor ?? graph.entryStep;
473
+ while (true) {
474
+ if (shouldStop?.()) {
475
+ return { state, signal: null, cursor, paused: true };
476
+ }
477
+ onBeforeStep?.(cursor, { ...state });
478
+ const step = graph.steps.find((s) => s.name === cursor);
479
+ callCountRef.current = 0;
480
+ let runSucceeded = false;
481
+ if (step.run !== void 0) {
482
+ const hadResponses = Object.keys(state.$interruptResponses).length > 0;
483
+ try {
484
+ const update = await step.run(
485
+ state,
486
+ ctx
487
+ );
488
+ applyUpdate(state, update, schema);
489
+ if (hadResponses) {
490
+ state.$interrupt = null;
491
+ state.$interruptResponses = {};
492
+ }
493
+ runSucceeded = true;
494
+ } catch (e) {
495
+ if (isInterruptPause(e)) {
496
+ onInterrupt?.(e.prompt, e.interruptId);
497
+ return { state, signal: "$interrupt", cursor, paused: true };
498
+ }
499
+ state.$error = e instanceof Error ? e : new Error(String(e));
500
+ onError?.(state.$error, cursor);
501
+ }
502
+ }
503
+ if (runSucceeded) {
504
+ state.$error = null;
505
+ onAfterStep?.(cursor, state);
506
+ }
507
+ const callRoute = step.route !== void 0 && (runSucceeded || step.run === void 0 || step.errorAware);
508
+ if (callRoute) {
509
+ const routeFn = step.route;
510
+ let signal;
511
+ try {
512
+ signal = routeFn(state);
513
+ } catch (e) {
514
+ state.$error = e instanceof Error ? e : new Error(String(e));
515
+ return { state, signal: "$error", cursor, paused: true };
516
+ }
517
+ const transition = step.transitions.find((t) => t.signal === signal);
518
+ if (transition === void 0) {
519
+ throw new UnknownSignalError(cursor, signal);
520
+ }
521
+ if (transition.target.kind === "end") {
522
+ onComplete?.(state, signal);
523
+ return { state, signal, cursor: null, paused: false };
524
+ }
525
+ cursor = transition.target.name;
526
+ } else if (step.run !== void 0 && !runSucceeded) {
527
+ if (graph.onError !== void 0) {
528
+ cursor = graph.onError;
529
+ } else {
530
+ return { state, signal: "$error", cursor, paused: true };
531
+ }
532
+ } else {
533
+ const next = step.next ?? implicitNextMap.get(cursor) ?? null;
534
+ if (next === null) {
535
+ throw new NoNextStepError(cursor);
536
+ }
537
+ cursor = next;
538
+ }
539
+ }
540
+ }
541
+
542
+ // src/agent/session-store.ts
543
+ function storedSessionToPhase(loaded) {
544
+ if (loaded === null) {
545
+ return { phase: "fresh" };
546
+ }
547
+ if (loaded.phase === "paused") {
548
+ const step = loaded.step ?? "";
549
+ if (loaded.signal !== void 0) {
550
+ return { phase: "paused", step, signal: loaded.signal };
551
+ }
552
+ return { phase: "paused", step };
553
+ }
554
+ if (loaded.signal !== void 0) {
555
+ return { phase: "completed", signal: loaded.signal };
556
+ }
557
+ return { phase: "completed" };
558
+ }
559
+ function initializeState(stored, initialStateArg, schema) {
560
+ const argKeys = new Set(Object.keys(initialStateArg));
561
+ if (stored === null) {
562
+ return buildFreshState(initialStateArg, argKeys, schema);
563
+ }
564
+ return buildResumedState(stored, initialStateArg, argKeys, schema);
565
+ }
566
+ function buildFreshState(initialStateArg, argKeys, schema) {
567
+ const result = { ...initialStateArg };
568
+ if (schema === void 0 || Object.keys(schema).length === 0) {
569
+ return result;
570
+ }
571
+ for (const key of Object.keys(schema)) {
572
+ if (argKeys.has(key)) {
573
+ continue;
574
+ }
575
+ const field2 = schema[key];
576
+ if (field2 !== void 0 && field2.default !== void 0) {
577
+ result[key] = field2.default();
578
+ }
579
+ }
580
+ return result;
581
+ }
582
+ function buildResumedState(stored, initialStateArg, argKeys, schema) {
583
+ const result = { ...stored.finalState };
584
+ for (const key of argKeys) {
585
+ const fieldDef = schema?.[key];
586
+ if (fieldDef?.reduce !== void 0) {
587
+ result[key] = fieldDef.reduce(stored.finalState[key], initialStateArg[key]);
588
+ } else {
589
+ result[key] = initialStateArg[key];
590
+ }
591
+ }
592
+ if (schema !== void 0) {
593
+ for (const key of Object.keys(schema)) {
594
+ if (argKeys.has(key)) continue;
595
+ if (key in stored.finalState) continue;
596
+ const field2 = schema[key];
597
+ if (field2 !== void 0 && field2.default !== void 0) {
598
+ result[key] = field2.default();
599
+ }
600
+ }
601
+ }
602
+ return result;
603
+ }
604
+
605
+ // src/agent/concurrency-errors.ts
606
+ var SessionInFlightError = class extends Error {
607
+ sessionId;
608
+ constructor(sessionId) {
609
+ super(`session "${sessionId}" is already in-flight`);
610
+ this.name = "SessionInFlightError";
611
+ this.sessionId = sessionId;
612
+ }
613
+ };
614
+ var SessionPendingInterruptError = class extends Error {
615
+ sessionId;
616
+ constructor(sessionId) {
617
+ super(`session "${sessionId}" is paused on an interrupt \u2014 use agent.resume() instead of agent.run()`);
618
+ this.name = "SessionPendingInterruptError";
619
+ this.sessionId = sessionId;
620
+ }
621
+ };
622
+ var StoreLoadError = class extends Error {
623
+ cause;
624
+ constructor(cause) {
625
+ super("session store failed to load session");
626
+ this.name = "StoreLoadError";
627
+ this.cause = cause;
628
+ }
629
+ };
630
+
631
+ // src/agent/session-lifecycle.ts
632
+ function resolveSessionStore(storeEntries) {
633
+ let result = void 0;
634
+ for (const entry of storeEntries) {
635
+ if (entry.kind !== "store") continue;
636
+ const value = entry.value;
637
+ if (value === null || typeof value !== "object") continue;
638
+ const rec = value;
639
+ if (!("session" in rec)) continue;
640
+ const session = rec["session"];
641
+ if (session === null || typeof session !== "object") continue;
642
+ const s = session;
643
+ if (typeof s["load"] === "function" && typeof s["save"] === "function") {
644
+ result = session;
645
+ }
646
+ }
647
+ return result;
648
+ }
649
+ async function querySessionPhase(store, sessionId) {
650
+ if (store === void 0) {
651
+ return { phase: "fresh" };
652
+ }
653
+ const loaded = await store.load(sessionId);
654
+ return storedSessionToPhase(loaded);
655
+ }
656
+ async function runWithSession(store, sessionId, graph, initialStateArg, schema, ctx, options) {
657
+ if (store === void 0) {
658
+ const state2 = initializeState(null, initialStateArg, schema);
659
+ return runLoop(graph, state2, ctx, schema, options?.shouldStop, void 0, options);
660
+ }
661
+ let loaded;
662
+ try {
663
+ loaded = await store.load(sessionId);
664
+ } catch (error) {
665
+ const storeError = new StoreLoadError(error);
666
+ options?.onStoreError?.(storeError, "load");
667
+ const failState = initializeState(null, initialStateArg, schema);
668
+ failState["$error"] = storeError;
669
+ return { state: failState, signal: "$error", paused: false, cursor: null };
670
+ }
671
+ const runId = randomUUID();
672
+ const state = initializeState(loaded, initialStateArg, schema);
673
+ const initialStateSnapshot = { ...state };
674
+ const startedAt = (/* @__PURE__ */ new Date()).toISOString();
675
+ const result = await runLoop(graph, state, ctx, schema, options?.shouldStop, loaded?.step, options);
676
+ const settledAt = (/* @__PURE__ */ new Date()).toISOString();
677
+ try {
678
+ if (result.paused) {
679
+ const saved = {
680
+ runId,
681
+ sessionId,
682
+ startedAt,
683
+ settledAt,
684
+ phase: "paused",
685
+ initialState: initialStateSnapshot,
686
+ finalState: result.state,
687
+ step: result.cursor,
688
+ ...result.signal !== null ? { signal: result.signal } : {}
689
+ };
690
+ await store.save(sessionId, saved);
691
+ } else {
692
+ const saved = {
693
+ runId,
694
+ sessionId,
695
+ startedAt,
696
+ settledAt,
697
+ phase: "completed",
698
+ initialState: initialStateSnapshot,
699
+ finalState: result.state,
700
+ ...result.signal !== null ? { signal: result.signal } : {}
701
+ };
702
+ await store.save(sessionId, saved);
703
+ }
704
+ } catch (error) {
705
+ options?.onStoreError?.(error, "persist");
706
+ }
707
+ return result;
708
+ }
709
+
710
+ // src/agent/interrupt-resume.ts
711
+ var NoInterruptError = class extends Error {
712
+ constructor() {
713
+ super(
714
+ 'resume() called but the session is not paused on a matching interrupt \u2014 verify the session exists, its phase is "paused", and interruptId matches'
715
+ );
716
+ this.name = "NoInterruptError";
717
+ }
718
+ };
719
+ async function injectInterruptResponse(store, sessionId, interruptId, response) {
720
+ const loaded = await store.load(sessionId);
721
+ if (loaded === null) throw new NoInterruptError();
722
+ if (loaded.phase !== "paused") throw new NoInterruptError();
723
+ const interrupt = loaded.finalState.$interrupt;
724
+ if (interrupt === null) throw new NoInterruptError();
725
+ if (interrupt.interruptId !== interruptId) throw new NoInterruptError();
726
+ const existingResponses = loaded.finalState.$interruptResponses ?? {};
727
+ const newState = {
728
+ ...loaded.finalState,
729
+ $interrupt: null,
730
+ $interruptResponses: { ...existingResponses, [interruptId]: response }
731
+ };
732
+ const updated = {
733
+ runId: loaded.runId,
734
+ sessionId: loaded.sessionId,
735
+ startedAt: loaded.startedAt,
736
+ settledAt: loaded.settledAt,
737
+ phase: "paused",
738
+ initialState: loaded.initialState,
739
+ finalState: newState,
740
+ ...loaded.step !== void 0 ? { step: loaded.step } : {},
741
+ ...loaded.signal !== void 0 ? { signal: loaded.signal } : {}
742
+ };
743
+ await store.save(sessionId, updated);
744
+ }
745
+
746
+ // src/agent/run-handle.ts
747
+ function createRunHandle(sessionId, execution, stopFlag, stepRef, resumeFn) {
748
+ let settledOutcome = null;
749
+ const settled = execution.then(
750
+ (outcome) => {
751
+ settledOutcome = outcome;
752
+ stepRef.current = null;
753
+ return outcome;
754
+ },
755
+ (err) => {
756
+ stepRef.current = null;
757
+ throw err;
758
+ }
759
+ );
760
+ const handle = {
761
+ then(onfulfilled, onrejected) {
762
+ return settled.then(onfulfilled, onrejected);
763
+ },
764
+ stop() {
765
+ stopFlag.stopped = true;
766
+ },
767
+ resume(response, interruptId) {
768
+ if (settledOutcome === null && resumeFn === void 0) {
769
+ throw new Error("not implemented \u2014 requires F9");
770
+ }
771
+ if (settledOutcome === null) {
772
+ throw new NoInterruptError();
773
+ }
774
+ if (settledOutcome.signal !== "$interrupt") {
775
+ throw new NoInterruptError();
776
+ }
777
+ if (resumeFn === void 0) {
778
+ throw new NoInterruptError();
779
+ }
780
+ return resumeFn(response, interruptId);
781
+ },
782
+ get sessionId() {
783
+ return sessionId;
784
+ },
785
+ get currentStep() {
786
+ return stepRef.current;
787
+ }
788
+ };
789
+ return handle;
790
+ }
791
+
792
+ // src/agent/create-agent.ts
793
+ import { randomUUID as randomUUID2 } from "crypto";
794
+
795
+ // src/agent/event-callbacks.ts
796
+ function extractRunEvents(resources) {
797
+ const raw = resources["events"];
798
+ if (raw === null || typeof raw !== "object" || Array.isArray(raw)) {
799
+ return {};
800
+ }
801
+ return raw;
802
+ }
803
+
804
+ // src/agent/create-agent.ts
805
+ var MissingLoopError = class extends Error {
806
+ constructor() {
807
+ super("harness has no loop \u2014 call h.loop() before createAgent()");
808
+ this.name = "MissingLoopError";
809
+ }
810
+ };
811
+ var MissingSlotError = class extends Error {
812
+ key;
813
+ constructor(key) {
814
+ super(`required slot "${key}" was not provided in createAgent slots`);
815
+ this.name = "MissingSlotError";
816
+ this.key = key;
817
+ }
818
+ };
819
+ var RuntimeSlotInAgentError = class extends Error {
820
+ key;
821
+ constructor(key) {
822
+ super(`slot "${key}" is a runtime() slot \u2014 provide it in agent.run(), not createAgent()`);
823
+ this.name = "RuntimeSlotInAgentError";
824
+ this.key = key;
825
+ }
826
+ };
827
+ var UnknownSlotError = class extends Error {
828
+ key;
829
+ constructor(key) {
830
+ super(`slot "${key}" is not declared in the harness \u2014 remove it or add h.provide("${key}", required())`);
831
+ this.name = "UnknownSlotError";
832
+ this.key = key;
833
+ }
834
+ };
835
+ var MissingRuntimeSlotError = class extends Error {
836
+ key;
837
+ constructor(key) {
838
+ super(`runtime slot "${key}" was not provided in agent.run() resources`);
839
+ this.name = "MissingRuntimeSlotError";
840
+ this.key = key;
841
+ }
842
+ };
843
+ var RequiredSlotInRunError = class extends Error {
844
+ key;
845
+ constructor(key) {
846
+ super(`slot "${key}" is a required() slot \u2014 provide it in createAgent(), not agent.run()`);
847
+ this.name = "RequiredSlotInRunError";
848
+ this.key = key;
849
+ }
850
+ };
851
+ var UnknownRunSlotError = class extends Error {
852
+ key;
853
+ constructor(key) {
854
+ super(`slot "${key}" is not a runtime() slot \u2014 do not pass it in agent.run() resources`);
855
+ this.name = "UnknownRunSlotError";
856
+ this.key = key;
857
+ }
858
+ };
859
+ var _agentInternals = /* @__PURE__ */ Symbol("_agentInternals");
860
+ function createAgent(h, slots) {
861
+ const internals = getInternals(h);
862
+ if (internals.loopDef === void 0) {
863
+ throw new MissingLoopError();
864
+ }
865
+ const providerMap = /* @__PURE__ */ new Map();
866
+ for (const entry of internals.providers) {
867
+ if (entry.kind === "provide") {
868
+ providerMap.set(entry.key, entry);
869
+ }
870
+ }
871
+ for (const key of Object.keys(slots)) {
872
+ if (!providerMap.has(key)) {
873
+ throw new UnknownSlotError(key);
874
+ }
875
+ }
876
+ for (const key of Object.keys(slots)) {
877
+ const entry = providerMap.get(key);
878
+ if (entry && isRuntimeMarker(entry.value)) {
879
+ throw new RuntimeSlotInAgentError(key);
880
+ }
881
+ }
882
+ for (const entry of internals.providers) {
883
+ if (entry.kind === "provide" && isRequiredMarker(entry.value)) {
884
+ if (!(entry.key in slots)) {
885
+ throw new MissingSlotError(entry.key);
886
+ }
887
+ }
888
+ }
889
+ const resolvedProviders = /* @__PURE__ */ new Map();
890
+ const storeEntries = [];
891
+ const runtimeKeys = /* @__PURE__ */ new Set();
892
+ const requiredKeys = /* @__PURE__ */ new Set();
893
+ for (const entry of internals.providers) {
894
+ if (entry.kind === "store") {
895
+ storeEntries.push(entry);
896
+ } else if (entry.kind === "provide") {
897
+ if (isRequiredMarker(entry.value)) {
898
+ resolvedProviders.set(entry.key, slots[entry.key]);
899
+ requiredKeys.add(entry.key);
900
+ } else if (isRuntimeMarker(entry.value)) {
901
+ runtimeKeys.add(entry.key);
902
+ } else {
903
+ resolvedProviders.set(entry.key, entry.value);
904
+ }
905
+ }
906
+ }
907
+ const capturedStore = resolveSessionStore(storeEntries);
908
+ const agentInternals = {
909
+ resolvedProviders,
910
+ storeEntries,
911
+ loopDef: internals.loopDef,
912
+ // Guaranteed to be non-undefined by validation 1
913
+ stateSchema: internals.stateSchema,
914
+ runtimeKeys,
915
+ requiredKeys
916
+ };
917
+ const reservedRunKeys = /* @__PURE__ */ new Set(["sessionId", "signal", "events", "listeners"]);
918
+ const inFlightSessions = /* @__PURE__ */ new Set();
919
+ const interruptPendingSessions = /* @__PURE__ */ new Set();
920
+ const makeAgentResumeHandle = (resp, sId, iId) => {
921
+ const flag = { stopped: false };
922
+ const ref = { current: null };
923
+ const exec = (async () => {
924
+ await Promise.resolve();
925
+ try {
926
+ if (capturedStore === void 0) throw new NoInterruptError();
927
+ await injectInterruptResponse(capturedStore, sId, iId, resp);
928
+ const agentCtx = {
929
+ ...Object.fromEntries(agentInternals.resolvedProviders),
930
+ sessionId: sId
931
+ };
932
+ const r = await runWithSession(
933
+ capturedStore,
934
+ sId,
935
+ agentInternals.loopDef,
936
+ {},
937
+ agentInternals.stateSchema,
938
+ agentCtx,
939
+ {
940
+ shouldStop: () => flag.stopped,
941
+ onBeforeStep: (n) => {
942
+ ref.current = n;
943
+ }
944
+ // _state unused in cross-process resume — no events available
945
+ }
946
+ );
947
+ if (r.signal === "$interrupt") interruptPendingSessions.add(sId);
948
+ return { state: r.state, signal: r.signal };
949
+ } finally {
950
+ inFlightSessions.delete(sId);
951
+ }
952
+ })();
953
+ const resumeFn = (r, i) => {
954
+ if (inFlightSessions.has(sId)) throw new SessionInFlightError(sId);
955
+ interruptPendingSessions.delete(sId);
956
+ inFlightSessions.add(sId);
957
+ return makeAgentResumeHandle(r, sId, i);
958
+ };
959
+ return createRunHandle(sId, exec, flag, ref, resumeFn);
960
+ };
961
+ const agent = {
962
+ run: (initialState, resources) => {
963
+ for (const key of Object.keys(resources)) {
964
+ if (reservedRunKeys.has(key)) continue;
965
+ if (!agentInternals.runtimeKeys.has(key) && !agentInternals.requiredKeys.has(key)) {
966
+ throw new UnknownRunSlotError(key);
967
+ }
968
+ }
969
+ for (const key of Object.keys(resources)) {
970
+ if (reservedRunKeys.has(key)) continue;
971
+ if (agentInternals.requiredKeys.has(key)) {
972
+ throw new RequiredSlotInRunError(key);
973
+ }
974
+ }
975
+ for (const key of agentInternals.runtimeKeys) {
976
+ if (!(key in resources)) {
977
+ throw new MissingRuntimeSlotError(key);
978
+ }
979
+ }
980
+ const sessionId = typeof resources["sessionId"] === "string" ? resources["sessionId"] : randomUUID2();
981
+ if (inFlightSessions.has(sessionId)) throw new SessionInFlightError(sessionId);
982
+ if (interruptPendingSessions.has(sessionId)) throw new SessionPendingInterruptError(sessionId);
983
+ inFlightSessions.add(sessionId);
984
+ const stopFlag = { stopped: false };
985
+ const rawSignal = resources["signal"];
986
+ if (rawSignal instanceof AbortSignal) {
987
+ rawSignal.addEventListener("abort", () => {
988
+ stopFlag.stopped = true;
989
+ }, { once: true });
990
+ if (rawSignal.aborted) stopFlag.stopped = true;
991
+ }
992
+ const stepRef = { current: null };
993
+ const runtimeSlots = {};
994
+ for (const key of agentInternals.runtimeKeys) {
995
+ if (reservedRunKeys.has(key)) continue;
996
+ runtimeSlots[key] = resources[key];
997
+ }
998
+ const ctx = {
999
+ ...Object.fromEntries(agentInternals.resolvedProviders),
1000
+ ...runtimeSlots,
1001
+ sessionId
1002
+ };
1003
+ const events = extractRunEvents(resources);
1004
+ const listeners = extractRunListeners(resources);
1005
+ const options = {
1006
+ shouldStop: () => stopFlag.stopped,
1007
+ onBeforeStep: (name, state) => {
1008
+ stepRef.current = name;
1009
+ events.onBeforeStep?.(name, state);
1010
+ },
1011
+ // spread optional event callbacks only when defined (exactOptionalPropertyTypes requires omission, not undefined)
1012
+ ...events.onStoreError !== void 0 ? { onStoreError: events.onStoreError } : {},
1013
+ ...events.onAfterStep !== void 0 ? { onAfterStep: events.onAfterStep } : {},
1014
+ ...events.onError !== void 0 ? { onError: events.onError } : {},
1015
+ ...events.onComplete !== void 0 ? { onComplete: events.onComplete } : {},
1016
+ ...events.onInterrupt !== void 0 ? { onInterrupt: events.onInterrupt } : {},
1017
+ ...Object.keys(listeners).length > 0 ? { listeners } : {}
1018
+ };
1019
+ let lastResult = null;
1020
+ const execution = (async () => {
1021
+ await Promise.resolve();
1022
+ try {
1023
+ const r = await runWithSession(
1024
+ capturedStore,
1025
+ sessionId,
1026
+ agentInternals.loopDef,
1027
+ initialState,
1028
+ agentInternals.stateSchema,
1029
+ ctx,
1030
+ options
1031
+ );
1032
+ if (r.signal === "$interrupt") interruptPendingSessions.add(sessionId);
1033
+ lastResult = { state: r.state, cursor: r.cursor };
1034
+ return { state: r.state, signal: r.signal };
1035
+ } finally {
1036
+ inFlightSessions.delete(sessionId);
1037
+ }
1038
+ })();
1039
+ const buildResumeFn = () => {
1040
+ return (response, interruptId) => {
1041
+ if (inFlightSessions.has(sessionId)) throw new SessionInFlightError(sessionId);
1042
+ interruptPendingSessions.delete(sessionId);
1043
+ inFlightSessions.add(sessionId);
1044
+ const resumeStopFlag = { stopped: false };
1045
+ const resumeStepRef = { current: null };
1046
+ const resumeExecution = (async () => {
1047
+ await Promise.resolve();
1048
+ try {
1049
+ if (capturedStore !== void 0) {
1050
+ await injectInterruptResponse(capturedStore, sessionId, interruptId, response);
1051
+ const r2 = await runWithSession(
1052
+ capturedStore,
1053
+ sessionId,
1054
+ agentInternals.loopDef,
1055
+ {},
1056
+ agentInternals.stateSchema,
1057
+ ctx,
1058
+ {
1059
+ shouldStop: () => resumeStopFlag.stopped,
1060
+ onBeforeStep: (n, s) => {
1061
+ resumeStepRef.current = n;
1062
+ events.onBeforeStep?.(n, s);
1063
+ },
1064
+ // spread optional event callbacks only when defined (exactOptionalPropertyTypes requires omission, not undefined)
1065
+ ...events.onStoreError !== void 0 ? { onStoreError: events.onStoreError } : {},
1066
+ ...events.onAfterStep !== void 0 ? { onAfterStep: events.onAfterStep } : {},
1067
+ ...events.onError !== void 0 ? { onError: events.onError } : {},
1068
+ ...events.onComplete !== void 0 ? { onComplete: events.onComplete } : {},
1069
+ ...events.onInterrupt !== void 0 ? { onInterrupt: events.onInterrupt } : {},
1070
+ ...Object.keys(listeners).length > 0 ? { listeners } : {}
1071
+ }
1072
+ );
1073
+ if (r2.signal === "$interrupt") interruptPendingSessions.add(sessionId);
1074
+ lastResult = { state: r2.state, cursor: r2.cursor };
1075
+ return { state: r2.state, signal: r2.signal };
1076
+ }
1077
+ const prev = lastResult;
1078
+ if (prev === null) throw new NoInterruptError();
1079
+ const state = prev.state;
1080
+ const existing = state.$interruptResponses ?? {};
1081
+ state.$interruptResponses = { ...existing, [interruptId]: response };
1082
+ state.$interrupt = null;
1083
+ const cursor = prev.cursor ?? void 0;
1084
+ const r = await runLoop(
1085
+ agentInternals.loopDef,
1086
+ state,
1087
+ ctx,
1088
+ agentInternals.stateSchema,
1089
+ () => resumeStopFlag.stopped,
1090
+ cursor,
1091
+ {
1092
+ onBeforeStep: (n, s) => {
1093
+ resumeStepRef.current = n;
1094
+ events.onBeforeStep?.(n, s);
1095
+ },
1096
+ ...events.onAfterStep !== void 0 ? { onAfterStep: events.onAfterStep } : {},
1097
+ ...events.onError !== void 0 ? { onError: events.onError } : {},
1098
+ ...events.onComplete !== void 0 ? { onComplete: events.onComplete } : {},
1099
+ ...events.onInterrupt !== void 0 ? { onInterrupt: events.onInterrupt } : {},
1100
+ ...Object.keys(listeners).length > 0 ? { listeners } : {}
1101
+ }
1102
+ );
1103
+ if (r.signal === "$interrupt") interruptPendingSessions.add(sessionId);
1104
+ lastResult = { state: r.state, cursor: r.cursor };
1105
+ return { state: r.state, signal: r.signal };
1106
+ } finally {
1107
+ inFlightSessions.delete(sessionId);
1108
+ }
1109
+ })();
1110
+ return createRunHandle(sessionId, resumeExecution, resumeStopFlag, resumeStepRef, buildResumeFn());
1111
+ };
1112
+ };
1113
+ return createRunHandle(sessionId, execution, stopFlag, stepRef, buildResumeFn());
1114
+ },
1115
+ resume: (response, sessionId, interruptId) => {
1116
+ if (inFlightSessions.has(sessionId)) throw new SessionInFlightError(sessionId);
1117
+ interruptPendingSessions.delete(sessionId);
1118
+ inFlightSessions.add(sessionId);
1119
+ return makeAgentResumeHandle(response, sessionId, interruptId);
1120
+ },
1121
+ status: (sessionId) => querySessionPhase(capturedStore, sessionId),
1122
+ [_agentInternals]: agentInternals
1123
+ };
1124
+ return agent;
1125
+ }
1126
+ export {
1127
+ NoInterruptError,
1128
+ REQUIRED_TAG,
1129
+ RUNTIME_TAG,
1130
+ SessionInFlightError,
1131
+ SessionPendingInterruptError,
1132
+ StoreLoadError,
1133
+ createAgent,
1134
+ createHarness,
1135
+ field,
1136
+ isRequiredMarker,
1137
+ isRuntimeMarker,
1138
+ required,
1139
+ runtime
1140
+ };
1141
+ //# sourceMappingURL=index.js.map