@acme-engine/core 0.1.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/dist/common.d.ts +40 -0
- package/dist/common.d.ts.map +1 -0
- package/dist/common.js +2 -0
- package/dist/common.js.map +1 -0
- package/dist/contracts.d.ts +65 -0
- package/dist/contracts.d.ts.map +1 -0
- package/dist/contracts.js +2 -0
- package/dist/contracts.js.map +1 -0
- package/dist/errors.d.ts +24 -0
- package/dist/errors.d.ts.map +1 -0
- package/dist/errors.js +20 -0
- package/dist/errors.js.map +1 -0
- package/dist/evaluation.d.ts +14 -0
- package/dist/evaluation.d.ts.map +1 -0
- package/dist/evaluation.js +2 -0
- package/dist/evaluation.js.map +1 -0
- package/dist/execution-engine.d.ts +30 -0
- package/dist/execution-engine.d.ts.map +1 -0
- package/dist/execution-engine.js +885 -0
- package/dist/execution-engine.js.map +1 -0
- package/dist/execution-identity.d.ts +35 -0
- package/dist/execution-identity.d.ts.map +1 -0
- package/dist/execution-identity.js +177 -0
- package/dist/execution-identity.js.map +1 -0
- package/dist/execution-status.d.ts +2 -0
- package/dist/execution-status.d.ts.map +1 -0
- package/dist/execution-status.js +2 -0
- package/dist/execution-status.js.map +1 -0
- package/dist/execution-types.d.ts +58 -0
- package/dist/execution-types.d.ts.map +1 -0
- package/dist/execution-types.js +2 -0
- package/dist/execution-types.js.map +1 -0
- package/dist/hashing.d.ts +6 -0
- package/dist/hashing.d.ts.map +1 -0
- package/dist/hashing.js +73 -0
- package/dist/hashing.js.map +1 -0
- package/dist/index.d.ts +34 -0
- package/dist/index.d.ts.map +1 -0
- package/dist/index.js +33 -0
- package/dist/index.js.map +1 -0
- package/dist/memory-engine.d.ts +13 -0
- package/dist/memory-engine.d.ts.map +1 -0
- package/dist/memory-engine.js +591 -0
- package/dist/memory-engine.js.map +1 -0
- package/dist/memory.d.ts +151 -0
- package/dist/memory.d.ts.map +1 -0
- package/dist/memory.js +2 -0
- package/dist/memory.js.map +1 -0
- package/dist/model-call-usage.d.ts +40 -0
- package/dist/model-call-usage.d.ts.map +1 -0
- package/dist/model-call-usage.js +0 -0
- package/dist/model-call-usage.js.map +1 -0
- package/dist/model-execution-engine.d.ts +16 -0
- package/dist/model-execution-engine.d.ts.map +1 -0
- package/dist/model-execution-engine.js +598 -0
- package/dist/model-execution-engine.js.map +1 -0
- package/dist/model-execution-identity.d.ts +16 -0
- package/dist/model-execution-identity.d.ts.map +1 -0
- package/dist/model-execution-identity.js +84 -0
- package/dist/model-execution-identity.js.map +1 -0
- package/dist/model-execution-types.d.ts +66 -0
- package/dist/model-execution-types.d.ts.map +1 -0
- package/dist/model-execution-types.js +8 -0
- package/dist/model-execution-types.js.map +1 -0
- package/dist/model-gateway-router.d.ts +11 -0
- package/dist/model-gateway-router.d.ts.map +1 -0
- package/dist/model-gateway-router.js +69 -0
- package/dist/model-gateway-router.js.map +1 -0
- package/dist/model-request-hash.d.ts +5 -0
- package/dist/model-request-hash.d.ts.map +1 -0
- package/dist/model-request-hash.js +11 -0
- package/dist/model-request-hash.js.map +1 -0
- package/dist/model-response-hash.d.ts +5 -0
- package/dist/model-response-hash.d.ts.map +1 -0
- package/dist/model-response-hash.js +11 -0
- package/dist/model-response-hash.js.map +1 -0
- package/dist/model-validation.d.ts +11 -0
- package/dist/model-validation.d.ts.map +1 -0
- package/dist/model-validation.js +580 -0
- package/dist/model-validation.js.map +1 -0
- package/dist/model.d.ts +125 -0
- package/dist/model.d.ts.map +1 -0
- package/dist/model.js +4 -0
- package/dist/model.js.map +1 -0
- package/dist/modules.d.ts +83 -0
- package/dist/modules.d.ts.map +1 -0
- package/dist/modules.js +7 -0
- package/dist/modules.js.map +1 -0
- package/dist/outbox.d.ts +88 -0
- package/dist/outbox.d.ts.map +1 -0
- package/dist/outbox.js +180 -0
- package/dist/outbox.js.map +1 -0
- package/dist/payload-encryptor.d.ts +92 -0
- package/dist/payload-encryptor.d.ts.map +1 -0
- package/dist/payload-encryptor.js +173 -0
- package/dist/payload-encryptor.js.map +1 -0
- package/dist/registries.d.ts +5 -0
- package/dist/registries.d.ts.map +1 -0
- package/dist/registries.js +159 -0
- package/dist/registries.js.map +1 -0
- package/dist/repository-digest.d.ts +5 -0
- package/dist/repository-digest.d.ts.map +1 -0
- package/dist/repository-digest.js +25 -0
- package/dist/repository-digest.js.map +1 -0
- package/dist/repository-model-call.d.ts +43 -0
- package/dist/repository-model-call.d.ts.map +1 -0
- package/dist/repository-model-call.js +2 -0
- package/dist/repository-model-call.js.map +1 -0
- package/dist/repository-model-execution.d.ts +49 -0
- package/dist/repository-model-execution.d.ts.map +1 -0
- package/dist/repository-model-execution.js +2 -0
- package/dist/repository-model-execution.js.map +1 -0
- package/dist/repository.d.ts +260 -0
- package/dist/repository.d.ts.map +1 -0
- package/dist/repository.js +2 -0
- package/dist/repository.js.map +1 -0
- package/dist/response-pipeline.d.ts +3 -0
- package/dist/response-pipeline.d.ts.map +1 -0
- package/dist/response-pipeline.js +132 -0
- package/dist/response-pipeline.js.map +1 -0
- package/dist/state-engine.d.ts +19 -0
- package/dist/state-engine.d.ts.map +1 -0
- package/dist/state-engine.js +272 -0
- package/dist/state-engine.js.map +1 -0
- package/dist/state-projection.d.ts +5 -0
- package/dist/state-projection.d.ts.map +1 -0
- package/dist/state-projection.js +145 -0
- package/dist/state-projection.js.map +1 -0
- package/dist/state.d.ts +41 -0
- package/dist/state.d.ts.map +1 -0
- package/dist/state.js +2 -0
- package/dist/state.js.map +1 -0
- package/dist/stranded-execution.d.ts +59 -0
- package/dist/stranded-execution.d.ts.map +1 -0
- package/dist/stranded-execution.js +235 -0
- package/dist/stranded-execution.js.map +1 -0
- package/package.json +36 -0
|
@@ -0,0 +1,885 @@
|
|
|
1
|
+
import { AcmeError } from './errors.js';
|
|
2
|
+
import { ACME_MEMORY_RETRIEVAL_LIMIT, computeRequestFingerprint, computeTaskInputHash, deriveExecutionId, deriveOperationKey, resolveExecutionPolicy, } from './execution-identity.js';
|
|
3
|
+
import { nodeHashing } from './hashing.js';
|
|
4
|
+
import { createMemoryEngine } from './memory-engine.js';
|
|
5
|
+
import { computeModelRequestHash } from './model-request-hash.js';
|
|
6
|
+
import { computeModelResponseHash } from './model-response-hash.js';
|
|
7
|
+
import { validateModelRequest, validateModelSelection, validateNormalizedModelResponse, } from './model-validation.js';
|
|
8
|
+
import { buildStateProjectionInput } from './state-projection.js';
|
|
9
|
+
import { computeOperationDigest } from './repository-digest.js';
|
|
10
|
+
const requestKeys = new Set([
|
|
11
|
+
'requestKey',
|
|
12
|
+
'namespace',
|
|
13
|
+
'task',
|
|
14
|
+
'entityId',
|
|
15
|
+
'expectedRevision',
|
|
16
|
+
'input',
|
|
17
|
+
'model',
|
|
18
|
+
'policy',
|
|
19
|
+
]);
|
|
20
|
+
const requiredRequestKeys = [
|
|
21
|
+
'requestKey',
|
|
22
|
+
'namespace',
|
|
23
|
+
'task',
|
|
24
|
+
'entityId',
|
|
25
|
+
'expectedRevision',
|
|
26
|
+
'input',
|
|
27
|
+
'model',
|
|
28
|
+
];
|
|
29
|
+
function invalid(code, message, stage, details, cause) {
|
|
30
|
+
const data = {
|
|
31
|
+
code,
|
|
32
|
+
message,
|
|
33
|
+
stage,
|
|
34
|
+
retryable: false,
|
|
35
|
+
...(details === undefined ? {} : { details }),
|
|
36
|
+
};
|
|
37
|
+
return cause === undefined
|
|
38
|
+
? new AcmeError(data)
|
|
39
|
+
: new AcmeError(data, { cause });
|
|
40
|
+
}
|
|
41
|
+
function isObject(value) {
|
|
42
|
+
return value !== null && typeof value === 'object' && !Array.isArray(value);
|
|
43
|
+
}
|
|
44
|
+
function requireText(value, field) {
|
|
45
|
+
if (typeof value !== 'string' || value.trim().length === 0) {
|
|
46
|
+
throw invalid('INVALID_REQUEST', `${field} must be a non-empty string.`, 'accepted', { field });
|
|
47
|
+
}
|
|
48
|
+
return value;
|
|
49
|
+
}
|
|
50
|
+
function requireResultText(value, field) {
|
|
51
|
+
if (typeof value !== 'string' || value.trim().length === 0) {
|
|
52
|
+
throw invalid('DOMAIN_INVALID_RESULT', `${field} must be a non-empty string.`, 'interpreting', { field });
|
|
53
|
+
}
|
|
54
|
+
return value;
|
|
55
|
+
}
|
|
56
|
+
function deepFreeze(value) {
|
|
57
|
+
if (value !== null && typeof value === 'object' && !Object.isFrozen(value)) {
|
|
58
|
+
Object.freeze(value);
|
|
59
|
+
for (const child of Object.values(value)) {
|
|
60
|
+
deepFreeze(child);
|
|
61
|
+
}
|
|
62
|
+
}
|
|
63
|
+
return value;
|
|
64
|
+
}
|
|
65
|
+
function schemaDetails(issues) {
|
|
66
|
+
return issues.map(({ code, message, path }) => ({
|
|
67
|
+
code,
|
|
68
|
+
message,
|
|
69
|
+
path: path.map((part) => (typeof part === 'symbol' ? String(part) : part)),
|
|
70
|
+
}));
|
|
71
|
+
}
|
|
72
|
+
function validateSchemaJson(schema, input, hashing, code, stage, label) {
|
|
73
|
+
let result;
|
|
74
|
+
try {
|
|
75
|
+
result = schema.safeParse(input);
|
|
76
|
+
}
|
|
77
|
+
catch (cause) {
|
|
78
|
+
throw invalid(code, `${label} schema execution failed.`, stage, { label }, cause);
|
|
79
|
+
}
|
|
80
|
+
if (!result.success) {
|
|
81
|
+
throw invalid(code, `${label} failed schema validation.`, stage, {
|
|
82
|
+
label,
|
|
83
|
+
issues: schemaDetails(result.error.issues),
|
|
84
|
+
});
|
|
85
|
+
}
|
|
86
|
+
let supplied;
|
|
87
|
+
let validated;
|
|
88
|
+
try {
|
|
89
|
+
supplied = hashing.canonicalJson(input);
|
|
90
|
+
validated = hashing.canonicalJson(result.data);
|
|
91
|
+
}
|
|
92
|
+
catch (cause) {
|
|
93
|
+
throw invalid(code, `${label} must remain a JSON value.`, stage, { label }, cause);
|
|
94
|
+
}
|
|
95
|
+
if (supplied !== validated) {
|
|
96
|
+
throw invalid(code, `${label} schema must not coerce or transform its value.`, stage, { label });
|
|
97
|
+
}
|
|
98
|
+
return deepFreeze(JSON.parse(validated));
|
|
99
|
+
}
|
|
100
|
+
function validateTimestamp(value) {
|
|
101
|
+
if (!/^\d{4}-\d{2}-\d{2}T\d{2}:\d{2}:\d{2}\.\d{3}Z$/u.test(value) ||
|
|
102
|
+
Number.isNaN(Date.parse(value)) ||
|
|
103
|
+
new Date(value).toISOString() !== value) {
|
|
104
|
+
throw invalid('INTERNAL', 'Clock returned a non-canonical UTC timestamp.', 'accepted');
|
|
105
|
+
}
|
|
106
|
+
return value;
|
|
107
|
+
}
|
|
108
|
+
function validateRequestEnvelope(request) {
|
|
109
|
+
if (!isObject(request)) {
|
|
110
|
+
throw invalid('INVALID_REQUEST', 'Execution request must be an object.', 'accepted');
|
|
111
|
+
}
|
|
112
|
+
const unexpected = Object.keys(request)
|
|
113
|
+
.filter((key) => !requestKeys.has(key))
|
|
114
|
+
.sort();
|
|
115
|
+
const missing = requiredRequestKeys.filter((key) => !Object.hasOwn(request, key));
|
|
116
|
+
if (unexpected.length > 0 || missing.length > 0) {
|
|
117
|
+
throw invalid('INVALID_REQUEST', 'Execution request has an invalid shape.', 'accepted', { missing, unexpected });
|
|
118
|
+
}
|
|
119
|
+
if (!Number.isSafeInteger(request.expectedRevision) ||
|
|
120
|
+
request.expectedRevision < 0) {
|
|
121
|
+
throw invalid('INVALID_REQUEST', 'expectedRevision must be a non-negative safe integer.', 'accepted');
|
|
122
|
+
}
|
|
123
|
+
return Object.freeze({
|
|
124
|
+
requestKey: requireText(request.requestKey, 'requestKey'),
|
|
125
|
+
namespace: requireText(request.namespace, 'namespace'),
|
|
126
|
+
task: requireText(request.task, 'task'),
|
|
127
|
+
entityId: requireText(request.entityId, 'entityId'),
|
|
128
|
+
expectedRevision: request.expectedRevision,
|
|
129
|
+
input: request.input,
|
|
130
|
+
model: validateModelSelection(request.model),
|
|
131
|
+
...(Object.hasOwn(request, 'policy')
|
|
132
|
+
? { policy: request.policy }
|
|
133
|
+
: {}),
|
|
134
|
+
});
|
|
135
|
+
}
|
|
136
|
+
function terminalStatus(error) {
|
|
137
|
+
if (error.code === 'EVALUATION_BLOCKED') {
|
|
138
|
+
return 'blocked';
|
|
139
|
+
}
|
|
140
|
+
if (error.code === 'CONFLICT_IDEMPOTENCY_KEY' ||
|
|
141
|
+
error.code === 'CONFLICT_STATE_REVISION' ||
|
|
142
|
+
error.code === 'CONFLICT_MEMORY_VERSION') {
|
|
143
|
+
return 'conflicted';
|
|
144
|
+
}
|
|
145
|
+
return error.code === 'CANCELLED' ? 'cancelled' : 'failed';
|
|
146
|
+
}
|
|
147
|
+
function errorData(error, stage) {
|
|
148
|
+
if (error instanceof AcmeError) {
|
|
149
|
+
return error.data;
|
|
150
|
+
}
|
|
151
|
+
if (isObject(error) &&
|
|
152
|
+
isObject(error.data) &&
|
|
153
|
+
typeof error.data.code === 'string' &&
|
|
154
|
+
typeof error.data.message === 'string' &&
|
|
155
|
+
typeof error.data.stage === 'string' &&
|
|
156
|
+
typeof error.data.retryable === 'boolean') {
|
|
157
|
+
return deepFreeze(error.data);
|
|
158
|
+
}
|
|
159
|
+
return Object.freeze({
|
|
160
|
+
code: 'INTERNAL',
|
|
161
|
+
message: 'Execution failed with an unexpected internal error.',
|
|
162
|
+
stage,
|
|
163
|
+
retryable: false,
|
|
164
|
+
});
|
|
165
|
+
}
|
|
166
|
+
function diagnostic(code, severity, value) {
|
|
167
|
+
return deepFreeze({
|
|
168
|
+
code,
|
|
169
|
+
severity,
|
|
170
|
+
...(value === undefined ? {} : { value }),
|
|
171
|
+
});
|
|
172
|
+
}
|
|
173
|
+
function canonicalEqual(left, right, hashing) {
|
|
174
|
+
return (hashing.canonicalJson(left) ===
|
|
175
|
+
hashing.canonicalJson(right));
|
|
176
|
+
}
|
|
177
|
+
function exactKeys(value, allowed, required, label) {
|
|
178
|
+
const unexpected = Object.keys(value)
|
|
179
|
+
.filter((key) => !allowed.includes(key))
|
|
180
|
+
.sort();
|
|
181
|
+
const missing = required.filter((key) => !Object.hasOwn(value, key));
|
|
182
|
+
if (unexpected.length > 0 || missing.length > 0) {
|
|
183
|
+
throw invalid('DOMAIN_INVALID_RESULT', `${label} has an invalid shape.`, 'interpreting', { label, missing, unexpected });
|
|
184
|
+
}
|
|
185
|
+
}
|
|
186
|
+
function validateModuleResult(raw, hashing) {
|
|
187
|
+
let cloned;
|
|
188
|
+
try {
|
|
189
|
+
cloned = deepFreeze(JSON.parse(hashing.canonicalJson(raw)));
|
|
190
|
+
}
|
|
191
|
+
catch (cause) {
|
|
192
|
+
throw invalid('DOMAIN_INVALID_RESULT', 'Module result must contain only canonical JSON values.', 'interpreting', undefined, cause);
|
|
193
|
+
}
|
|
194
|
+
if (!isObject(cloned)) {
|
|
195
|
+
throw invalid('DOMAIN_INVALID_RESULT', 'Module result must be an object.', 'interpreting');
|
|
196
|
+
}
|
|
197
|
+
exactKeys(cloned, ['documents', 'memories', 'stateIntent', 'events', 'diagnostics'], ['documents', 'memories', 'events', 'diagnostics'], 'Module result');
|
|
198
|
+
for (const field of [
|
|
199
|
+
'documents',
|
|
200
|
+
'memories',
|
|
201
|
+
'events',
|
|
202
|
+
'diagnostics',
|
|
203
|
+
]) {
|
|
204
|
+
if (!Array.isArray(cloned[field])) {
|
|
205
|
+
throw invalid('DOMAIN_INVALID_RESULT', `Module result ${field} must be an array.`, 'interpreting');
|
|
206
|
+
}
|
|
207
|
+
}
|
|
208
|
+
const documents = cloned.documents;
|
|
209
|
+
const memories = cloned.memories;
|
|
210
|
+
const events = cloned.events;
|
|
211
|
+
const diagnostics = cloned.diagnostics;
|
|
212
|
+
const documentKeys = new Set();
|
|
213
|
+
for (const [index, value] of documents.entries()) {
|
|
214
|
+
if (!isObject(value)) {
|
|
215
|
+
throw invalid('DOMAIN_INVALID_RESULT', `Module document ${index} must be an object.`, 'interpreting');
|
|
216
|
+
}
|
|
217
|
+
exactKeys(value, ['key', 'kind', 'schemaVersion', 'value', 'contentHash'], ['key', 'kind', 'schemaVersion', 'value', 'contentHash'], `Module document ${index}`);
|
|
218
|
+
const key = requireResultText(value.key, `documents[${index}].key`);
|
|
219
|
+
requireResultText(value.kind, `documents[${index}].kind`);
|
|
220
|
+
requireResultText(value.schemaVersion, `documents[${index}].schemaVersion`);
|
|
221
|
+
requireResultText(value.contentHash, `documents[${index}].contentHash`);
|
|
222
|
+
if (documentKeys.has(key)) {
|
|
223
|
+
throw invalid('DOMAIN_INVALID_RESULT', 'Module document keys must be unique.', 'interpreting', { key });
|
|
224
|
+
}
|
|
225
|
+
documentKeys.add(key);
|
|
226
|
+
}
|
|
227
|
+
const memoryKeys = new Set();
|
|
228
|
+
for (const [index, value] of memories.entries()) {
|
|
229
|
+
if (!isObject(value)) {
|
|
230
|
+
throw invalid('DOMAIN_INVALID_RESULT', `Module memory ${index} must be an object.`, 'interpreting');
|
|
231
|
+
}
|
|
232
|
+
const key = requireResultText(value.key, `memories[${index}].key`);
|
|
233
|
+
if (memoryKeys.has(key)) {
|
|
234
|
+
throw invalid('DOMAIN_INVALID_RESULT', 'Module memory keys must be unique.', 'interpreting', { key });
|
|
235
|
+
}
|
|
236
|
+
memoryKeys.add(key);
|
|
237
|
+
}
|
|
238
|
+
const eventKeys = new Set();
|
|
239
|
+
for (const [index, value] of events.entries()) {
|
|
240
|
+
if (!isObject(value)) {
|
|
241
|
+
throw invalid('DOMAIN_INVALID_RESULT', `Module event ${index} must be an object.`, 'interpreting');
|
|
242
|
+
}
|
|
243
|
+
exactKeys(value, ['key', 'type', 'schemaVersion', 'payload'], ['key', 'type', 'schemaVersion', 'payload'], `Module event ${index}`);
|
|
244
|
+
const key = requireResultText(value.key, `events[${index}].key`);
|
|
245
|
+
requireResultText(value.type, `events[${index}].type`);
|
|
246
|
+
requireResultText(value.schemaVersion, `events[${index}].schemaVersion`);
|
|
247
|
+
if (eventKeys.has(key)) {
|
|
248
|
+
throw invalid('DOMAIN_INVALID_RESULT', 'Module event keys must be unique.', 'interpreting', { key });
|
|
249
|
+
}
|
|
250
|
+
eventKeys.add(key);
|
|
251
|
+
}
|
|
252
|
+
if (cloned.stateIntent !== undefined) {
|
|
253
|
+
if (!isObject(cloned.stateIntent)) {
|
|
254
|
+
throw invalid('DOMAIN_INVALID_RESULT', 'Module state intent must be an object.', 'interpreting');
|
|
255
|
+
}
|
|
256
|
+
exactKeys(cloned.stateIntent, ['schemaVersion', 'value'], ['schemaVersion', 'value'], 'Module state intent');
|
|
257
|
+
requireResultText(cloned.stateIntent.schemaVersion, 'stateIntent.schemaVersion');
|
|
258
|
+
}
|
|
259
|
+
for (const [index, value] of diagnostics.entries()) {
|
|
260
|
+
if (!isObject(value)) {
|
|
261
|
+
throw invalid('DOMAIN_INVALID_RESULT', `Module diagnostic ${index} must be an object.`, 'interpreting');
|
|
262
|
+
}
|
|
263
|
+
exactKeys(value, ['code', 'severity', 'value'], ['code', 'severity'], `Module diagnostic ${index}`);
|
|
264
|
+
requireResultText(value.code, `diagnostics[${index}].code`);
|
|
265
|
+
if (value.severity !== 'debug' &&
|
|
266
|
+
value.severity !== 'info' &&
|
|
267
|
+
value.severity !== 'warning' &&
|
|
268
|
+
value.severity !== 'error') {
|
|
269
|
+
throw invalid('DOMAIN_INVALID_RESULT', `Module diagnostic ${index} severity is invalid.`, 'interpreting');
|
|
270
|
+
}
|
|
271
|
+
}
|
|
272
|
+
return cloned;
|
|
273
|
+
}
|
|
274
|
+
function recordedReadSet(loaded, ranked) {
|
|
275
|
+
const retrievedMemories = ranked.map((entry, index) => deepFreeze({
|
|
276
|
+
record: entry.record,
|
|
277
|
+
score: entry.score,
|
|
278
|
+
reasons: entry.reasons,
|
|
279
|
+
rank: index + 1,
|
|
280
|
+
}));
|
|
281
|
+
return deepFreeze({
|
|
282
|
+
state: loaded.state,
|
|
283
|
+
loadedMemories: loaded.memories,
|
|
284
|
+
retrievedMemories,
|
|
285
|
+
documents: loaded.documents,
|
|
286
|
+
});
|
|
287
|
+
}
|
|
288
|
+
function readContext(executionId, entityId, now, readSet) {
|
|
289
|
+
return deepFreeze({
|
|
290
|
+
executionId,
|
|
291
|
+
entityId,
|
|
292
|
+
now,
|
|
293
|
+
state: readSet.state,
|
|
294
|
+
memories: readSet.retrievedMemories.map(({ record }) => record),
|
|
295
|
+
documents: readSet.documents,
|
|
296
|
+
});
|
|
297
|
+
}
|
|
298
|
+
function memoryQuery(namespace, entityId, task) {
|
|
299
|
+
return Object.freeze({
|
|
300
|
+
namespace,
|
|
301
|
+
entityId,
|
|
302
|
+
task,
|
|
303
|
+
limit: ACME_MEMORY_RETRIEVAL_LIMIT,
|
|
304
|
+
});
|
|
305
|
+
}
|
|
306
|
+
function enforceResponseBudgets(response, policy) {
|
|
307
|
+
const checks = [
|
|
308
|
+
['inputTokens', response.usage.inputTokens, policy.maxInputTokens],
|
|
309
|
+
['outputTokens', response.usage.outputTokens, policy.maxOutputTokens],
|
|
310
|
+
[
|
|
311
|
+
'estimatedCostMinor',
|
|
312
|
+
response.usage.estimatedCostMinor,
|
|
313
|
+
policy.maxEstimatedCostMinor,
|
|
314
|
+
],
|
|
315
|
+
];
|
|
316
|
+
for (const [field, actual, limit] of checks) {
|
|
317
|
+
if (limit !== undefined && actual !== undefined && actual > limit) {
|
|
318
|
+
throw invalid('BUDGET_EXCEEDED', `Model response exceeded ${field} budget.`, 'calling-model', { field, actual, limit });
|
|
319
|
+
}
|
|
320
|
+
}
|
|
321
|
+
}
|
|
322
|
+
class SingleTaskExecutionEngine {
|
|
323
|
+
#clock;
|
|
324
|
+
#ids;
|
|
325
|
+
#modules;
|
|
326
|
+
#contracts;
|
|
327
|
+
#pipeline;
|
|
328
|
+
#gateway;
|
|
329
|
+
#memory;
|
|
330
|
+
#state;
|
|
331
|
+
#repository;
|
|
332
|
+
#hashing;
|
|
333
|
+
constructor(options) {
|
|
334
|
+
this.#clock = options.clock;
|
|
335
|
+
this.#ids = options.ids;
|
|
336
|
+
this.#modules = options.modules;
|
|
337
|
+
this.#contracts = options.contracts;
|
|
338
|
+
this.#pipeline = options.pipeline;
|
|
339
|
+
this.#gateway = options.gateway;
|
|
340
|
+
this.#memory = options.memory;
|
|
341
|
+
this.#state = options.state;
|
|
342
|
+
this.#repository = options.repository;
|
|
343
|
+
this.#hashing = options.hashing ?? nodeHashing;
|
|
344
|
+
}
|
|
345
|
+
async execute(request, options) {
|
|
346
|
+
const callSignal = options?.signal;
|
|
347
|
+
const envelope = validateRequestEnvelope(request);
|
|
348
|
+
const effectivePolicy = resolveExecutionPolicy(envelope.policy);
|
|
349
|
+
const resolved = this.#resolve(envelope.namespace, envelope.task, envelope.input);
|
|
350
|
+
const executionId = deriveExecutionId(envelope.namespace, envelope.requestKey, this.#hashing);
|
|
351
|
+
const requestFingerprint = computeRequestFingerprint({
|
|
352
|
+
namespace: envelope.namespace,
|
|
353
|
+
task: envelope.task,
|
|
354
|
+
entityId: envelope.entityId,
|
|
355
|
+
expectedRevision: envelope.expectedRevision,
|
|
356
|
+
input: resolved.taskInput,
|
|
357
|
+
contractFingerprint: resolved.contractFingerprint,
|
|
358
|
+
stateSchemaVersion: resolved.module.stateSchemaVersion,
|
|
359
|
+
model: envelope.model,
|
|
360
|
+
}, this.#hashing);
|
|
361
|
+
const now = validateTimestamp(this.#clock.now());
|
|
362
|
+
const storedRequest = deepFreeze({
|
|
363
|
+
requestKey: envelope.requestKey,
|
|
364
|
+
namespace: envelope.namespace,
|
|
365
|
+
task: envelope.task,
|
|
366
|
+
entityId: envelope.entityId,
|
|
367
|
+
expectedRevision: envelope.expectedRevision,
|
|
368
|
+
input: resolved.taskInput,
|
|
369
|
+
model: envelope.model,
|
|
370
|
+
policy: effectivePolicy,
|
|
371
|
+
});
|
|
372
|
+
const acceptance = await this.#repository.accept({
|
|
373
|
+
executionId,
|
|
374
|
+
request: storedRequest,
|
|
375
|
+
requestFingerprint,
|
|
376
|
+
inputHash: computeTaskInputHash(resolved.taskInput, this.#hashing),
|
|
377
|
+
contract: resolved.contract.ref,
|
|
378
|
+
contractFingerprint: resolved.contractFingerprint,
|
|
379
|
+
effectivePolicy,
|
|
380
|
+
createdAt: now,
|
|
381
|
+
});
|
|
382
|
+
if (acceptance.kind === 'conflict') {
|
|
383
|
+
return deepFreeze({
|
|
384
|
+
status: 'conflicted',
|
|
385
|
+
executionId: acceptance.existingExecutionId,
|
|
386
|
+
error: {
|
|
387
|
+
code: 'CONFLICT_IDEMPOTENCY_KEY',
|
|
388
|
+
message: 'Request key was already accepted with different identity.',
|
|
389
|
+
stage: 'accepted',
|
|
390
|
+
retryable: false,
|
|
391
|
+
details: {
|
|
392
|
+
namespace: envelope.namespace,
|
|
393
|
+
requestKey: envelope.requestKey,
|
|
394
|
+
},
|
|
395
|
+
},
|
|
396
|
+
});
|
|
397
|
+
}
|
|
398
|
+
if (acceptance.kind === 'existing') {
|
|
399
|
+
const result = acceptance.execution.result;
|
|
400
|
+
if (result !== undefined) {
|
|
401
|
+
return result.status === 'committed'
|
|
402
|
+
? deepFreeze({
|
|
403
|
+
...result,
|
|
404
|
+
replayed: true,
|
|
405
|
+
reuseReason: 'committed-execution',
|
|
406
|
+
})
|
|
407
|
+
: result;
|
|
408
|
+
}
|
|
409
|
+
}
|
|
410
|
+
try {
|
|
411
|
+
const resumption = acceptance.kind === 'existing'
|
|
412
|
+
? await this.#planResume(executionId)
|
|
413
|
+
: undefined;
|
|
414
|
+
return await this.#runAccepted(storedRequest, resolved, executionId, effectivePolicy, now, resumption, callSignal);
|
|
415
|
+
}
|
|
416
|
+
catch (error) {
|
|
417
|
+
const data = errorData(error, 'failed');
|
|
418
|
+
const status = terminalStatus(data);
|
|
419
|
+
await this.#repository.markTerminal({
|
|
420
|
+
executionId,
|
|
421
|
+
status,
|
|
422
|
+
error: data,
|
|
423
|
+
terminalAt: now,
|
|
424
|
+
});
|
|
425
|
+
return deepFreeze({ status, executionId, error: data });
|
|
426
|
+
}
|
|
427
|
+
}
|
|
428
|
+
async replayVerify(executionId) {
|
|
429
|
+
requireText(executionId, 'executionId');
|
|
430
|
+
const evidence = await this.#repository.loadReplayEvidence(executionId);
|
|
431
|
+
if (evidence === null) {
|
|
432
|
+
return deepFreeze({
|
|
433
|
+
executionId,
|
|
434
|
+
mode: 'verify',
|
|
435
|
+
status: 'unavailable',
|
|
436
|
+
differences: [
|
|
437
|
+
diagnostic('REPLAY_EVIDENCE_UNAVAILABLE', 'error', { executionId }),
|
|
438
|
+
],
|
|
439
|
+
});
|
|
440
|
+
}
|
|
441
|
+
return this.#replay(evidence);
|
|
442
|
+
}
|
|
443
|
+
#resolve(namespace, taskName, input) {
|
|
444
|
+
const module = this.#modules.get(namespace);
|
|
445
|
+
const task = module.tasks[taskName];
|
|
446
|
+
if (task === undefined) {
|
|
447
|
+
throw invalid('NOT_FOUND_TASK', `Task not found: ${namespace}.${taskName}.`, 'accepted', { namespace, task: taskName });
|
|
448
|
+
}
|
|
449
|
+
const contract = this.#contracts.get(task.contract);
|
|
450
|
+
const contractFingerprint = this.#contracts.fingerprint(task.contract);
|
|
451
|
+
const taskInput = validateSchemaJson(task.inputSchema, input, this.#hashing, 'INVALID_REQUEST', 'accepted', 'Task input');
|
|
452
|
+
return Object.freeze({
|
|
453
|
+
module,
|
|
454
|
+
task,
|
|
455
|
+
contract,
|
|
456
|
+
contractFingerprint,
|
|
457
|
+
taskInput,
|
|
458
|
+
});
|
|
459
|
+
}
|
|
460
|
+
async #stage(executionId, attemptNumber, stage, now) {
|
|
461
|
+
await this.#repository.appendAttempt({
|
|
462
|
+
executionId,
|
|
463
|
+
attemptNumber,
|
|
464
|
+
stage,
|
|
465
|
+
outcome: 'started',
|
|
466
|
+
occurredAt: now,
|
|
467
|
+
});
|
|
468
|
+
}
|
|
469
|
+
/**
|
|
470
|
+
* Decide how an accepted but non-terminal execution continues (ADR-0017).
|
|
471
|
+
* Resume never calls the provider: it either continues from a recorded
|
|
472
|
+
* response, runs from the beginning when no reservation exists, or throws a
|
|
473
|
+
* terminal error.
|
|
474
|
+
*/
|
|
475
|
+
async #planResume(executionId) {
|
|
476
|
+
const state = await this.#repository.loadResumeState(executionId);
|
|
477
|
+
if (state === null) {
|
|
478
|
+
throw invalid('PERSISTENCE_CORRUPTION', 'Accepted execution has no recorded resume state.', 'accepted', { executionId });
|
|
479
|
+
}
|
|
480
|
+
const attemptNumber = state.lastAttemptNumber + 1;
|
|
481
|
+
const call = state.modelCalls.find((candidate) => candidate.callKey === 'model:0' &&
|
|
482
|
+
candidate.attempt === 1 &&
|
|
483
|
+
candidate.purpose === 'primary');
|
|
484
|
+
// No reservation exists, and reservation precedes dispatch, so no request
|
|
485
|
+
// can have left the process. Running from the beginning is safe.
|
|
486
|
+
if (call === undefined) {
|
|
487
|
+
return Object.freeze({ attemptNumber });
|
|
488
|
+
}
|
|
489
|
+
if (call.status === 'succeeded') {
|
|
490
|
+
if (call.response === undefined) {
|
|
491
|
+
throw invalid('RESUME_EVIDENCE_UNAVAILABLE', 'Recorded model response is not readable; the execution cannot be resumed.', 'calling-model', { modelCallId: call.modelCallId });
|
|
492
|
+
}
|
|
493
|
+
return Object.freeze({ attemptNumber, recordedResponse: call.response });
|
|
494
|
+
}
|
|
495
|
+
if (call.status === 'failed' || call.status === 'ambiguous') {
|
|
496
|
+
throw new AcmeError(call.error ?? {
|
|
497
|
+
code: 'MODEL_UNAVAILABLE',
|
|
498
|
+
message: `Recorded model call is ${call.status} without a recorded error.`,
|
|
499
|
+
stage: 'calling-model',
|
|
500
|
+
retryable: false,
|
|
501
|
+
});
|
|
502
|
+
}
|
|
503
|
+
// Reserved or in-flight: the outcome was never recorded, so the request may
|
|
504
|
+
// have reached the provider. ADR-0014 forbids guessing in that direction.
|
|
505
|
+
throw invalid('MODEL_UNAVAILABLE', `Recorded model call is ${call.status}; its outcome was never observed and it is not retried.`, 'calling-model', { modelCallId: call.modelCallId });
|
|
506
|
+
}
|
|
507
|
+
#nextCallId() {
|
|
508
|
+
let id;
|
|
509
|
+
try {
|
|
510
|
+
id = this.#ids.next('call');
|
|
511
|
+
}
|
|
512
|
+
catch (cause) {
|
|
513
|
+
throw invalid('INTERNAL', 'Model-call ID allocation failed.', 'calling-model', undefined, cause);
|
|
514
|
+
}
|
|
515
|
+
if (typeof id !== 'string' || id.trim().length === 0) {
|
|
516
|
+
throw invalid('INTERNAL', 'Model-call ID allocation returned an invalid ID.', 'calling-model');
|
|
517
|
+
}
|
|
518
|
+
return id;
|
|
519
|
+
}
|
|
520
|
+
async #runAccepted(request, resolved, executionId, policy, now, resumption, callSignal) {
|
|
521
|
+
const attemptNumber = resumption?.attemptNumber ?? 1;
|
|
522
|
+
await this.#stage(executionId, attemptNumber, 'loading', now);
|
|
523
|
+
const query = memoryQuery(request.namespace, request.entityId, request.task);
|
|
524
|
+
const loaded = await this.#repository.loadContext({
|
|
525
|
+
namespace: request.namespace,
|
|
526
|
+
entityId: request.entityId,
|
|
527
|
+
expectedRevision: request.expectedRevision,
|
|
528
|
+
memory: query,
|
|
529
|
+
});
|
|
530
|
+
const ranked = this.#memory.retrieve(resolved.module.memoryPolicy, query, loaded.memories);
|
|
531
|
+
const replayReadSet = recordedReadSet(loaded, ranked);
|
|
532
|
+
const context = readContext(executionId, request.entityId, now, replayReadSet);
|
|
533
|
+
const contractInput = await resolved.task.project(resolved.taskInput, context);
|
|
534
|
+
const validatedContractInput = validateSchemaJson(resolved.contract.inputSchema, contractInput, this.#hashing, 'DOMAIN_INVALID_RESULT', 'loading', 'Projected contract input');
|
|
535
|
+
const modelRequest = validateModelRequest(resolved.contract.buildRequest(validatedContractInput, {
|
|
536
|
+
executionId,
|
|
537
|
+
now,
|
|
538
|
+
}));
|
|
539
|
+
const requestHash = computeModelRequestHash(modelRequest, this.#hashing);
|
|
540
|
+
await this.#stage(executionId, attemptNumber, 'calling-model', now);
|
|
541
|
+
let response;
|
|
542
|
+
if (resumption?.recordedResponse !== undefined) {
|
|
543
|
+
// ADR-0017: the recorded response replaces the call. The provider is not
|
|
544
|
+
// contacted, no reservation is made and no model-call ID is allocated.
|
|
545
|
+
response = validateNormalizedModelResponse(resumption.recordedResponse);
|
|
546
|
+
}
|
|
547
|
+
else {
|
|
548
|
+
const modelCallId = this.#nextCallId();
|
|
549
|
+
await this.#repository.reserveModelCall({
|
|
550
|
+
modelCallId,
|
|
551
|
+
executionId,
|
|
552
|
+
callKey: 'model:0',
|
|
553
|
+
attempt: 1,
|
|
554
|
+
purpose: 'primary',
|
|
555
|
+
selection: request.model,
|
|
556
|
+
requestHash,
|
|
557
|
+
startedAt: now,
|
|
558
|
+
});
|
|
559
|
+
try {
|
|
560
|
+
response = validateNormalizedModelResponse(await this.#gateway.generate(modelRequest, {
|
|
561
|
+
executionId,
|
|
562
|
+
callKey: 'model:0',
|
|
563
|
+
selection: request.model,
|
|
564
|
+
requiredCapabilities: resolved.contract.requiredCapabilities,
|
|
565
|
+
timeoutMs: policy.timeoutMs,
|
|
566
|
+
signal: callSignal ?? new AbortController().signal,
|
|
567
|
+
}));
|
|
568
|
+
}
|
|
569
|
+
catch (error) {
|
|
570
|
+
const data = errorData(error, 'calling-model');
|
|
571
|
+
await this.#repository.failModelCall({
|
|
572
|
+
modelCallId,
|
|
573
|
+
error: data,
|
|
574
|
+
ambiguous: false,
|
|
575
|
+
completedAt: now,
|
|
576
|
+
});
|
|
577
|
+
throw new AcmeError(data);
|
|
578
|
+
}
|
|
579
|
+
await this.#repository.completeModelCall({
|
|
580
|
+
modelCallId,
|
|
581
|
+
response,
|
|
582
|
+
responseHash: computeModelResponseHash(response, this.#hashing),
|
|
583
|
+
completedAt: now,
|
|
584
|
+
});
|
|
585
|
+
}
|
|
586
|
+
enforceResponseBudgets(response, policy);
|
|
587
|
+
await this.#stage(executionId, attemptNumber, 'validating', now);
|
|
588
|
+
let pipeline = this.#pipeline.process(response, resolved.contract, validatedContractInput);
|
|
589
|
+
// ADR-0045 §5: a recoverably invalid response is repaired within its
|
|
590
|
+
// budget instead of being paid for and discarded. Each repair is its own
|
|
591
|
+
// recorded model call. Repair never fires on the ADR-0017 resume path,
|
|
592
|
+
// which completes from recorded evidence and must not contact the
|
|
593
|
+
// provider, and never fires for a contract that offers no repair request.
|
|
594
|
+
let repairsUsed = 0;
|
|
595
|
+
while (!pipeline.ok &&
|
|
596
|
+
pipeline.repairable &&
|
|
597
|
+
resumption?.recordedResponse === undefined &&
|
|
598
|
+
resolved.contract.buildRepairRequest !== undefined &&
|
|
599
|
+
repairsUsed < policy.maxRepairCalls) {
|
|
600
|
+
repairsUsed += 1;
|
|
601
|
+
const repairCallKey = `repair:${String(repairsUsed)}`;
|
|
602
|
+
const repairRequest = validateModelRequest(resolved.contract.buildRepairRequest(validatedContractInput, {
|
|
603
|
+
executionId,
|
|
604
|
+
now,
|
|
605
|
+
attempt: repairsUsed,
|
|
606
|
+
issues: pipeline.issues,
|
|
607
|
+
}));
|
|
608
|
+
const repairCallId = this.#nextCallId();
|
|
609
|
+
await this.#repository.reserveModelCall({
|
|
610
|
+
modelCallId: repairCallId,
|
|
611
|
+
executionId,
|
|
612
|
+
callKey: repairCallKey,
|
|
613
|
+
attempt: repairsUsed,
|
|
614
|
+
purpose: 'repair',
|
|
615
|
+
selection: request.model,
|
|
616
|
+
requestHash: computeModelRequestHash(repairRequest, this.#hashing),
|
|
617
|
+
startedAt: now,
|
|
618
|
+
});
|
|
619
|
+
await this.#stage(executionId, attemptNumber, 'calling-model', now);
|
|
620
|
+
let repaired;
|
|
621
|
+
try {
|
|
622
|
+
repaired = validateNormalizedModelResponse(await this.#gateway.generate(repairRequest, {
|
|
623
|
+
executionId,
|
|
624
|
+
callKey: repairCallKey,
|
|
625
|
+
selection: request.model,
|
|
626
|
+
requiredCapabilities: resolved.contract.requiredCapabilities,
|
|
627
|
+
timeoutMs: policy.timeoutMs,
|
|
628
|
+
signal: callSignal ?? new AbortController().signal,
|
|
629
|
+
}));
|
|
630
|
+
}
|
|
631
|
+
catch (error) {
|
|
632
|
+
const data = errorData(error, 'calling-model');
|
|
633
|
+
await this.#repository.failModelCall({
|
|
634
|
+
modelCallId: repairCallId,
|
|
635
|
+
error: data,
|
|
636
|
+
ambiguous: false,
|
|
637
|
+
completedAt: now,
|
|
638
|
+
});
|
|
639
|
+
throw new AcmeError(data);
|
|
640
|
+
}
|
|
641
|
+
await this.#repository.completeModelCall({
|
|
642
|
+
modelCallId: repairCallId,
|
|
643
|
+
response: repaired,
|
|
644
|
+
responseHash: computeModelResponseHash(repaired, this.#hashing),
|
|
645
|
+
completedAt: now,
|
|
646
|
+
});
|
|
647
|
+
response = repaired;
|
|
648
|
+
enforceResponseBudgets(response, policy);
|
|
649
|
+
await this.#stage(executionId, attemptNumber, 'validating', now);
|
|
650
|
+
pipeline = this.#pipeline.process(response, resolved.contract, validatedContractInput);
|
|
651
|
+
}
|
|
652
|
+
if (!pipeline.ok) {
|
|
653
|
+
throw invalid('MODEL_INVALID_RESPONSE', 'Model response failed the bounded response pipeline.', 'validating', {
|
|
654
|
+
pipelineStage: pipeline.stage,
|
|
655
|
+
repairable: pipeline.repairable,
|
|
656
|
+
repairCalls: repairsUsed,
|
|
657
|
+
issues: pipeline.issues,
|
|
658
|
+
});
|
|
659
|
+
}
|
|
660
|
+
await this.#stage(executionId, attemptNumber, 'interpreting', now);
|
|
661
|
+
const moduleResult = validateModuleResult(await resolved.task.interpret(pipeline.value, resolved.taskInput, context), this.#hashing);
|
|
662
|
+
await this.#stage(executionId, attemptNumber, 'evaluating', now);
|
|
663
|
+
await this.#stage(executionId, attemptNumber, 'preparing-commit', now);
|
|
664
|
+
const preparedMemory = this.#memory.prepare(resolved.module.memoryPolicy, moduleResult.memories, loaded.memories, {
|
|
665
|
+
namespace: request.namespace,
|
|
666
|
+
entityId: request.entityId,
|
|
667
|
+
executionId,
|
|
668
|
+
now,
|
|
669
|
+
});
|
|
670
|
+
const projectionInput = buildStateProjectionInput(moduleResult, preparedMemory, this.#hashing);
|
|
671
|
+
const delta = resolved.task.projectState(projectionInput, context);
|
|
672
|
+
const operationKey = deriveOperationKey({
|
|
673
|
+
executionId,
|
|
674
|
+
namespace: request.namespace,
|
|
675
|
+
task: request.task,
|
|
676
|
+
entityId: request.entityId,
|
|
677
|
+
}, this.#hashing);
|
|
678
|
+
const preparedState = this.#state.prepare(resolved.module, loaded.state, request.expectedRevision, delta, {
|
|
679
|
+
entityId: request.entityId,
|
|
680
|
+
executionId,
|
|
681
|
+
operationKey,
|
|
682
|
+
now,
|
|
683
|
+
});
|
|
684
|
+
const content = {
|
|
685
|
+
executionId,
|
|
686
|
+
expectedRevision: request.expectedRevision,
|
|
687
|
+
documents: moduleResult.documents,
|
|
688
|
+
memoryCandidates: moduleResult.memories,
|
|
689
|
+
memory: preparedMemory,
|
|
690
|
+
state: preparedState,
|
|
691
|
+
evaluatorRuns: [],
|
|
692
|
+
events: moduleResult.events,
|
|
693
|
+
committedAt: now,
|
|
694
|
+
replayEvidence: {
|
|
695
|
+
taskInput: resolved.taskInput,
|
|
696
|
+
readSet: replayReadSet,
|
|
697
|
+
},
|
|
698
|
+
};
|
|
699
|
+
const prepared = {
|
|
700
|
+
...content,
|
|
701
|
+
operationDigest: computeOperationDigest(content, this.#hashing),
|
|
702
|
+
};
|
|
703
|
+
const committed = await this.#repository.commit(prepared);
|
|
704
|
+
return deepFreeze({
|
|
705
|
+
status: 'committed',
|
|
706
|
+
executionId,
|
|
707
|
+
replayed: false,
|
|
708
|
+
reuseReason: resumption?.recordedResponse === undefined
|
|
709
|
+
? 'fresh'
|
|
710
|
+
: 'recorded-response-resume',
|
|
711
|
+
revision: committed.revision,
|
|
712
|
+
documentKeys: committed.documentKeys,
|
|
713
|
+
eventIds: committed.eventIds,
|
|
714
|
+
});
|
|
715
|
+
}
|
|
716
|
+
async #replay(evidence) {
|
|
717
|
+
const differences = [];
|
|
718
|
+
const recordedDigest = evidence.preparedCommit.operationDigest;
|
|
719
|
+
const unavailable = (code, value) => deepFreeze({
|
|
720
|
+
executionId: evidence.executionId,
|
|
721
|
+
mode: 'verify',
|
|
722
|
+
status: 'unavailable',
|
|
723
|
+
recordedDigest,
|
|
724
|
+
differences: [...differences, diagnostic(code, 'error', value)],
|
|
725
|
+
});
|
|
726
|
+
const changed = (code, value) => deepFreeze({
|
|
727
|
+
executionId: evidence.executionId,
|
|
728
|
+
mode: 'verify',
|
|
729
|
+
status: 'different',
|
|
730
|
+
recordedDigest,
|
|
731
|
+
differences: [...differences, diagnostic(code, 'error', value)],
|
|
732
|
+
});
|
|
733
|
+
const call = evidence.modelCalls.find((candidate) => candidate.callKey === 'model:0' &&
|
|
734
|
+
candidate.attempt === 1 &&
|
|
735
|
+
candidate.purpose === 'primary');
|
|
736
|
+
if (call === undefined ||
|
|
737
|
+
call.status !== 'succeeded' ||
|
|
738
|
+
call.response === undefined) {
|
|
739
|
+
return unavailable('REPLAY_MODEL_RESPONSE_UNAVAILABLE', {
|
|
740
|
+
retention: evidence.effectivePolicy.retention,
|
|
741
|
+
});
|
|
742
|
+
}
|
|
743
|
+
try {
|
|
744
|
+
const resolved = this.#resolve(evidence.request.namespace, evidence.request.task, evidence.taskInput);
|
|
745
|
+
if (resolved.contractFingerprint !== evidence.contractFingerprint) {
|
|
746
|
+
differences.push(diagnostic('REPLAY_CONTRACT_FINGERPRINT_DIFFERENT', 'warning', {
|
|
747
|
+
recorded: evidence.contractFingerprint,
|
|
748
|
+
current: resolved.contractFingerprint,
|
|
749
|
+
}));
|
|
750
|
+
}
|
|
751
|
+
const inputHash = computeTaskInputHash(resolved.taskInput, this.#hashing);
|
|
752
|
+
if (inputHash !== evidence.inputHash) {
|
|
753
|
+
differences.push(diagnostic('REPLAY_TASK_INPUT_HASH_DIFFERENT', 'warning', {
|
|
754
|
+
recorded: evidence.inputHash,
|
|
755
|
+
current: inputHash,
|
|
756
|
+
}));
|
|
757
|
+
}
|
|
758
|
+
const now = evidence.preparedCommit.committedAt;
|
|
759
|
+
const context = readContext(evidence.executionId, evidence.request.entityId, now, evidence.readSet);
|
|
760
|
+
const projected = await resolved.task.project(resolved.taskInput, context);
|
|
761
|
+
const contractInput = validateSchemaJson(resolved.contract.inputSchema, projected, this.#hashing, 'DOMAIN_INVALID_RESULT', 'loading', 'Replay contract input');
|
|
762
|
+
const modelRequest = validateModelRequest(resolved.contract.buildRequest(contractInput, {
|
|
763
|
+
executionId: evidence.executionId,
|
|
764
|
+
now,
|
|
765
|
+
}));
|
|
766
|
+
const requestHash = computeModelRequestHash(modelRequest, this.#hashing);
|
|
767
|
+
if (requestHash !== call.requestHash) {
|
|
768
|
+
differences.push(diagnostic('REPLAY_MODEL_REQUEST_HASH_DIFFERENT', 'warning', {
|
|
769
|
+
recorded: call.requestHash,
|
|
770
|
+
current: requestHash,
|
|
771
|
+
}));
|
|
772
|
+
}
|
|
773
|
+
const response = validateNormalizedModelResponse(call.response);
|
|
774
|
+
const responseHash = computeModelResponseHash(response, this.#hashing);
|
|
775
|
+
if (responseHash !== call.responseHash) {
|
|
776
|
+
differences.push(diagnostic('REPLAY_MODEL_RESPONSE_HASH_DIFFERENT', 'warning', {
|
|
777
|
+
recorded: call.responseHash ?? null,
|
|
778
|
+
current: responseHash,
|
|
779
|
+
}));
|
|
780
|
+
}
|
|
781
|
+
const pipeline = this.#pipeline.process(response, resolved.contract, contractInput);
|
|
782
|
+
if (!pipeline.ok) {
|
|
783
|
+
return changed('REPLAY_RESPONSE_PIPELINE_DIFFERENT', {
|
|
784
|
+
stage: pipeline.stage,
|
|
785
|
+
issues: pipeline.issues,
|
|
786
|
+
});
|
|
787
|
+
}
|
|
788
|
+
const moduleResult = validateModuleResult(await resolved.task.interpret(pipeline.value, resolved.taskInput, context), this.#hashing);
|
|
789
|
+
const replayQuery = memoryQuery(evidence.request.namespace, evidence.request.entityId, evidence.request.task);
|
|
790
|
+
const replayIds = evidence.preparedCommit.memory.mutations
|
|
791
|
+
.filter((mutation) => mutation.action === 'create')
|
|
792
|
+
.map((mutation) => mutation.record.memoryId);
|
|
793
|
+
let idIndex = 0;
|
|
794
|
+
const replayMemory = createMemoryEngine({
|
|
795
|
+
hashing: this.#hashing,
|
|
796
|
+
ids: {
|
|
797
|
+
next(kind) {
|
|
798
|
+
if (kind !== 'memory') {
|
|
799
|
+
throw new Error(`Replay forbids ${kind} ID allocation.`);
|
|
800
|
+
}
|
|
801
|
+
const id = replayIds[idIndex];
|
|
802
|
+
if (id === undefined) {
|
|
803
|
+
throw new Error('Replay has no recorded memory ID remaining.');
|
|
804
|
+
}
|
|
805
|
+
idIndex += 1;
|
|
806
|
+
return id;
|
|
807
|
+
},
|
|
808
|
+
},
|
|
809
|
+
});
|
|
810
|
+
const reranked = replayMemory.retrieve(resolved.module.memoryPolicy, replayQuery, evidence.readSet.loadedMemories);
|
|
811
|
+
const recordedRanked = evidence.readSet.retrievedMemories.map(({ record, score, reasons }) => ({ record, score, reasons }));
|
|
812
|
+
if (!canonicalEqual(reranked, recordedRanked, this.#hashing)) {
|
|
813
|
+
differences.push(diagnostic('REPLAY_RETRIEVAL_DIFFERENT', 'warning'));
|
|
814
|
+
}
|
|
815
|
+
const preparedMemory = replayMemory.prepare(resolved.module.memoryPolicy, moduleResult.memories, evidence.readSet.loadedMemories, {
|
|
816
|
+
namespace: evidence.request.namespace,
|
|
817
|
+
entityId: evidence.request.entityId,
|
|
818
|
+
executionId: evidence.executionId,
|
|
819
|
+
now,
|
|
820
|
+
});
|
|
821
|
+
if (idIndex !== replayIds.length) {
|
|
822
|
+
differences.push(diagnostic('REPLAY_RECORDED_MEMORY_IDS_UNUSED', 'warning', {
|
|
823
|
+
recorded: replayIds.length,
|
|
824
|
+
used: idIndex,
|
|
825
|
+
}));
|
|
826
|
+
}
|
|
827
|
+
const projection = buildStateProjectionInput(moduleResult, preparedMemory, this.#hashing);
|
|
828
|
+
const delta = resolved.task.projectState(projection, context);
|
|
829
|
+
const operationKey = deriveOperationKey({
|
|
830
|
+
executionId: evidence.executionId,
|
|
831
|
+
namespace: evidence.request.namespace,
|
|
832
|
+
task: evidence.request.task,
|
|
833
|
+
entityId: evidence.request.entityId,
|
|
834
|
+
}, this.#hashing);
|
|
835
|
+
const preparedState = this.#state.prepare(resolved.module, evidence.readSet.state, evidence.request.expectedRevision, delta, {
|
|
836
|
+
entityId: evidence.request.entityId,
|
|
837
|
+
executionId: evidence.executionId,
|
|
838
|
+
operationKey,
|
|
839
|
+
now,
|
|
840
|
+
});
|
|
841
|
+
const content = {
|
|
842
|
+
executionId: evidence.executionId,
|
|
843
|
+
expectedRevision: evidence.request.expectedRevision,
|
|
844
|
+
documents: moduleResult.documents,
|
|
845
|
+
memoryCandidates: moduleResult.memories,
|
|
846
|
+
memory: preparedMemory,
|
|
847
|
+
state: preparedState,
|
|
848
|
+
evaluatorRuns: [],
|
|
849
|
+
events: moduleResult.events,
|
|
850
|
+
committedAt: now,
|
|
851
|
+
replayEvidence: {
|
|
852
|
+
taskInput: resolved.taskInput,
|
|
853
|
+
readSet: evidence.readSet,
|
|
854
|
+
},
|
|
855
|
+
};
|
|
856
|
+
const replayDigest = computeOperationDigest(content, this.#hashing);
|
|
857
|
+
if (replayDigest !== recordedDigest) {
|
|
858
|
+
differences.push(diagnostic('REPLAY_OPERATION_DIGEST_DIFFERENT', 'error', {
|
|
859
|
+
recorded: recordedDigest,
|
|
860
|
+
current: replayDigest,
|
|
861
|
+
}));
|
|
862
|
+
}
|
|
863
|
+
return deepFreeze({
|
|
864
|
+
executionId: evidence.executionId,
|
|
865
|
+
mode: 'verify',
|
|
866
|
+
status: replayDigest === recordedDigest ? 'match' : 'different',
|
|
867
|
+
recordedDigest,
|
|
868
|
+
replayDigest,
|
|
869
|
+
differences,
|
|
870
|
+
});
|
|
871
|
+
}
|
|
872
|
+
catch (error) {
|
|
873
|
+
const data = errorData(error, 'failed');
|
|
874
|
+
return changed('REPLAY_RECOMPUTATION_FAILED', {
|
|
875
|
+
code: data.code,
|
|
876
|
+
stage: data.stage,
|
|
877
|
+
message: data.message,
|
|
878
|
+
});
|
|
879
|
+
}
|
|
880
|
+
}
|
|
881
|
+
}
|
|
882
|
+
export function createExecutionEngine(options) {
|
|
883
|
+
return new SingleTaskExecutionEngine(options);
|
|
884
|
+
}
|
|
885
|
+
//# sourceMappingURL=execution-engine.js.map
|