@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.
- package/LICENSE +21 -0
- package/README.md +22 -2
- package/dist/audit.d.ts +70 -0
- package/dist/audit.d.ts.map +1 -0
- package/dist/audit.js +166 -0
- package/dist/audit.js.map +1 -0
- package/dist/cells/fit.d.ts +6 -0
- package/dist/cells/fit.d.ts.map +1 -0
- package/dist/cells/fit.js +44 -0
- package/dist/cells/fit.js.map +1 -0
- package/dist/content.d.ts +17 -0
- package/dist/content.d.ts.map +1 -0
- package/dist/content.js +71 -0
- package/dist/content.js.map +1 -0
- package/dist/endpoint.d.ts +90 -0
- package/dist/endpoint.d.ts.map +1 -0
- package/dist/endpoint.js +789 -0
- package/dist/endpoint.js.map +1 -0
- package/dist/entry.d.ts +36 -0
- package/dist/entry.d.ts.map +1 -0
- package/dist/entry.js +409 -0
- package/dist/entry.js.map +1 -0
- package/dist/env.d.ts +38 -0
- package/dist/env.d.ts.map +1 -0
- package/dist/env.js +69 -0
- package/dist/env.js.map +1 -0
- package/dist/harness-binding.d.ts +223 -0
- package/dist/harness-binding.d.ts.map +1 -0
- package/dist/harness-binding.js +589 -0
- package/dist/harness-binding.js.map +1 -0
- package/dist/index.d.ts +17 -0
- package/dist/index.d.ts.map +1 -0
- package/dist/index.js +17 -0
- package/dist/index.js.map +1 -0
- package/dist/manifest/index.d.ts +3 -0
- package/dist/manifest/index.d.ts.map +1 -0
- package/dist/manifest/index.js +3 -0
- package/dist/manifest/index.js.map +1 -0
- package/dist/manifest/manifest.d.ts +172 -0
- package/dist/manifest/manifest.d.ts.map +1 -0
- package/dist/manifest/manifest.js +443 -0
- package/dist/manifest/manifest.js.map +1 -0
- package/dist/manifest/render.d.ts +6 -0
- package/dist/manifest/render.d.ts.map +1 -0
- package/dist/manifest/render.js +39 -0
- package/dist/manifest/render.js.map +1 -0
- package/dist/null-harness.d.ts +32 -0
- package/dist/null-harness.d.ts.map +1 -0
- package/dist/null-harness.js +162 -0
- package/dist/null-harness.js.map +1 -0
- package/dist/program.d.ts +250 -0
- package/dist/program.d.ts.map +1 -0
- package/dist/program.js +877 -0
- package/dist/program.js.map +1 -0
- package/dist/region.d.ts +72 -0
- package/dist/region.d.ts.map +1 -0
- package/dist/region.js +92 -0
- package/dist/region.js.map +1 -0
- package/dist/registry.d.ts +57 -0
- package/dist/registry.d.ts.map +1 -0
- package/dist/registry.js +101 -0
- package/dist/registry.js.map +1 -0
- package/dist/wire/descriptor.d.ts +38 -0
- package/dist/wire/descriptor.d.ts.map +1 -0
- package/dist/wire/descriptor.js +144 -0
- package/dist/wire/descriptor.js.map +1 -0
- package/dist/wire/framing.d.ts +11 -0
- package/dist/wire/framing.d.ts.map +1 -0
- package/dist/wire/framing.js +42 -0
- package/dist/wire/framing.js.map +1 -0
- package/dist/wire/index.d.ts +4 -0
- package/dist/wire/index.d.ts.map +1 -0
- package/dist/wire/index.js +4 -0
- package/dist/wire/index.js.map +1 -0
- package/dist/wire/protocol.d.ts +255 -0
- package/dist/wire/protocol.d.ts.map +1 -0
- package/dist/wire/protocol.js +15 -0
- package/dist/wire/protocol.js.map +1 -0
- package/package.json +36 -5
package/dist/program.js
ADDED
|
@@ -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
|