@abaigram/abai-stub 0.0.0-stage → 0.1.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (79) hide show
  1. package/LICENSE +21 -0
  2. package/README.md +22 -2
  3. package/dist/audit.d.ts +70 -0
  4. package/dist/audit.d.ts.map +1 -0
  5. package/dist/audit.js +166 -0
  6. package/dist/audit.js.map +1 -0
  7. package/dist/cells/fit.d.ts +6 -0
  8. package/dist/cells/fit.d.ts.map +1 -0
  9. package/dist/cells/fit.js +44 -0
  10. package/dist/cells/fit.js.map +1 -0
  11. package/dist/content.d.ts +17 -0
  12. package/dist/content.d.ts.map +1 -0
  13. package/dist/content.js +71 -0
  14. package/dist/content.js.map +1 -0
  15. package/dist/endpoint.d.ts +90 -0
  16. package/dist/endpoint.d.ts.map +1 -0
  17. package/dist/endpoint.js +789 -0
  18. package/dist/endpoint.js.map +1 -0
  19. package/dist/entry.d.ts +36 -0
  20. package/dist/entry.d.ts.map +1 -0
  21. package/dist/entry.js +409 -0
  22. package/dist/entry.js.map +1 -0
  23. package/dist/env.d.ts +38 -0
  24. package/dist/env.d.ts.map +1 -0
  25. package/dist/env.js +69 -0
  26. package/dist/env.js.map +1 -0
  27. package/dist/harness-binding.d.ts +223 -0
  28. package/dist/harness-binding.d.ts.map +1 -0
  29. package/dist/harness-binding.js +589 -0
  30. package/dist/harness-binding.js.map +1 -0
  31. package/dist/index.d.ts +17 -0
  32. package/dist/index.d.ts.map +1 -0
  33. package/dist/index.js +17 -0
  34. package/dist/index.js.map +1 -0
  35. package/dist/manifest/index.d.ts +3 -0
  36. package/dist/manifest/index.d.ts.map +1 -0
  37. package/dist/manifest/index.js +3 -0
  38. package/dist/manifest/index.js.map +1 -0
  39. package/dist/manifest/manifest.d.ts +172 -0
  40. package/dist/manifest/manifest.d.ts.map +1 -0
  41. package/dist/manifest/manifest.js +443 -0
  42. package/dist/manifest/manifest.js.map +1 -0
  43. package/dist/manifest/render.d.ts +6 -0
  44. package/dist/manifest/render.d.ts.map +1 -0
  45. package/dist/manifest/render.js +39 -0
  46. package/dist/manifest/render.js.map +1 -0
  47. package/dist/null-harness.d.ts +32 -0
  48. package/dist/null-harness.d.ts.map +1 -0
  49. package/dist/null-harness.js +162 -0
  50. package/dist/null-harness.js.map +1 -0
  51. package/dist/program.d.ts +250 -0
  52. package/dist/program.d.ts.map +1 -0
  53. package/dist/program.js +877 -0
  54. package/dist/program.js.map +1 -0
  55. package/dist/region.d.ts +72 -0
  56. package/dist/region.d.ts.map +1 -0
  57. package/dist/region.js +92 -0
  58. package/dist/region.js.map +1 -0
  59. package/dist/registry.d.ts +57 -0
  60. package/dist/registry.d.ts.map +1 -0
  61. package/dist/registry.js +101 -0
  62. package/dist/registry.js.map +1 -0
  63. package/dist/wire/descriptor.d.ts +38 -0
  64. package/dist/wire/descriptor.d.ts.map +1 -0
  65. package/dist/wire/descriptor.js +144 -0
  66. package/dist/wire/descriptor.js.map +1 -0
  67. package/dist/wire/framing.d.ts +11 -0
  68. package/dist/wire/framing.d.ts.map +1 -0
  69. package/dist/wire/framing.js +42 -0
  70. package/dist/wire/framing.js.map +1 -0
  71. package/dist/wire/index.d.ts +4 -0
  72. package/dist/wire/index.d.ts.map +1 -0
  73. package/dist/wire/index.js +4 -0
  74. package/dist/wire/index.js.map +1 -0
  75. package/dist/wire/protocol.d.ts +255 -0
  76. package/dist/wire/protocol.d.ts.map +1 -0
  77. package/dist/wire/protocol.js +15 -0
  78. package/dist/wire/protocol.js.map +1 -0
  79. package/package.json +36 -5
