omk-agent-core 0.98.2 → 0.98.3
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/CHANGELOG.md +644 -0
- package/dist/agent.d.ts.map +1 -1
- package/dist/agent.js +3 -5
- package/dist/agent.js.map +1 -1
- package/dist/effects/effect-journal.d.ts +43 -0
- package/dist/effects/effect-journal.d.ts.map +1 -0
- package/dist/effects/effect-journal.js +186 -0
- package/dist/effects/effect-journal.js.map +1 -0
- package/dist/effects/effect-recovery.d.ts +70 -0
- package/dist/effects/effect-recovery.d.ts.map +1 -0
- package/dist/effects/effect-recovery.js +120 -0
- package/dist/effects/effect-recovery.js.map +1 -0
- package/dist/effects/effect-transitions.d.ts +34 -0
- package/dist/effects/effect-transitions.d.ts.map +1 -0
- package/dist/effects/effect-transitions.js +148 -0
- package/dist/effects/effect-transitions.js.map +1 -0
- package/dist/effects/effect-types.d.ts +135 -0
- package/dist/effects/effect-types.d.ts.map +1 -0
- package/dist/effects/effect-types.js +32 -0
- package/dist/effects/effect-types.js.map +1 -0
- package/dist/harness/abort-delivery.d.ts +26 -0
- package/dist/harness/abort-delivery.d.ts.map +1 -0
- package/dist/harness/abort-delivery.js +36 -0
- package/dist/harness/abort-delivery.js.map +1 -0
- package/dist/harness/agent-harness.d.ts +18 -0
- package/dist/harness/agent-harness.d.ts.map +1 -1
- package/dist/harness/agent-harness.js +37 -28
- package/dist/harness/agent-harness.js.map +1 -1
- package/dist/harness/canonical-digest.d.ts +32 -0
- package/dist/harness/canonical-digest.d.ts.map +1 -0
- package/dist/harness/canonical-digest.js +164 -0
- package/dist/harness/canonical-digest.js.map +1 -0
- package/dist/harness/deferred-commands.d.ts +53 -0
- package/dist/harness/deferred-commands.d.ts.map +1 -0
- package/dist/harness/deferred-commands.js +96 -0
- package/dist/harness/deferred-commands.js.map +1 -0
- package/dist/harness/operation-outcome.d.ts +18 -10
- package/dist/harness/operation-outcome.d.ts.map +1 -1
- package/dist/harness/operation-outcome.js +71 -35
- package/dist/harness/operation-outcome.js.map +1 -1
- package/dist/harness/operation-trace-divergence.d.ts +60 -0
- package/dist/harness/operation-trace-divergence.d.ts.map +1 -0
- package/dist/harness/operation-trace-divergence.js +199 -0
- package/dist/harness/operation-trace-divergence.js.map +1 -0
- package/dist/harness/operation-trace.d.ts +134 -0
- package/dist/harness/operation-trace.d.ts.map +1 -0
- package/dist/harness/operation-trace.js +161 -0
- package/dist/harness/operation-trace.js.map +1 -0
- package/dist/harness/subscriber-fanout.d.ts +14 -1
- package/dist/harness/subscriber-fanout.d.ts.map +1 -1
- package/dist/harness/subscriber-fanout.js +25 -6
- package/dist/harness/subscriber-fanout.js.map +1 -1
- package/dist/index.d.ts +2 -0
- package/dist/index.d.ts.map +1 -1
- package/dist/index.js +2 -0
- package/dist/index.js.map +1 -1
- package/dist/listener-delivery.d.ts +22 -0
- package/dist/listener-delivery.d.ts.map +1 -0
- package/dist/listener-delivery.js +36 -0
- package/dist/listener-delivery.js.map +1 -0
- package/package.json +4 -3
|
@@ -0,0 +1,164 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Browser-safe canonical serialization and SHA-256 for harness identities.
|
|
3
|
+
*
|
|
4
|
+
* Every digest the harness binds to an operation, attempt, effect, or trace
|
|
5
|
+
* must be reproducible from the same logical value on any host, so this module
|
|
6
|
+
* fixes two things and imports nothing:
|
|
7
|
+
*
|
|
8
|
+
* 1. `canonicalJson` — a JCS-style (RFC 8785) serialization: object keys sorted
|
|
9
|
+
* by UTF-16 code unit, no whitespace, ES number formatting, `undefined`
|
|
10
|
+
* properties omitted. Non-finite numbers, bigint, functions, symbols, and
|
|
11
|
+
* non-plain objects are rejected instead of silently coerced, because a
|
|
12
|
+
* digest over a lossy encoding is not a commitment to the value.
|
|
13
|
+
* 2. `sha256Hex` — a synchronous pure-TypeScript SHA-256, so pure reducers can
|
|
14
|
+
* derive identities without an async `crypto.subtle` round trip or a Node
|
|
15
|
+
* `node:crypto` import that the browser entry point cannot carry.
|
|
16
|
+
*
|
|
17
|
+
* `domainDigest` adds domain separation: the same parts hashed under two
|
|
18
|
+
* domains never collide, and a part list is length-delimited through the
|
|
19
|
+
* canonical encoding so `["ab","c"]` and `["a","bc"]` differ.
|
|
20
|
+
*/
|
|
21
|
+
export function canonicalJson(value) {
|
|
22
|
+
return serialize(value, new WeakSet());
|
|
23
|
+
}
|
|
24
|
+
function serialize(value, ancestors) {
|
|
25
|
+
if (value === null)
|
|
26
|
+
return "null";
|
|
27
|
+
switch (typeof value) {
|
|
28
|
+
case "string":
|
|
29
|
+
return JSON.stringify(value);
|
|
30
|
+
case "boolean":
|
|
31
|
+
return value ? "true" : "false";
|
|
32
|
+
case "number":
|
|
33
|
+
if (!Number.isFinite(value))
|
|
34
|
+
throw new TypeError("canonicalJson: non-finite numbers are not representable");
|
|
35
|
+
return JSON.stringify(value);
|
|
36
|
+
case "object":
|
|
37
|
+
break;
|
|
38
|
+
default:
|
|
39
|
+
throw new TypeError(`canonicalJson: unsupported value of type ${typeof value}`);
|
|
40
|
+
}
|
|
41
|
+
const object = value;
|
|
42
|
+
if (ancestors.has(object))
|
|
43
|
+
throw new TypeError("canonicalJson: cyclic values are not representable");
|
|
44
|
+
ancestors.add(object);
|
|
45
|
+
try {
|
|
46
|
+
if (Array.isArray(object)) {
|
|
47
|
+
const items = object.map((item) => {
|
|
48
|
+
if (item === undefined)
|
|
49
|
+
throw new TypeError("canonicalJson: undefined array elements are not representable");
|
|
50
|
+
return serialize(item, ancestors);
|
|
51
|
+
});
|
|
52
|
+
return `[${items.join(",")}]`;
|
|
53
|
+
}
|
|
54
|
+
const prototype = Object.getPrototypeOf(object);
|
|
55
|
+
if (prototype !== Object.prototype && prototype !== null) {
|
|
56
|
+
throw new TypeError("canonicalJson: only plain objects are representable");
|
|
57
|
+
}
|
|
58
|
+
const record = object;
|
|
59
|
+
const keys = Object.keys(record)
|
|
60
|
+
.filter((key) => record[key] !== undefined)
|
|
61
|
+
.sort(compareCodeUnits);
|
|
62
|
+
const members = keys.map((key) => `${JSON.stringify(key)}:${serialize(record[key], ancestors)}`);
|
|
63
|
+
return `{${members.join(",")}}`;
|
|
64
|
+
}
|
|
65
|
+
finally {
|
|
66
|
+
ancestors.delete(object);
|
|
67
|
+
}
|
|
68
|
+
}
|
|
69
|
+
/** RFC 8785 orders keys by UTF-16 code unit, which is what `<` gives on JS strings. */
|
|
70
|
+
function compareCodeUnits(left, right) {
|
|
71
|
+
return left < right ? -1 : left > right ? 1 : 0;
|
|
72
|
+
}
|
|
73
|
+
const ROUND_CONSTANTS = new Uint32Array([
|
|
74
|
+
0x428a2f98, 0x71374491, 0xb5c0fbcf, 0xe9b5dba5, 0x3956c25b, 0x59f111f1, 0x923f82a4, 0xab1c5ed5, 0xd807aa98,
|
|
75
|
+
0x12835b01, 0x243185be, 0x550c7dc3, 0x72be5d74, 0x80deb1fe, 0x9bdc06a7, 0xc19bf174, 0xe49b69c1, 0xefbe4786,
|
|
76
|
+
0x0fc19dc6, 0x240ca1cc, 0x2de92c6f, 0x4a7484aa, 0x5cb0a9dc, 0x76f988da, 0x983e5152, 0xa831c66d, 0xb00327c8,
|
|
77
|
+
0xbf597fc7, 0xc6e00bf3, 0xd5a79147, 0x06ca6351, 0x14292967, 0x27b70a85, 0x2e1b2138, 0x4d2c6dfc, 0x53380d13,
|
|
78
|
+
0x650a7354, 0x766a0abb, 0x81c2c92e, 0x92722c85, 0xa2bfe8a1, 0xa81a664b, 0xc24b8b70, 0xc76c51a3, 0xd192e819,
|
|
79
|
+
0xd6990624, 0xf40e3585, 0x106aa070, 0x19a4c116, 0x1e376c08, 0x2748774c, 0x34b0bcb5, 0x391c0cb3, 0x4ed8aa4a,
|
|
80
|
+
0x5b9cca4f, 0x682e6ff3, 0x748f82ee, 0x78a5636f, 0x84c87814, 0x8cc70208, 0x90befffa, 0xa4506ceb, 0xbef9a3f7,
|
|
81
|
+
0xc67178f2,
|
|
82
|
+
]);
|
|
83
|
+
const INITIAL_STATE = new Uint32Array([
|
|
84
|
+
0x6a09e667, 0xbb67ae85, 0x3c6ef372, 0xa54ff53a, 0x510e527f, 0x9b05688c, 0x1f83d9ab, 0x5be0cd19,
|
|
85
|
+
]);
|
|
86
|
+
function rotateRight(value, bits) {
|
|
87
|
+
return (value >>> bits) | (value << (32 - bits));
|
|
88
|
+
}
|
|
89
|
+
/** Pad the message per FIPS 180-4 §5.1.1: 0x80, zeros, then the 64-bit big-endian bit length. */
|
|
90
|
+
function padMessage(bytes) {
|
|
91
|
+
const bitLength = bytes.length * 8;
|
|
92
|
+
const paddedLength = Math.ceil((bytes.length + 9) / 64) * 64;
|
|
93
|
+
const padded = new Uint8Array(paddedLength);
|
|
94
|
+
padded.set(bytes);
|
|
95
|
+
padded[bytes.length] = 0x80;
|
|
96
|
+
const view = new DataView(padded.buffer);
|
|
97
|
+
view.setUint32(paddedLength - 8, Math.floor(bitLength / 0x100000000), false);
|
|
98
|
+
view.setUint32(paddedLength - 4, bitLength >>> 0, false);
|
|
99
|
+
return view;
|
|
100
|
+
}
|
|
101
|
+
function compressBlock(state, schedule, view, offset) {
|
|
102
|
+
for (let index = 0; index < 16; index++)
|
|
103
|
+
schedule[index] = view.getUint32(offset + index * 4, false);
|
|
104
|
+
for (let index = 16; index < 64; index++) {
|
|
105
|
+
const w15 = schedule[index - 15];
|
|
106
|
+
const w2 = schedule[index - 2];
|
|
107
|
+
const sigma0 = rotateRight(w15, 7) ^ rotateRight(w15, 18) ^ (w15 >>> 3);
|
|
108
|
+
const sigma1 = rotateRight(w2, 17) ^ rotateRight(w2, 19) ^ (w2 >>> 10);
|
|
109
|
+
schedule[index] = (schedule[index - 16] + sigma0 + schedule[index - 7] + sigma1) >>> 0;
|
|
110
|
+
}
|
|
111
|
+
let [a, b, c, d, e, f, g, h] = state;
|
|
112
|
+
for (let index = 0; index < 64; index++) {
|
|
113
|
+
const bigSigma1 = rotateRight(e, 6) ^ rotateRight(e, 11) ^ rotateRight(e, 25);
|
|
114
|
+
const choose = (e & f) ^ (~e & g);
|
|
115
|
+
const temp1 = (h + bigSigma1 + choose + ROUND_CONSTANTS[index] + schedule[index]) >>> 0;
|
|
116
|
+
const bigSigma0 = rotateRight(a, 2) ^ rotateRight(a, 13) ^ rotateRight(a, 22);
|
|
117
|
+
const majority = (a & b) ^ (a & c) ^ (b & c);
|
|
118
|
+
const temp2 = (bigSigma0 + majority) >>> 0;
|
|
119
|
+
h = g;
|
|
120
|
+
g = f;
|
|
121
|
+
f = e;
|
|
122
|
+
e = (d + temp1) >>> 0;
|
|
123
|
+
d = c;
|
|
124
|
+
c = b;
|
|
125
|
+
b = a;
|
|
126
|
+
a = (temp1 + temp2) >>> 0;
|
|
127
|
+
}
|
|
128
|
+
state[0] = (state[0] + a) >>> 0;
|
|
129
|
+
state[1] = (state[1] + b) >>> 0;
|
|
130
|
+
state[2] = (state[2] + c) >>> 0;
|
|
131
|
+
state[3] = (state[3] + d) >>> 0;
|
|
132
|
+
state[4] = (state[4] + e) >>> 0;
|
|
133
|
+
state[5] = (state[5] + f) >>> 0;
|
|
134
|
+
state[6] = (state[6] + g) >>> 0;
|
|
135
|
+
state[7] = (state[7] + h) >>> 0;
|
|
136
|
+
}
|
|
137
|
+
/** Lowercase hex SHA-256 of a UTF-8 string or raw bytes. Synchronous and dependency-free. */
|
|
138
|
+
export function sha256Hex(input) {
|
|
139
|
+
const bytes = typeof input === "string" ? new TextEncoder().encode(input) : input;
|
|
140
|
+
const view = padMessage(bytes);
|
|
141
|
+
const state = new Uint32Array(INITIAL_STATE);
|
|
142
|
+
const schedule = new Uint32Array(64);
|
|
143
|
+
for (let offset = 0; offset < view.byteLength; offset += 64)
|
|
144
|
+
compressBlock(state, schedule, view, offset);
|
|
145
|
+
let hex = "";
|
|
146
|
+
for (const word of state)
|
|
147
|
+
hex += word.toString(16).padStart(8, "0");
|
|
148
|
+
return hex;
|
|
149
|
+
}
|
|
150
|
+
/** SHA-256 of the canonical JSON encoding of `value`. */
|
|
151
|
+
export function canonicalDigest(value) {
|
|
152
|
+
return sha256Hex(canonicalJson(value));
|
|
153
|
+
}
|
|
154
|
+
/**
|
|
155
|
+
* Domain-separated digest over an ordered part list. The parts are committed
|
|
156
|
+
* through canonical JSON, so boundaries between parts are unambiguous and a
|
|
157
|
+
* different domain string always yields a different digest.
|
|
158
|
+
*/
|
|
159
|
+
export function domainDigest(domain, parts) {
|
|
160
|
+
if (domain.length === 0)
|
|
161
|
+
throw new TypeError("domainDigest: domain must be non-empty");
|
|
162
|
+
return sha256Hex(canonicalJson({ domain, parts }));
|
|
163
|
+
}
|
|
164
|
+
//# sourceMappingURL=canonical-digest.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"canonical-digest.js","sourceRoot":"","sources":["../../src/harness/canonical-digest.ts"],"names":[],"mappings":"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","sourcesContent":["/**\n * Browser-safe canonical serialization and SHA-256 for harness identities.\n *\n * Every digest the harness binds to an operation, attempt, effect, or trace\n * must be reproducible from the same logical value on any host, so this module\n * fixes two things and imports nothing:\n *\n * 1. `canonicalJson` — a JCS-style (RFC 8785) serialization: object keys sorted\n * by UTF-16 code unit, no whitespace, ES number formatting, `undefined`\n * properties omitted. Non-finite numbers, bigint, functions, symbols, and\n * non-plain objects are rejected instead of silently coerced, because a\n * digest over a lossy encoding is not a commitment to the value.\n * 2. `sha256Hex` — a synchronous pure-TypeScript SHA-256, so pure reducers can\n * derive identities without an async `crypto.subtle` round trip or a Node\n * `node:crypto` import that the browser entry point cannot carry.\n *\n * `domainDigest` adds domain separation: the same parts hashed under two\n * domains never collide, and a part list is length-delimited through the\n * canonical encoding so `[\"ab\",\"c\"]` and `[\"a\",\"bc\"]` differ.\n */\n\nexport function canonicalJson(value: unknown): string {\n\treturn serialize(value, new WeakSet<object>());\n}\n\nfunction serialize(value: unknown, ancestors: WeakSet<object>): string {\n\tif (value === null) return \"null\";\n\tswitch (typeof value) {\n\t\tcase \"string\":\n\t\t\treturn JSON.stringify(value);\n\t\tcase \"boolean\":\n\t\t\treturn value ? \"true\" : \"false\";\n\t\tcase \"number\":\n\t\t\tif (!Number.isFinite(value)) throw new TypeError(\"canonicalJson: non-finite numbers are not representable\");\n\t\t\treturn JSON.stringify(value);\n\t\tcase \"object\":\n\t\t\tbreak;\n\t\tdefault:\n\t\t\tthrow new TypeError(`canonicalJson: unsupported value of type ${typeof value}`);\n\t}\n\tconst object = value as object;\n\tif (ancestors.has(object)) throw new TypeError(\"canonicalJson: cyclic values are not representable\");\n\tancestors.add(object);\n\ttry {\n\t\tif (Array.isArray(object)) {\n\t\t\tconst items = object.map((item) => {\n\t\t\t\tif (item === undefined)\n\t\t\t\t\tthrow new TypeError(\"canonicalJson: undefined array elements are not representable\");\n\t\t\t\treturn serialize(item, ancestors);\n\t\t\t});\n\t\t\treturn `[${items.join(\",\")}]`;\n\t\t}\n\t\tconst prototype = Object.getPrototypeOf(object);\n\t\tif (prototype !== Object.prototype && prototype !== null) {\n\t\t\tthrow new TypeError(\"canonicalJson: only plain objects are representable\");\n\t\t}\n\t\tconst record = object as Record<string, unknown>;\n\t\tconst keys = Object.keys(record)\n\t\t\t.filter((key) => record[key] !== undefined)\n\t\t\t.sort(compareCodeUnits);\n\t\tconst members = keys.map((key) => `${JSON.stringify(key)}:${serialize(record[key], ancestors)}`);\n\t\treturn `{${members.join(\",\")}}`;\n\t} finally {\n\t\tancestors.delete(object);\n\t}\n}\n\n/** RFC 8785 orders keys by UTF-16 code unit, which is what `<` gives on JS strings. */\nfunction compareCodeUnits(left: string, right: string): number {\n\treturn left < right ? -1 : left > right ? 1 : 0;\n}\n\nconst ROUND_CONSTANTS = new Uint32Array([\n\t0x428a2f98, 0x71374491, 0xb5c0fbcf, 0xe9b5dba5, 0x3956c25b, 0x59f111f1, 0x923f82a4, 0xab1c5ed5, 0xd807aa98,\n\t0x12835b01, 0x243185be, 0x550c7dc3, 0x72be5d74, 0x80deb1fe, 0x9bdc06a7, 0xc19bf174, 0xe49b69c1, 0xefbe4786,\n\t0x0fc19dc6, 0x240ca1cc, 0x2de92c6f, 0x4a7484aa, 0x5cb0a9dc, 0x76f988da, 0x983e5152, 0xa831c66d, 0xb00327c8,\n\t0xbf597fc7, 0xc6e00bf3, 0xd5a79147, 0x06ca6351, 0x14292967, 0x27b70a85, 0x2e1b2138, 0x4d2c6dfc, 0x53380d13,\n\t0x650a7354, 0x766a0abb, 0x81c2c92e, 0x92722c85, 0xa2bfe8a1, 0xa81a664b, 0xc24b8b70, 0xc76c51a3, 0xd192e819,\n\t0xd6990624, 0xf40e3585, 0x106aa070, 0x19a4c116, 0x1e376c08, 0x2748774c, 0x34b0bcb5, 0x391c0cb3, 0x4ed8aa4a,\n\t0x5b9cca4f, 0x682e6ff3, 0x748f82ee, 0x78a5636f, 0x84c87814, 0x8cc70208, 0x90befffa, 0xa4506ceb, 0xbef9a3f7,\n\t0xc67178f2,\n]);\n\nconst INITIAL_STATE = new Uint32Array([\n\t0x6a09e667, 0xbb67ae85, 0x3c6ef372, 0xa54ff53a, 0x510e527f, 0x9b05688c, 0x1f83d9ab, 0x5be0cd19,\n]);\n\nfunction rotateRight(value: number, bits: number): number {\n\treturn (value >>> bits) | (value << (32 - bits));\n}\n\n/** Pad the message per FIPS 180-4 §5.1.1: 0x80, zeros, then the 64-bit big-endian bit length. */\nfunction padMessage(bytes: Uint8Array): DataView {\n\tconst bitLength = bytes.length * 8;\n\tconst paddedLength = Math.ceil((bytes.length + 9) / 64) * 64;\n\tconst padded = new Uint8Array(paddedLength);\n\tpadded.set(bytes);\n\tpadded[bytes.length] = 0x80;\n\tconst view = new DataView(padded.buffer);\n\tview.setUint32(paddedLength - 8, Math.floor(bitLength / 0x100000000), false);\n\tview.setUint32(paddedLength - 4, bitLength >>> 0, false);\n\treturn view;\n}\n\nfunction compressBlock(state: Uint32Array, schedule: Uint32Array, view: DataView, offset: number): void {\n\tfor (let index = 0; index < 16; index++) schedule[index] = view.getUint32(offset + index * 4, false);\n\tfor (let index = 16; index < 64; index++) {\n\t\tconst w15 = schedule[index - 15];\n\t\tconst w2 = schedule[index - 2];\n\t\tconst sigma0 = rotateRight(w15, 7) ^ rotateRight(w15, 18) ^ (w15 >>> 3);\n\t\tconst sigma1 = rotateRight(w2, 17) ^ rotateRight(w2, 19) ^ (w2 >>> 10);\n\t\tschedule[index] = (schedule[index - 16] + sigma0 + schedule[index - 7] + sigma1) >>> 0;\n\t}\n\tlet [a, b, c, d, e, f, g, h] = state;\n\tfor (let index = 0; index < 64; index++) {\n\t\tconst bigSigma1 = rotateRight(e, 6) ^ rotateRight(e, 11) ^ rotateRight(e, 25);\n\t\tconst choose = (e & f) ^ (~e & g);\n\t\tconst temp1 = (h + bigSigma1 + choose + ROUND_CONSTANTS[index] + schedule[index]) >>> 0;\n\t\tconst bigSigma0 = rotateRight(a, 2) ^ rotateRight(a, 13) ^ rotateRight(a, 22);\n\t\tconst majority = (a & b) ^ (a & c) ^ (b & c);\n\t\tconst temp2 = (bigSigma0 + majority) >>> 0;\n\t\th = g;\n\t\tg = f;\n\t\tf = e;\n\t\te = (d + temp1) >>> 0;\n\t\td = c;\n\t\tc = b;\n\t\tb = a;\n\t\ta = (temp1 + temp2) >>> 0;\n\t}\n\tstate[0] = (state[0] + a) >>> 0;\n\tstate[1] = (state[1] + b) >>> 0;\n\tstate[2] = (state[2] + c) >>> 0;\n\tstate[3] = (state[3] + d) >>> 0;\n\tstate[4] = (state[4] + e) >>> 0;\n\tstate[5] = (state[5] + f) >>> 0;\n\tstate[6] = (state[6] + g) >>> 0;\n\tstate[7] = (state[7] + h) >>> 0;\n}\n\n/** Lowercase hex SHA-256 of a UTF-8 string or raw bytes. Synchronous and dependency-free. */\nexport function sha256Hex(input: string | Uint8Array): string {\n\tconst bytes = typeof input === \"string\" ? new TextEncoder().encode(input) : input;\n\tconst view = padMessage(bytes);\n\tconst state = new Uint32Array(INITIAL_STATE);\n\tconst schedule = new Uint32Array(64);\n\tfor (let offset = 0; offset < view.byteLength; offset += 64) compressBlock(state, schedule, view, offset);\n\tlet hex = \"\";\n\tfor (const word of state) hex += word.toString(16).padStart(8, \"0\");\n\treturn hex;\n}\n\n/** SHA-256 of the canonical JSON encoding of `value`. */\nexport function canonicalDigest(value: unknown): string {\n\treturn sha256Hex(canonicalJson(value));\n}\n\n/**\n * Domain-separated digest over an ordered part list. The parts are committed\n * through canonical JSON, so boundaries between parts are unambiguous and a\n * different domain string always yields a different digest.\n */\nexport function domainDigest(domain: string, parts: readonly string[]): string {\n\tif (domain.length === 0) throw new TypeError(\"domainDigest: domain must be non-empty\");\n\treturn sha256Hex(canonicalJson({ domain, parts }));\n}\n"]}
|
|
@@ -0,0 +1,53 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Deferred harness commands: the callback-safe alternative to waiting on the
|
|
3
|
+
* operation a callback is inside.
|
|
4
|
+
*
|
|
5
|
+
* A listener that awaits `waitForIdle()` or `abortAndWait()` for the operation
|
|
6
|
+
* whose emission it is blocking forms a cycle: settlement awaits the listener
|
|
7
|
+
* and the listener awaits settlement. `AgentHarness.runWhenIdle()` breaks that
|
|
8
|
+
* cycle by enqueueing the work and returning a ref without waiting, so the
|
|
9
|
+
* callback never depends on the operation it is inside.
|
|
10
|
+
*
|
|
11
|
+
* Commands run in registration order, only while the harness reports idle, and
|
|
12
|
+
* each outcome is captured in the ref instead of rejecting — a deferred command
|
|
13
|
+
* is nobody's caller, so an unhandled rejection would be invisible.
|
|
14
|
+
*/
|
|
15
|
+
export type DeferredCommandStatus = "queued" | "running" | "completed" | "failed" | "cancelled";
|
|
16
|
+
export interface DeferredHarnessCommand {
|
|
17
|
+
/** Short label used in the ref and in diagnostics. */
|
|
18
|
+
readonly name: string;
|
|
19
|
+
/** Runs once the harness is idle. */
|
|
20
|
+
run(): Promise<unknown> | unknown;
|
|
21
|
+
}
|
|
22
|
+
export interface DeferredCommandOutcome {
|
|
23
|
+
readonly status: "completed" | "failed" | "cancelled";
|
|
24
|
+
readonly value?: unknown;
|
|
25
|
+
readonly error?: unknown;
|
|
26
|
+
}
|
|
27
|
+
export interface CommandRef {
|
|
28
|
+
readonly commandId: string;
|
|
29
|
+
readonly name: string;
|
|
30
|
+
readonly status: DeferredCommandStatus;
|
|
31
|
+
/** Settles once the command completed, failed, or was cancelled. Never rejects. */
|
|
32
|
+
readonly done: Promise<DeferredCommandOutcome>;
|
|
33
|
+
/**
|
|
34
|
+
* Cancel while the command is still queued. Returns false once it has started:
|
|
35
|
+
* a running command owns its own lifecycle (a deferred command that begins a
|
|
36
|
+
* harness operation is stopped through the normal abort path instead).
|
|
37
|
+
*/
|
|
38
|
+
cancel(): boolean;
|
|
39
|
+
}
|
|
40
|
+
export declare class DeferredCommandQueue {
|
|
41
|
+
/** Insertion-ordered, so registration order is the execution order. */
|
|
42
|
+
private readonly entries;
|
|
43
|
+
private readonly isIdle;
|
|
44
|
+
private nextSequence;
|
|
45
|
+
private draining;
|
|
46
|
+
constructor(isIdle: () => boolean);
|
|
47
|
+
get size(): number;
|
|
48
|
+
enqueue(command: DeferredHarnessCommand): CommandRef;
|
|
49
|
+
/** Run queued commands in order while the harness stays idle. Never throws. */
|
|
50
|
+
drain(): Promise<void>;
|
|
51
|
+
private cancelEntry;
|
|
52
|
+
}
|
|
53
|
+
//# sourceMappingURL=deferred-commands.d.ts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"deferred-commands.d.ts","sourceRoot":"","sources":["../../src/harness/deferred-commands.ts"],"names":[],"mappings":"AAAA;;;;;;;;;;;;;GAaG;AAEH,MAAM,MAAM,qBAAqB,GAAG,QAAQ,GAAG,SAAS,GAAG,WAAW,GAAG,QAAQ,GAAG,WAAW,CAAC;AAEhG,MAAM,WAAW,sBAAsB;IACtC,sDAAsD;IACtD,QAAQ,CAAC,IAAI,EAAE,MAAM,CAAC;IACtB,qCAAqC;IACrC,GAAG,IAAI,OAAO,CAAC,OAAO,CAAC,GAAG,OAAO,CAAC;CAClC;AAED,MAAM,WAAW,sBAAsB;IACtC,QAAQ,CAAC,MAAM,EAAE,WAAW,GAAG,QAAQ,GAAG,WAAW,CAAC;IACtD,QAAQ,CAAC,KAAK,CAAC,EAAE,OAAO,CAAC;IACzB,QAAQ,CAAC,KAAK,CAAC,EAAE,OAAO,CAAC;CACzB;AAED,MAAM,WAAW,UAAU;IAC1B,QAAQ,CAAC,SAAS,EAAE,MAAM,CAAC;IAC3B,QAAQ,CAAC,IAAI,EAAE,MAAM,CAAC;IACtB,QAAQ,CAAC,MAAM,EAAE,qBAAqB,CAAC;IACvC,mFAAmF;IACnF,QAAQ,CAAC,IAAI,EAAE,OAAO,CAAC,sBAAsB,CAAC,CAAC;IAC/C;;;;OAIG;IACH,MAAM,IAAI,OAAO,CAAC;CAClB;AAWD,qBAAa,oBAAoB;IAChC,uEAAuE;IACvE,OAAO,CAAC,QAAQ,CAAC,OAAO,CAA4B;IACpD,OAAO,CAAC,QAAQ,CAAC,MAAM,CAAgB;IACvC,OAAO,CAAC,YAAY,CAAK;IACzB,OAAO,CAAC,QAAQ,CAAS;IAEzB,YAAY,MAAM,EAAE,MAAM,OAAO,EAEhC;IAED,IAAI,IAAI,IAAI,MAAM,CAEjB;IAED,OAAO,CAAC,OAAO,EAAE,sBAAsB,GAAG,UAAU,CA4BnD;IAED,+EAA+E;IACzE,KAAK,IAAI,OAAO,CAAC,IAAI,CAAC,CAwB3B;IAED,OAAO,CAAC,WAAW;CAOnB","sourcesContent":["/**\n * Deferred harness commands: the callback-safe alternative to waiting on the\n * operation a callback is inside.\n *\n * A listener that awaits `waitForIdle()` or `abortAndWait()` for the operation\n * whose emission it is blocking forms a cycle: settlement awaits the listener\n * and the listener awaits settlement. `AgentHarness.runWhenIdle()` breaks that\n * cycle by enqueueing the work and returning a ref without waiting, so the\n * callback never depends on the operation it is inside.\n *\n * Commands run in registration order, only while the harness reports idle, and\n * each outcome is captured in the ref instead of rejecting — a deferred command\n * is nobody's caller, so an unhandled rejection would be invisible.\n */\n\nexport type DeferredCommandStatus = \"queued\" | \"running\" | \"completed\" | \"failed\" | \"cancelled\";\n\nexport interface DeferredHarnessCommand {\n\t/** Short label used in the ref and in diagnostics. */\n\treadonly name: string;\n\t/** Runs once the harness is idle. */\n\trun(): Promise<unknown> | unknown;\n}\n\nexport interface DeferredCommandOutcome {\n\treadonly status: \"completed\" | \"failed\" | \"cancelled\";\n\treadonly value?: unknown;\n\treadonly error?: unknown;\n}\n\nexport interface CommandRef {\n\treadonly commandId: string;\n\treadonly name: string;\n\treadonly status: DeferredCommandStatus;\n\t/** Settles once the command completed, failed, or was cancelled. Never rejects. */\n\treadonly done: Promise<DeferredCommandOutcome>;\n\t/**\n\t * Cancel while the command is still queued. Returns false once it has started:\n\t * a running command owns its own lifecycle (a deferred command that begins a\n\t * harness operation is stopped through the normal abort path instead).\n\t */\n\tcancel(): boolean;\n}\n\ninterface Entry {\n\treadonly commandId: string;\n\treadonly name: string;\n\treadonly command: DeferredHarnessCommand;\n\treadonly done: Promise<DeferredCommandOutcome>;\n\treadonly resolveDone: (outcome: DeferredCommandOutcome) => void;\n\tstatus: DeferredCommandStatus;\n}\n\nexport class DeferredCommandQueue {\n\t/** Insertion-ordered, so registration order is the execution order. */\n\tprivate readonly entries = new Map<string, Entry>();\n\tprivate readonly isIdle: () => boolean;\n\tprivate nextSequence = 0;\n\tprivate draining = false;\n\n\tconstructor(isIdle: () => boolean) {\n\t\tthis.isIdle = isIdle;\n\t}\n\n\tget size(): number {\n\t\treturn this.entries.size;\n\t}\n\n\tenqueue(command: DeferredHarnessCommand): CommandRef {\n\t\tthis.nextSequence += 1;\n\t\tconst commandId = `cmd-${this.nextSequence}`;\n\t\tlet resolveDone: (outcome: DeferredCommandOutcome) => void = () => undefined;\n\t\tconst done = new Promise<DeferredCommandOutcome>((resolve) => {\n\t\t\tresolveDone = resolve;\n\t\t});\n\t\tconst entry: Entry = {\n\t\t\tcommandId,\n\t\t\tname: command.name,\n\t\t\tcommand,\n\t\t\tdone,\n\t\t\tresolveDone,\n\t\t\tstatus: \"queued\",\n\t\t};\n\t\tthis.entries.set(commandId, entry);\n\t\t// \"When idle\" includes now: an idle harness starts the command immediately,\n\t\t// while a busy one leaves it queued for the next idle transition.\n\t\tvoid this.drain();\n\t\treturn {\n\t\t\tcommandId,\n\t\t\tname: entry.name,\n\t\t\tget status() {\n\t\t\t\treturn entry.status;\n\t\t\t},\n\t\t\tdone,\n\t\t\tcancel: () => this.cancelEntry(entry),\n\t\t};\n\t}\n\n\t/** Run queued commands in order while the harness stays idle. Never throws. */\n\tasync drain(): Promise<void> {\n\t\tif (this.draining) return;\n\t\tthis.draining = true;\n\t\ttry {\n\t\t\tfor (;;) {\n\t\t\t\t// A command may start a new operation; the rest of the queue then waits\n\t\t\t\t// for the next idle transition instead of reentering the lifecycle.\n\t\t\t\tif (!this.isIdle()) return;\n\t\t\t\tconst entry = this.entries.values().next().value as Entry | undefined;\n\t\t\t\tif (entry === undefined) return;\n\t\t\t\tthis.entries.delete(entry.commandId);\n\t\t\t\tentry.status = \"running\";\n\t\t\t\ttry {\n\t\t\t\t\tconst value = await entry.command.run();\n\t\t\t\t\tentry.status = \"completed\";\n\t\t\t\t\tentry.resolveDone({ status: \"completed\", value });\n\t\t\t\t} catch (error) {\n\t\t\t\t\tentry.status = \"failed\";\n\t\t\t\t\tentry.resolveDone({ status: \"failed\", error });\n\t\t\t\t}\n\t\t\t}\n\t\t} finally {\n\t\t\tthis.draining = false;\n\t\t}\n\t}\n\n\tprivate cancelEntry(entry: Entry): boolean {\n\t\tif (entry.status !== \"queued\") return false;\n\t\tthis.entries.delete(entry.commandId);\n\t\tentry.status = \"cancelled\";\n\t\tentry.resolveDone({ status: \"cancelled\" });\n\t\treturn true;\n\t}\n}\n"]}
|
|
@@ -0,0 +1,96 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Deferred harness commands: the callback-safe alternative to waiting on the
|
|
3
|
+
* operation a callback is inside.
|
|
4
|
+
*
|
|
5
|
+
* A listener that awaits `waitForIdle()` or `abortAndWait()` for the operation
|
|
6
|
+
* whose emission it is blocking forms a cycle: settlement awaits the listener
|
|
7
|
+
* and the listener awaits settlement. `AgentHarness.runWhenIdle()` breaks that
|
|
8
|
+
* cycle by enqueueing the work and returning a ref without waiting, so the
|
|
9
|
+
* callback never depends on the operation it is inside.
|
|
10
|
+
*
|
|
11
|
+
* Commands run in registration order, only while the harness reports idle, and
|
|
12
|
+
* each outcome is captured in the ref instead of rejecting — a deferred command
|
|
13
|
+
* is nobody's caller, so an unhandled rejection would be invisible.
|
|
14
|
+
*/
|
|
15
|
+
export class DeferredCommandQueue {
|
|
16
|
+
/** Insertion-ordered, so registration order is the execution order. */
|
|
17
|
+
entries = new Map();
|
|
18
|
+
isIdle;
|
|
19
|
+
nextSequence = 0;
|
|
20
|
+
draining = false;
|
|
21
|
+
constructor(isIdle) {
|
|
22
|
+
this.isIdle = isIdle;
|
|
23
|
+
}
|
|
24
|
+
get size() {
|
|
25
|
+
return this.entries.size;
|
|
26
|
+
}
|
|
27
|
+
enqueue(command) {
|
|
28
|
+
this.nextSequence += 1;
|
|
29
|
+
const commandId = `cmd-${this.nextSequence}`;
|
|
30
|
+
let resolveDone = () => undefined;
|
|
31
|
+
const done = new Promise((resolve) => {
|
|
32
|
+
resolveDone = resolve;
|
|
33
|
+
});
|
|
34
|
+
const entry = {
|
|
35
|
+
commandId,
|
|
36
|
+
name: command.name,
|
|
37
|
+
command,
|
|
38
|
+
done,
|
|
39
|
+
resolveDone,
|
|
40
|
+
status: "queued",
|
|
41
|
+
};
|
|
42
|
+
this.entries.set(commandId, entry);
|
|
43
|
+
// "When idle" includes now: an idle harness starts the command immediately,
|
|
44
|
+
// while a busy one leaves it queued for the next idle transition.
|
|
45
|
+
void this.drain();
|
|
46
|
+
return {
|
|
47
|
+
commandId,
|
|
48
|
+
name: entry.name,
|
|
49
|
+
get status() {
|
|
50
|
+
return entry.status;
|
|
51
|
+
},
|
|
52
|
+
done,
|
|
53
|
+
cancel: () => this.cancelEntry(entry),
|
|
54
|
+
};
|
|
55
|
+
}
|
|
56
|
+
/** Run queued commands in order while the harness stays idle. Never throws. */
|
|
57
|
+
async drain() {
|
|
58
|
+
if (this.draining)
|
|
59
|
+
return;
|
|
60
|
+
this.draining = true;
|
|
61
|
+
try {
|
|
62
|
+
for (;;) {
|
|
63
|
+
// A command may start a new operation; the rest of the queue then waits
|
|
64
|
+
// for the next idle transition instead of reentering the lifecycle.
|
|
65
|
+
if (!this.isIdle())
|
|
66
|
+
return;
|
|
67
|
+
const entry = this.entries.values().next().value;
|
|
68
|
+
if (entry === undefined)
|
|
69
|
+
return;
|
|
70
|
+
this.entries.delete(entry.commandId);
|
|
71
|
+
entry.status = "running";
|
|
72
|
+
try {
|
|
73
|
+
const value = await entry.command.run();
|
|
74
|
+
entry.status = "completed";
|
|
75
|
+
entry.resolveDone({ status: "completed", value });
|
|
76
|
+
}
|
|
77
|
+
catch (error) {
|
|
78
|
+
entry.status = "failed";
|
|
79
|
+
entry.resolveDone({ status: "failed", error });
|
|
80
|
+
}
|
|
81
|
+
}
|
|
82
|
+
}
|
|
83
|
+
finally {
|
|
84
|
+
this.draining = false;
|
|
85
|
+
}
|
|
86
|
+
}
|
|
87
|
+
cancelEntry(entry) {
|
|
88
|
+
if (entry.status !== "queued")
|
|
89
|
+
return false;
|
|
90
|
+
this.entries.delete(entry.commandId);
|
|
91
|
+
entry.status = "cancelled";
|
|
92
|
+
entry.resolveDone({ status: "cancelled" });
|
|
93
|
+
return true;
|
|
94
|
+
}
|
|
95
|
+
}
|
|
96
|
+
//# sourceMappingURL=deferred-commands.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"deferred-commands.js","sourceRoot":"","sources":["../../src/harness/deferred-commands.ts"],"names":[],"mappings":"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","sourcesContent":["/**\n * Deferred harness commands: the callback-safe alternative to waiting on the\n * operation a callback is inside.\n *\n * A listener that awaits `waitForIdle()` or `abortAndWait()` for the operation\n * whose emission it is blocking forms a cycle: settlement awaits the listener\n * and the listener awaits settlement. `AgentHarness.runWhenIdle()` breaks that\n * cycle by enqueueing the work and returning a ref without waiting, so the\n * callback never depends on the operation it is inside.\n *\n * Commands run in registration order, only while the harness reports idle, and\n * each outcome is captured in the ref instead of rejecting — a deferred command\n * is nobody's caller, so an unhandled rejection would be invisible.\n */\n\nexport type DeferredCommandStatus = \"queued\" | \"running\" | \"completed\" | \"failed\" | \"cancelled\";\n\nexport interface DeferredHarnessCommand {\n\t/** Short label used in the ref and in diagnostics. */\n\treadonly name: string;\n\t/** Runs once the harness is idle. */\n\trun(): Promise<unknown> | unknown;\n}\n\nexport interface DeferredCommandOutcome {\n\treadonly status: \"completed\" | \"failed\" | \"cancelled\";\n\treadonly value?: unknown;\n\treadonly error?: unknown;\n}\n\nexport interface CommandRef {\n\treadonly commandId: string;\n\treadonly name: string;\n\treadonly status: DeferredCommandStatus;\n\t/** Settles once the command completed, failed, or was cancelled. Never rejects. */\n\treadonly done: Promise<DeferredCommandOutcome>;\n\t/**\n\t * Cancel while the command is still queued. Returns false once it has started:\n\t * a running command owns its own lifecycle (a deferred command that begins a\n\t * harness operation is stopped through the normal abort path instead).\n\t */\n\tcancel(): boolean;\n}\n\ninterface Entry {\n\treadonly commandId: string;\n\treadonly name: string;\n\treadonly command: DeferredHarnessCommand;\n\treadonly done: Promise<DeferredCommandOutcome>;\n\treadonly resolveDone: (outcome: DeferredCommandOutcome) => void;\n\tstatus: DeferredCommandStatus;\n}\n\nexport class DeferredCommandQueue {\n\t/** Insertion-ordered, so registration order is the execution order. */\n\tprivate readonly entries = new Map<string, Entry>();\n\tprivate readonly isIdle: () => boolean;\n\tprivate nextSequence = 0;\n\tprivate draining = false;\n\n\tconstructor(isIdle: () => boolean) {\n\t\tthis.isIdle = isIdle;\n\t}\n\n\tget size(): number {\n\t\treturn this.entries.size;\n\t}\n\n\tenqueue(command: DeferredHarnessCommand): CommandRef {\n\t\tthis.nextSequence += 1;\n\t\tconst commandId = `cmd-${this.nextSequence}`;\n\t\tlet resolveDone: (outcome: DeferredCommandOutcome) => void = () => undefined;\n\t\tconst done = new Promise<DeferredCommandOutcome>((resolve) => {\n\t\t\tresolveDone = resolve;\n\t\t});\n\t\tconst entry: Entry = {\n\t\t\tcommandId,\n\t\t\tname: command.name,\n\t\t\tcommand,\n\t\t\tdone,\n\t\t\tresolveDone,\n\t\t\tstatus: \"queued\",\n\t\t};\n\t\tthis.entries.set(commandId, entry);\n\t\t// \"When idle\" includes now: an idle harness starts the command immediately,\n\t\t// while a busy one leaves it queued for the next idle transition.\n\t\tvoid this.drain();\n\t\treturn {\n\t\t\tcommandId,\n\t\t\tname: entry.name,\n\t\t\tget status() {\n\t\t\t\treturn entry.status;\n\t\t\t},\n\t\t\tdone,\n\t\t\tcancel: () => this.cancelEntry(entry),\n\t\t};\n\t}\n\n\t/** Run queued commands in order while the harness stays idle. Never throws. */\n\tasync drain(): Promise<void> {\n\t\tif (this.draining) return;\n\t\tthis.draining = true;\n\t\ttry {\n\t\t\tfor (;;) {\n\t\t\t\t// A command may start a new operation; the rest of the queue then waits\n\t\t\t\t// for the next idle transition instead of reentering the lifecycle.\n\t\t\t\tif (!this.isIdle()) return;\n\t\t\t\tconst entry = this.entries.values().next().value as Entry | undefined;\n\t\t\t\tif (entry === undefined) return;\n\t\t\t\tthis.entries.delete(entry.commandId);\n\t\t\t\tentry.status = \"running\";\n\t\t\t\ttry {\n\t\t\t\t\tconst value = await entry.command.run();\n\t\t\t\t\tentry.status = \"completed\";\n\t\t\t\t\tentry.resolveDone({ status: \"completed\", value });\n\t\t\t\t} catch (error) {\n\t\t\t\t\tentry.status = \"failed\";\n\t\t\t\t\tentry.resolveDone({ status: \"failed\", error });\n\t\t\t\t}\n\t\t\t}\n\t\t} finally {\n\t\t\tthis.draining = false;\n\t\t}\n\t}\n\n\tprivate cancelEntry(entry: Entry): boolean {\n\t\tif (entry.status !== \"queued\") return false;\n\t\tthis.entries.delete(entry.commandId);\n\t\tentry.status = \"cancelled\";\n\t\tentry.resolveDone({ status: \"cancelled\" });\n\t\treturn true;\n\t}\n}\n"]}
|
|
@@ -47,20 +47,19 @@ export declare function resolveOperationOutcome<T>(input: {
|
|
|
47
47
|
readonly fallbackCode: AgentHarnessError["code"];
|
|
48
48
|
}): HarnessOperationOutcome;
|
|
49
49
|
/**
|
|
50
|
-
*
|
|
51
|
-
*
|
|
52
|
-
*
|
|
53
|
-
* `AggregateError`, classified by the first (primary) failure. This is what
|
|
54
|
-
* lets a failing boundary flush report *alongside* the body or listener error
|
|
55
|
-
* it followed instead of erasing it.
|
|
50
|
+
* Run boundary steps in order and collect their errors instead of stopping at
|
|
51
|
+
* the first. A boundary that must still report, flush, or settle after one step
|
|
52
|
+
* fails uses this so one failure cannot strand the rest.
|
|
56
53
|
*/
|
|
57
|
-
export declare function
|
|
54
|
+
export declare function collectStepErrors(steps: ReadonlyArray<() => Promise<void> | void>): Promise<Error[]>;
|
|
58
55
|
/**
|
|
59
56
|
* Which error a public operation rejects with, or `undefined` on success.
|
|
60
57
|
*
|
|
61
|
-
*
|
|
62
|
-
*
|
|
63
|
-
*
|
|
58
|
+
* The top-level code comes from the same `flush > body` source the recorded
|
|
59
|
+
* outcome uses, so `outcome.code === rejection.code` for every failed outcome;
|
|
60
|
+
* settlement only ever contributes a cause. Every concurrent cause stays
|
|
61
|
+
* reachable through one `AggregateError` in body, flush, settle order, so an
|
|
62
|
+
* audit can still see that, say, the body and the final flush failed together.
|
|
64
63
|
*/
|
|
65
64
|
export declare function resolveOperationFailure(input: {
|
|
66
65
|
readonly bodyError: unknown;
|
|
@@ -68,4 +67,13 @@ export declare function resolveOperationFailure(input: {
|
|
|
68
67
|
readonly settleError: unknown;
|
|
69
68
|
readonly fallbackCode: AgentHarnessError["code"];
|
|
70
69
|
}): AgentHarnessError | undefined;
|
|
70
|
+
/**
|
|
71
|
+
* The error a boundary should throw after several steps may have failed, or
|
|
72
|
+
* `undefined` when none did. A single failure is returned untouched so its
|
|
73
|
+
* own classification survives; several are kept reachable through one
|
|
74
|
+
* `AggregateError`, classified by the first (primary) failure. This is what
|
|
75
|
+
* lets a failing boundary flush report *alongside* the body or listener error
|
|
76
|
+
* it followed instead of erasing it.
|
|
77
|
+
*/
|
|
78
|
+
export declare function combineBoundaryErrors(errors: readonly unknown[], message: string, fallbackCode: AgentHarnessError["code"]): unknown;
|
|
71
79
|
//# sourceMappingURL=operation-outcome.d.ts.map
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"operation-outcome.d.ts","sourceRoot":"","sources":["../../src/harness/operation-outcome.ts"],"names":[],"mappings":"AAAA;;;;;;;GAOG;AAEH,OAAO,EAAE,KAAK,gBAAgB,EAAqB,MAAM,QAAQ,CAAC;AAClE,OAAO,KAAK,EAAE,qBAAqB,EAAE,uBAAuB,EAAE,MAAM,gCAAgC,CAAC;AACrG,OAAO,KAAK,EAAE,kBAAkB,EAAE,MAAM,YAAY,CAAC;AACrD,OAAO,EAAE,iBAAiB,EAA8D,MAAM,YAAY,CAAC;AAE3G;;;;;GAKG;AACH,wBAAgB,oBAAoB,CAAC,KAAK,EAAE,OAAO,GAAG,OAAO,CAI5D;AAED,iFAAiF;AACjF,wBAAgB,qBAAqB,CAAC,KAAK,EAAE,OAAO,EAAE,YAAY,EAAE,iBAAiB,CAAC,MAAM,CAAC,GAAG,iBAAiB,CAOhH;AAED,6GAA6G;AAC7G,wBAAgB,wBAAwB,CAAC,OAAO,EAAE,gBAAgB,GAAG,uBAAuB,GAAG,SAAS,CAMvG;AAED,2EAA2E;AAC3E,wBAAgB,2BAA2B,CAAC,MAAM,EAAE,kBAAkB,GAAG,uBAAuB,CAE/F;AAED,0FAA0F;AAC1F,wBAAgB,sBAAsB,CAAC,KAAK,EAAE,OAAO,GAAG,qBAAqB,CAE5E;AAED,iFAAiF;AACjF,wBAAgB,sBAAsB,CACrC,OAAO,EAAE,gBAAgB,EACzB,aAAa,EAAE,MAAM,GAAG,SAAS,GAC/B,qBAAqB,CAKvB;AAED;;;;;;;;;;GAUG;AACH,wBAAgB,uBAAuB,CAAC,CAAC,EAAE,KAAK,EAAE;IACjD,QAAQ,CAAC,aAAa,EAAE,OAAO,CAAC;IAChC,QAAQ,CAAC,MAAM,EAAE,CAAC,GAAG,SAAS,CAAC;IAC/B,QAAQ,CAAC,SAAS,EAAE,OAAO,CAAC;IAC5B,QAAQ,CAAC,UAAU,EAAE,OAAO,CAAC;IAC7B,QAAQ,CAAC,cAAc,EAAE,CAAC,CAAC,MAAM,EAAE,CAAC,KAAK,uBAAuB,GAAG,SAAS,CAAC,GAAG,SAAS,CAAC;IAC1F,QAAQ,CAAC,YAAY,EAAE,iBAAiB,CAAC,MAAM,CAAC,CAAC;CACjD,GAAG,uBAAuB,CAiB1B;AAED;;;;;;;GAOG;AACH,wBAAgB,qBAAqB,CACpC,MAAM,EAAE,SAAS,OAAO,EAAE,EAC1B,OAAO,EAAE,MAAM,EACf,YAAY,EAAE,iBAAiB,CAAC,MAAM,CAAC,GACrC,OAAO,CAKT;AAED;;;;;;GAMG;AACH,wBAAgB,uBAAuB,CAAC,KAAK,EAAE;IAC9C,QAAQ,CAAC,SAAS,EAAE,OAAO,CAAC;IAC5B,QAAQ,CAAC,UAAU,EAAE,OAAO,CAAC;IAC7B,QAAQ,CAAC,WAAW,EAAE,OAAO,CAAC;IAC9B,QAAQ,CAAC,YAAY,EAAE,iBAAiB,CAAC,MAAM,CAAC,CAAC;CACjD,GAAG,iBAAiB,GAAG,SAAS,CAoBhC","sourcesContent":["/**\n * Pure outcome classification and error aggregation for harness operations.\n *\n * Everything here is a total function over already-observed results: it never\n * touches lifecycle state, sessions, providers, or clocks. Keeping the rules in\n * one leaf module makes the precedence auditable in isolation and keeps\n * `agent-harness.ts` free of the branch-heavy classification tables.\n */\n\nimport { type AssistantMessage, isContextOverflow } from \"omk-ai\";\nimport type { HarnessAttemptOutcome, HarnessOperationOutcome } from \"./operation-lifecycle-types.ts\";\nimport type { NavigateTreeResult } from \"./types.ts\";\nimport { AgentHarnessError, BranchSummaryError, CompactionError, SessionError, toError } from \"./types.ts\";\n\n/**\n * True only for an error that *is* an abort, not merely an error raised while\n * an abort signal happened to be up. `AgentHarnessError` has no \"aborted\" code,\n * so an explicit abort reaches us as a subsystem error carrying code \"aborted\"\n * or as a DOM-style `AbortError`.\n */\nexport function isExplicitAbortError(error: unknown): boolean {\n\tconst cause = toError(error);\n\tif (cause.name === \"AbortError\") return true;\n\treturn (cause instanceof CompactionError || cause instanceof BranchSummaryError) && cause.code === \"aborted\";\n}\n\n/** Map a subsystem failure onto the harness' stable top-level classification. */\nexport function normalizeHarnessError(error: unknown, fallbackCode: AgentHarnessError[\"code\"]): AgentHarnessError {\n\tif (error instanceof AgentHarnessError) return error;\n\tconst cause = toError(error);\n\tif (cause instanceof SessionError) return new AgentHarnessError(\"session\", cause.message, cause);\n\tif (cause instanceof CompactionError) return new AgentHarnessError(\"compaction\", cause.message, cause);\n\tif (cause instanceof BranchSummaryError) return new AgentHarnessError(\"branch_summary\", cause.message, cause);\n\treturn new AgentHarnessError(fallbackCode, cause.message, cause);\n}\n\n/** Result-based outcome for prompt-family operations that resolve with a failure/abort assistant message. */\nexport function classifyAssistantOutcome(message: AssistantMessage): HarnessOperationOutcome | undefined {\n\tif (message.stopReason === \"aborted\") return { status: \"aborted\" };\n\tif (message.stopReason === \"error\") {\n\t\treturn { status: \"failed\", code: \"provider\", message: message.errorMessage ?? \"Provider error\" };\n\t}\n\treturn undefined;\n}\n\n/** Structural cancellation is a distinct, non-failure terminal outcome. */\nexport function classifyNavigateTreeOutcome(result: NavigateTreeResult): HarnessOperationOutcome {\n\treturn result.cancelled ? { status: \"cancelled\", reason: \"tree_navigation_cancelled\" } : { status: \"completed\" };\n}\n\n/** A thrown attempt body is an aborted attempt only when the error itself is an abort. */\nexport function classifyAttemptFailure(error: unknown): HarnessAttemptOutcome {\n\treturn isExplicitAbortError(error) ? \"aborted\" : \"failed\";\n}\n\n/** Context overflow is a recoverable attempt outcome, not an attempt failure. */\nexport function classifyAttemptOutcome(\n\tmessage: AssistantMessage,\n\tcontextWindow: number | undefined,\n): HarnessAttemptOutcome {\n\tif (message.stopReason === \"aborted\") return \"aborted\";\n\tif (isContextOverflow(message, contextWindow)) return \"overflow\";\n\tif (message.stopReason === \"error\") return \"failed\";\n\treturn \"completed\";\n}\n\n/**\n * Single outcome-precedence rule for every public operation:\n *\n * session persistence failure > non-abort body/hook failure >\n * explicit abort > result-classified outcome > completed\n *\n * A raised abort signal alone never downgrades another failure to \"aborted\":\n * only an error that *is* an abort does. Otherwise a flush failure during an\n * aborted turn would settle as \"aborted\" while the public promise rejected\n * with \"session\".\n */\nexport function resolveOperationOutcome<T>(input: {\n\treadonly signalAborted: boolean;\n\treadonly result: T | undefined;\n\treadonly bodyError: unknown;\n\treadonly flushError: unknown;\n\treadonly classifyResult: ((result: T) => HarnessOperationOutcome | undefined) | undefined;\n\treadonly fallbackCode: AgentHarnessError[\"code\"];\n}): HarnessOperationOutcome {\n\tif (input.flushError !== undefined) {\n\t\t// Mirror `resolveOperationFailure`: a flush error that already carries a\n\t\t// harness classification (e.g. an `invalid_state` coordinator reentry)\n\t\t// keeps it, so the recorded outcome and the rejection never disagree.\n\t\tconst error = normalizeHarnessError(input.flushError, \"session\");\n\t\treturn { status: \"failed\", code: error.code, message: error.message };\n\t}\n\tif (input.bodyError !== undefined) {\n\t\tif (isExplicitAbortError(input.bodyError)) return { status: \"aborted\" };\n\t\tconst error = normalizeHarnessError(input.bodyError, input.fallbackCode);\n\t\treturn { status: \"failed\", code: error.code, message: error.message };\n\t}\n\treturn (\n\t\tinput.classifyResult?.(input.result as T) ??\n\t\t(input.signalAborted ? { status: \"aborted\" } : { status: \"completed\" })\n\t);\n}\n\n/**\n * The error a boundary should throw after several steps may have failed, or\n * `undefined` when none did. A single failure is returned untouched so its\n * own classification survives; several are kept reachable through one\n * `AggregateError`, classified by the first (primary) failure. This is what\n * lets a failing boundary flush report *alongside* the body or listener error\n * it followed instead of erasing it.\n */\nexport function combineBoundaryErrors(\n\terrors: readonly unknown[],\n\tmessage: string,\n\tfallbackCode: AgentHarnessError[\"code\"],\n): unknown {\n\tconst present = errors.filter((error) => error !== undefined);\n\tif (present.length <= 1) return present[0];\n\tconst cause = new AggregateError(present.map(toError), message);\n\treturn new AgentHarnessError(normalizeHarnessError(present[0], fallbackCode).code, cause.message, cause);\n}\n\n/**\n * Which error a public operation rejects with, or `undefined` on success.\n *\n * Mirrors the outcome precedence, but every concurrent cause is preserved in an\n * `AggregateError` so an audit can still see that, say, the body and the final\n * flush failed together.\n */\nexport function resolveOperationFailure(input: {\n\treadonly bodyError: unknown;\n\treadonly flushError: unknown;\n\treadonly settleError: unknown;\n\treadonly fallbackCode: AgentHarnessError[\"code\"];\n}): AgentHarnessError | undefined {\n\tconst primaryError = input.bodyError ?? input.flushError;\n\tif (primaryError !== undefined && input.settleError !== undefined) {\n\t\tconst cause = new AggregateError(\n\t\t\t[toError(primaryError), toError(input.settleError)],\n\t\t\t\"Operation failed and settlement failed\",\n\t\t);\n\t\treturn new AgentHarnessError(normalizeHarnessError(primaryError, input.fallbackCode).code, cause.message, cause);\n\t}\n\tif (input.settleError !== undefined) return normalizeHarnessError(input.settleError, \"hook\");\n\tif (input.flushError !== undefined) {\n\t\tif (input.bodyError === undefined) return normalizeHarnessError(input.flushError, \"session\");\n\t\tconst cause = new AggregateError(\n\t\t\t[toError(input.bodyError), toError(input.flushError)],\n\t\t\t\"Operation failed and the final flush failed\",\n\t\t);\n\t\treturn new AgentHarnessError(\"session\", cause.message, cause);\n\t}\n\tif (input.bodyError !== undefined) return normalizeHarnessError(input.bodyError, input.fallbackCode);\n\treturn undefined;\n}\n"]}
|
|
1
|
+
{"version":3,"file":"operation-outcome.d.ts","sourceRoot":"","sources":["../../src/harness/operation-outcome.ts"],"names":[],"mappings":"AAAA;;;;;;;GAOG;AAEH,OAAO,EAAE,KAAK,gBAAgB,EAAqB,MAAM,QAAQ,CAAC;AAClE,OAAO,KAAK,EAAE,qBAAqB,EAAE,uBAAuB,EAAE,MAAM,gCAAgC,CAAC;AACrG,OAAO,KAAK,EAAE,kBAAkB,EAAE,MAAM,YAAY,CAAC;AACrD,OAAO,EAAE,iBAAiB,EAA8D,MAAM,YAAY,CAAC;AAE3G;;;;;GAKG;AACH,wBAAgB,oBAAoB,CAAC,KAAK,EAAE,OAAO,GAAG,OAAO,CAI5D;AAED,iFAAiF;AACjF,wBAAgB,qBAAqB,CAAC,KAAK,EAAE,OAAO,EAAE,YAAY,EAAE,iBAAiB,CAAC,MAAM,CAAC,GAAG,iBAAiB,CAOhH;AAED,6GAA6G;AAC7G,wBAAgB,wBAAwB,CAAC,OAAO,EAAE,gBAAgB,GAAG,uBAAuB,GAAG,SAAS,CAMvG;AAED,2EAA2E;AAC3E,wBAAgB,2BAA2B,CAAC,MAAM,EAAE,kBAAkB,GAAG,uBAAuB,CAE/F;AAED,0FAA0F;AAC1F,wBAAgB,sBAAsB,CAAC,KAAK,EAAE,OAAO,GAAG,qBAAqB,CAE5E;AAED,iFAAiF;AACjF,wBAAgB,sBAAsB,CACrC,OAAO,EAAE,gBAAgB,EACzB,aAAa,EAAE,MAAM,GAAG,SAAS,GAC/B,qBAAqB,CAKvB;AA2BD;;;;;;;;;;GAUG;AACH,wBAAgB,uBAAuB,CAAC,CAAC,EAAE,KAAK,EAAE;IACjD,QAAQ,CAAC,aAAa,EAAE,OAAO,CAAC;IAChC,QAAQ,CAAC,MAAM,EAAE,CAAC,GAAG,SAAS,CAAC;IAC/B,QAAQ,CAAC,SAAS,EAAE,OAAO,CAAC;IAC5B,QAAQ,CAAC,UAAU,EAAE,OAAO,CAAC;IAC7B,QAAQ,CAAC,cAAc,EAAE,CAAC,CAAC,MAAM,EAAE,CAAC,KAAK,uBAAuB,GAAG,SAAS,CAAC,GAAG,SAAS,CAAC;IAC1F,QAAQ,CAAC,YAAY,EAAE,iBAAiB,CAAC,MAAM,CAAC,CAAC;CACjD,GAAG,uBAAuB,CAa1B;AAED;;;;GAIG;AACH,wBAAsB,iBAAiB,CAAC,KAAK,EAAE,aAAa,CAAC,MAAM,OAAO,CAAC,IAAI,CAAC,GAAG,IAAI,CAAC,GAAG,OAAO,CAAC,KAAK,EAAE,CAAC,CAU1G;AAED;;;;;;;;GAQG;AACH,wBAAgB,uBAAuB,CAAC,KAAK,EAAE;IAC9C,QAAQ,CAAC,SAAS,EAAE,OAAO,CAAC;IAC5B,QAAQ,CAAC,UAAU,EAAE,OAAO,CAAC;IAC7B,QAAQ,CAAC,WAAW,EAAE,OAAO,CAAC;IAC9B,QAAQ,CAAC,YAAY,EAAE,iBAAiB,CAAC,MAAM,CAAC,CAAC;CACjD,GAAG,iBAAiB,GAAG,SAAS,CAiBhC;AAED;;;;;;;GAOG;AACH,wBAAgB,qBAAqB,CACpC,MAAM,EAAE,SAAS,OAAO,EAAE,EAC1B,OAAO,EAAE,MAAM,EACf,YAAY,EAAE,iBAAiB,CAAC,MAAM,CAAC,GACrC,OAAO,CAKT","sourcesContent":["/**\n * Pure outcome classification and error aggregation for harness operations.\n *\n * Everything here is a total function over already-observed results: it never\n * touches lifecycle state, sessions, providers, or clocks. Keeping the rules in\n * one leaf module makes the precedence auditable in isolation and keeps\n * `agent-harness.ts` free of the branch-heavy classification tables.\n */\n\nimport { type AssistantMessage, isContextOverflow } from \"omk-ai\";\nimport type { HarnessAttemptOutcome, HarnessOperationOutcome } from \"./operation-lifecycle-types.ts\";\nimport type { NavigateTreeResult } from \"./types.ts\";\nimport { AgentHarnessError, BranchSummaryError, CompactionError, SessionError, toError } from \"./types.ts\";\n\n/**\n * True only for an error that *is* an abort, not merely an error raised while\n * an abort signal happened to be up. `AgentHarnessError` has no \"aborted\" code,\n * so an explicit abort reaches us as a subsystem error carrying code \"aborted\"\n * or as a DOM-style `AbortError`.\n */\nexport function isExplicitAbortError(error: unknown): boolean {\n\tconst cause = toError(error);\n\tif (cause.name === \"AbortError\") return true;\n\treturn (cause instanceof CompactionError || cause instanceof BranchSummaryError) && cause.code === \"aborted\";\n}\n\n/** Map a subsystem failure onto the harness' stable top-level classification. */\nexport function normalizeHarnessError(error: unknown, fallbackCode: AgentHarnessError[\"code\"]): AgentHarnessError {\n\tif (error instanceof AgentHarnessError) return error;\n\tconst cause = toError(error);\n\tif (cause instanceof SessionError) return new AgentHarnessError(\"session\", cause.message, cause);\n\tif (cause instanceof CompactionError) return new AgentHarnessError(\"compaction\", cause.message, cause);\n\tif (cause instanceof BranchSummaryError) return new AgentHarnessError(\"branch_summary\", cause.message, cause);\n\treturn new AgentHarnessError(fallbackCode, cause.message, cause);\n}\n\n/** Result-based outcome for prompt-family operations that resolve with a failure/abort assistant message. */\nexport function classifyAssistantOutcome(message: AssistantMessage): HarnessOperationOutcome | undefined {\n\tif (message.stopReason === \"aborted\") return { status: \"aborted\" };\n\tif (message.stopReason === \"error\") {\n\t\treturn { status: \"failed\", code: \"provider\", message: message.errorMessage ?? \"Provider error\" };\n\t}\n\treturn undefined;\n}\n\n/** Structural cancellation is a distinct, non-failure terminal outcome. */\nexport function classifyNavigateTreeOutcome(result: NavigateTreeResult): HarnessOperationOutcome {\n\treturn result.cancelled ? { status: \"cancelled\", reason: \"tree_navigation_cancelled\" } : { status: \"completed\" };\n}\n\n/** A thrown attempt body is an aborted attempt only when the error itself is an abort. */\nexport function classifyAttemptFailure(error: unknown): HarnessAttemptOutcome {\n\treturn isExplicitAbortError(error) ? \"aborted\" : \"failed\";\n}\n\n/** Context overflow is a recoverable attempt outcome, not an attempt failure. */\nexport function classifyAttemptOutcome(\n\tmessage: AssistantMessage,\n\tcontextWindow: number | undefined,\n): HarnessAttemptOutcome {\n\tif (message.stopReason === \"aborted\") return \"aborted\";\n\tif (isContextOverflow(message, contextWindow)) return \"overflow\";\n\tif (message.stopReason === \"error\") return \"failed\";\n\treturn \"completed\";\n}\n\n/**\n * The single classification source for a failed operation: `flush > body`.\n *\n * Session persistence outranks the body because a flush failure after a provider\n * success must never record or report a completed operation, and a flush error\n * that already carries a harness classification (e.g. an `invalid_state`\n * coordinator reentry) keeps it. Both the recorded outcome and the public\n * rejection read their top-level code from here, so the two can never disagree.\n * Settlement is not a source: it runs after the outcome is recorded, so it can\n * only add a cause and a rejection.\n */\nfunction classificationSource(input: {\n\treadonly bodyError: unknown;\n\treadonly flushError: unknown;\n\treadonly fallbackCode: AgentHarnessError[\"code\"];\n}):\n\t| { readonly stage: \"body\" | \"flush\"; readonly error: unknown; readonly fallbackCode: AgentHarnessError[\"code\"] }\n\t| undefined {\n\tif (input.flushError !== undefined) return { stage: \"flush\", error: input.flushError, fallbackCode: \"session\" };\n\tif (input.bodyError !== undefined) {\n\t\treturn { stage: \"body\", error: input.bodyError, fallbackCode: input.fallbackCode };\n\t}\n\treturn undefined;\n}\n\n/**\n * Single outcome-precedence rule for every public operation:\n *\n * session persistence failure > non-abort body/hook failure >\n * explicit abort > result-classified outcome > completed\n *\n * A raised abort signal alone never downgrades another failure to \"aborted\":\n * only an error that *is* an abort does. Otherwise a flush failure during an\n * aborted turn would settle as \"aborted\" while the public promise rejected\n * with \"session\".\n */\nexport function resolveOperationOutcome<T>(input: {\n\treadonly signalAborted: boolean;\n\treadonly result: T | undefined;\n\treadonly bodyError: unknown;\n\treadonly flushError: unknown;\n\treadonly classifyResult: ((result: T) => HarnessOperationOutcome | undefined) | undefined;\n\treadonly fallbackCode: AgentHarnessError[\"code\"];\n}): HarnessOperationOutcome {\n\tconst source = classificationSource(input);\n\tif (source !== undefined) {\n\t\t// Only a body failure can be an abort; a flush or settle failure is a failure\n\t\t// even when the abort signal happens to be up.\n\t\tif (source.stage === \"body\" && isExplicitAbortError(source.error)) return { status: \"aborted\" };\n\t\tconst error = normalizeHarnessError(source.error, source.fallbackCode);\n\t\treturn { status: \"failed\", code: error.code, message: error.message };\n\t}\n\treturn (\n\t\tinput.classifyResult?.(input.result as T) ??\n\t\t(input.signalAborted ? { status: \"aborted\" } : { status: \"completed\" })\n\t);\n}\n\n/**\n * Run boundary steps in order and collect their errors instead of stopping at\n * the first. A boundary that must still report, flush, or settle after one step\n * fails uses this so one failure cannot strand the rest.\n */\nexport async function collectStepErrors(steps: ReadonlyArray<() => Promise<void> | void>): Promise<Error[]> {\n\tconst errors: Error[] = [];\n\tfor (const step of steps) {\n\t\ttry {\n\t\t\tawait step();\n\t\t} catch (error) {\n\t\t\terrors.push(toError(error));\n\t\t}\n\t}\n\treturn errors;\n}\n\n/**\n * Which error a public operation rejects with, or `undefined` on success.\n *\n * The top-level code comes from the same `flush > body` source the recorded\n * outcome uses, so `outcome.code === rejection.code` for every failed outcome;\n * settlement only ever contributes a cause. Every concurrent cause stays\n * reachable through one `AggregateError` in body, flush, settle order, so an\n * audit can still see that, say, the body and the final flush failed together.\n */\nexport function resolveOperationFailure(input: {\n\treadonly bodyError: unknown;\n\treadonly flushError: unknown;\n\treadonly settleError: unknown;\n\treadonly fallbackCode: AgentHarnessError[\"code\"];\n}): AgentHarnessError | undefined {\n\tconst causes = [input.bodyError, input.flushError, input.settleError].filter((error) => error !== undefined);\n\tif (causes.length === 0) return undefined;\n\tconst source = classificationSource(input) ?? {\n\t\tstage: \"settle\" as const,\n\t\terror: input.settleError,\n\t\tfallbackCode: \"hook\" as const,\n\t};\n\tconst code = normalizeHarnessError(source.error, source.fallbackCode).code;\n\tif (causes.length === 1) return normalizeHarnessError(causes[0], source.fallbackCode);\n\tconst stages = [\n\t\tinput.bodyError !== undefined ? \"body\" : undefined,\n\t\tinput.flushError !== undefined ? \"final flush\" : undefined,\n\t\tinput.settleError !== undefined ? \"settlement\" : undefined,\n\t].filter((stage) => stage !== undefined);\n\tconst cause = new AggregateError(causes.map(toError), `Operation failed (${stages.join(\", \")})`);\n\treturn new AgentHarnessError(code, cause.message, cause);\n}\n\n/**\n * The error a boundary should throw after several steps may have failed, or\n * `undefined` when none did. A single failure is returned untouched so its\n * own classification survives; several are kept reachable through one\n * `AggregateError`, classified by the first (primary) failure. This is what\n * lets a failing boundary flush report *alongside* the body or listener error\n * it followed instead of erasing it.\n */\nexport function combineBoundaryErrors(\n\terrors: readonly unknown[],\n\tmessage: string,\n\tfallbackCode: AgentHarnessError[\"code\"],\n): unknown {\n\tconst present = errors.filter((error) => error !== undefined);\n\tif (present.length <= 1) return present[0];\n\tconst cause = new AggregateError(present.map(toError), message);\n\treturn new AgentHarnessError(normalizeHarnessError(present[0], fallbackCode).code, cause.message, cause);\n}\n"]}
|
|
@@ -60,6 +60,25 @@ export function classifyAttemptOutcome(message, contextWindow) {
|
|
|
60
60
|
return "failed";
|
|
61
61
|
return "completed";
|
|
62
62
|
}
|
|
63
|
+
/**
|
|
64
|
+
* The single classification source for a failed operation: `flush > body`.
|
|
65
|
+
*
|
|
66
|
+
* Session persistence outranks the body because a flush failure after a provider
|
|
67
|
+
* success must never record or report a completed operation, and a flush error
|
|
68
|
+
* that already carries a harness classification (e.g. an `invalid_state`
|
|
69
|
+
* coordinator reentry) keeps it. Both the recorded outcome and the public
|
|
70
|
+
* rejection read their top-level code from here, so the two can never disagree.
|
|
71
|
+
* Settlement is not a source: it runs after the outcome is recorded, so it can
|
|
72
|
+
* only add a cause and a rejection.
|
|
73
|
+
*/
|
|
74
|
+
function classificationSource(input) {
|
|
75
|
+
if (input.flushError !== undefined)
|
|
76
|
+
return { stage: "flush", error: input.flushError, fallbackCode: "session" };
|
|
77
|
+
if (input.bodyError !== undefined) {
|
|
78
|
+
return { stage: "body", error: input.bodyError, fallbackCode: input.fallbackCode };
|
|
79
|
+
}
|
|
80
|
+
return undefined;
|
|
81
|
+
}
|
|
63
82
|
/**
|
|
64
83
|
* Single outcome-precedence rule for every public operation:
|
|
65
84
|
*
|
|
@@ -72,22 +91,64 @@ export function classifyAttemptOutcome(message, contextWindow) {
|
|
|
72
91
|
* with "session".
|
|
73
92
|
*/
|
|
74
93
|
export function resolveOperationOutcome(input) {
|
|
75
|
-
|
|
76
|
-
|
|
77
|
-
//
|
|
78
|
-
//
|
|
79
|
-
|
|
80
|
-
return { status: "failed", code: error.code, message: error.message };
|
|
81
|
-
}
|
|
82
|
-
if (input.bodyError !== undefined) {
|
|
83
|
-
if (isExplicitAbortError(input.bodyError))
|
|
94
|
+
const source = classificationSource(input);
|
|
95
|
+
if (source !== undefined) {
|
|
96
|
+
// Only a body failure can be an abort; a flush or settle failure is a failure
|
|
97
|
+
// even when the abort signal happens to be up.
|
|
98
|
+
if (source.stage === "body" && isExplicitAbortError(source.error))
|
|
84
99
|
return { status: "aborted" };
|
|
85
|
-
const error = normalizeHarnessError(
|
|
100
|
+
const error = normalizeHarnessError(source.error, source.fallbackCode);
|
|
86
101
|
return { status: "failed", code: error.code, message: error.message };
|
|
87
102
|
}
|
|
88
103
|
return (input.classifyResult?.(input.result) ??
|
|
89
104
|
(input.signalAborted ? { status: "aborted" } : { status: "completed" }));
|
|
90
105
|
}
|
|
106
|
+
/**
|
|
107
|
+
* Run boundary steps in order and collect their errors instead of stopping at
|
|
108
|
+
* the first. A boundary that must still report, flush, or settle after one step
|
|
109
|
+
* fails uses this so one failure cannot strand the rest.
|
|
110
|
+
*/
|
|
111
|
+
export async function collectStepErrors(steps) {
|
|
112
|
+
const errors = [];
|
|
113
|
+
for (const step of steps) {
|
|
114
|
+
try {
|
|
115
|
+
await step();
|
|
116
|
+
}
|
|
117
|
+
catch (error) {
|
|
118
|
+
errors.push(toError(error));
|
|
119
|
+
}
|
|
120
|
+
}
|
|
121
|
+
return errors;
|
|
122
|
+
}
|
|
123
|
+
/**
|
|
124
|
+
* Which error a public operation rejects with, or `undefined` on success.
|
|
125
|
+
*
|
|
126
|
+
* The top-level code comes from the same `flush > body` source the recorded
|
|
127
|
+
* outcome uses, so `outcome.code === rejection.code` for every failed outcome;
|
|
128
|
+
* settlement only ever contributes a cause. Every concurrent cause stays
|
|
129
|
+
* reachable through one `AggregateError` in body, flush, settle order, so an
|
|
130
|
+
* audit can still see that, say, the body and the final flush failed together.
|
|
131
|
+
*/
|
|
132
|
+
export function resolveOperationFailure(input) {
|
|
133
|
+
const causes = [input.bodyError, input.flushError, input.settleError].filter((error) => error !== undefined);
|
|
134
|
+
if (causes.length === 0)
|
|
135
|
+
return undefined;
|
|
136
|
+
const source = classificationSource(input) ?? {
|
|
137
|
+
stage: "settle",
|
|
138
|
+
error: input.settleError,
|
|
139
|
+
fallbackCode: "hook",
|
|
140
|
+
};
|
|
141
|
+
const code = normalizeHarnessError(source.error, source.fallbackCode).code;
|
|
142
|
+
if (causes.length === 1)
|
|
143
|
+
return normalizeHarnessError(causes[0], source.fallbackCode);
|
|
144
|
+
const stages = [
|
|
145
|
+
input.bodyError !== undefined ? "body" : undefined,
|
|
146
|
+
input.flushError !== undefined ? "final flush" : undefined,
|
|
147
|
+
input.settleError !== undefined ? "settlement" : undefined,
|
|
148
|
+
].filter((stage) => stage !== undefined);
|
|
149
|
+
const cause = new AggregateError(causes.map(toError), `Operation failed (${stages.join(", ")})`);
|
|
150
|
+
return new AgentHarnessError(code, cause.message, cause);
|
|
151
|
+
}
|
|
91
152
|
/**
|
|
92
153
|
* The error a boundary should throw after several steps may have failed, or
|
|
93
154
|
* `undefined` when none did. A single failure is returned untouched so its
|
|
@@ -103,29 +164,4 @@ export function combineBoundaryErrors(errors, message, fallbackCode) {
|
|
|
103
164
|
const cause = new AggregateError(present.map(toError), message);
|
|
104
165
|
return new AgentHarnessError(normalizeHarnessError(present[0], fallbackCode).code, cause.message, cause);
|
|
105
166
|
}
|
|
106
|
-
/**
|
|
107
|
-
* Which error a public operation rejects with, or `undefined` on success.
|
|
108
|
-
*
|
|
109
|
-
* Mirrors the outcome precedence, but every concurrent cause is preserved in an
|
|
110
|
-
* `AggregateError` so an audit can still see that, say, the body and the final
|
|
111
|
-
* flush failed together.
|
|
112
|
-
*/
|
|
113
|
-
export function resolveOperationFailure(input) {
|
|
114
|
-
const primaryError = input.bodyError ?? input.flushError;
|
|
115
|
-
if (primaryError !== undefined && input.settleError !== undefined) {
|
|
116
|
-
const cause = new AggregateError([toError(primaryError), toError(input.settleError)], "Operation failed and settlement failed");
|
|
117
|
-
return new AgentHarnessError(normalizeHarnessError(primaryError, input.fallbackCode).code, cause.message, cause);
|
|
118
|
-
}
|
|
119
|
-
if (input.settleError !== undefined)
|
|
120
|
-
return normalizeHarnessError(input.settleError, "hook");
|
|
121
|
-
if (input.flushError !== undefined) {
|
|
122
|
-
if (input.bodyError === undefined)
|
|
123
|
-
return normalizeHarnessError(input.flushError, "session");
|
|
124
|
-
const cause = new AggregateError([toError(input.bodyError), toError(input.flushError)], "Operation failed and the final flush failed");
|
|
125
|
-
return new AgentHarnessError("session", cause.message, cause);
|
|
126
|
-
}
|
|
127
|
-
if (input.bodyError !== undefined)
|
|
128
|
-
return normalizeHarnessError(input.bodyError, input.fallbackCode);
|
|
129
|
-
return undefined;
|
|
130
|
-
}
|
|
131
167
|
//# sourceMappingURL=operation-outcome.js.map
|