adaptive-bitmask 1.0.0-rc.1
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 +454 -0
- package/dist/ai/index.d.mts +123 -0
- package/dist/ai/index.d.ts +123 -0
- package/dist/ai/index.js +1141 -0
- package/dist/ai/index.mjs +173 -0
- package/dist/chunk-ZWEXRT33.mjs +2402 -0
- package/dist/coordinator-Df48t6yJ.d.mts +521 -0
- package/dist/coordinator-Df48t6yJ.d.ts +521 -0
- package/dist/index.d.mts +431 -0
- package/dist/index.d.ts +431 -0
- package/dist/index.js +2484 -0
- package/dist/index.mjs +118 -0
- package/package.json +82 -0
package/dist/ai/index.js
ADDED
|
@@ -0,0 +1,1141 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
var __defProp = Object.defineProperty;
|
|
3
|
+
var __getOwnPropDesc = Object.getOwnPropertyDescriptor;
|
|
4
|
+
var __getOwnPropNames = Object.getOwnPropertyNames;
|
|
5
|
+
var __hasOwnProp = Object.prototype.hasOwnProperty;
|
|
6
|
+
var __export = (target, all) => {
|
|
7
|
+
for (var name in all)
|
|
8
|
+
__defProp(target, name, { get: all[name], enumerable: true });
|
|
9
|
+
};
|
|
10
|
+
var __copyProps = (to, from, except, desc) => {
|
|
11
|
+
if (from && typeof from === "object" || typeof from === "function") {
|
|
12
|
+
for (let key of __getOwnPropNames(from))
|
|
13
|
+
if (!__hasOwnProp.call(to, key) && key !== except)
|
|
14
|
+
__defProp(to, key, { get: () => from[key], enumerable: !(desc = __getOwnPropDesc(from, key)) || desc.enumerable });
|
|
15
|
+
}
|
|
16
|
+
return to;
|
|
17
|
+
};
|
|
18
|
+
var __toCommonJS = (mod) => __copyProps(__defProp({}, "__esModule", { value: true }), mod);
|
|
19
|
+
|
|
20
|
+
// src/ai/index.ts
|
|
21
|
+
var ai_exports = {};
|
|
22
|
+
__export(ai_exports, {
|
|
23
|
+
CoordinationSession: () => CoordinationSession,
|
|
24
|
+
createCoordinationMiddleware: () => createCoordinationMiddleware,
|
|
25
|
+
createCoordinationTools: () => createCoordinationTools
|
|
26
|
+
});
|
|
27
|
+
module.exports = __toCommonJS(ai_exports);
|
|
28
|
+
|
|
29
|
+
// src/transports/websocket.ts
|
|
30
|
+
var import_ws = require("ws");
|
|
31
|
+
|
|
32
|
+
// src/message.ts
|
|
33
|
+
var MESSAGE_SIZE_BYTES = 24;
|
|
34
|
+
var UINT32_MAX = 4294967295;
|
|
35
|
+
var UINT64_MAX = (1n << 64n) - 1n;
|
|
36
|
+
var BitmaskMessage = class _BitmaskMessage {
|
|
37
|
+
mask;
|
|
38
|
+
agentId;
|
|
39
|
+
timestampMs;
|
|
40
|
+
schemaVersion;
|
|
41
|
+
constructor(data) {
|
|
42
|
+
this._assertValid(data);
|
|
43
|
+
this.mask = data.mask;
|
|
44
|
+
this.agentId = data.agentId;
|
|
45
|
+
this.timestampMs = data.timestampMs;
|
|
46
|
+
this.schemaVersion = data.schemaVersion;
|
|
47
|
+
}
|
|
48
|
+
/** Create a message with current timestamp. */
|
|
49
|
+
static now(mask, agentId, schemaVersion) {
|
|
50
|
+
return new _BitmaskMessage({
|
|
51
|
+
mask,
|
|
52
|
+
agentId,
|
|
53
|
+
timestampMs: Date.now(),
|
|
54
|
+
schemaVersion
|
|
55
|
+
});
|
|
56
|
+
}
|
|
57
|
+
/** Wire size in bytes. */
|
|
58
|
+
get sizeBytes() {
|
|
59
|
+
return MESSAGE_SIZE_BYTES;
|
|
60
|
+
}
|
|
61
|
+
/**
|
|
62
|
+
* Serialize to 24-byte ArrayBuffer (little-endian).
|
|
63
|
+
*
|
|
64
|
+
* This is the canonical wire format. gRPC/WebSocket transports
|
|
65
|
+
* should send these bytes directly.
|
|
66
|
+
*/
|
|
67
|
+
serialize() {
|
|
68
|
+
const buf = new ArrayBuffer(MESSAGE_SIZE_BYTES);
|
|
69
|
+
const view = new DataView(buf);
|
|
70
|
+
view.setBigUint64(0, this.mask, true);
|
|
71
|
+
view.setUint32(8, this.agentId, true);
|
|
72
|
+
view.setBigInt64(12, BigInt(this.timestampMs), true);
|
|
73
|
+
view.setUint32(20, this.schemaVersion, true);
|
|
74
|
+
return buf;
|
|
75
|
+
}
|
|
76
|
+
/** Serialize to Uint8Array. */
|
|
77
|
+
toBytes() {
|
|
78
|
+
return new Uint8Array(this.serialize());
|
|
79
|
+
}
|
|
80
|
+
/**
|
|
81
|
+
* Deserialize from ArrayBuffer or Uint8Array.
|
|
82
|
+
* Validates length before parsing.
|
|
83
|
+
*/
|
|
84
|
+
static deserialize(data) {
|
|
85
|
+
const buf = data instanceof Uint8Array ? data.buffer.slice(data.byteOffset, data.byteOffset + data.byteLength) : data;
|
|
86
|
+
if (buf.byteLength !== MESSAGE_SIZE_BYTES) {
|
|
87
|
+
throw new Error(
|
|
88
|
+
`Invalid message: expected exactly ${MESSAGE_SIZE_BYTES} bytes, got ${buf.byteLength}`
|
|
89
|
+
);
|
|
90
|
+
}
|
|
91
|
+
const view = new DataView(buf);
|
|
92
|
+
return new _BitmaskMessage({
|
|
93
|
+
mask: view.getBigUint64(0, true),
|
|
94
|
+
agentId: view.getUint32(8, true),
|
|
95
|
+
timestampMs: Number(view.getBigInt64(12, true)),
|
|
96
|
+
schemaVersion: view.getUint32(20, true)
|
|
97
|
+
});
|
|
98
|
+
}
|
|
99
|
+
/**
|
|
100
|
+
* JSON-equivalent size for comparison.
|
|
101
|
+
* Useful for demonstrating compression ratio.
|
|
102
|
+
*/
|
|
103
|
+
get jsonSize() {
|
|
104
|
+
return JSON.stringify({
|
|
105
|
+
mask: this.mask.toString(),
|
|
106
|
+
agentId: this.agentId,
|
|
107
|
+
timestampMs: this.timestampMs,
|
|
108
|
+
schemaVersion: this.schemaVersion
|
|
109
|
+
}).length;
|
|
110
|
+
}
|
|
111
|
+
/** Compression ratio vs JSON encoding. */
|
|
112
|
+
get compressionVsJson() {
|
|
113
|
+
return this.jsonSize / MESSAGE_SIZE_BYTES;
|
|
114
|
+
}
|
|
115
|
+
/** Human-readable string for debugging. */
|
|
116
|
+
toString() {
|
|
117
|
+
return `BitmaskMessage(agent=${this.agentId}, v=${this.schemaVersion}, bits=${this.mask.toString(2).padStart(64, "0")}, t=${this.timestampMs})`;
|
|
118
|
+
}
|
|
119
|
+
_assertValid(data) {
|
|
120
|
+
if (typeof data.mask !== "bigint") {
|
|
121
|
+
throw new TypeError(`mask must be bigint, got ${typeof data.mask}`);
|
|
122
|
+
}
|
|
123
|
+
if (data.mask < 0n || data.mask > UINT64_MAX) {
|
|
124
|
+
throw new RangeError(`mask out of uint64 range: ${data.mask.toString()}`);
|
|
125
|
+
}
|
|
126
|
+
this._assertUint32("agentId", data.agentId);
|
|
127
|
+
this._assertUint32("schemaVersion", data.schemaVersion);
|
|
128
|
+
if (!Number.isSafeInteger(data.timestampMs)) {
|
|
129
|
+
throw new RangeError(
|
|
130
|
+
`timestampMs must be a safe integer, got ${data.timestampMs}`
|
|
131
|
+
);
|
|
132
|
+
}
|
|
133
|
+
}
|
|
134
|
+
_assertUint32(field, value) {
|
|
135
|
+
if (!Number.isInteger(value) || value < 0 || value > UINT32_MAX) {
|
|
136
|
+
throw new RangeError(
|
|
137
|
+
`${field} must be an integer in [0, ${UINT32_MAX}], got ${value}`
|
|
138
|
+
);
|
|
139
|
+
}
|
|
140
|
+
}
|
|
141
|
+
};
|
|
142
|
+
|
|
143
|
+
// src/bitmask.ts
|
|
144
|
+
var BITMASK_WIDTH = 64;
|
|
145
|
+
var EMERGENCY_RANGE = [56, 63];
|
|
146
|
+
var HIGH_FREQ_RANGE = [0, 47];
|
|
147
|
+
var MED_FREQ_RANGE = [48, 55];
|
|
148
|
+
var SINGLE_BIT_TO_POSITION = new Map(
|
|
149
|
+
Array.from({ length: BITMASK_WIDTH }, (_, i) => [1n << BigInt(i), i])
|
|
150
|
+
);
|
|
151
|
+
function activeBits(mask) {
|
|
152
|
+
const bits = [];
|
|
153
|
+
forEachSetBit(mask, (bit) => bits.push(bit));
|
|
154
|
+
return bits;
|
|
155
|
+
}
|
|
156
|
+
function forEachSetBit(mask, fn) {
|
|
157
|
+
if (mask < 0n) {
|
|
158
|
+
throw new RangeError("Bitmask must be non-negative");
|
|
159
|
+
}
|
|
160
|
+
let m = mask;
|
|
161
|
+
while (m !== 0n) {
|
|
162
|
+
const leastSignificantBit = m & -m;
|
|
163
|
+
const position = SINGLE_BIT_TO_POSITION.get(leastSignificantBit);
|
|
164
|
+
if (position === void 0) {
|
|
165
|
+
throw new Error(`Invalid 64-bit mask: ${mask.toString()}`);
|
|
166
|
+
}
|
|
167
|
+
fn(position);
|
|
168
|
+
m ^= leastSignificantBit;
|
|
169
|
+
}
|
|
170
|
+
}
|
|
171
|
+
function hasEmergency(mask) {
|
|
172
|
+
const emergencyMask = 0xFFn << 56n;
|
|
173
|
+
return (mask & emergencyMask) !== 0n;
|
|
174
|
+
}
|
|
175
|
+
function encode(features, schema, options = {}) {
|
|
176
|
+
let mask = 0n;
|
|
177
|
+
let mapped = 0;
|
|
178
|
+
let unmapped = 0;
|
|
179
|
+
const unknownFeatures = [];
|
|
180
|
+
for (const feature of features) {
|
|
181
|
+
const bit = schema.get(feature);
|
|
182
|
+
if (bit !== void 0) {
|
|
183
|
+
mask |= 1n << BigInt(bit);
|
|
184
|
+
mapped++;
|
|
185
|
+
} else {
|
|
186
|
+
unmapped++;
|
|
187
|
+
unknownFeatures.push(feature);
|
|
188
|
+
}
|
|
189
|
+
}
|
|
190
|
+
if (options.throwOnUnknownFeatures && unknownFeatures.length > 0) {
|
|
191
|
+
const uniqueUnknown = [...new Set(unknownFeatures)];
|
|
192
|
+
throw new Error(
|
|
193
|
+
`Unknown features (${uniqueUnknown.length}): ${uniqueUnknown.join(", ")}`
|
|
194
|
+
);
|
|
195
|
+
}
|
|
196
|
+
return { mask, mapped, unmapped };
|
|
197
|
+
}
|
|
198
|
+
function decode(mask, reverseSchema) {
|
|
199
|
+
const features = [];
|
|
200
|
+
for (let i = 0; i < BITMASK_WIDTH; i++) {
|
|
201
|
+
if (mask & 1n << BigInt(i)) {
|
|
202
|
+
const feats = reverseSchema.get(i);
|
|
203
|
+
if (feats) {
|
|
204
|
+
features.push(...feats);
|
|
205
|
+
}
|
|
206
|
+
}
|
|
207
|
+
}
|
|
208
|
+
return features;
|
|
209
|
+
}
|
|
210
|
+
|
|
211
|
+
// src/schema.ts
|
|
212
|
+
var MAX_EMERGENCY_FEATURES = EMERGENCY_RANGE[1] - EMERGENCY_RANGE[0] + 1;
|
|
213
|
+
var MAX_REGULAR_FEATURES = HIGH_FREQ_RANGE[1] - HIGH_FREQ_RANGE[0] + 1 + (MED_FREQ_RANGE[1] - MED_FREQ_RANGE[0] + 1);
|
|
214
|
+
var FNV_OFFSET_64 = 0xcbf29ce484222325n;
|
|
215
|
+
var FNV_PRIME_64 = 0x100000001b3n;
|
|
216
|
+
var MASK_64 = (1n << 64n) - 1n;
|
|
217
|
+
var SchemaManager = class {
|
|
218
|
+
_version = 0;
|
|
219
|
+
_featureToBit = /* @__PURE__ */ new Map();
|
|
220
|
+
_bitToFeatures = /* @__PURE__ */ new Map();
|
|
221
|
+
_activationCounts = /* @__PURE__ */ new Map();
|
|
222
|
+
_totalActivations = 0;
|
|
223
|
+
_emergencyFeatures;
|
|
224
|
+
_emergencyPrefix;
|
|
225
|
+
_maxFeatures;
|
|
226
|
+
constructor(config = {}) {
|
|
227
|
+
this._maxFeatures = config.maxFeatures ?? BITMASK_WIDTH;
|
|
228
|
+
this._emergencyPrefix = config.emergencyPrefix ?? "EMERGENCY_";
|
|
229
|
+
this._emergencyFeatures = new Set(config.emergencyFeatures ?? []);
|
|
230
|
+
}
|
|
231
|
+
/** Current schema version. */
|
|
232
|
+
get version() {
|
|
233
|
+
return this._version;
|
|
234
|
+
}
|
|
235
|
+
/** Read-only view of feature → bit mapping. */
|
|
236
|
+
get featureToBit() {
|
|
237
|
+
return this._featureToBit;
|
|
238
|
+
}
|
|
239
|
+
/** Read-only view of bit → features mapping. */
|
|
240
|
+
get bitToFeatures() {
|
|
241
|
+
return this._bitToFeatures;
|
|
242
|
+
}
|
|
243
|
+
/** Number of actively mapped features. */
|
|
244
|
+
get activeFeatureCount() {
|
|
245
|
+
return this._featureToBit.size;
|
|
246
|
+
}
|
|
247
|
+
/** Deterministic fingerprint of current schema mapping and version. */
|
|
248
|
+
get fingerprint() {
|
|
249
|
+
return this._computeFingerprint(
|
|
250
|
+
this._version,
|
|
251
|
+
this._featureToBit,
|
|
252
|
+
this._emergencyPrefix,
|
|
253
|
+
this._emergencyFeatures
|
|
254
|
+
);
|
|
255
|
+
}
|
|
256
|
+
/** Check if a feature is an emergency feature. */
|
|
257
|
+
isEmergency(feature) {
|
|
258
|
+
return this._emergencyFeatures.has(feature) || feature.startsWith(this._emergencyPrefix);
|
|
259
|
+
}
|
|
260
|
+
/**
|
|
261
|
+
* Register a feature in the schema.
|
|
262
|
+
* Emergency features are pinned to bits 56-63.
|
|
263
|
+
* Regular features fill bits 0-55 in order.
|
|
264
|
+
*
|
|
265
|
+
* Returns the assigned bit position, or -1 if schema is full.
|
|
266
|
+
*/
|
|
267
|
+
register(feature) {
|
|
268
|
+
const existing = this._featureToBit.get(feature);
|
|
269
|
+
if (existing !== void 0) return existing;
|
|
270
|
+
if (this._featureToBit.size >= this._maxFeatures) return -1;
|
|
271
|
+
if (this.isEmergency(feature)) {
|
|
272
|
+
return this._registerEmergency(feature);
|
|
273
|
+
}
|
|
274
|
+
return this._registerRegular(feature);
|
|
275
|
+
}
|
|
276
|
+
/**
|
|
277
|
+
* Register multiple features at once.
|
|
278
|
+
* Returns a map of feature → assigned bit position.
|
|
279
|
+
*/
|
|
280
|
+
registerAll(features) {
|
|
281
|
+
const result = /* @__PURE__ */ new Map();
|
|
282
|
+
for (const feature of features) {
|
|
283
|
+
result.set(feature, this.register(feature));
|
|
284
|
+
}
|
|
285
|
+
return result;
|
|
286
|
+
}
|
|
287
|
+
/**
|
|
288
|
+
* Record feature activations for frequency tracking.
|
|
289
|
+
* Call this every coordination round with the observed features.
|
|
290
|
+
*/
|
|
291
|
+
recordActivations(features) {
|
|
292
|
+
for (const feature of features) {
|
|
293
|
+
const count = this._activationCounts.get(feature) ?? 0;
|
|
294
|
+
this._activationCounts.set(feature, count + 1);
|
|
295
|
+
this._totalActivations++;
|
|
296
|
+
}
|
|
297
|
+
}
|
|
298
|
+
/**
|
|
299
|
+
* Frequency-based pruning (Section 3.3).
|
|
300
|
+
*
|
|
301
|
+
* Sorts features by activation frequency, retains:
|
|
302
|
+
* - All emergency features in bits 56-63 (never pruned)
|
|
303
|
+
* - Top 48 regular features in bits 0-47 (high-frequency)
|
|
304
|
+
* - Next 8 regular features in bits 48-55 (medium-frequency)
|
|
305
|
+
*
|
|
306
|
+
* Increments schema version.
|
|
307
|
+
*/
|
|
308
|
+
prune() {
|
|
309
|
+
const knownFeatures = this._collectKnownFeatures();
|
|
310
|
+
const emergencyList = knownFeatures.filter((feature) => this.isEmergency(feature)).sort((a, b) => this._compareByFrequencyThenName(a, b));
|
|
311
|
+
const regularList = knownFeatures.filter((feature) => !this.isEmergency(feature)).sort((a, b) => this._compareByFrequencyThenName(a, b));
|
|
312
|
+
const newFeatureToBit = /* @__PURE__ */ new Map();
|
|
313
|
+
const newBitToFeatures = /* @__PURE__ */ new Map();
|
|
314
|
+
const [emergStart] = EMERGENCY_RANGE;
|
|
315
|
+
for (let i = 0; i < Math.min(emergencyList.length, MAX_EMERGENCY_FEATURES); i++) {
|
|
316
|
+
const bit = emergStart + i;
|
|
317
|
+
const feature = emergencyList[i];
|
|
318
|
+
newFeatureToBit.set(feature, bit);
|
|
319
|
+
newBitToFeatures.set(bit, [feature]);
|
|
320
|
+
}
|
|
321
|
+
const [highStart] = HIGH_FREQ_RANGE;
|
|
322
|
+
const highCount = HIGH_FREQ_RANGE[1] - HIGH_FREQ_RANGE[0] + 1;
|
|
323
|
+
for (let i = 0; i < Math.min(regularList.length, highCount); i++) {
|
|
324
|
+
const bit = highStart + i;
|
|
325
|
+
const feature = regularList[i];
|
|
326
|
+
newFeatureToBit.set(feature, bit);
|
|
327
|
+
newBitToFeatures.set(bit, [feature]);
|
|
328
|
+
}
|
|
329
|
+
const [medStart] = MED_FREQ_RANGE;
|
|
330
|
+
for (let i = highCount; i < Math.min(regularList.length, MAX_REGULAR_FEATURES); i++) {
|
|
331
|
+
const bit = medStart + (i - highCount);
|
|
332
|
+
const feature = regularList[i];
|
|
333
|
+
newFeatureToBit.set(feature, bit);
|
|
334
|
+
newBitToFeatures.set(bit, [feature]);
|
|
335
|
+
}
|
|
336
|
+
const excludedFeatures = [
|
|
337
|
+
...regularList.slice(MAX_REGULAR_FEATURES),
|
|
338
|
+
...emergencyList.slice(MAX_EMERGENCY_FEATURES)
|
|
339
|
+
];
|
|
340
|
+
const mappingChanged = !this._mapsEqual(this._featureToBit, newFeatureToBit);
|
|
341
|
+
if (mappingChanged) {
|
|
342
|
+
this._featureToBit = newFeatureToBit;
|
|
343
|
+
this._bitToFeatures = newBitToFeatures;
|
|
344
|
+
this._version++;
|
|
345
|
+
}
|
|
346
|
+
return {
|
|
347
|
+
pruned: excludedFeatures.length,
|
|
348
|
+
retained: this._featureToBit.size,
|
|
349
|
+
version: this._version,
|
|
350
|
+
excludedFeatures
|
|
351
|
+
};
|
|
352
|
+
}
|
|
353
|
+
/** Get a serializable snapshot of the current schema state. */
|
|
354
|
+
snapshot() {
|
|
355
|
+
let emergencyCount = 0;
|
|
356
|
+
for (const [feat] of this._featureToBit) {
|
|
357
|
+
if (this.isEmergency(feat)) emergencyCount++;
|
|
358
|
+
}
|
|
359
|
+
return {
|
|
360
|
+
version: this._version,
|
|
361
|
+
featureToBit: new Map(this._featureToBit),
|
|
362
|
+
bitToFeatures: new Map(
|
|
363
|
+
[...this._bitToFeatures].map(([k, v]) => [k, [...v]])
|
|
364
|
+
),
|
|
365
|
+
totalTracked: this._activationCounts.size,
|
|
366
|
+
activeFeatures: this._featureToBit.size,
|
|
367
|
+
emergencyCount
|
|
368
|
+
};
|
|
369
|
+
}
|
|
370
|
+
/**
|
|
371
|
+
* Export canonical schema state for distribution.
|
|
372
|
+
* Entries are sorted by bit (then feature) for deterministic serialization.
|
|
373
|
+
*/
|
|
374
|
+
exportSchema() {
|
|
375
|
+
const entries = [...this._featureToBit.entries()].sort((a, b) => {
|
|
376
|
+
if (a[1] !== b[1]) return a[1] - b[1];
|
|
377
|
+
return a[0] < b[0] ? -1 : a[0] > b[0] ? 1 : 0;
|
|
378
|
+
}).map(([feature, bit]) => [feature, bit]);
|
|
379
|
+
return {
|
|
380
|
+
version: this._version,
|
|
381
|
+
entries,
|
|
382
|
+
emergencyPrefix: this._emergencyPrefix,
|
|
383
|
+
emergencyFeatures: [...this._emergencyFeatures].sort(
|
|
384
|
+
(a, b) => a < b ? -1 : a > b ? 1 : 0
|
|
385
|
+
),
|
|
386
|
+
fingerprint: this.fingerprint
|
|
387
|
+
};
|
|
388
|
+
}
|
|
389
|
+
/**
|
|
390
|
+
* Import an exported schema state.
|
|
391
|
+
* Replaces active mappings and resets activation counters.
|
|
392
|
+
*/
|
|
393
|
+
importSchema(schema) {
|
|
394
|
+
this._assertExportedSchema(schema);
|
|
395
|
+
const newFeatureToBit = /* @__PURE__ */ new Map();
|
|
396
|
+
const newBitToFeatures = /* @__PURE__ */ new Map();
|
|
397
|
+
for (const [feature, bit] of schema.entries) {
|
|
398
|
+
newFeatureToBit.set(feature, bit);
|
|
399
|
+
newBitToFeatures.set(bit, [feature]);
|
|
400
|
+
}
|
|
401
|
+
const expectedFingerprint = this._computeFingerprint(
|
|
402
|
+
schema.version,
|
|
403
|
+
newFeatureToBit,
|
|
404
|
+
schema.emergencyPrefix,
|
|
405
|
+
new Set(schema.emergencyFeatures)
|
|
406
|
+
);
|
|
407
|
+
if (schema.fingerprint !== expectedFingerprint) {
|
|
408
|
+
throw new Error(
|
|
409
|
+
`Schema fingerprint mismatch: expected ${expectedFingerprint}, got ${schema.fingerprint}`
|
|
410
|
+
);
|
|
411
|
+
}
|
|
412
|
+
this._featureToBit = newFeatureToBit;
|
|
413
|
+
this._bitToFeatures = newBitToFeatures;
|
|
414
|
+
this._emergencyPrefix = schema.emergencyPrefix;
|
|
415
|
+
this._emergencyFeatures = new Set(schema.emergencyFeatures);
|
|
416
|
+
this._activationCounts.clear();
|
|
417
|
+
this._totalActivations = 0;
|
|
418
|
+
this._version = schema.version;
|
|
419
|
+
}
|
|
420
|
+
/** Get activation frequency for a feature. */
|
|
421
|
+
getFrequency(feature) {
|
|
422
|
+
return this._activationCounts.get(feature) ?? 0;
|
|
423
|
+
}
|
|
424
|
+
/** Get all features ranked by activation frequency. */
|
|
425
|
+
getRankedFeatures() {
|
|
426
|
+
const ranked = [];
|
|
427
|
+
for (const [feature, count] of this._activationCounts) {
|
|
428
|
+
ranked.push({
|
|
429
|
+
feature,
|
|
430
|
+
count,
|
|
431
|
+
bit: this._featureToBit.get(feature)
|
|
432
|
+
});
|
|
433
|
+
}
|
|
434
|
+
ranked.sort((a, b) => b.count - a.count);
|
|
435
|
+
return ranked;
|
|
436
|
+
}
|
|
437
|
+
/** Reset all activation counts (but keep schema mappings). */
|
|
438
|
+
resetCounts() {
|
|
439
|
+
this._activationCounts.clear();
|
|
440
|
+
this._totalActivations = 0;
|
|
441
|
+
}
|
|
442
|
+
/**
|
|
443
|
+
* Collision probability for a specific feature:
|
|
444
|
+
* P(collision) = 1 - (1 - 1/64)^(m - 1)
|
|
445
|
+
* where m is active feature count.
|
|
446
|
+
*/
|
|
447
|
+
get theoreticalCollisionRate() {
|
|
448
|
+
const m = this._featureToBit.size;
|
|
449
|
+
if (m <= 1) return 0;
|
|
450
|
+
return 1 - Math.pow(1 - 1 / BITMASK_WIDTH, m - 1);
|
|
451
|
+
}
|
|
452
|
+
/**
|
|
453
|
+
* Expected excluded feature count under uniform assignment:
|
|
454
|
+
* E[excluded] = m - 64 * (1 - (1 - 1/64)^m)
|
|
455
|
+
*/
|
|
456
|
+
expectedExcludedFeatures(featureCount = this._featureToBit.size) {
|
|
457
|
+
if (!Number.isInteger(featureCount) || featureCount < 0) {
|
|
458
|
+
throw new RangeError(
|
|
459
|
+
`featureCount must be a non-negative integer, got ${featureCount}`
|
|
460
|
+
);
|
|
461
|
+
}
|
|
462
|
+
if (featureCount === 0) return 0;
|
|
463
|
+
return featureCount - BITMASK_WIDTH * (1 - Math.pow(1 - 1 / BITMASK_WIDTH, featureCount));
|
|
464
|
+
}
|
|
465
|
+
// ── Private ──
|
|
466
|
+
_registerEmergency(feature) {
|
|
467
|
+
const [start, end] = EMERGENCY_RANGE;
|
|
468
|
+
for (let bit = start; bit <= end; bit++) {
|
|
469
|
+
if (!this._bitToFeatures.has(bit)) {
|
|
470
|
+
this._featureToBit.set(feature, bit);
|
|
471
|
+
this._bitToFeatures.set(bit, [feature]);
|
|
472
|
+
this._version++;
|
|
473
|
+
return bit;
|
|
474
|
+
}
|
|
475
|
+
}
|
|
476
|
+
return -1;
|
|
477
|
+
}
|
|
478
|
+
_registerRegular(feature) {
|
|
479
|
+
const [highStart, highEnd] = HIGH_FREQ_RANGE;
|
|
480
|
+
for (let bit = highStart; bit <= highEnd; bit++) {
|
|
481
|
+
if (!this._bitToFeatures.has(bit)) {
|
|
482
|
+
this._featureToBit.set(feature, bit);
|
|
483
|
+
this._bitToFeatures.set(bit, [feature]);
|
|
484
|
+
this._version++;
|
|
485
|
+
return bit;
|
|
486
|
+
}
|
|
487
|
+
}
|
|
488
|
+
const [medStart, medEnd] = MED_FREQ_RANGE;
|
|
489
|
+
for (let bit = medStart; bit <= medEnd; bit++) {
|
|
490
|
+
if (!this._bitToFeatures.has(bit)) {
|
|
491
|
+
this._featureToBit.set(feature, bit);
|
|
492
|
+
this._bitToFeatures.set(bit, [feature]);
|
|
493
|
+
this._version++;
|
|
494
|
+
return bit;
|
|
495
|
+
}
|
|
496
|
+
}
|
|
497
|
+
return -1;
|
|
498
|
+
}
|
|
499
|
+
_collectKnownFeatures() {
|
|
500
|
+
const all = /* @__PURE__ */ new Set();
|
|
501
|
+
for (const feature of this._activationCounts.keys()) all.add(feature);
|
|
502
|
+
for (const feature of this._featureToBit.keys()) all.add(feature);
|
|
503
|
+
for (const feature of this._emergencyFeatures) all.add(feature);
|
|
504
|
+
return [...all];
|
|
505
|
+
}
|
|
506
|
+
_compareByFrequencyThenName(a, b) {
|
|
507
|
+
const countA = this._activationCounts.get(a) ?? 0;
|
|
508
|
+
const countB = this._activationCounts.get(b) ?? 0;
|
|
509
|
+
if (countA !== countB) return countB - countA;
|
|
510
|
+
if (a < b) return -1;
|
|
511
|
+
if (a > b) return 1;
|
|
512
|
+
return 0;
|
|
513
|
+
}
|
|
514
|
+
_mapsEqual(a, b) {
|
|
515
|
+
if (a.size !== b.size) return false;
|
|
516
|
+
for (const [feature, bit] of a) {
|
|
517
|
+
if (b.get(feature) !== bit) return false;
|
|
518
|
+
}
|
|
519
|
+
return true;
|
|
520
|
+
}
|
|
521
|
+
_assertExportedSchema(schema) {
|
|
522
|
+
if (!Number.isInteger(schema.version) || schema.version < 0) {
|
|
523
|
+
throw new RangeError(`Schema version must be a non-negative integer, got ${schema.version}`);
|
|
524
|
+
}
|
|
525
|
+
if (typeof schema.fingerprint !== "string" || schema.fingerprint.length === 0) {
|
|
526
|
+
throw new TypeError("Schema fingerprint must be a non-empty string");
|
|
527
|
+
}
|
|
528
|
+
const seenFeatures = /* @__PURE__ */ new Set();
|
|
529
|
+
const seenBits = /* @__PURE__ */ new Set();
|
|
530
|
+
for (const [feature, bit] of schema.entries) {
|
|
531
|
+
if (!feature || typeof feature !== "string") {
|
|
532
|
+
throw new TypeError(`Invalid feature name in schema export: ${String(feature)}`);
|
|
533
|
+
}
|
|
534
|
+
if (!Number.isInteger(bit) || bit < 0 || bit >= BITMASK_WIDTH) {
|
|
535
|
+
throw new RangeError(`Invalid bit position in schema export: ${bit}`);
|
|
536
|
+
}
|
|
537
|
+
if (seenFeatures.has(feature)) {
|
|
538
|
+
throw new Error(`Duplicate feature in schema export: ${feature}`);
|
|
539
|
+
}
|
|
540
|
+
if (seenBits.has(bit)) {
|
|
541
|
+
throw new Error(`Duplicate bit in schema export: ${bit}`);
|
|
542
|
+
}
|
|
543
|
+
seenFeatures.add(feature);
|
|
544
|
+
seenBits.add(bit);
|
|
545
|
+
}
|
|
546
|
+
}
|
|
547
|
+
_computeFingerprint(version, mapping, emergencyPrefix, emergencyFeatures) {
|
|
548
|
+
const canonicalEntries = [...mapping.entries()].sort((a, b) => {
|
|
549
|
+
if (a[1] !== b[1]) return a[1] - b[1];
|
|
550
|
+
if (a[0] < b[0]) return -1;
|
|
551
|
+
if (a[0] > b[0]) return 1;
|
|
552
|
+
return 0;
|
|
553
|
+
}).map(([feature, bit]) => `${bit}:${feature}`).join("|");
|
|
554
|
+
const canonicalEmergency = [...emergencyFeatures].sort((a, b) => a < b ? -1 : a > b ? 1 : 0).join("|");
|
|
555
|
+
const canonical = `v=${version};ep=${emergencyPrefix};ef=${canonicalEmergency};m=${canonicalEntries}`;
|
|
556
|
+
let hash = FNV_OFFSET_64;
|
|
557
|
+
for (const char of canonical) {
|
|
558
|
+
hash ^= BigInt(char.codePointAt(0) ?? 0);
|
|
559
|
+
hash = hash * FNV_PRIME_64 & MASK_64;
|
|
560
|
+
}
|
|
561
|
+
return hash.toString(16).padStart(16, "0");
|
|
562
|
+
}
|
|
563
|
+
};
|
|
564
|
+
|
|
565
|
+
// src/coordinator.ts
|
|
566
|
+
var Coordinator = class {
|
|
567
|
+
_buffer = [];
|
|
568
|
+
_seenAgents = /* @__PURE__ */ new Set();
|
|
569
|
+
_schemaVersion;
|
|
570
|
+
_deadlineMs;
|
|
571
|
+
_staleMessagePolicy;
|
|
572
|
+
_onTelemetry;
|
|
573
|
+
_roundStartTime = 0;
|
|
574
|
+
_aggregationCount = 0;
|
|
575
|
+
_droppedStaleMessages = 0;
|
|
576
|
+
constructor(config = {}) {
|
|
577
|
+
this._deadlineMs = config.deadlineMs ?? 15;
|
|
578
|
+
this._schemaVersion = config.schemaVersion;
|
|
579
|
+
this._staleMessagePolicy = config.staleMessagePolicy ?? "accept";
|
|
580
|
+
this._onTelemetry = config.onTelemetry;
|
|
581
|
+
}
|
|
582
|
+
/** Number of messages in current buffer. */
|
|
583
|
+
get bufferedCount() {
|
|
584
|
+
return this._buffer.length;
|
|
585
|
+
}
|
|
586
|
+
/** Total aggregation rounds performed. */
|
|
587
|
+
get aggregationCount() {
|
|
588
|
+
return this._aggregationCount;
|
|
589
|
+
}
|
|
590
|
+
/** Update expected schema version. */
|
|
591
|
+
set schemaVersion(version) {
|
|
592
|
+
this._schemaVersion = version;
|
|
593
|
+
}
|
|
594
|
+
/** Start a new coordination round. Clears the buffer. */
|
|
595
|
+
startRound() {
|
|
596
|
+
this._buffer = [];
|
|
597
|
+
this._seenAgents.clear();
|
|
598
|
+
this._roundStartTime = performance.now();
|
|
599
|
+
this._droppedStaleMessages = 0;
|
|
600
|
+
}
|
|
601
|
+
/**
|
|
602
|
+
* Receive a message from an agent.
|
|
603
|
+
* Returns false if the message was dropped (deadline exceeded or duplicate agent).
|
|
604
|
+
*/
|
|
605
|
+
receive(message) {
|
|
606
|
+
if (this._roundStartTime > 0) {
|
|
607
|
+
const elapsed = performance.now() - this._roundStartTime;
|
|
608
|
+
if (elapsed > this._deadlineMs) {
|
|
609
|
+
this._emitTelemetry({
|
|
610
|
+
type: "message_dropped",
|
|
611
|
+
agentId: message.agentId,
|
|
612
|
+
reason: "deadline"
|
|
613
|
+
});
|
|
614
|
+
return false;
|
|
615
|
+
}
|
|
616
|
+
}
|
|
617
|
+
const isStale = this._schemaVersion !== void 0 && message.schemaVersion !== this._schemaVersion;
|
|
618
|
+
if (isStale) {
|
|
619
|
+
if (this._staleMessagePolicy === "drop") {
|
|
620
|
+
this._droppedStaleMessages++;
|
|
621
|
+
this._emitTelemetry({
|
|
622
|
+
type: "message_dropped",
|
|
623
|
+
agentId: message.agentId,
|
|
624
|
+
reason: "stale"
|
|
625
|
+
});
|
|
626
|
+
return false;
|
|
627
|
+
}
|
|
628
|
+
if (this._staleMessagePolicy === "warn") {
|
|
629
|
+
console.warn(
|
|
630
|
+
`adaptive-bitmask: stale schema version from agent ${message.agentId} (expected ${this._schemaVersion}, got ${message.schemaVersion})`
|
|
631
|
+
);
|
|
632
|
+
}
|
|
633
|
+
}
|
|
634
|
+
if (this._seenAgents.has(message.agentId)) {
|
|
635
|
+
const idx = this._buffer.findIndex((m) => m.agentId === message.agentId);
|
|
636
|
+
if (idx !== -1) {
|
|
637
|
+
this._buffer[idx] = message;
|
|
638
|
+
}
|
|
639
|
+
this._emitTelemetry({
|
|
640
|
+
type: "message_accepted",
|
|
641
|
+
agentId: message.agentId,
|
|
642
|
+
stale: isStale,
|
|
643
|
+
replaced: true
|
|
644
|
+
});
|
|
645
|
+
return true;
|
|
646
|
+
}
|
|
647
|
+
this._seenAgents.add(message.agentId);
|
|
648
|
+
this._buffer.push(message);
|
|
649
|
+
this._emitTelemetry({
|
|
650
|
+
type: "message_accepted",
|
|
651
|
+
agentId: message.agentId,
|
|
652
|
+
stale: isStale,
|
|
653
|
+
replaced: false
|
|
654
|
+
});
|
|
655
|
+
return true;
|
|
656
|
+
}
|
|
657
|
+
/**
|
|
658
|
+
* Receive multiple messages at once.
|
|
659
|
+
* Returns number of messages accepted.
|
|
660
|
+
*/
|
|
661
|
+
receiveAll(messages) {
|
|
662
|
+
let accepted = 0;
|
|
663
|
+
for (const msg of messages) {
|
|
664
|
+
if (this.receive(msg)) accepted++;
|
|
665
|
+
}
|
|
666
|
+
return accepted;
|
|
667
|
+
}
|
|
668
|
+
/**
|
|
669
|
+
* Aggregate all buffered messages into a consensus result.
|
|
670
|
+
* Clears the buffer after aggregation.
|
|
671
|
+
*/
|
|
672
|
+
aggregate() {
|
|
673
|
+
const t0 = performance.now();
|
|
674
|
+
let aggregated = 0n;
|
|
675
|
+
const bitVotes = /* @__PURE__ */ new Map();
|
|
676
|
+
let staleCount = 0;
|
|
677
|
+
const uniqueAgents = /* @__PURE__ */ new Set();
|
|
678
|
+
for (const msg of this._buffer) {
|
|
679
|
+
uniqueAgents.add(msg.agentId);
|
|
680
|
+
if (this._schemaVersion !== void 0 && msg.schemaVersion !== this._schemaVersion) {
|
|
681
|
+
staleCount++;
|
|
682
|
+
}
|
|
683
|
+
aggregated |= msg.mask;
|
|
684
|
+
forEachSetBit(msg.mask, (bit) => {
|
|
685
|
+
bitVotes.set(bit, (bitVotes.get(bit) ?? 0) + 1);
|
|
686
|
+
});
|
|
687
|
+
}
|
|
688
|
+
const messageCount = this._buffer.length;
|
|
689
|
+
const confidence = /* @__PURE__ */ new Map();
|
|
690
|
+
for (const [bit, votes] of bitVotes) {
|
|
691
|
+
confidence.set(bit, messageCount > 0 ? votes / messageCount : 0);
|
|
692
|
+
}
|
|
693
|
+
const elapsed = (performance.now() - t0) * 1e3;
|
|
694
|
+
this._aggregationCount++;
|
|
695
|
+
this._buffer = [];
|
|
696
|
+
this._seenAgents.clear();
|
|
697
|
+
const result = {
|
|
698
|
+
aggregatedMask: aggregated,
|
|
699
|
+
confidence,
|
|
700
|
+
messageCount,
|
|
701
|
+
uniqueAgents: uniqueAgents.size,
|
|
702
|
+
staleMessages: staleCount,
|
|
703
|
+
droppedStaleMessages: this._droppedStaleMessages,
|
|
704
|
+
aggregationTimeUs: elapsed
|
|
705
|
+
};
|
|
706
|
+
this._emitTelemetry({ type: "round_aggregated", result });
|
|
707
|
+
return result;
|
|
708
|
+
}
|
|
709
|
+
_emitTelemetry(event) {
|
|
710
|
+
this._onTelemetry?.(event);
|
|
711
|
+
}
|
|
712
|
+
};
|
|
713
|
+
|
|
714
|
+
// src/arbiter.ts
|
|
715
|
+
var Arbiter = class {
|
|
716
|
+
_weights;
|
|
717
|
+
_weightSum;
|
|
718
|
+
_executeThreshold;
|
|
719
|
+
_synthesizeThreshold;
|
|
720
|
+
_emergencyOverride;
|
|
721
|
+
_onTelemetry;
|
|
722
|
+
_decisionCount = 0;
|
|
723
|
+
constructor(config = {}) {
|
|
724
|
+
this._executeThreshold = config.executeThreshold ?? 0.55;
|
|
725
|
+
this._synthesizeThreshold = config.synthesizeThreshold ?? 0.4;
|
|
726
|
+
this._emergencyOverride = config.emergencyOverride ?? true;
|
|
727
|
+
this._onTelemetry = config.onTelemetry;
|
|
728
|
+
this._weights = new Float64Array(BITMASK_WIDTH);
|
|
729
|
+
if (config.weights) {
|
|
730
|
+
if (config.weights.length !== BITMASK_WIDTH) {
|
|
731
|
+
throw new Error(`Weight vector must have ${BITMASK_WIDTH} elements, got ${config.weights.length}`);
|
|
732
|
+
}
|
|
733
|
+
this._weights.set(config.weights);
|
|
734
|
+
} else {
|
|
735
|
+
this._weights.fill(1);
|
|
736
|
+
}
|
|
737
|
+
this._weightSum = this._weights.reduce((sum, w) => sum + w, 0);
|
|
738
|
+
}
|
|
739
|
+
/** Number of decisions made since creation. */
|
|
740
|
+
get decisionCount() {
|
|
741
|
+
return this._decisionCount;
|
|
742
|
+
}
|
|
743
|
+
/** Set the weight for a specific bit position. */
|
|
744
|
+
setWeight(position, weight) {
|
|
745
|
+
if (position < 0 || position >= BITMASK_WIDTH) {
|
|
746
|
+
throw new RangeError(`Position ${position} out of range`);
|
|
747
|
+
}
|
|
748
|
+
this._weightSum -= this._weights[position];
|
|
749
|
+
this._weights[position] = weight;
|
|
750
|
+
this._weightSum += weight;
|
|
751
|
+
}
|
|
752
|
+
/** Get the current weight vector (copy). */
|
|
753
|
+
get weights() {
|
|
754
|
+
return Array.from(this._weights);
|
|
755
|
+
}
|
|
756
|
+
/**
|
|
757
|
+
* Score an aggregated bitmask and produce a decision.
|
|
758
|
+
*
|
|
759
|
+
* @param aggregatedMask — OR-aggregated mask from all agents
|
|
760
|
+
* @param confidence — Optional per-bit confidence (fraction of agents that set each bit)
|
|
761
|
+
*/
|
|
762
|
+
score(aggregatedMask, confidence) {
|
|
763
|
+
const t0 = performance.now();
|
|
764
|
+
const active = activeBits(aggregatedMask);
|
|
765
|
+
const emergency = hasEmergency(aggregatedMask);
|
|
766
|
+
if (emergency && this._emergencyOverride) {
|
|
767
|
+
const elapsed2 = (performance.now() - t0) * 1e3;
|
|
768
|
+
this._decisionCount++;
|
|
769
|
+
const result2 = {
|
|
770
|
+
decision: "REJECT",
|
|
771
|
+
rawScore: 0,
|
|
772
|
+
confidenceScore: 0,
|
|
773
|
+
finalScore: 0,
|
|
774
|
+
activeBitCount: active.length,
|
|
775
|
+
hasEmergency: true,
|
|
776
|
+
scoringTimeUs: elapsed2
|
|
777
|
+
};
|
|
778
|
+
this._emitTelemetry({ type: "decision", result: result2 });
|
|
779
|
+
return result2;
|
|
780
|
+
}
|
|
781
|
+
let numerator = 0;
|
|
782
|
+
for (const bit of active) {
|
|
783
|
+
numerator += this._weights[bit];
|
|
784
|
+
}
|
|
785
|
+
const rawScore = this._weightSum > 0 ? numerator / this._weightSum : 0;
|
|
786
|
+
let confidenceScore = rawScore;
|
|
787
|
+
if (confidence && confidence.size > 0) {
|
|
788
|
+
let confNumerator = 0;
|
|
789
|
+
let confDenominator = 0;
|
|
790
|
+
for (const bit of active) {
|
|
791
|
+
const conf = confidence.get(bit) ?? 0;
|
|
792
|
+
confNumerator += conf * this._weights[bit];
|
|
793
|
+
confDenominator += this._weights[bit];
|
|
794
|
+
}
|
|
795
|
+
confidenceScore = confDenominator > 0 ? confNumerator / confDenominator : rawScore;
|
|
796
|
+
}
|
|
797
|
+
const finalScore = Math.min(1, rawScore * 0.6 + confidenceScore * 0.4);
|
|
798
|
+
let decision;
|
|
799
|
+
if (finalScore >= this._executeThreshold) {
|
|
800
|
+
decision = "EXECUTE";
|
|
801
|
+
} else if (finalScore >= this._synthesizeThreshold) {
|
|
802
|
+
decision = "SYNTHESIZE";
|
|
803
|
+
} else {
|
|
804
|
+
decision = "REJECT";
|
|
805
|
+
}
|
|
806
|
+
const elapsed = (performance.now() - t0) * 1e3;
|
|
807
|
+
this._decisionCount++;
|
|
808
|
+
const result = {
|
|
809
|
+
decision,
|
|
810
|
+
rawScore,
|
|
811
|
+
confidenceScore,
|
|
812
|
+
finalScore,
|
|
813
|
+
activeBitCount: active.length,
|
|
814
|
+
hasEmergency: emergency,
|
|
815
|
+
scoringTimeUs: elapsed
|
|
816
|
+
};
|
|
817
|
+
this._emitTelemetry({ type: "decision", result });
|
|
818
|
+
return result;
|
|
819
|
+
}
|
|
820
|
+
/**
|
|
821
|
+
* Convenience: aggregate messages then score.
|
|
822
|
+
* OR-merges all message masks, computes per-bit confidence,
|
|
823
|
+
* validates schema versions, then runs scoring.
|
|
824
|
+
*/
|
|
825
|
+
scoreMessages(messages, expectedSchemaVersion) {
|
|
826
|
+
if (messages.length === 0) {
|
|
827
|
+
return {
|
|
828
|
+
decision: "REJECT",
|
|
829
|
+
rawScore: 0,
|
|
830
|
+
confidenceScore: 0,
|
|
831
|
+
finalScore: 0,
|
|
832
|
+
activeBitCount: 0,
|
|
833
|
+
hasEmergency: false,
|
|
834
|
+
scoringTimeUs: 0,
|
|
835
|
+
staleCount: 0
|
|
836
|
+
};
|
|
837
|
+
}
|
|
838
|
+
let aggregated = 0n;
|
|
839
|
+
const bitVotes = /* @__PURE__ */ new Map();
|
|
840
|
+
let staleCount = 0;
|
|
841
|
+
for (const msg of messages) {
|
|
842
|
+
if (expectedSchemaVersion !== void 0 && msg.schemaVersion !== expectedSchemaVersion) {
|
|
843
|
+
staleCount++;
|
|
844
|
+
}
|
|
845
|
+
aggregated |= msg.mask;
|
|
846
|
+
forEachSetBit(msg.mask, (bit) => {
|
|
847
|
+
bitVotes.set(bit, (bitVotes.get(bit) ?? 0) + 1);
|
|
848
|
+
});
|
|
849
|
+
}
|
|
850
|
+
const confidence = /* @__PURE__ */ new Map();
|
|
851
|
+
for (const [bit, votes] of bitVotes) {
|
|
852
|
+
confidence.set(bit, votes / messages.length);
|
|
853
|
+
}
|
|
854
|
+
const result = this.score(aggregated, confidence);
|
|
855
|
+
this._emitTelemetry({
|
|
856
|
+
type: "score_messages",
|
|
857
|
+
messageCount: messages.length,
|
|
858
|
+
staleCount,
|
|
859
|
+
result
|
|
860
|
+
});
|
|
861
|
+
return { ...result, staleCount };
|
|
862
|
+
}
|
|
863
|
+
/**
|
|
864
|
+
* Paper-canonical strategy ranking and decision logic (Section 6).
|
|
865
|
+
* Uses ŝ_final = 0.6*ŝ_raw + 0.4*c, then applies:
|
|
866
|
+
* - EXECUTE if lead > 15 points
|
|
867
|
+
* - SYNTHESIZE if top strategies are within threshold
|
|
868
|
+
* - REJECT if top score < 40%
|
|
869
|
+
*/
|
|
870
|
+
scoreStrategies(candidates, options = {}) {
|
|
871
|
+
if (candidates.length === 0) {
|
|
872
|
+
const emptyResult = {
|
|
873
|
+
decision: "REJECT",
|
|
874
|
+
leadScore: 0,
|
|
875
|
+
rankings: []
|
|
876
|
+
};
|
|
877
|
+
this._emitTelemetry({ type: "strategy_decision", result: emptyResult });
|
|
878
|
+
return emptyResult;
|
|
879
|
+
}
|
|
880
|
+
const leadThreshold = options.leadThreshold ?? 0.15;
|
|
881
|
+
const rejectThreshold = options.rejectThreshold ?? 0.4;
|
|
882
|
+
const rankings = candidates.map((candidate) => {
|
|
883
|
+
const confidence = candidate.confidence ?? options.globalConfidence;
|
|
884
|
+
const scored = this._scoreMaskComponents(candidate.mask, confidence);
|
|
885
|
+
return {
|
|
886
|
+
id: candidate.id,
|
|
887
|
+
mask: candidate.mask,
|
|
888
|
+
rawScore: scored.rawScore,
|
|
889
|
+
confidenceScore: scored.confidenceScore,
|
|
890
|
+
finalScore: scored.finalScore
|
|
891
|
+
};
|
|
892
|
+
}).sort((a, b) => {
|
|
893
|
+
if (b.finalScore !== a.finalScore) return b.finalScore - a.finalScore;
|
|
894
|
+
if (a.id < b.id) return -1;
|
|
895
|
+
if (a.id > b.id) return 1;
|
|
896
|
+
return 0;
|
|
897
|
+
});
|
|
898
|
+
const top1 = rankings[0];
|
|
899
|
+
const top2 = rankings[1];
|
|
900
|
+
const leadScore = top2 ? top1.finalScore - top2.finalScore : top1.finalScore;
|
|
901
|
+
let result;
|
|
902
|
+
if (top1.finalScore < rejectThreshold) {
|
|
903
|
+
result = {
|
|
904
|
+
decision: "REJECT",
|
|
905
|
+
leadScore,
|
|
906
|
+
rankings
|
|
907
|
+
};
|
|
908
|
+
} else if (leadScore > leadThreshold) {
|
|
909
|
+
result = {
|
|
910
|
+
decision: "EXECUTE",
|
|
911
|
+
selectedStrategyId: top1.id,
|
|
912
|
+
leadScore,
|
|
913
|
+
rankings
|
|
914
|
+
};
|
|
915
|
+
} else {
|
|
916
|
+
const contenders = rankings.slice(0, Math.min(3, rankings.length)).filter((candidate) => top1.finalScore - candidate.finalScore <= leadThreshold);
|
|
917
|
+
const contenderMasks = contenders.length >= 2 ? contenders.map((candidate) => candidate.mask) : rankings.slice(0, Math.min(2, rankings.length)).map((candidate) => candidate.mask);
|
|
918
|
+
result = {
|
|
919
|
+
decision: "SYNTHESIZE",
|
|
920
|
+
synthesizedMask: this._synthesizeMask(contenderMasks),
|
|
921
|
+
leadScore,
|
|
922
|
+
rankings
|
|
923
|
+
};
|
|
924
|
+
}
|
|
925
|
+
this._decisionCount++;
|
|
926
|
+
this._emitTelemetry({ type: "strategy_decision", result });
|
|
927
|
+
return result;
|
|
928
|
+
}
|
|
929
|
+
_scoreMaskComponents(mask, confidence) {
|
|
930
|
+
if (hasEmergency(mask) && this._emergencyOverride) {
|
|
931
|
+
return { rawScore: 0, confidenceScore: 0, finalScore: 0 };
|
|
932
|
+
}
|
|
933
|
+
const active = activeBits(mask);
|
|
934
|
+
let numerator = 0;
|
|
935
|
+
for (const bit of active) {
|
|
936
|
+
numerator += this._weights[bit];
|
|
937
|
+
}
|
|
938
|
+
const rawScore = this._weightSum > 0 ? numerator / this._weightSum : 0;
|
|
939
|
+
let confidenceScore = rawScore;
|
|
940
|
+
if (confidence && confidence.size > 0) {
|
|
941
|
+
let confNumerator = 0;
|
|
942
|
+
let confDenominator = 0;
|
|
943
|
+
for (const bit of active) {
|
|
944
|
+
const conf = confidence.get(bit) ?? 0;
|
|
945
|
+
confNumerator += conf * this._weights[bit];
|
|
946
|
+
confDenominator += this._weights[bit];
|
|
947
|
+
}
|
|
948
|
+
confidenceScore = confDenominator > 0 ? confNumerator / confDenominator : rawScore;
|
|
949
|
+
}
|
|
950
|
+
return {
|
|
951
|
+
rawScore,
|
|
952
|
+
confidenceScore,
|
|
953
|
+
finalScore: Math.min(1, rawScore * 0.6 + confidenceScore * 0.4)
|
|
954
|
+
};
|
|
955
|
+
}
|
|
956
|
+
_synthesizeMask(masks) {
|
|
957
|
+
if (masks.length === 0) return 0n;
|
|
958
|
+
const votes = /* @__PURE__ */ new Map();
|
|
959
|
+
for (const mask of masks) {
|
|
960
|
+
forEachSetBit(mask, (bit) => {
|
|
961
|
+
votes.set(bit, (votes.get(bit) ?? 0) + 1);
|
|
962
|
+
});
|
|
963
|
+
}
|
|
964
|
+
const requiredVotes = Math.floor(masks.length / 2) + 1;
|
|
965
|
+
let synthesized = 0n;
|
|
966
|
+
for (const [bit, count] of votes) {
|
|
967
|
+
if (count >= requiredVotes) {
|
|
968
|
+
synthesized |= 1n << BigInt(bit);
|
|
969
|
+
}
|
|
970
|
+
}
|
|
971
|
+
return synthesized;
|
|
972
|
+
}
|
|
973
|
+
_emitTelemetry(event) {
|
|
974
|
+
this._onTelemetry?.(event);
|
|
975
|
+
}
|
|
976
|
+
};
|
|
977
|
+
|
|
978
|
+
// src/ai/session.ts
|
|
979
|
+
var CoordinationSession = class {
|
|
980
|
+
schema;
|
|
981
|
+
coordinator;
|
|
982
|
+
arbiter;
|
|
983
|
+
_agentIds = /* @__PURE__ */ new Map();
|
|
984
|
+
constructor(config) {
|
|
985
|
+
this.schema = new SchemaManager({
|
|
986
|
+
emergencyPrefix: config.emergencyPrefix ?? "EMERGENCY_",
|
|
987
|
+
emergencyFeatures: config.emergencyFeatures
|
|
988
|
+
});
|
|
989
|
+
this.schema.registerAll(config.features);
|
|
990
|
+
this.coordinator = new Coordinator({
|
|
991
|
+
...config.coordinatorConfig,
|
|
992
|
+
schemaVersion: this.schema.version
|
|
993
|
+
});
|
|
994
|
+
this.arbiter = new Arbiter(config.arbiterConfig);
|
|
995
|
+
}
|
|
996
|
+
/** Deterministic agent ID: FNV-1a hash of name → uint32. */
|
|
997
|
+
agentId(name) {
|
|
998
|
+
const cached = this._agentIds.get(name);
|
|
999
|
+
if (cached !== void 0) return cached;
|
|
1000
|
+
let hash = 2166136261;
|
|
1001
|
+
for (let i = 0; i < name.length; i++) {
|
|
1002
|
+
hash ^= name.charCodeAt(i);
|
|
1003
|
+
hash = Math.imul(hash, 16777619);
|
|
1004
|
+
}
|
|
1005
|
+
const id = hash >>> 0;
|
|
1006
|
+
this._agentIds.set(name, id);
|
|
1007
|
+
return id;
|
|
1008
|
+
}
|
|
1009
|
+
/** Start a new coordination round. Clears the coordinator buffer. */
|
|
1010
|
+
startRound() {
|
|
1011
|
+
this.coordinator.startRound();
|
|
1012
|
+
}
|
|
1013
|
+
/** Encode features + create message + receive in one call. */
|
|
1014
|
+
report(agentName, features) {
|
|
1015
|
+
const { mask, mapped, unmapped } = encode(
|
|
1016
|
+
features,
|
|
1017
|
+
this.schema.featureToBit
|
|
1018
|
+
);
|
|
1019
|
+
const msg = BitmaskMessage.now(
|
|
1020
|
+
mask,
|
|
1021
|
+
this.agentId(agentName),
|
|
1022
|
+
this.schema.version
|
|
1023
|
+
);
|
|
1024
|
+
const accepted = this.coordinator.receive(msg);
|
|
1025
|
+
return { accepted, mapped, unmapped };
|
|
1026
|
+
}
|
|
1027
|
+
/** Aggregate current buffer + score via arbiter. */
|
|
1028
|
+
decide() {
|
|
1029
|
+
const { aggregatedMask, confidence } = this.coordinator.aggregate();
|
|
1030
|
+
const aggregatedFeatures = decode(aggregatedMask, this.schema.bitToFeatures);
|
|
1031
|
+
const result = this.arbiter.score(aggregatedMask, confidence);
|
|
1032
|
+
return {
|
|
1033
|
+
decision: result.decision,
|
|
1034
|
+
aggregatedFeatures,
|
|
1035
|
+
confidence,
|
|
1036
|
+
result
|
|
1037
|
+
};
|
|
1038
|
+
}
|
|
1039
|
+
};
|
|
1040
|
+
|
|
1041
|
+
// src/ai/tools.ts
|
|
1042
|
+
var import_ai = require("ai");
|
|
1043
|
+
var import_zod = require("zod");
|
|
1044
|
+
function createCoordinationTools(session) {
|
|
1045
|
+
const reportObservation = (0, import_ai.tool)({
|
|
1046
|
+
description: "Report observed features to the coordination layer. Each feature string is encoded into the shared bitmask.",
|
|
1047
|
+
parameters: import_zod.z.object({
|
|
1048
|
+
agentName: import_zod.z.string().describe("Name identifying this agent"),
|
|
1049
|
+
features: import_zod.z.array(import_zod.z.string()).describe("Feature names observed by this agent")
|
|
1050
|
+
}),
|
|
1051
|
+
execute: async ({ agentName, features }) => {
|
|
1052
|
+
const result = session.report(agentName, features);
|
|
1053
|
+
return {
|
|
1054
|
+
accepted: result.accepted,
|
|
1055
|
+
mapped: result.mapped,
|
|
1056
|
+
unmapped: result.unmapped
|
|
1057
|
+
};
|
|
1058
|
+
}
|
|
1059
|
+
});
|
|
1060
|
+
const getConsensus = (0, import_ai.tool)({
|
|
1061
|
+
description: "Query the current aggregated consensus state across all agents.",
|
|
1062
|
+
parameters: import_zod.z.object({}),
|
|
1063
|
+
execute: async () => {
|
|
1064
|
+
const { aggregatedMask, confidence, uniqueAgents } = session.coordinator.aggregate();
|
|
1065
|
+
const features = decode(aggregatedMask, session.schema.bitToFeatures);
|
|
1066
|
+
const confidenceObj = {};
|
|
1067
|
+
for (const [bit, conf] of confidence) {
|
|
1068
|
+
confidenceObj[String(bit)] = conf;
|
|
1069
|
+
}
|
|
1070
|
+
return {
|
|
1071
|
+
features,
|
|
1072
|
+
confidence: confidenceObj,
|
|
1073
|
+
agentCount: uniqueAgents
|
|
1074
|
+
};
|
|
1075
|
+
}
|
|
1076
|
+
});
|
|
1077
|
+
const requestDecision = (0, import_ai.tool)({
|
|
1078
|
+
description: "Trigger the arbiter to score the current aggregated state and return a decision.",
|
|
1079
|
+
parameters: import_zod.z.object({}),
|
|
1080
|
+
execute: async () => {
|
|
1081
|
+
const { decision, aggregatedFeatures, result } = session.decide();
|
|
1082
|
+
return {
|
|
1083
|
+
decision,
|
|
1084
|
+
score: result.finalScore,
|
|
1085
|
+
features: aggregatedFeatures,
|
|
1086
|
+
hasEmergency: result.hasEmergency
|
|
1087
|
+
};
|
|
1088
|
+
}
|
|
1089
|
+
});
|
|
1090
|
+
return { reportObservation, getConsensus, requestDecision };
|
|
1091
|
+
}
|
|
1092
|
+
|
|
1093
|
+
// src/ai/middleware.ts
|
|
1094
|
+
function createCoordinationMiddleware(session, options = {}) {
|
|
1095
|
+
const { injectConsensus = false, autoEncodeToolCalls = false, agentName } = options;
|
|
1096
|
+
if (autoEncodeToolCalls && !agentName) {
|
|
1097
|
+
throw new Error("agentName is required when autoEncodeToolCalls is enabled");
|
|
1098
|
+
}
|
|
1099
|
+
const middleware = {};
|
|
1100
|
+
if (injectConsensus) {
|
|
1101
|
+
middleware.transformParams = async ({ params }) => {
|
|
1102
|
+
const { aggregatedMask, confidence } = session.coordinator.aggregate();
|
|
1103
|
+
const features = decode(aggregatedMask, session.schema.bitToFeatures);
|
|
1104
|
+
const emergency = hasEmergency(aggregatedMask);
|
|
1105
|
+
const confidenceEntries = Array.from(confidence.entries()).map(([bit, conf]) => ` bit ${bit}: ${(conf * 100).toFixed(0)}%`).join("\n");
|
|
1106
|
+
const consensusText = [
|
|
1107
|
+
"[Coordination Consensus]",
|
|
1108
|
+
`Features: ${features.length > 0 ? features.join(", ") : "none"}`,
|
|
1109
|
+
`Emergency: ${emergency}`,
|
|
1110
|
+
confidenceEntries ? `Confidence:
|
|
1111
|
+
${confidenceEntries}` : ""
|
|
1112
|
+
].filter(Boolean).join("\n");
|
|
1113
|
+
return {
|
|
1114
|
+
...params,
|
|
1115
|
+
prompt: [
|
|
1116
|
+
{ role: "system", content: consensusText },
|
|
1117
|
+
...params.prompt
|
|
1118
|
+
]
|
|
1119
|
+
};
|
|
1120
|
+
};
|
|
1121
|
+
}
|
|
1122
|
+
if (autoEncodeToolCalls) {
|
|
1123
|
+
middleware.wrapGenerate = async ({ doGenerate }) => {
|
|
1124
|
+
const result = await doGenerate();
|
|
1125
|
+
if (result.toolCalls && result.toolCalls.length > 0) {
|
|
1126
|
+
const matchingFeatures = result.toolCalls.map((tc) => tc.toolName).filter((name) => session.schema.featureToBit.has(name));
|
|
1127
|
+
if (matchingFeatures.length > 0) {
|
|
1128
|
+
session.report(agentName, matchingFeatures);
|
|
1129
|
+
}
|
|
1130
|
+
}
|
|
1131
|
+
return result;
|
|
1132
|
+
};
|
|
1133
|
+
}
|
|
1134
|
+
return middleware;
|
|
1135
|
+
}
|
|
1136
|
+
// Annotate the CommonJS export names for ESM import in node:
|
|
1137
|
+
0 && (module.exports = {
|
|
1138
|
+
CoordinationSession,
|
|
1139
|
+
createCoordinationMiddleware,
|
|
1140
|
+
createCoordinationTools
|
|
1141
|
+
});
|