@@ -0,0 +1,877 @@
1
+ var __rewriteRelativeImportExtension = (this && this.__rewriteRelativeImportExtension) || function (path, preserveJsx) {
2
+ if (typeof path === "string" && /^\.\.?\//.test(path)) {
3
+ return path.replace(/\.(tsx)$|((?:\.d)?)((?:\.[^./]+?)?)\.([cm]?)ts$/i, function (m, tsx, d, ext, cm) {
4
+ return tsx ? preserveJsx ? ".jsx" : ".js" : d && (!ext || !cm) ? m : (d + ext + "." + cm.toLowerCase() + "js");
5
+ });
6
+ }
7
+ return path;
8
+ };
9
+ // createAbaigram: the program-facing API of the stub (abaigrammar sections 4
10
+ // and 5). The program declares its manifest in code, obtains cell, knob and
11
+ // event handles once, and then writes cells, emits events, serves
12
+ // capabilities and calls touchpoints. With no harness attached, every
13
+ // operation here costs what section 11.1 says: cell writes store, emits and
14
+ // safepoints load one flag, decisions and turns load the flag and count.
15
+ //
16
+ // Touchpoints are awaited promises (see the instantiation's deviations in
17
+ // registry.ts). The attached paths arrive with the harness binding; this
18
+ // file carries the detached behaviour and the seams the binding fills.
19
+ import { spawn } from "node:child_process";
20
+ import { existsSync } from "node:fs";
21
+ import { dirname, join } from "node:path";
22
+ import { fitValue } from "./cells/fit.js";
23
+ import { createContent, findRoot } from "./content.js";
24
+ import { Endpoint } from "./endpoint.js";
25
+ import { attachWaitMsFrom, ENV_HARNESS, ENV_HARNESS_BIN, ENV_LAUNCH_SOCKET, runDirFrom, selectHarness, selectMode, } from "./env.js";
26
+ import { DirectBinding } from "./harness-binding.js";
27
+ import { cutUtf8, durationMs, encodedBytes, fieldOf, LIBRARY_EVENTS, } from "./manifest/manifest.js";
28
+ import { attachNullHarness } from "./null-harness.js";
29
+ import { FLAG_ATTACHED, FLAG_PAUSED, FLAG_QUEUE, FLAG_WORK, LIBRARY_EVENT_BASE, Region } from "./region.js";
30
+ import { buildManifest, } from "./registry.js";
31
+ /** Thrown by `decide` when the applied default is `abort` (section 5.4). */
32
+ export class AbaigramAbort extends Error {
33
+ decision;
34
+ constructor(decision, cause) {
35
+ super(`decision ${decision} applied its default, abort (${cause})`);
36
+ this.name = "AbaigramAbort";
37
+ this.decision = decision;
38
+ }
39
+ }
40
+ export class Abaigram {
41
+ manifest;
42
+ mode;
43
+ modeFallback;
44
+ harnessSelection;
45
+ /** A problem with the environment's harness selection, reported by doctor, never thrown. */
46
+ harnessSelectionError;
47
+ content;
48
+ /**
49
+ * The sidecar endpoint, listening from startup when the selected mode is
50
+ * sidecar and a real harness is wanted (section 6.3). A check run under
51
+ * `ABAI_HARNESS` keeps its null harness in-process and opens no socket.
52
+ */
53
+ endpoint;
54
+ /** A launcher handed this program its connection (section 6.3): the full lifecycle is available to it. */
55
+ launched = false;
56
+ startedAt = new Date().toISOString();
57
+ /** The root given at construction, if any; otherwise the root is found lazily. */
58
+ explicitRoot;
59
+ /** @internal The region; tests and the binding reach it, programs do not. */
60
+ region;
61
+ now;
62
+ cellEntries = new Map();
63
+ eventEntries = new Map();
64
+ libraryEvents = new Map();
65
+ cellHandles = new Map();
66
+ eventHandles = new Map();
67
+ knobHandlers = new Map();
68
+ capabilityHandlers = new Map();
69
+ decisionIndex = new Map();
70
+ turnIndex = new Map();
71
+ queue = [];
72
+ harness;
73
+ binding;
74
+ nullHarness;
75
+ lastAttachError;
76
+ harnessFactory;
77
+ auditPathOverride;
78
+ report;
79
+ env;
80
+ stopHandler;
81
+ /** Who waits for a cooperative pause to lift, and the ref'd timer that keeps a paused program alive. */
82
+ resumeWaiters = [];
83
+ pauseKeepAlive;
84
+ /** Under launch: the launcher connected at least once, so nothing waits for it a second time. */
85
+ launcherSeen = false;
86
+ constructor(options, extras = {}) {
87
+ this.manifest = buildManifest(options);
88
+ this.now = extras.now ?? Date.now;
89
+ const env = extras.env ?? process.env;
90
+ this.env = env;
91
+ const selection = selectMode(this.manifest.modes, env);
92
+ this.mode = selection.mode;
93
+ this.modeFallback = selection.fallback;
94
+ const harness = selectHarness(env);
95
+ this.harnessSelection = harness.selection;
96
+ this.harnessSelectionError = harness.error;
97
+ this.stopHandler = options.lifecycle?.stop?.handler;
98
+ this.explicitRoot = extras.root;
99
+ this.harnessFactory = extras.harness;
100
+ this.auditPathOverride = extras.auditPath;
101
+ this.report =
102
+ extras.report ??
103
+ ((text) => {
104
+ process.stderr.write(`${text}\n`);
105
+ });
106
+ let scalars = 0;
107
+ let sized = 0;
108
+ const pending = [];
109
+ this.manifest.cells.forEach((decl, ordinal) => {
110
+ const scalar = decl.type !== "string" && decl.type !== "object" && decl.type !== "list";
111
+ pending.push([decl, ordinal, scalar ? scalars++ : sized++, scalar]);
112
+ });
113
+ this.manifest.decisions.forEach((decl, i) => {
114
+ this.decisionIndex.set(decl.name, i);
115
+ });
116
+ this.manifest.turns.forEach((decl, i) => {
117
+ this.turnIndex.set(decl.name, i);
118
+ });
119
+ this.region = new Region({
120
+ scalars,
121
+ sized,
122
+ counters: this.manifest.decisions.length + this.manifest.turns.length + 1,
123
+ });
124
+ if (this.modeFallback)
125
+ this.region.counters[this.region.counters.length - 1] += 1;
126
+ for (const [decl, ordinal, slot, scalar] of pending)
127
+ this.cellEntries.set(decl.name, this.makeEntry(decl, ordinal, slot, scalar));
128
+ this.manifest.events.forEach((decl, ordinal) => {
129
+ const fields = Object.entries(decl.payload).map(([key, field]) => {
130
+ const f = fieldOf(field);
131
+ return { key, type: f.type, max: f.max };
132
+ });
133
+ this.eventEntries.set(decl.name, { decl, ordinal, fields });
134
+ });
135
+ LIBRARY_EVENTS.forEach((name, i) => {
136
+ this.libraryEvents.set(name, LIBRARY_EVENT_BASE - i);
137
+ });
138
+ for (const [name, cell] of Object.entries(options.cells ?? {})) {
139
+ if (cell.access === "readwrite") {
140
+ this.cellEntries.get(name)?.set(cell.default);
141
+ if (cell.onChange)
142
+ this.knobHandlers.set(name, cell.onChange);
143
+ }
144
+ }
145
+ for (const [name, capability] of Object.entries(options.capabilities ?? {})) {
146
+ if (capability.handler)
147
+ this.capabilityHandlers.set(name, capability.handler);
148
+ }
149
+ // the root is found on first use, so a program that never attaches never touches the filesystem
150
+ this.content = createContent(this.manifest, () => extras.root ?? findRoot(this.manifest.name) ?? process.cwd());
151
+ // startup is one of the five places (section 4.3): a null harness attaches here
152
+ if (this.harnessSelection === "null" || this.harnessSelection === "exercise")
153
+ this.attachNull(this.harnessSelection);
154
+ // sidecar: the receiving end waits from startup (section 6.3); its failure to listen is a reason
155
+ // the next attach reports, never a throw into the program
156
+ const launchSocket = env[ENV_LAUNCH_SOCKET]?.trim();
157
+ if (this.mode === "sidecar" && this.harnessSelection === "real") {
158
+ // launched: the parent chose the socket and is waiting on it. Read once and removed, so a program
159
+ // that spawns a copy of itself never hands it this very socket
160
+ this.launched = launchSocket !== undefined && launchSocket !== "";
161
+ if (this.launched && env === process.env)
162
+ delete process.env[ENV_LAUNCH_SOCKET];
163
+ this.endpoint = new Endpoint(this, {
164
+ runDir: this.launched ? dirname(launchSocket) : runDirFrom(env),
165
+ ...(this.launched ? { socketPath: launchSocket } : {}),
166
+ });
167
+ this.endpoint.ready.catch((error) => {
168
+ this.lastAttachError = `the endpoint could not listen: ${error instanceof Error ? error.message : String(error)}`;
169
+ });
170
+ }
171
+ else if (launchSocket !== undefined && launchSocket !== "") {
172
+ // a launcher is waiting, but this program cannot take the connection up: it runs as itself, observed
173
+ // from outside only (section 5.7), and says why when asked
174
+ if (env === process.env)
175
+ delete process.env[ENV_LAUNCH_SOCKET];
176
+ this.lastAttachError =
177
+ this.harnessSelection === "real"
178
+ ? `launched, but ${this.manifest.name} declares no sidecar mode (declared: ${this.manifest.modes.join(", ")})`
179
+ : `launched under ${ENV_HARNESS}=${this.harnessSelection}`;
180
+ }
181
+ }
182
+ get name() {
183
+ return this.manifest.name;
184
+ }
185
+ get attached() {
186
+ return this.region.flags[FLAG_ATTACHED] !== 0;
187
+ }
188
+ /** Why the last attach did not happen, if it did not. */
189
+ get attachError() {
190
+ return this.lastAttachError;
191
+ }
192
+ // -----------------------------------------------------------------------
193
+ // Cells and knobs (section 5.2)
194
+ // -----------------------------------------------------------------------
195
+ /** The typed store and load of one cell, built once so the hot path is the store alone. */
196
+ makeEntry(decl, ordinal, slot, scalar) {
197
+ const { values, versions, sized } = this.region;
198
+ const entry = { decl, ordinal, slot, scalar, set: () => { }, get: () => undefined };
199
+ if (!scalar) {
200
+ entry.set = (value) => {
201
+ sized[slot] = fitValue(value, decl);
202
+ versions[ordinal] += 1;
203
+ };
204
+ entry.get = () => sized[slot];
205
+ return entry;
206
+ }
207
+ switch (decl.type) {
208
+ case "bool":
209
+ entry.set = (value) => {
210
+ values[slot] = value ? 1 : 0;
211
+ versions[ordinal] += 1;
212
+ };
213
+ entry.get = () => values[slot] !== 0;
214
+ break;
215
+ case "enum": {
216
+ const index = new Map((decl.enum ?? []).map((value, i) => [value, i]));
217
+ const names = decl.enum ?? [];
218
+ entry.enumIndex = index;
219
+ entry.set = (value) => {
220
+ const i = index.get(value);
221
+ if (i === undefined)
222
+ throw new Error(`${decl.name}: ${String(value)} is not a declared value`);
223
+ values[slot] = i;
224
+ versions[ordinal] += 1;
225
+ };
226
+ entry.get = () => names[values[slot]];
227
+ break;
228
+ }
229
+ default:
230
+ entry.set = (value) => {
231
+ values[slot] = value;
232
+ versions[ordinal] += 1;
233
+ };
234
+ entry.get = () => values[slot];
235
+ }
236
+ return entry;
237
+ }
238
+ entry(name) {
239
+ const entry = this.cellEntries.get(name);
240
+ if (!entry)
241
+ throw new Error(`${this.name} declares no cell ${name}`);
242
+ return entry;
243
+ }
244
+ /** A handle, created once per cell and cached. */
245
+ cell(name) {
246
+ const cached = this.cellHandles.get(name);
247
+ if (cached)
248
+ return cached;
249
+ const entry = this.entry(name);
250
+ const handle = { name, get: entry.get, set: entry.set };
251
+ this.cellHandles.set(name, handle);
252
+ return handle;
253
+ }
254
+ /** A knob handle; the cell must be `readwrite`. */
255
+ knob(name) {
256
+ const entry = this.entry(name);
257
+ if (entry.decl.access !== "readwrite")
258
+ throw new Error(`${name} is a read cell, not a knob`);
259
+ const handle = this.cell(name);
260
+ if (handle.reset === undefined) {
261
+ handle.reset = () => {
262
+ entry.set(entry.decl.default);
263
+ if (!entry.scalar)
264
+ this.emitLibrary("abai.knob.reset", { cell: name, value: entry.get() });
265
+ };
266
+ handle.onChange = (handler) => {
267
+ if (entry.scalar)
268
+ throw new Error(`${name} is a scalar knob; the program reads it, nothing is delivered`);
269
+ this.knobHandlers.set(name, handler);
270
+ };
271
+ }
272
+ return handle;
273
+ }
274
+ /** @internal The handler a sized knob delivers to, for the binding. */
275
+ knobHandler(name) {
276
+ return this.knobHandlers.get(name);
277
+ }
278
+ /** @internal Every cell's current value, keyed by name, for the binding. */
279
+ readCells() {
280
+ const cells = {};
281
+ for (const [name, entry] of this.cellEntries)
282
+ cells[name] = entry.get();
283
+ return cells;
284
+ }
285
+ /** @internal A cell's version counter. */
286
+ version(name) {
287
+ return this.region.versions[this.entry(name).ordinal];
288
+ }
289
+ // -----------------------------------------------------------------------
290
+ // Lifecycle (section 5.5): what the program itself can do
291
+ // -----------------------------------------------------------------------
292
+ /** A cooperative pause is in effect. */
293
+ get paused() {
294
+ return this.region.flags[FLAG_PAUSED] !== 0;
295
+ }
296
+ /**
297
+ * @internal A cooperative pause: the flag is set, and the program is held
298
+ * at its next touchpoint (this instantiation's deviation: a safepoint
299
+ * cannot block the loop the resume arrives on). A ref'd timer keeps a
300
+ * paused program alive, since nothing else of its own may be pending.
301
+ */
302
+ requestPause() {
303
+ if (this.paused)
304
+ return;
305
+ this.region.flags[FLAG_PAUSED] = 1;
306
+ this.region.flags[FLAG_WORK] = 1;
307
+ this.pauseKeepAlive = setInterval(() => { }, 2 ** 31 - 1);
308
+ }
309
+ /** @internal Lift the pause: whoever is held at a touchpoint goes on, their clocks starting now. */
310
+ resumeFromPause() {
311
+ if (!this.paused)
312
+ return;
313
+ this.region.flags[FLAG_PAUSED] = 0;
314
+ this.region.flags[FLAG_WORK] = this.queue.length > 0 ? 1 : 0;
315
+ if (this.pauseKeepAlive !== undefined)
316
+ clearInterval(this.pauseKeepAlive);
317
+ this.pauseKeepAlive = undefined;
318
+ for (const waiter of this.resumeWaiters.splice(0))
319
+ waiter();
320
+ }
321
+ /** Hold here while a cooperative pause is in effect; the agent is told what the pause holds (4.3). */
322
+ awaitResume(touchpoint) {
323
+ if (!this.paused)
324
+ return Promise.resolve();
325
+ this.emitLibrary("abai.pause.blocked", { touchpoint });
326
+ return new Promise((resolve) => {
327
+ this.resumeWaiters.push(resolve);
328
+ });
329
+ }
330
+ /** @internal What the stub knows of its own process, for `lifecycle status`. */
331
+ lifecycleStatus() {
332
+ return {
333
+ pid: process.pid,
334
+ name: this.manifest.name,
335
+ mode: this.mode,
336
+ cwd: process.cwd(),
337
+ startedAt: this.startedAt,
338
+ connections: this.endpoint?.connectionCount ?? (this.attached ? 1 : 0),
339
+ paused: this.paused,
340
+ launched: this.launched,
341
+ };
342
+ }
343
+ // -----------------------------------------------------------------------
344
+ // Events (section 5.1)
345
+ // -----------------------------------------------------------------------
346
+ /** A handle for a declared event, created once and cached; the name is checked here, never on the hot path. */
347
+ event(name) {
348
+ const cached = this.eventHandles.get(name);
349
+ if (cached)
350
+ return cached;
351
+ const entry = this.eventEntries.get(name);
352
+ if (!entry)
353
+ throw new Error(`${this.name} declares no event ${name}`);
354
+ const flags = this.region.flags;
355
+ const handle = {
356
+ name,
357
+ emit: (payload = {}) => {
358
+ if (flags[FLAG_ATTACHED] === 0)
359
+ return;
360
+ this.publish(entry, payload);
361
+ },
362
+ };
363
+ this.eventHandles.set(name, handle);
364
+ return handle;
365
+ }
366
+ /** Copy the declared fields, cut sized ones to their max, publish, and tell the harness. */
367
+ publish(entry, payload) {
368
+ let truncated = false;
369
+ const copy = {};
370
+ for (const field of entry.fields) {
371
+ const value = payload[field.key];
372
+ if (value === undefined)
373
+ continue;
374
+ switch (field.type) {
375
+ case "string": {
376
+ const text = String(value);
377
+ const cut = cutUtf8(text, field.max ?? 256);
378
+ if (cut !== text)
379
+ truncated = true;
380
+ copy[field.key] = cut;
381
+ break;
382
+ }
383
+ case "object":
384
+ case "list": {
385
+ const bytes = encodedBytes(value);
386
+ if (bytes > (field.max ?? 1024)) {
387
+ truncated = true;
388
+ copy[field.key] = { truncated: true, bytes };
389
+ }
390
+ else {
391
+ copy[field.key] = JSON.parse(JSON.stringify(value));
392
+ }
393
+ break;
394
+ }
395
+ default:
396
+ copy[field.key] = value;
397
+ }
398
+ }
399
+ const seq = this.region.emit(entry.ordinal, copy, truncated, this.now());
400
+ this.harness?.emitted(seq);
401
+ }
402
+ /** @internal A library event under the reserved prefix. */
403
+ emitLibrary(name, payload) {
404
+ if (this.region.flags[FLAG_ATTACHED] === 0)
405
+ return;
406
+ const ordinal = this.libraryEvents.get(name);
407
+ if (ordinal === undefined)
408
+ throw new Error(`${name} is not a library event`);
409
+ const seq = this.region.emit(ordinal, payload, false, this.now());
410
+ this.harness?.emitted(seq);
411
+ }
412
+ /** @internal The declared event for a ring slot's ordinal, or the library event's name. */
413
+ eventName(ordinal) {
414
+ if (ordinal < 0)
415
+ return LIBRARY_EVENTS[LIBRARY_EVENT_BASE - ordinal] ?? `abai.unknown.${ordinal}`;
416
+ return this.manifest.events[ordinal]?.name ?? `unknown.${ordinal}`;
417
+ }
418
+ // -----------------------------------------------------------------------
419
+ // Capabilities and safepoints (section 5.3)
420
+ // -----------------------------------------------------------------------
421
+ /** Register the handler of a declared capability (or replace one given at declaration). */
422
+ capability(name, handler) {
423
+ if (!this.manifest.capabilities.some((c) => c.name === name)) {
424
+ throw new Error(`${this.name} declares no capability ${name}`);
425
+ }
426
+ this.capabilityHandlers.set(name, handler);
427
+ }
428
+ /** @internal */
429
+ capabilityHandler(name) {
430
+ return this.capabilityHandlers.get(name);
431
+ }
432
+ /** @internal Queue work for the next safepoint; the binding uses it for safepoint dispatch. */
433
+ enqueue(work) {
434
+ this.queue.push(work);
435
+ this.region.flags[FLAG_QUEUE] = this.queue.length;
436
+ this.region.flags[FLAG_WORK] = 1;
437
+ }
438
+ /** One load of a flag when nothing is queued and no pause is in effect. */
439
+ safepoint() {
440
+ if (this.region.flags[FLAG_WORK] === 0)
441
+ return;
442
+ this.drain();
443
+ }
444
+ /** @internal Remove queued work that will not run (a timed-out dispatch). */
445
+ dequeue(work) {
446
+ const index = this.queue.indexOf(work);
447
+ if (index === -1)
448
+ return;
449
+ this.queue.splice(index, 1);
450
+ const flags = this.region.flags;
451
+ flags[FLAG_QUEUE] = this.queue.length;
452
+ if (this.queue.length === 0 && flags[FLAG_PAUSED] === 0)
453
+ flags[FLAG_WORK] = 0;
454
+ }
455
+ /** @internal Run everything queued, now, on this thread: a touchpoint holds the program at a safepoint. */
456
+ drain() {
457
+ const flags = this.region.flags;
458
+ while (this.queue.length > 0) {
459
+ const work = this.queue.shift();
460
+ flags[FLAG_QUEUE] = this.queue.length;
461
+ try {
462
+ work();
463
+ }
464
+ catch {
465
+ // a handler's failure is reported through its reply, never through the program's safepoint
466
+ }
467
+ }
468
+ flags[FLAG_WORK] = flags[FLAG_PAUSED] !== 0 ? 1 : 0;
469
+ }
470
+ // -----------------------------------------------------------------------
471
+ // Decisions and turns (sections 5.4, 4.3)
472
+ // -----------------------------------------------------------------------
473
+ applyDefault(decl, cause) {
474
+ if (decl.default === "abort")
475
+ throw new AbaigramAbort(decl.name, cause);
476
+ return decl.default;
477
+ }
478
+ /** Ask which way to go. With no harness attached the default applies at once and is counted. */
479
+ async decide(name, context = {}) {
480
+ const index = this.decisionIndex.get(name);
481
+ if (index === undefined)
482
+ throw new Error(`${this.name} declares no decision ${name}`);
483
+ const decl = this.manifest.decisions[index];
484
+ const harness = this.harness;
485
+ if (this.region.flags[FLAG_ATTACHED] === 0 || harness === undefined) {
486
+ this.region.counters[index] += 1;
487
+ return this.applyDefault(decl, "no_harness");
488
+ }
489
+ // a cooperative pause holds the decision here; its own clock starts when the pause lifts (4.3)
490
+ if (this.paused)
491
+ await this.awaitResume(name);
492
+ if (this.region.flags[FLAG_ATTACHED] === 0 || this.harness === undefined) {
493
+ this.region.counters[index] += 1;
494
+ return this.applyDefault(decl, "no_harness");
495
+ }
496
+ this.drain();
497
+ try {
498
+ return (await this.harness.decide(decl, context, this.now() + durationMs(decl.timeout)));
499
+ }
500
+ catch (error) {
501
+ return this.applyDefault(decl, error instanceof Error ? error.message : String(error));
502
+ }
503
+ }
504
+ /** Give the agent a turn. With no harness attached it returns at once and is counted. */
505
+ async turn(name) {
506
+ const index = this.turnIndex.get(name);
507
+ if (index === undefined)
508
+ throw new Error(`${this.name} declares no turn ${name}`);
509
+ const decl = this.manifest.turns[index];
510
+ const harness = this.harness;
511
+ if (this.region.flags[FLAG_ATTACHED] === 0 || harness === undefined) {
512
+ this.region.counters[this.manifest.decisions.length + index] += 1;
513
+ return;
514
+ }
515
+ if (this.paused)
516
+ await this.awaitResume(name);
517
+ if (this.region.flags[FLAG_ATTACHED] === 0 || this.harness === undefined) {
518
+ this.region.counters[this.manifest.decisions.length + index] += 1;
519
+ return;
520
+ }
521
+ this.drain();
522
+ try {
523
+ await this.harness.turn(decl, this.now() + durationMs(decl.timeout));
524
+ }
525
+ catch {
526
+ // a turn that fails is a turn that ended
527
+ }
528
+ this.drain();
529
+ }
530
+ /** The counters, by kind and name: defaulted decisions, skipped turns, and the mode fallback. */
531
+ counters() {
532
+ const snapshot = {};
533
+ this.manifest.decisions.forEach((decl, i) => {
534
+ snapshot[`decision:${decl.name}`] = this.region.counters[i];
535
+ });
536
+ this.manifest.turns.forEach((decl, i) => {
537
+ snapshot[`turn:${decl.name}`] = this.region.counters[this.manifest.decisions.length + i];
538
+ });
539
+ snapshot["abai.mode_fallback"] = this.region.counters[this.region.counters.length - 1];
540
+ return snapshot;
541
+ }
542
+ /** @internal Read and reset the counters, for the harness that attaches. */
543
+ takeCounters() {
544
+ const snapshot = this.counters();
545
+ this.region.counters.fill(0);
546
+ return snapshot;
547
+ }
548
+ // -----------------------------------------------------------------------
549
+ // Attaching (section 4.3)
550
+ // -----------------------------------------------------------------------
551
+ /** @internal The binding attaches a harness. */
552
+ setHarness(harness) {
553
+ this.harness = harness;
554
+ this.region.flags[FLAG_ATTACHED] = 1;
555
+ this.lastAttachError = undefined;
556
+ }
557
+ /** @internal The binding detaches it; a decision in flight takes its default with `reason`. */
558
+ clearHarness(reason) {
559
+ const harness = this.harness;
560
+ this.harness = undefined;
561
+ this.binding = undefined;
562
+ this.nullHarness = undefined;
563
+ this.region.flags[FLAG_ATTACHED] = 0;
564
+ this.lastAttachError = reason;
565
+ // a pause is the attached harness's: when the harness goes the program runs as itself again (6.3)
566
+ this.resumeFromPause();
567
+ harness?.detach(reason);
568
+ }
569
+ /** The direct binding, while an in-process harness is attached. */
570
+ get directBinding() {
571
+ return this.binding;
572
+ }
573
+ /** The null harness, while one is attached. */
574
+ get attachedNullHarness() {
575
+ return this.nullHarness;
576
+ }
577
+ createBinding(connection) {
578
+ const binding = new DirectBinding(this, {
579
+ ...(this.auditPathOverride === undefined ? {} : { auditPath: this.auditPathOverride }),
580
+ ...(connection === undefined ? {} : { connection }),
581
+ });
582
+ this.binding = binding;
583
+ this.setHarness(binding);
584
+ binding.flushCounters();
585
+ return binding;
586
+ }
587
+ /**
588
+ * @internal A front end of the library's own (the sidecar endpoint)
589
+ * attaches the program through a binding it then drives; the connection's
590
+ * id labels its acts in the audit. Refused while something else is
591
+ * attached: a second harness would take the program over silently.
592
+ */
593
+ bind(connection) {
594
+ if (this.attached) {
595
+ throw new Error(`${this.name} already has a harness attached; this connection cannot take it over`);
596
+ }
597
+ if (this.launched)
598
+ this.launcherSeen = true;
599
+ return this.createBinding(connection);
600
+ }
601
+ attachNull(form) {
602
+ const binding = this.createBinding();
603
+ this.nullHarness = attachNullHarness(binding, form);
604
+ if (form === "exercise") {
605
+ const harness = this.nullHarness;
606
+ process.once("exit", () => {
607
+ if (this.nullHarness === harness)
608
+ this.report(JSON.stringify(harness.report()));
609
+ });
610
+ }
611
+ }
612
+ /**
613
+ * Connect or spawn a harness (one of the five places). Resolves true when
614
+ * attached. The real harness arrives through the `harness` extra, or the
615
+ * harness package's dynamic import once it exists; until then the reason
616
+ * is recorded.
617
+ */
618
+ async attach() {
619
+ if (this.attached)
620
+ return true;
621
+ switch (this.harnessSelection) {
622
+ case "off":
623
+ this.lastAttachError = `${ENV_HARNESS}=off`;
624
+ return false;
625
+ case "null":
626
+ case "exercise":
627
+ this.attachNull(this.harnessSelection);
628
+ return true;
629
+ default:
630
+ if (this.harnessSelectionError !== undefined) {
631
+ this.lastAttachError = this.harnessSelectionError;
632
+ return false;
633
+ }
634
+ if (this.endpoint) {
635
+ // launched: the parent is already connecting; the program never spawns anything itself
636
+ return this.launched ? this.awaitLauncher(this.endpoint) : this.attachSidecar(this.endpoint);
637
+ }
638
+ try {
639
+ const factory = this.harnessFactory ??
640
+ (await installedHarness(this.env[ENV_HARNESS_MODULE] || this.manifest.harness?.path || "@abaigram/abai-embedded-harness"));
641
+ if (factory === undefined) {
642
+ this.lastAttachError =
643
+ "no harness is installed (the @abaigram/abai-embedded-harness package is not resolvable)";
644
+ return false;
645
+ }
646
+ const binding = this.createBinding();
647
+ await factory(binding);
648
+ return this.attached;
649
+ }
650
+ catch (error) {
651
+ const reason = error instanceof Error ? error.message : String(error);
652
+ this.binding?.detach(reason);
653
+ this.lastAttachError = reason;
654
+ return false;
655
+ }
656
+ }
657
+ }
658
+ /** Under launch: the launcher connects to the socket it chose; the program waits for it once, bounded. */
659
+ async awaitLauncher(endpoint) {
660
+ try {
661
+ await endpoint.ready;
662
+ }
663
+ catch (error) {
664
+ this.lastAttachError = `the endpoint could not listen: ${error instanceof Error ? error.message : String(error)}`;
665
+ return false;
666
+ }
667
+ if (this.attached)
668
+ return true;
669
+ if (this.launcherSeen) {
670
+ // it was here and went: a later attach place returns at once, as 4.3 says of every touchpoint after a detach
671
+ this.lastAttachError = this.lastAttachError ?? "the launcher disconnected";
672
+ return false;
673
+ }
674
+ const waitMs = attachWaitMsFrom(this.env);
675
+ if (await endpoint.waitForConnection(waitMs)) {
676
+ this.launcherSeen = true;
677
+ return true;
678
+ }
679
+ this.lastAttachError = `launched, but the launcher did not connect within ${waitMs} ms`;
680
+ return false;
681
+ }
682
+ /**
683
+ * Sidecar attach (section 6.3): a harness already connected to the
684
+ * endpoint means attached; otherwise the harness binary is spawned with
685
+ * the terminal inherited and given `ABAI_ATTACH_WAIT_MS` to connect. A
686
+ * child that exits first fails the attach at once with its code, so an
687
+ * unconfigured machine costs well under a second (section 4.3); a child
688
+ * that never connects is ended at the wait and the attach fails with the
689
+ * reason. The child is unref'd: a program whose work is done exits, and
690
+ * the closing socket tells the harness.
691
+ */
692
+ async attachSidecar(endpoint) {
693
+ try {
694
+ await endpoint.ready;
695
+ }
696
+ catch {
697
+ // spawnHarness reports it
698
+ }
699
+ // a harness that connected while the endpoint came up, or at any time before: attached already
700
+ if (this.attached)
701
+ return true;
702
+ const spawned = await this.spawnHarness(endpoint, "attach");
703
+ return spawned !== undefined;
704
+ }
705
+ /**
706
+ * Spawn the harness binary with a verb against this program's endpoint
707
+ * and wait for it to connect. Resolves to the child once attached, or to
708
+ * undefined with the reason in `attachError`: the endpoint could not
709
+ * listen, the child exited first (its code), it never connected within
710
+ * the wait (it is ended), or it could not be started at all.
711
+ */
712
+ async spawnHarness(endpoint, verb) {
713
+ try {
714
+ await endpoint.ready;
715
+ }
716
+ catch (error) {
717
+ this.lastAttachError = `the endpoint could not listen: ${error instanceof Error ? error.message : String(error)}`;
718
+ return undefined;
719
+ }
720
+ const waitMs = attachWaitMsFrom(this.env);
721
+ const binary = harnessBinary(this.env, this.content.root);
722
+ const args = [...binary.args, verb, "--target", endpoint.socketPath, "--run-dir", endpoint.runDir];
723
+ let child;
724
+ try {
725
+ child = spawn(binary.command, args, { stdio: "inherit", env: { ...process.env, ...this.env } });
726
+ }
727
+ catch (error) {
728
+ this.lastAttachError = `the harness binary could not be started: ${error instanceof Error ? error.message : String(error)}`;
729
+ return undefined;
730
+ }
731
+ child.unref();
732
+ // a ChildProcess with no error listener throws into the program; one stays for the child's whole life
733
+ child.on("error", () => { });
734
+ const outcome = await new Promise((resolve) => {
735
+ let settled = false;
736
+ const settle = (result) => {
737
+ if (settled)
738
+ return;
739
+ settled = true;
740
+ child.off("exit", onExit);
741
+ child.off("error", onError);
742
+ resolve(result);
743
+ };
744
+ const onExit = (code, signal) => settle(`exited:${code === null ? `signal ${signal ?? "unknown"}` : String(code)}`);
745
+ const onError = (error) => settle(`error:${error.code ?? error.message}`);
746
+ child.once("exit", onExit);
747
+ child.once("error", onError);
748
+ void endpoint.waitForConnection(waitMs).then((connected) => settle(connected ? "connected" : "timeout"));
749
+ });
750
+ if (outcome === "connected")
751
+ return child;
752
+ if (outcome === "timeout") {
753
+ // the one process this program started, by its own handle: nothing else is touched
754
+ child.kill();
755
+ this.lastAttachError = `the harness binary did not connect within ${waitMs} ms (${binary.command})`;
756
+ return undefined;
757
+ }
758
+ if (outcome.startsWith("exited:")) {
759
+ this.lastAttachError = `the harness binary exited with ${outcome.slice("exited:".length)} before connecting`;
760
+ return undefined;
761
+ }
762
+ this.lastAttachError = `the harness binary could not be started: ${outcome.slice("error:".length)} (${binary.command})`;
763
+ return undefined;
764
+ }
765
+ /**
766
+ * @internal `<prog> abai mcp` under sidecar: the binary's MCP server is
767
+ * spawned against this program's endpoint with the terminal inherited, so
768
+ * the MCP client on this process's stdin and stdout talks to it, and the
769
+ * program waits at its entry, a safepoint it holds open, until the client
770
+ * goes. Resolves to the child's exit code; -1 when nothing could be
771
+ * spawned (the reason is in `attachError`).
772
+ */
773
+ async serveMcpSidecar(endpoint) {
774
+ if (this.attached) {
775
+ this.lastAttachError = `${this.name} already has a harness attached`;
776
+ return -1;
777
+ }
778
+ const child = await this.spawnHarness(endpoint, "mcp");
779
+ if (child === undefined)
780
+ return -1;
781
+ // the program's whole purpose now is to wait for its server: the child holds the event loop open,
782
+ // where the spawn left it unref'd for the attach case, whose touchpoint holds the loop itself
783
+ child.ref();
784
+ // the MCP client spawned this program and ends it with a signal; the server behind it must go too
785
+ const forward = (signal) => {
786
+ if (child.exitCode === null && child.signalCode === null)
787
+ child.kill(signal);
788
+ };
789
+ const forwarded = ["SIGTERM", "SIGINT", "SIGHUP"];
790
+ for (const signal of forwarded)
791
+ process.on(signal, forward);
792
+ const binding = this.binding;
793
+ binding?.enterSafepoint();
794
+ try {
795
+ if (child.exitCode !== null)
796
+ return child.exitCode;
797
+ return await new Promise((resolve) => {
798
+ child.once("exit", (code) => resolve(code ?? 1));
799
+ });
800
+ }
801
+ finally {
802
+ for (const signal of forwarded)
803
+ process.off(signal, forward);
804
+ binding?.leaveSafepoint();
805
+ }
806
+ }
807
+ async touchpoint(kind) {
808
+ if (!(await this.attach()))
809
+ return;
810
+ if (this.paused)
811
+ await this.awaitResume(kind);
812
+ const binding = this.binding;
813
+ if (!binding)
814
+ return;
815
+ this.drain();
816
+ await binding.touchpoint(kind);
817
+ this.drain();
818
+ }
819
+ /** Run the install touchpoint; returns when the harness finishes, or at once when none can be attached. */
820
+ install() {
821
+ return this.touchpoint("install");
822
+ }
823
+ /** Open a session; returns when the harness ends it, or at once when none can be attached. */
824
+ session() {
825
+ return this.touchpoint("session");
826
+ }
827
+ }
828
+ /** Overrides where the in-process harness is imported from (a path or a package name). */
829
+ export const ENV_HARNESS_MODULE = "ABAI_HARNESS_MODULE";
830
+ /**
831
+ * The harness binary to spawn under sidecar: `ABAI_HARNESS_BIN` (a script
832
+ * path runs under this very node), else the program's own
833
+ * `node_modules/.bin/abai-embedded-harness`, else the name on PATH.
834
+ */
835
+ export function harnessBinary(env, root) {
836
+ const explicit = env[ENV_HARNESS_BIN]?.trim();
837
+ if (explicit) {
838
+ return /\.(?:[mc]?js|ts)$/.test(explicit)
839
+ ? { command: process.execPath, args: [explicit] }
840
+ : { command: explicit, args: [] };
841
+ }
842
+ const local = join(root, "node_modules", ".bin", "abai-embedded-harness");
843
+ if (existsSync(local))
844
+ return { command: local, args: [] };
845
+ return { command: "abai-embedded-harness", args: [] };
846
+ }
847
+ /** The harness package, found by dynamic import when a program installed it; undefined when it does not resolve. */
848
+ export async function importHarnessModule(env, manifest) {
849
+ const specifier = env[ENV_HARNESS_MODULE] || manifest.harness?.path || "@abaigram/abai-embedded-harness";
850
+ try {
851
+ return (await import(__rewriteRelativeImportExtension(specifier)));
852
+ }
853
+ catch {
854
+ return undefined;
855
+ }
856
+ }
857
+ /** The harness package's in-process entry. */
858
+ async function installedHarness(specifier) {
859
+ let module;
860
+ try {
861
+ module = (await import(__rewriteRelativeImportExtension(specifier)));
862
+ }
863
+ catch {
864
+ return undefined;
865
+ }
866
+ if (typeof module.attachEmbedded !== "function")
867
+ return undefined;
868
+ const attach = module.attachEmbedded;
869
+ return async (binding) => {
870
+ await attach(binding);
871
+ };
872
+ }
873
+ /** The program's one call: declarations in, the abaigram out. Throws when a declaration does not validate. */
874
+ export function createAbaigram(options, extras = {}) {
875
+ return new Abaigram(options, extras);
876
+ }
877
+ //# sourceMappingURL=program.js.map