@inventarch/agent-system 0.0.0-stage → 1.0.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/LICENSE +202 -0
- package/NOTICE +4 -0
- package/README.md +2 -2
- package/SPEC.md +3 -0
- package/dist/action.d.ts +12 -0
- package/dist/action.d.ts.map +1 -0
- package/dist/action.js +148 -0
- package/dist/action.js.map +1 -0
- package/dist/contracts.d.ts +34 -0
- package/dist/contracts.d.ts.map +1 -0
- package/dist/contracts.js +166 -0
- package/dist/contracts.js.map +1 -0
- package/dist/engine.d.ts +46 -0
- package/dist/engine.d.ts.map +1 -0
- package/dist/engine.js +1083 -0
- package/dist/engine.js.map +1 -0
- package/dist/index.d.ts +4 -0
- package/dist/index.d.ts.map +1 -0
- package/dist/index.js +4 -0
- package/dist/index.js.map +1 -0
- package/dist/types.d.ts +230 -0
- package/dist/types.d.ts.map +1 -0
- package/dist/types.js +3 -0
- package/dist/types.js.map +1 -0
- package/package.json +63 -3
package/dist/engine.js
ADDED
|
@@ -0,0 +1,1083 @@
|
|
|
1
|
+
import { randomUUID } from 'node:crypto';
|
|
2
|
+
import { canonical, copy, digest, terminal, unresolved, humanWaitRuns, SessionError } from '@inventarch/session-system';
|
|
3
|
+
import { check, EngineError, parseAction, validateShape } from './action.js';
|
|
4
|
+
import { DEFAULT_MODEL_REQUEST_BYTES, MAX_MODEL_REQUEST_BYTES } from './types.js';
|
|
5
|
+
import { admittedReview, boundedLimits, causes, evaluateCompletion, narrowGrant, offerReview, operationAllowed, taskContract, } from './contracts.js';
|
|
6
|
+
const json = (value) => JSON.parse(canonical(value));
|
|
7
|
+
const modelOutputTokens = 2048;
|
|
8
|
+
async function bounded(work, signal) {
|
|
9
|
+
signal.throwIfAborted();
|
|
10
|
+
let abort;
|
|
11
|
+
const stopped = new Promise((_, reject) => {
|
|
12
|
+
abort = () => reject(signal.reason);
|
|
13
|
+
signal.addEventListener('abort', abort, { once: true });
|
|
14
|
+
});
|
|
15
|
+
try {
|
|
16
|
+
// The losing adapter promise retains rejection handling, but no admission callback.
|
|
17
|
+
const result = await Promise.race([
|
|
18
|
+
Promise.resolve().then(() => {
|
|
19
|
+
signal.throwIfAborted();
|
|
20
|
+
return work();
|
|
21
|
+
}),
|
|
22
|
+
stopped,
|
|
23
|
+
]);
|
|
24
|
+
signal.throwIfAborted();
|
|
25
|
+
return result;
|
|
26
|
+
}
|
|
27
|
+
finally {
|
|
28
|
+
signal.removeEventListener('abort', abort);
|
|
29
|
+
}
|
|
30
|
+
}
|
|
31
|
+
const repairableActionCodes = new Set([
|
|
32
|
+
'IA-ENGINE-ACTION-INVALID',
|
|
33
|
+
'IA-ENGINE-INPUT-INVALID',
|
|
34
|
+
'IA-ENGINE-DELEGATION-DENIED',
|
|
35
|
+
'IA-ENGINE-TASK-INVALID',
|
|
36
|
+
'IA-ENGINE-OUTCOME-DENIED',
|
|
37
|
+
'IA-ENGINE-EVIDENCE-INVALID',
|
|
38
|
+
'IA-ENGINE-COMPLETION-DENIED',
|
|
39
|
+
'IA-ENGINE-CANDIDATE-UNVERIFIED',
|
|
40
|
+
]);
|
|
41
|
+
export function manifestDigest(manifest) {
|
|
42
|
+
const { digest: _digest, ...body } = manifest;
|
|
43
|
+
return digest(body);
|
|
44
|
+
}
|
|
45
|
+
/** One shared scheduler; hosts provide single-attempt adapters and authenticated grants. */
|
|
46
|
+
export class Engine {
|
|
47
|
+
host;
|
|
48
|
+
manifest;
|
|
49
|
+
constructor(manifest, host) {
|
|
50
|
+
this.host = host;
|
|
51
|
+
check(manifest.version === 1 && manifest.digest === manifestDigest(manifest), 'IA-ENGINE-MANIFEST-INVALID', 'Manifest version/digest mismatch');
|
|
52
|
+
// Reactions need a qualified causal outbox consumer before they may be enabled.
|
|
53
|
+
check(manifest.reactions.length === 0, 'IA-ENGINE-BINDING-UNAVAILABLE', 'Event reaction bindings are not installed in this execution profile');
|
|
54
|
+
for (const profile of Object.values(manifest.profiles))
|
|
55
|
+
if (profile.contract) {
|
|
56
|
+
check(profile.contract.completionEvaluator === 'ia.completion.v1' &&
|
|
57
|
+
Number.isSafeInteger(profile.contract.repairAttempts) &&
|
|
58
|
+
profile.contract.repairAttempts >= 0 &&
|
|
59
|
+
Number.isSafeInteger(profile.contract.maxAttempts) &&
|
|
60
|
+
profile.contract.maxAttempts > 0, 'IA-ENGINE-BINDING-UNAVAILABLE', 'Unsupported completion or recovery contract');
|
|
61
|
+
const requestBytes = profile.contract.requestBytes;
|
|
62
|
+
check(requestBytes === undefined ||
|
|
63
|
+
(Number.isSafeInteger(requestBytes) && requestBytes > 0 && requestBytes <= MAX_MODEL_REQUEST_BYTES), 'IA-ENGINE-BINDING-UNAVAILABLE', 'Unsupported installed model request ceiling');
|
|
64
|
+
}
|
|
65
|
+
this.manifest = copy(manifest);
|
|
66
|
+
}
|
|
67
|
+
now() {
|
|
68
|
+
return this.host.now?.() ?? Date.now();
|
|
69
|
+
}
|
|
70
|
+
attemptSignal(state, run, grant, timeoutMs, caller) {
|
|
71
|
+
const remaining = Math.min(state?.limits.deadline ?? Infinity, run?.limits?.deadline ?? Infinity, grant?.limits.deadline ?? Infinity, grant?.expiresAt ?? Infinity) - this.now();
|
|
72
|
+
const timeout = AbortSignal.timeout(Math.max(0, Math.ceil(Math.min(timeoutMs, remaining))));
|
|
73
|
+
return caller ? AbortSignal.any([caller, timeout]) : timeout;
|
|
74
|
+
}
|
|
75
|
+
checks(profile, phase) {
|
|
76
|
+
return profile.contract
|
|
77
|
+
? profile.contract.checks.filter((c) => c.phases.includes(phase)).map((c) => c.id)
|
|
78
|
+
: profile.checks;
|
|
79
|
+
}
|
|
80
|
+
dispatchBudget(state, run, grant) {
|
|
81
|
+
const used = state.budget, limits = grant.limits;
|
|
82
|
+
check(this.now() < Math.min(state.limits.deadline, run.limits?.deadline ?? Infinity, limits.deadline) &&
|
|
83
|
+
used.steps <= limits.steps &&
|
|
84
|
+
used.modelCalls <= limits.modelCalls &&
|
|
85
|
+
used.operations <= limits.operations &&
|
|
86
|
+
used.children <= limits.children &&
|
|
87
|
+
used.usedTokens + used.reservedTokens <= limits.tokens &&
|
|
88
|
+
used.retainedBytes <= limits.bytes, 'IA-ENGINE-LIMIT-EXCEEDED', 'Current task budget no longer admits dispatch');
|
|
89
|
+
}
|
|
90
|
+
async grant(principal, state, profile, run, signal) {
|
|
91
|
+
check(state === null || (state.principal === principal && state.manifestDigest === digest(this.manifest)), 'IA-ENGINE-AUTHORITY-DENIED', 'Principal or manifest changed');
|
|
92
|
+
const timed = this.attemptSignal(state, run, undefined, 60_000, signal);
|
|
93
|
+
check(await bounded(() => this.host.verifyManifest(this.manifest), timed), 'IA-ENGINE-SOURCE-CHANGED', 'Pinned inputs or executable bindings are unavailable');
|
|
94
|
+
const grant = await bounded(() => this.host.authorize(principal, this.manifest, state), timed);
|
|
95
|
+
check(grant.principal === principal &&
|
|
96
|
+
grant.workspace === this.manifest.workspace &&
|
|
97
|
+
grant.expiresAt > this.now() &&
|
|
98
|
+
grant.profiles.includes(profile.id), 'IA-ENGINE-AUTHORITY-DENIED', 'Current grant does not authorize this profile');
|
|
99
|
+
const effective = narrowGrant(grant, run?.authority);
|
|
100
|
+
effective.operations = effective.operations.filter((id) => profile.operations.includes(id));
|
|
101
|
+
if (profile.contract)
|
|
102
|
+
effective.effects = effective.effects.filter((e) => profile.contract.effects.includes(e));
|
|
103
|
+
check(effective.profiles.includes(profile.id), 'IA-ENGINE-AUTHORITY-DENIED', 'Profile exceeds the inherited authority');
|
|
104
|
+
return effective;
|
|
105
|
+
}
|
|
106
|
+
effectContext(state, run, profile, operation, action, grant, owner, id, invocationId, review, signal, reconciling = false) {
|
|
107
|
+
// Keep a stable abort source in every signal derived for this attempt. A fresh grant
|
|
108
|
+
// must stop an already-running adapter race, not merely replace its context property.
|
|
109
|
+
const narrowed = new AbortController();
|
|
110
|
+
const context = {
|
|
111
|
+
sessionId: state.id,
|
|
112
|
+
runId: run.id,
|
|
113
|
+
invocationId,
|
|
114
|
+
attemptId: id,
|
|
115
|
+
principal: state.principal,
|
|
116
|
+
agent: profile.agent,
|
|
117
|
+
grant,
|
|
118
|
+
manifest: this.manifest,
|
|
119
|
+
owner,
|
|
120
|
+
signal: AbortSignal.any([signal, narrowed.signal]),
|
|
121
|
+
...(review ? { review } : {}),
|
|
122
|
+
};
|
|
123
|
+
context.assertCurrent = async () => {
|
|
124
|
+
context.signal.throwIfAborted();
|
|
125
|
+
await this.host.store.validate(owner);
|
|
126
|
+
const latest = await this.host.store.read(state.id), active = latest.runs[run.id], current = await this.grant(state.principal, latest, profile, active, context.signal);
|
|
127
|
+
const attempt = latest.attempts[id];
|
|
128
|
+
check(attempt &&
|
|
129
|
+
attempt.inputDigest === digest(action.input) &&
|
|
130
|
+
attempt.bindingDigest === operation.digest &&
|
|
131
|
+
(reconciling
|
|
132
|
+
? attempt.status === 'dispatched' || attempt.effect === 'unknown' || attempt.effect === 'partial'
|
|
133
|
+
: attempt.status === 'prepared' || attempt.status === 'dispatched'), 'IA-ENGINE-BINDING-UNAVAILABLE', 'Prepared effect contract is unavailable');
|
|
134
|
+
this.dispatchBudget(latest, active, current);
|
|
135
|
+
operationAllowed(operation, profile, current);
|
|
136
|
+
admittedReview(latest, active, operation, action, current, this.now(), id);
|
|
137
|
+
context.grant = current;
|
|
138
|
+
const ceiling = this.attemptSignal(latest, active, current, operation.timeoutMs, context.signal);
|
|
139
|
+
if (ceiling.aborted)
|
|
140
|
+
narrowed.abort(ceiling.reason);
|
|
141
|
+
else
|
|
142
|
+
ceiling.addEventListener('abort', () => narrowed.abort(ceiling.reason), { once: true });
|
|
143
|
+
for (const checkId of this.checks(profile, 'before-effect')) {
|
|
144
|
+
const result = await bounded(() => this.host.evaluate(checkId, json({ phase: 'before-effect', action, contract: active.contract ?? null }), current), context.signal);
|
|
145
|
+
check(result.status === 'pass', 'IA-ENGINE-GOVERNANCE-DENIED', `Required evaluator ${checkId} did not pass`);
|
|
146
|
+
}
|
|
147
|
+
check(this.host.preflight &&
|
|
148
|
+
(await bounded(() => this.host.preflight(operation, action.input, context), context.signal)), 'IA-ENGINE-DESTINATION-DENIED', 'Current destination/binding preflight is unavailable or denied');
|
|
149
|
+
const controls = await this.host.store.read(state.id);
|
|
150
|
+
check(reconciling ||
|
|
151
|
+
(!controls.runs[run.id].cancelRequested &&
|
|
152
|
+
!controls.runs[run.id].pauseRequested &&
|
|
153
|
+
!controls.controls.some((c) => !c.applied)), 'IA-ENGINE-AUTHORITY-DENIED', 'A control stopped the pending effect');
|
|
154
|
+
context.signal.throwIfAborted();
|
|
155
|
+
await this.host.store.validate(owner);
|
|
156
|
+
context.signal.throwIfAborted();
|
|
157
|
+
check(current.expiresAt > this.now(), 'IA-ENGINE-AUTHORITY-DENIED', 'Current effect grant expired before publication');
|
|
158
|
+
this.dispatchBudget(latest, active, current);
|
|
159
|
+
};
|
|
160
|
+
return context;
|
|
161
|
+
}
|
|
162
|
+
async start(request) {
|
|
163
|
+
const profile = this.manifest.profiles[request.profile];
|
|
164
|
+
check(profile, 'IA-ENGINE-PROFILE-UNAVAILABLE', 'Profile is not installed');
|
|
165
|
+
const grant = await this.grant(request.principal, null, profile);
|
|
166
|
+
let prior = null;
|
|
167
|
+
try {
|
|
168
|
+
prior = await this.host.store.read(request.sessionId);
|
|
169
|
+
}
|
|
170
|
+
catch (error) {
|
|
171
|
+
if (!(error instanceof SessionError && error.code === 'IA-SESSION-NOT-FOUND'))
|
|
172
|
+
throw error;
|
|
173
|
+
}
|
|
174
|
+
const limits = boundedLimits({ ...(prior?.limits ?? grant.limits), ...request.limits }, profile.contract?.limits, this.now());
|
|
175
|
+
for (const key of Object.keys(limits))
|
|
176
|
+
check(limits[key] <= grant.limits[key], 'IA-ENGINE-AUTHORITY-DENIED', 'Requested budget exceeds grant');
|
|
177
|
+
const contract = taskContract(profile, request.task, request.artifact);
|
|
178
|
+
await this.host.store.command({
|
|
179
|
+
id: request.commandId,
|
|
180
|
+
sessionId: request.sessionId,
|
|
181
|
+
actor: request.principal,
|
|
182
|
+
expected: 0,
|
|
183
|
+
mutation: {
|
|
184
|
+
type: 'session.create',
|
|
185
|
+
sessionId: request.sessionId,
|
|
186
|
+
principal: request.principal,
|
|
187
|
+
workspace: this.manifest.workspace,
|
|
188
|
+
manifest: json(this.manifest),
|
|
189
|
+
manifestDigest: digest(this.manifest),
|
|
190
|
+
rootId: 'root',
|
|
191
|
+
profile: profile.id,
|
|
192
|
+
agent: profile.agent,
|
|
193
|
+
task: request.task,
|
|
194
|
+
limits,
|
|
195
|
+
...(!prior || prior.runs[prior.rootId]?.contract
|
|
196
|
+
? { contract: json(contract), authority: prior?.runs[prior.rootId]?.authority ?? json(grant) }
|
|
197
|
+
: {}),
|
|
198
|
+
},
|
|
199
|
+
});
|
|
200
|
+
return this.host.store.read(request.sessionId);
|
|
201
|
+
}
|
|
202
|
+
async advance(sessionId, principal, options = {}) {
|
|
203
|
+
check(Number.isSafeInteger(options.maxActions ?? 32) &&
|
|
204
|
+
(options.maxActions ?? 32) > 0 &&
|
|
205
|
+
(options.maxActions ?? 32) <= 128, 'IA-ENGINE-INPUT-INVALID', 'Advance requires 1..128 scheduler actions');
|
|
206
|
+
const store = this.host.store, initial = await store.read(sessionId);
|
|
207
|
+
check(initial.principal === principal, 'IA-ENGINE-AUTHORITY-DENIED', 'Session belongs to another principal');
|
|
208
|
+
const owner = await store.acquire(sessionId);
|
|
209
|
+
const save = async (mutation, following) => {
|
|
210
|
+
// Controls may arrive during an attempt. Retry the same internal mutation against its new head.
|
|
211
|
+
for (let retry = 0;; retry++) {
|
|
212
|
+
const state = await store.read(sessionId);
|
|
213
|
+
try {
|
|
214
|
+
await store.command({
|
|
215
|
+
id: randomUUID(),
|
|
216
|
+
sessionId,
|
|
217
|
+
actor: principal,
|
|
218
|
+
expected: state.sequence,
|
|
219
|
+
mutation,
|
|
220
|
+
...(following ? { following } : {}),
|
|
221
|
+
}, owner);
|
|
222
|
+
return;
|
|
223
|
+
}
|
|
224
|
+
catch (error) {
|
|
225
|
+
if (!(error instanceof SessionError && error.code === 'IA-SESSION-REVISION-CONFLICT' && retry < 4))
|
|
226
|
+
throw error;
|
|
227
|
+
}
|
|
228
|
+
}
|
|
229
|
+
};
|
|
230
|
+
try {
|
|
231
|
+
for (let count = 0; count < (options.maxActions ?? 32); count++) {
|
|
232
|
+
let state = await store.read(sessionId);
|
|
233
|
+
if (terminal(state.runs[state.rootId]))
|
|
234
|
+
break;
|
|
235
|
+
const control = state.controls.find((c) => !c.applied);
|
|
236
|
+
if (control) {
|
|
237
|
+
await save({ type: 'control.apply', controlId: control.id });
|
|
238
|
+
continue;
|
|
239
|
+
}
|
|
240
|
+
const pending = unresolved(state)[0];
|
|
241
|
+
if (pending) {
|
|
242
|
+
if (!(await this.recover(state, pending, owner, save, options.signal)))
|
|
243
|
+
break;
|
|
244
|
+
continue;
|
|
245
|
+
}
|
|
246
|
+
const finishedChild = Object.values(state.runs).find((r) => r.parentId !== null && terminal(r) && !r.returned && !terminal(state.runs[r.parentId]));
|
|
247
|
+
if (finishedChild) {
|
|
248
|
+
await save({ type: 'child.return', childId: finishedChild.id });
|
|
249
|
+
continue;
|
|
250
|
+
}
|
|
251
|
+
const run = Object.values(state.runs)
|
|
252
|
+
.filter((r) => !terminal(r))
|
|
253
|
+
.sort((a, b) => b.depth - a.depth)[0];
|
|
254
|
+
if (run.cancelRequested || run.pauseRequested) {
|
|
255
|
+
if (run.status === 'paused')
|
|
256
|
+
break;
|
|
257
|
+
const id = randomUUID();
|
|
258
|
+
await save({
|
|
259
|
+
type: 'control.submit',
|
|
260
|
+
control: {
|
|
261
|
+
id,
|
|
262
|
+
actor: principal,
|
|
263
|
+
runId: run.id,
|
|
264
|
+
kind: run.cancelRequested ? 'cancel' : 'pause',
|
|
265
|
+
applied: false,
|
|
266
|
+
},
|
|
267
|
+
}, [{ type: 'control.apply', controlId: id }]);
|
|
268
|
+
continue;
|
|
269
|
+
}
|
|
270
|
+
if (run.status === 'waiting') {
|
|
271
|
+
const ready = run.wait?.reason === 'input'
|
|
272
|
+
? run.wait.objectIds.every((id) => state.questions[id]?.status === 'answered')
|
|
273
|
+
: run.wait?.reason === 'review'
|
|
274
|
+
? run.wait.objectIds.every((id) => ['accepted', 'rejected'].includes(state.proposals[id]?.status ?? ''))
|
|
275
|
+
: run.wait?.reason === 'external-model'
|
|
276
|
+
? run.wait.objectIds.every((id) => state.attempts[id]?.status === 'observed')
|
|
277
|
+
: false;
|
|
278
|
+
if (!ready)
|
|
279
|
+
break;
|
|
280
|
+
if (options.signal?.aborted)
|
|
281
|
+
break;
|
|
282
|
+
const profile = this.manifest.profiles[run.profile];
|
|
283
|
+
check(profile, 'IA-ENGINE-PROFILE-UNAVAILABLE', 'Pinned profile unavailable');
|
|
284
|
+
const grant = await this.grant(principal, state, profile, run, options.signal);
|
|
285
|
+
this.dispatchBudget(state, run, grant);
|
|
286
|
+
await save({ type: 'run.ready', runId: run.id });
|
|
287
|
+
continue;
|
|
288
|
+
}
|
|
289
|
+
if (run.status === 'created') {
|
|
290
|
+
await save({ type: 'run.ready', runId: run.id });
|
|
291
|
+
continue;
|
|
292
|
+
}
|
|
293
|
+
if (options.signal?.aborted)
|
|
294
|
+
break;
|
|
295
|
+
const profile = this.manifest.profiles[run.profile];
|
|
296
|
+
check(profile, 'IA-ENGINE-PROFILE-UNAVAILABLE', 'Pinned profile unavailable');
|
|
297
|
+
const grant = await this.grant(principal, state, profile, run, options.signal);
|
|
298
|
+
if (run.pendingAction !== null) {
|
|
299
|
+
try {
|
|
300
|
+
await this.act(state, run, profile, grant, parseAction(run.pendingAction), save, owner, options.signal);
|
|
301
|
+
}
|
|
302
|
+
catch (error) {
|
|
303
|
+
if (!(error instanceof EngineError) || !repairableActionCodes.has(error.code) || run.retryOf)
|
|
304
|
+
throw error;
|
|
305
|
+
// Only reject an undispatched model action. Never reinterpret an adapter/host failure
|
|
306
|
+
// after preparation as a repair, or erase an operation's effect/retry contract.
|
|
307
|
+
const latest = await store.read(sessionId), current = latest.runs[run.id];
|
|
308
|
+
const source = run.cause?.modelAttemptId
|
|
309
|
+
? state.attempts[run.cause.modelAttemptId]
|
|
310
|
+
: Object.values(state.attempts)
|
|
311
|
+
.reverse()
|
|
312
|
+
.find((a) => a.runId === run.id &&
|
|
313
|
+
a.kind === 'model' &&
|
|
314
|
+
a.actionAccepted &&
|
|
315
|
+
a.output !== null &&
|
|
316
|
+
digest(a.output.action) === digest(run.pendingAction));
|
|
317
|
+
if (!source ||
|
|
318
|
+
source.kind !== 'model' ||
|
|
319
|
+
source.status !== 'observed' ||
|
|
320
|
+
source.error !== null ||
|
|
321
|
+
current.pendingAction === null ||
|
|
322
|
+
digest(current.pendingAction) !== digest(run.pendingAction) ||
|
|
323
|
+
unresolved(latest, run.id).length)
|
|
324
|
+
throw error;
|
|
325
|
+
await save({
|
|
326
|
+
type: 'run.wait',
|
|
327
|
+
runId: run.id,
|
|
328
|
+
wait: {
|
|
329
|
+
id: source.id,
|
|
330
|
+
reason: 'invalid-model-action',
|
|
331
|
+
objectIds: [source.id],
|
|
332
|
+
continuation: 'repair',
|
|
333
|
+
details: { code: error.code },
|
|
334
|
+
},
|
|
335
|
+
}, [{ type: 'action.clear', runId: run.id }]);
|
|
336
|
+
break;
|
|
337
|
+
}
|
|
338
|
+
continue;
|
|
339
|
+
}
|
|
340
|
+
const response = Object.values(state.attempts).find((a) => a.runId === run.id &&
|
|
341
|
+
a.kind === 'model' &&
|
|
342
|
+
a.status === 'observed' &&
|
|
343
|
+
a.error === null &&
|
|
344
|
+
!a.actionAccepted);
|
|
345
|
+
if (response) {
|
|
346
|
+
try {
|
|
347
|
+
const action = parseAction(response.output.action);
|
|
348
|
+
this.limits(state, grant, 'action', 0, run);
|
|
349
|
+
await save({
|
|
350
|
+
type: 'action.accept',
|
|
351
|
+
runId: run.id,
|
|
352
|
+
attemptId: response.id,
|
|
353
|
+
action: json(action),
|
|
354
|
+
cause: causes(state, run, randomUUID(), response.id),
|
|
355
|
+
});
|
|
356
|
+
}
|
|
357
|
+
catch (error) {
|
|
358
|
+
if (!(error instanceof EngineError) || error.code !== 'IA-ENGINE-ACTION-INVALID')
|
|
359
|
+
throw error;
|
|
360
|
+
await save({
|
|
361
|
+
type: 'run.wait',
|
|
362
|
+
runId: run.id,
|
|
363
|
+
wait: {
|
|
364
|
+
id: response.id,
|
|
365
|
+
reason: 'invalid-model-action',
|
|
366
|
+
objectIds: [response.id],
|
|
367
|
+
continuation: 'repair',
|
|
368
|
+
details: { code: error.code },
|
|
369
|
+
},
|
|
370
|
+
});
|
|
371
|
+
break;
|
|
372
|
+
}
|
|
373
|
+
continue;
|
|
374
|
+
}
|
|
375
|
+
await this.infer(state, run, profile, grant, save, owner, options.signal);
|
|
376
|
+
}
|
|
377
|
+
await store.checkpoint(sessionId, owner);
|
|
378
|
+
return await store.read(sessionId);
|
|
379
|
+
}
|
|
380
|
+
finally {
|
|
381
|
+
await store.release(owner);
|
|
382
|
+
}
|
|
383
|
+
}
|
|
384
|
+
limits(state, grant, kind, reservation, run) {
|
|
385
|
+
const used = state.budget, allowed = grant.limits;
|
|
386
|
+
const stepLimit = Math.min(state.limits.steps, allowed.steps);
|
|
387
|
+
check(this.now() < Math.min(state.limits.deadline, allowed.deadline) &&
|
|
388
|
+
(kind === 'operation' ? used.steps <= stepLimit : used.steps < stepLimit) &&
|
|
389
|
+
used.usedTokens + used.reservedTokens + reservation <= Math.min(state.limits.tokens, allowed.tokens) &&
|
|
390
|
+
(kind === 'model'
|
|
391
|
+
? used.modelCalls < Math.min(state.limits.modelCalls, allowed.modelCalls)
|
|
392
|
+
: kind === 'operation'
|
|
393
|
+
? used.operations < Math.min(state.limits.operations, allowed.operations)
|
|
394
|
+
: true), 'IA-ENGINE-LIMIT-EXCEEDED', 'Current budget/deadline exhausted');
|
|
395
|
+
for (let parent = run; parent; parent = parent.parentId ? state.runs[parent.parentId] : undefined)
|
|
396
|
+
if (parent.limits) {
|
|
397
|
+
const ids = new Set([parent.id]);
|
|
398
|
+
for (const child of Object.values(state.runs).sort((a, b) => a.depth - b.depth))
|
|
399
|
+
if (child.parentId && ids.has(child.parentId))
|
|
400
|
+
ids.add(child.id);
|
|
401
|
+
const attempts = Object.values(state.attempts).filter((a) => ids.has(a.runId)), bounds = parent.limits;
|
|
402
|
+
const steps = Object.values(state.runs)
|
|
403
|
+
.filter((r) => ids.has(r.id))
|
|
404
|
+
.reduce((sum, r) => sum + r.transcript.filter((row) => row?.kind === 'action').length, 0);
|
|
405
|
+
check(this.now() < bounds.deadline &&
|
|
406
|
+
(kind === 'action' ? steps < bounds.steps : steps <= bounds.steps) &&
|
|
407
|
+
attempts.reduce((sum, a) => sum + (a.status === 'observed' ? (a.usage ?? a.reservation) : a.reservation), 0) +
|
|
408
|
+
reservation <=
|
|
409
|
+
bounds.tokens &&
|
|
410
|
+
(kind === 'action' ||
|
|
411
|
+
attempts.reduce((sum, a) => sum +
|
|
412
|
+
(kind === 'model'
|
|
413
|
+
? Number(a.kind === 'model')
|
|
414
|
+
: a.kind === 'model'
|
|
415
|
+
? 0
|
|
416
|
+
: 1 + (a.reconciliations ?? 0)), 0) < (kind === 'model' ? bounds.modelCalls : bounds.operations)), 'IA-ENGINE-LIMIT-EXCEEDED', 'Run/delegation budget exhausted');
|
|
417
|
+
}
|
|
418
|
+
}
|
|
419
|
+
async infer(state, run, profile, grant, save, owner, signal) {
|
|
420
|
+
check(grant.models.includes(profile.model), 'IA-ENGINE-AUTHORITY-DENIED', 'Model is not granted');
|
|
421
|
+
const id = randomUUID(), contextSignal = this.attemptSignal(state, run, grant, 60_000, signal);
|
|
422
|
+
const request = {
|
|
423
|
+
version: 1,
|
|
424
|
+
sessionId: state.id,
|
|
425
|
+
runId: run.id,
|
|
426
|
+
attemptId: id,
|
|
427
|
+
task: run.task,
|
|
428
|
+
profile,
|
|
429
|
+
history: run.transcript,
|
|
430
|
+
context: null,
|
|
431
|
+
operations: profile.operations
|
|
432
|
+
.filter((op) => grant.operations.includes(op))
|
|
433
|
+
.map((op) => this.manifest.operations[op]),
|
|
434
|
+
maxOutputTokens: modelOutputTokens,
|
|
435
|
+
};
|
|
436
|
+
const requestBytes = profile.contract?.requestBytes ?? DEFAULT_MODEL_REQUEST_BYTES;
|
|
437
|
+
const available = requestBytes - Buffer.byteLength(canonical(request)) + 4;
|
|
438
|
+
check(available > 0, 'IA-ENGINE-CONTEXT-TOO-LARGE', 'Retained history and request wrapper exceed the installed context limit');
|
|
439
|
+
request.context = await bounded(() => this.host.context(profile, run.task, grant, Object.freeze({ bytes: available })), contextSignal);
|
|
440
|
+
await this.host.store.validate(owner);
|
|
441
|
+
contextSignal.throwIfAborted();
|
|
442
|
+
const bytes = Buffer.byteLength(canonical(request));
|
|
443
|
+
check(bytes <= requestBytes, 'IA-ENGINE-CONTEXT-TOO-LARGE', 'Model request exceeds the installed context limit');
|
|
444
|
+
const reservation = bytes + request.maxOutputTokens; // Conservative UTF-8 byte bound includes wrapper/history.
|
|
445
|
+
this.limits(state, grant, 'model', reservation, run);
|
|
446
|
+
await save({
|
|
447
|
+
type: 'attempt.prepare',
|
|
448
|
+
attempt: {
|
|
449
|
+
id,
|
|
450
|
+
invocationId: randomUUID(),
|
|
451
|
+
runId: run.id,
|
|
452
|
+
kind: 'model',
|
|
453
|
+
target: profile.model,
|
|
454
|
+
bindingDigest: digest({ adapter: this.host.model.id, model: profile.model }),
|
|
455
|
+
input: json(request),
|
|
456
|
+
inputDigest: digest(request),
|
|
457
|
+
authority: json(grant),
|
|
458
|
+
recovery: 'manual',
|
|
459
|
+
reservation,
|
|
460
|
+
cause: causes(state, run, randomUUID(), null),
|
|
461
|
+
},
|
|
462
|
+
});
|
|
463
|
+
const latest = await this.host.store.read(state.id), current = await this.grant(state.principal, latest, profile, run, contextSignal);
|
|
464
|
+
this.dispatchBudget(latest, run, current);
|
|
465
|
+
check(current.models.includes(profile.model) && digest(current.sources) === digest(grant.sources), 'IA-ENGINE-AUTHORITY-DENIED', 'Model or disclosed source authority changed before dispatch');
|
|
466
|
+
await save({ type: 'attempt.dispatch', attemptId: id });
|
|
467
|
+
await this.host.store.validate(owner);
|
|
468
|
+
let output = null, error = null, usage = null;
|
|
469
|
+
try {
|
|
470
|
+
const timed = this.attemptSignal(latest, run, current, 60_000, signal);
|
|
471
|
+
const result = await bounded(() => this.host.model.generate(request, timed), timed);
|
|
472
|
+
await this.host.store.validate(owner);
|
|
473
|
+
timed.throwIfAborted();
|
|
474
|
+
if ('deferred' in result) {
|
|
475
|
+
await save({
|
|
476
|
+
type: 'run.wait',
|
|
477
|
+
runId: run.id,
|
|
478
|
+
wait: { id, reason: 'external-model', objectIds: [id], continuation: 'submit-model-response', details: null },
|
|
479
|
+
});
|
|
480
|
+
return;
|
|
481
|
+
}
|
|
482
|
+
check(result.model === profile.model &&
|
|
483
|
+
(result.usage === null || (Number.isSafeInteger(result.usage) && result.usage >= 0)), 'IA-ENGINE-MODEL-INVALID', 'Model or usage mismatch');
|
|
484
|
+
output = json(result);
|
|
485
|
+
check(Buffer.byteLength(canonical(output)) <= 65_536, 'IA-ENGINE-MODEL-INVALID', 'Model response too large');
|
|
486
|
+
usage = result.usage;
|
|
487
|
+
}
|
|
488
|
+
catch {
|
|
489
|
+
error = 'model-attempt-failed';
|
|
490
|
+
}
|
|
491
|
+
await save({ type: 'attempt.observe', attemptId: id, output: error ? null : output, error, usage, effect: 'none' });
|
|
492
|
+
if (error)
|
|
493
|
+
await save({
|
|
494
|
+
type: 'run.wait',
|
|
495
|
+
runId: run.id,
|
|
496
|
+
wait: { id, reason: 'model-error', objectIds: [id], continuation: 'retry-authorized', details: null },
|
|
497
|
+
});
|
|
498
|
+
}
|
|
499
|
+
async act(state, run, profile, grant, action, save, owner, signal) {
|
|
500
|
+
this.dispatchBudget(state, run, grant);
|
|
501
|
+
if (action.type === 'continue') {
|
|
502
|
+
await save({ type: 'action.clear', runId: run.id });
|
|
503
|
+
return;
|
|
504
|
+
}
|
|
505
|
+
if (action.type === 'delegate') {
|
|
506
|
+
const child = this.manifest.profiles[action.profile];
|
|
507
|
+
check(child &&
|
|
508
|
+
profile.delegates.includes(child.id) &&
|
|
509
|
+
child.operations.every((id) => profile.operations.includes(id)), 'IA-ENGINE-DELEGATION-DENIED', 'Delegate is unavailable or exceeds the parent operation boundary');
|
|
510
|
+
check(grant.profiles.includes(child.id), 'IA-ENGINE-AUTHORITY-DENIED', 'Current grant does not authorize this delegate');
|
|
511
|
+
check(state.budget.children < grant.limits.children && run.depth < grant.limits.depth, 'IA-ENGINE-LIMIT-EXCEEDED', 'Current delegation limits exhausted');
|
|
512
|
+
const edge = profile.contract?.delegation.find((d) => d.profile === child.id);
|
|
513
|
+
check(!profile.contract || edge, 'IA-ENGINE-DELEGATION-DENIED', 'Missing compiled delegation contract');
|
|
514
|
+
const childLimits = boundedLimits(boundedLimits(narrowGrant(grant, { ...grant, limits: run.limits ?? state.limits }).limits, child.contract?.limits, this.now()), edge?.limits, this.now());
|
|
515
|
+
check(Object.values(state.runs).filter((r) => r.parentId === run.id).length <
|
|
516
|
+
(run.limits?.children ?? state.limits.children) && run.depth < (run.limits?.depth ?? state.limits.depth), 'IA-ENGINE-LIMIT-EXCEEDED', 'Parent delegation ceiling exhausted');
|
|
517
|
+
const contract = taskContract(child, action.task);
|
|
518
|
+
await save({
|
|
519
|
+
type: 'child.create',
|
|
520
|
+
parentId: run.id,
|
|
521
|
+
childId: randomUUID(),
|
|
522
|
+
profile: child.id,
|
|
523
|
+
agent: child.agent,
|
|
524
|
+
task: action.task,
|
|
525
|
+
contract: json(contract),
|
|
526
|
+
authority: json(grant),
|
|
527
|
+
limits: childLimits,
|
|
528
|
+
});
|
|
529
|
+
return;
|
|
530
|
+
}
|
|
531
|
+
if (action.type === 'invoke') {
|
|
532
|
+
const operation = this.manifest.operations[action.operation], adapter = operation ? this.host.operations[operation.handler] : undefined;
|
|
533
|
+
check(operation && profile.operations.includes(operation.id), 'IA-ENGINE-ACTION-INVALID', 'Action names an operation outside its installed profile');
|
|
534
|
+
operationAllowed(operation, profile, grant);
|
|
535
|
+
check(adapter, 'IA-ENGINE-BINDING-UNAVAILABLE', 'Operation adapter is not installed');
|
|
536
|
+
check(validateShape(operation.input, action.input), 'IA-ENGINE-INPUT-INVALID', 'Operation input failed its installed contract');
|
|
537
|
+
this.limits(state, grant, 'operation', 0, run);
|
|
538
|
+
const review = admittedReview(state, run, operation, action, grant, this.now());
|
|
539
|
+
const retry = run.retryOf ? state.attempts[run.retryOf] : undefined;
|
|
540
|
+
const id = randomUUID(), invocationId = retry?.invocationId ?? randomUUID(), timed = this.attemptSignal(state, run, grant, operation.timeoutMs, signal);
|
|
541
|
+
const context = this.effectContext(state, run, profile, operation, action, grant, owner, id, invocationId, review, timed);
|
|
542
|
+
await save({
|
|
543
|
+
type: 'attempt.prepare',
|
|
544
|
+
attempt: {
|
|
545
|
+
id,
|
|
546
|
+
invocationId,
|
|
547
|
+
runId: run.id,
|
|
548
|
+
kind: 'operation',
|
|
549
|
+
target: operation.id,
|
|
550
|
+
bindingDigest: operation.digest,
|
|
551
|
+
input: action.input,
|
|
552
|
+
inputDigest: digest(action.input),
|
|
553
|
+
authority: json(grant),
|
|
554
|
+
recovery: operation.recovery,
|
|
555
|
+
reservation: 0,
|
|
556
|
+
cause: retry?.cause ?? run.cause ?? causes(state, run, randomUUID(), null),
|
|
557
|
+
...(review ? { review } : {}),
|
|
558
|
+
},
|
|
559
|
+
});
|
|
560
|
+
const latest = await this.host.store.read(state.id), current = await this.grant(state.principal, latest, profile, run, timed);
|
|
561
|
+
this.dispatchBudget(latest, run, current);
|
|
562
|
+
operationAllowed(operation, profile, current);
|
|
563
|
+
admittedReview(latest, run, operation, action, current, this.now(), id);
|
|
564
|
+
context.grant = current;
|
|
565
|
+
context.signal = this.attemptSignal(latest, run, current, operation.timeoutMs, context.signal);
|
|
566
|
+
for (const id of this.checks(profile, 'before-effect')) {
|
|
567
|
+
const result = await bounded(() => this.host.evaluate(id, json({ phase: 'before-effect', action, contract: run.contract ?? null }), current), context.signal);
|
|
568
|
+
check(result.status === 'pass', 'IA-ENGINE-GOVERNANCE-DENIED', `Required evaluator ${id} did not pass`);
|
|
569
|
+
}
|
|
570
|
+
if (review)
|
|
571
|
+
check(this.host.preflight &&
|
|
572
|
+
(await bounded(() => this.host.preflight(operation, action.input, context), context.signal)), 'IA-ENGINE-DESTINATION-DENIED', 'Current destination/binding preflight is unavailable or denied');
|
|
573
|
+
context.signal.throwIfAborted();
|
|
574
|
+
await save({ type: 'attempt.dispatch', attemptId: id });
|
|
575
|
+
await this.host.store.validate(owner);
|
|
576
|
+
let output = null, error = null, effect = operation.effects.every((v) => v === 'read') ? 'none' : 'unknown';
|
|
577
|
+
try {
|
|
578
|
+
const dispatchSignal = context.signal;
|
|
579
|
+
const result = await bounded(() => adapter.execute(action.input, context), dispatchSignal);
|
|
580
|
+
await this.host.store.validate(owner);
|
|
581
|
+
dispatchSignal.throwIfAborted();
|
|
582
|
+
context.signal.throwIfAborted();
|
|
583
|
+
check(context.grant.expiresAt > this.now(), 'IA-ENGINE-AUTHORITY-DENIED', 'Current effect grant expired before result admission');
|
|
584
|
+
effect = result.effect;
|
|
585
|
+
check(Buffer.byteLength(canonical(result.output)) <= (operation.maxOutputBytes ?? 65_536) &&
|
|
586
|
+
validateShape(operation.output, result.output), 'IA-ENGINE-OUTPUT-INVALID', 'Operation output failed its installed contract');
|
|
587
|
+
output = result.output;
|
|
588
|
+
}
|
|
589
|
+
catch {
|
|
590
|
+
error = 'operation-attempt-failed';
|
|
591
|
+
}
|
|
592
|
+
await save({ type: 'attempt.observe', attemptId: id, output, error, effect, usage: 0 });
|
|
593
|
+
if (error && effect === 'none')
|
|
594
|
+
await save({
|
|
595
|
+
type: 'run.wait',
|
|
596
|
+
runId: run.id,
|
|
597
|
+
wait: {
|
|
598
|
+
id,
|
|
599
|
+
reason: 'operation-error',
|
|
600
|
+
objectIds: [id],
|
|
601
|
+
continuation: operation.effects.every((e) => e === 'read') ? 'retry' : 'unsupported-recovery',
|
|
602
|
+
details: null,
|
|
603
|
+
},
|
|
604
|
+
});
|
|
605
|
+
return;
|
|
606
|
+
}
|
|
607
|
+
check(profile.outcomes.includes(action.kind), 'IA-ENGINE-OUTCOME-DENIED', 'Outcome is unavailable in this profile');
|
|
608
|
+
const observedEvidence = new Set(state.receipts.flatMap((receipt) => {
|
|
609
|
+
if (receipt.runId !== run.id || !this.manifest.operations[receipt.target] || receipt.error !== null)
|
|
610
|
+
return [];
|
|
611
|
+
const output = receipt.output;
|
|
612
|
+
return [
|
|
613
|
+
receipt.id,
|
|
614
|
+
...(Array.isArray(output?.citations)
|
|
615
|
+
? output.citations.filter((v) => typeof v === 'string')
|
|
616
|
+
: []),
|
|
617
|
+
];
|
|
618
|
+
}));
|
|
619
|
+
check((action.evidence ?? []).every((ref) => observedEvidence.has(ref)), 'IA-ENGINE-EVIDENCE-INVALID', 'Outcome cites evidence not observed by this session');
|
|
620
|
+
const questions = (action.questions ?? []).map((q) => {
|
|
621
|
+
const contract = { prompt: q.prompt, respondent: state.principal, choices: q.choices, revision: 1 };
|
|
622
|
+
return {
|
|
623
|
+
...contract,
|
|
624
|
+
id: randomUUID(),
|
|
625
|
+
runId: run.id,
|
|
626
|
+
required: q.required,
|
|
627
|
+
answer: null,
|
|
628
|
+
status: 'open',
|
|
629
|
+
digest: digest(contract),
|
|
630
|
+
...(run.cause ? { cause: run.cause } : {}),
|
|
631
|
+
};
|
|
632
|
+
});
|
|
633
|
+
const review = offerReview(state, run, profile, grant, this.manifest, action, this.now());
|
|
634
|
+
const proposal = action.proposal === undefined
|
|
635
|
+
? undefined
|
|
636
|
+
: {
|
|
637
|
+
id: randomUUID(),
|
|
638
|
+
revision: 1,
|
|
639
|
+
runId: run.id,
|
|
640
|
+
candidate: action.proposal,
|
|
641
|
+
digest: digest(action.proposal),
|
|
642
|
+
status: 'offered',
|
|
643
|
+
decisionId: null,
|
|
644
|
+
...(review ? { review } : {}),
|
|
645
|
+
...(run.cause ? { cause: run.cause } : {}),
|
|
646
|
+
};
|
|
647
|
+
const end = action.continuation === 'finish' || action.continuation === 'fail';
|
|
648
|
+
if (end && action.continuation === 'finish') {
|
|
649
|
+
evaluateCompletion(state, run, profile, action);
|
|
650
|
+
const timed = this.attemptSignal(state, run, grant, 60_000, signal);
|
|
651
|
+
for (const id of this.checks(profile, 'completion')) {
|
|
652
|
+
const result = await bounded(() => this.host.evaluate(id, json({ phase: 'completion', action, contract: run.contract ?? null }), grant), timed);
|
|
653
|
+
check(result.status === 'pass', 'IA-ENGINE-GOVERNANCE-DENIED', `Completion evaluator ${id} did not pass`);
|
|
654
|
+
}
|
|
655
|
+
timed.throwIfAborted();
|
|
656
|
+
}
|
|
657
|
+
let wait;
|
|
658
|
+
if (action.continuation.startsWith('await-'))
|
|
659
|
+
wait = {
|
|
660
|
+
id: randomUUID(),
|
|
661
|
+
reason: action.continuation === 'await-input'
|
|
662
|
+
? 'input'
|
|
663
|
+
: action.continuation === 'await-review'
|
|
664
|
+
? 'review'
|
|
665
|
+
: 'dependency',
|
|
666
|
+
objectIds: action.continuation === 'await-input'
|
|
667
|
+
? questions.filter((q) => q.required).map((q) => q.id)
|
|
668
|
+
: proposal
|
|
669
|
+
? [proposal.id]
|
|
670
|
+
: [],
|
|
671
|
+
continuation: 'continue',
|
|
672
|
+
details: null,
|
|
673
|
+
};
|
|
674
|
+
const outcome = json({
|
|
675
|
+
...action,
|
|
676
|
+
contractDigest: digest(run.contract ?? null),
|
|
677
|
+
references: {
|
|
678
|
+
questions: questions.map((q) => q.id),
|
|
679
|
+
proposals: proposal ? [proposal.id] : [],
|
|
680
|
+
decisions: run.cause?.decisions ?? [],
|
|
681
|
+
receipts: action.artifacts ?? [],
|
|
682
|
+
},
|
|
683
|
+
...(run.cause ? { cause: run.cause } : {}),
|
|
684
|
+
});
|
|
685
|
+
await save({
|
|
686
|
+
type: 'outcome.accept',
|
|
687
|
+
runId: run.id,
|
|
688
|
+
outcome,
|
|
689
|
+
terminal: end ? (action.continuation === 'fail' ? 'failed' : 'completed') : null,
|
|
690
|
+
questions,
|
|
691
|
+
...(proposal ? { proposal } : {}),
|
|
692
|
+
...(wait ? { wait } : {}),
|
|
693
|
+
});
|
|
694
|
+
}
|
|
695
|
+
async recover(state, attempt, owner, save, signal) {
|
|
696
|
+
const run = state.runs[attempt.runId];
|
|
697
|
+
if (attempt.kind === 'model' &&
|
|
698
|
+
run.wait?.reason === 'external-model' &&
|
|
699
|
+
!run.cancelRequested &&
|
|
700
|
+
!run.pauseRequested)
|
|
701
|
+
return false;
|
|
702
|
+
if (attempt.status === 'prepared') {
|
|
703
|
+
await save({ type: 'attempt.abandon', attemptId: attempt.id });
|
|
704
|
+
if (attempt.kind === 'operation')
|
|
705
|
+
await save({
|
|
706
|
+
type: 'run.wait',
|
|
707
|
+
runId: run.id,
|
|
708
|
+
wait: {
|
|
709
|
+
id: attempt.id,
|
|
710
|
+
reason: 'operation-error',
|
|
711
|
+
objectIds: [attempt.id],
|
|
712
|
+
continuation: 'retry',
|
|
713
|
+
details: null,
|
|
714
|
+
},
|
|
715
|
+
});
|
|
716
|
+
return true;
|
|
717
|
+
}
|
|
718
|
+
if (attempt.kind === 'model') {
|
|
719
|
+
await save({
|
|
720
|
+
type: 'attempt.reconcile',
|
|
721
|
+
attemptId: attempt.id,
|
|
722
|
+
effect: 'none',
|
|
723
|
+
output: null,
|
|
724
|
+
evidence: 'Owner recovery: response unavailable; reserved model usage charged.',
|
|
725
|
+
});
|
|
726
|
+
return true;
|
|
727
|
+
}
|
|
728
|
+
const definition = this.manifest.operations[attempt.target];
|
|
729
|
+
if (definition?.digest === attempt.bindingDigest && definition.effects.every((effect) => effect === 'read')) {
|
|
730
|
+
await this.host.store.validate(owner);
|
|
731
|
+
await save({
|
|
732
|
+
type: 'attempt.reconcile',
|
|
733
|
+
attemptId: attempt.id,
|
|
734
|
+
effect: 'none',
|
|
735
|
+
output: null,
|
|
736
|
+
evidence: 'Installed immutable read contract has no application effect.',
|
|
737
|
+
});
|
|
738
|
+
await save({
|
|
739
|
+
type: 'run.wait',
|
|
740
|
+
runId: run.id,
|
|
741
|
+
wait: {
|
|
742
|
+
id: attempt.id,
|
|
743
|
+
reason: 'operation-error',
|
|
744
|
+
objectIds: [attempt.id],
|
|
745
|
+
continuation: 'retry',
|
|
746
|
+
details: null,
|
|
747
|
+
},
|
|
748
|
+
});
|
|
749
|
+
return true;
|
|
750
|
+
}
|
|
751
|
+
const adapter = definition && this.host.operations[definition.handler], profile = this.manifest.profiles[run.profile];
|
|
752
|
+
const wait = async (reason, details = null) => {
|
|
753
|
+
const next = { id: attempt.id, reason, objectIds: [attempt.id], continuation: 'reconcile', details };
|
|
754
|
+
if (run.status !== 'waiting' || digest(run.wait) !== digest(next))
|
|
755
|
+
await save({ type: 'run.wait', runId: run.id, wait: next });
|
|
756
|
+
return false;
|
|
757
|
+
};
|
|
758
|
+
if (definition?.digest === attempt.bindingDigest &&
|
|
759
|
+
definition.recovery === 'reconcile' &&
|
|
760
|
+
adapter?.reconcile &&
|
|
761
|
+
profile &&
|
|
762
|
+
attempt.review) {
|
|
763
|
+
const allowance = profile.contract?.maxAttempts ?? 2;
|
|
764
|
+
if ((attempt.reconciliations ?? 0) >= allowance)
|
|
765
|
+
return wait('reconciliation-exhausted');
|
|
766
|
+
let context;
|
|
767
|
+
try {
|
|
768
|
+
const preflightSignal = this.attemptSignal(state, run, undefined, definition.timeoutMs, signal);
|
|
769
|
+
const grant = await this.grant(state.principal, state, profile, run, preflightSignal);
|
|
770
|
+
this.limits(state, grant, 'operation', 0, run);
|
|
771
|
+
const { proposalId, revision, digest: proposalDigest, decisionId } = attempt.review;
|
|
772
|
+
const action = {
|
|
773
|
+
type: 'invoke',
|
|
774
|
+
operation: attempt.target,
|
|
775
|
+
input: attempt.input,
|
|
776
|
+
review: { proposalId, revision, digest: proposalDigest, decisionId },
|
|
777
|
+
};
|
|
778
|
+
context = this.effectContext(state, run, profile, definition, action, grant, owner, attempt.id, attempt.invocationId, attempt.review, this.attemptSignal(state, run, grant, definition.timeoutMs, preflightSignal), true);
|
|
779
|
+
await context.assertCurrent();
|
|
780
|
+
}
|
|
781
|
+
catch (error) {
|
|
782
|
+
return wait('reconciliation-unavailable', { code: error instanceof SessionError ? error.code : 'unavailable' });
|
|
783
|
+
}
|
|
784
|
+
await save({ type: 'attempt.reconcile.start', attemptId: attempt.id, maxAttempts: allowance });
|
|
785
|
+
let effect = 'unknown', output = null, error = 'reconciliation-unavailable';
|
|
786
|
+
try {
|
|
787
|
+
await this.host.store.validate(owner);
|
|
788
|
+
const observed = await bounded(() => adapter.reconcile(attempt.input, context), context.signal);
|
|
789
|
+
await this.host.store.validate(owner);
|
|
790
|
+
context.signal.throwIfAborted();
|
|
791
|
+
check(context.grant.expiresAt > this.now(), 'IA-ENGINE-AUTHORITY-DENIED', 'Current effect grant expired before reconciliation admission');
|
|
792
|
+
check(['absent', 'applied', 'partial', 'unknown'].includes(observed.status), 'IA-ENGINE-OUTPUT-INVALID', 'Invalid reconciliation status');
|
|
793
|
+
effect = observed.status === 'absent' ? 'none' : observed.status;
|
|
794
|
+
if (effect === 'applied') {
|
|
795
|
+
check(Buffer.byteLength(canonical(observed.output)) <= (definition.maxOutputBytes ?? 65536) &&
|
|
796
|
+
validateShape(definition.output, observed.output), 'IA-ENGINE-OUTPUT-INVALID', 'Reconciled output failed its installed contract');
|
|
797
|
+
output = observed.output;
|
|
798
|
+
error = null;
|
|
799
|
+
}
|
|
800
|
+
else
|
|
801
|
+
error = effect === 'none' ? 'reconciled-absent' : `reconciliation-${effect}`;
|
|
802
|
+
}
|
|
803
|
+
catch {
|
|
804
|
+
/* Retain an observed application even when its output cannot be validated. */
|
|
805
|
+
}
|
|
806
|
+
await save({
|
|
807
|
+
type: 'attempt.reconcile',
|
|
808
|
+
attemptId: attempt.id,
|
|
809
|
+
effect,
|
|
810
|
+
output,
|
|
811
|
+
error,
|
|
812
|
+
evidence: `Installed ${attempt.target} reconciliation: ${effect}`,
|
|
813
|
+
});
|
|
814
|
+
if (effect === 'partial' || effect === 'unknown')
|
|
815
|
+
return wait('unknown-effect', { effect });
|
|
816
|
+
if (run.cancelRequested || run.pauseRequested)
|
|
817
|
+
return true;
|
|
818
|
+
if (effect === 'none') {
|
|
819
|
+
await save({
|
|
820
|
+
type: 'run.wait',
|
|
821
|
+
runId: run.id,
|
|
822
|
+
wait: {
|
|
823
|
+
id: attempt.id,
|
|
824
|
+
reason: 'operation-error',
|
|
825
|
+
objectIds: [attempt.id],
|
|
826
|
+
continuation: 'retry',
|
|
827
|
+
details: null,
|
|
828
|
+
},
|
|
829
|
+
});
|
|
830
|
+
return false;
|
|
831
|
+
}
|
|
832
|
+
if (error) {
|
|
833
|
+
await save({
|
|
834
|
+
type: 'run.wait',
|
|
835
|
+
runId: run.id,
|
|
836
|
+
wait: {
|
|
837
|
+
id: attempt.id,
|
|
838
|
+
reason: 'operation-output-invalid',
|
|
839
|
+
objectIds: [attempt.id],
|
|
840
|
+
continuation: 'remediate',
|
|
841
|
+
details: null,
|
|
842
|
+
},
|
|
843
|
+
});
|
|
844
|
+
return false;
|
|
845
|
+
}
|
|
846
|
+
await save({ type: 'run.ready', runId: run.id });
|
|
847
|
+
return true;
|
|
848
|
+
}
|
|
849
|
+
return wait('unknown-effect', { target: attempt.target, recovery: attempt.recovery });
|
|
850
|
+
}
|
|
851
|
+
async renewalGrant(state, principal, signal) {
|
|
852
|
+
check(state.principal === principal && state.manifestDigest === digest(this.manifest), 'IA-ENGINE-AUTHORITY-DENIED', 'Only the session creator can renew its pinned execution');
|
|
853
|
+
check(this.host.authorizeRenewal, 'IA-ENGINE-RENEWAL-UNAVAILABLE', 'The host does not grant renewal');
|
|
854
|
+
const timed = this.attemptSignal(null, undefined, undefined, 60_000, signal);
|
|
855
|
+
check(await bounded(() => this.host.verifyManifest(this.manifest), timed), 'IA-ENGINE-SOURCE-CHANGED', 'Pinned inputs or executable bindings are unavailable');
|
|
856
|
+
const grant = copy(await bounded(() => this.host.authorizeRenewal(principal, this.manifest, state), timed));
|
|
857
|
+
check(await bounded(() => this.host.verifyManifest(this.manifest), this.attemptSignal(null, undefined, grant, 60_000, timed)), 'IA-ENGINE-SOURCE-CHANGED', 'Pinned bindings changed during renewal authorization');
|
|
858
|
+
check(grant.principal === principal &&
|
|
859
|
+
grant.workspace === state.workspace &&
|
|
860
|
+
grant.sources.includes(this.manifest.sourceDigest) &&
|
|
861
|
+
grant.expiresAt > this.now() &&
|
|
862
|
+
grant.limits.deadline > this.now(), 'IA-ENGINE-AUTHORITY-DENIED', 'Current authority does not permit renewal');
|
|
863
|
+
return grant;
|
|
864
|
+
}
|
|
865
|
+
renewalPlan(state, grant, requested) {
|
|
866
|
+
const live = humanWaitRuns(state), now = this.now();
|
|
867
|
+
check(live.length, 'IA-ENGINE-RENEWAL-UNAVAILABLE', 'Only settled human waits and their waiting ancestors can renew');
|
|
868
|
+
const root = state.runs[state.rootId], rootProfile = this.manifest.profiles[root.profile];
|
|
869
|
+
check(rootProfile, 'IA-ENGINE-PROFILE-UNAVAILABLE', 'Retained root profile is unavailable');
|
|
870
|
+
const maximum = boundedLimits({ ...grant.limits, deadline: Math.min(grant.expiresAt, grant.limits.deadline) }, rootProfile.contract?.limits, now).deadline;
|
|
871
|
+
const deadline = requested ?? maximum;
|
|
872
|
+
check(Number.isSafeInteger(deadline) && deadline > Math.max(now, state.limits.deadline) && deadline <= maximum, 'IA-ENGINE-RENEWAL-UNAVAILABLE', 'Renewal deadline exceeds the current permitted window');
|
|
873
|
+
const mutation = {
|
|
874
|
+
type: 'session.renew',
|
|
875
|
+
expectedSequence: state.sequence,
|
|
876
|
+
previousDeadline: state.limits.deadline,
|
|
877
|
+
deadline,
|
|
878
|
+
runs: {},
|
|
879
|
+
};
|
|
880
|
+
const projected = copy(state);
|
|
881
|
+
projected.limits.deadline = deadline;
|
|
882
|
+
for (const run of live) {
|
|
883
|
+
const profile = this.manifest.profiles[run.profile], retained = run.authority;
|
|
884
|
+
check(profile &&
|
|
885
|
+
run.limits &&
|
|
886
|
+
retained?.limits &&
|
|
887
|
+
['profiles', 'operations', 'effects', 'sources', 'models'].every((key) => Array.isArray(retained[key])), 'IA-ENGINE-RENEWAL-UNAVAILABLE', 'Legacy run authority cannot be renewed');
|
|
888
|
+
const parent = run.parentId ? state.runs[run.parentId] : null, edge = parent
|
|
889
|
+
? this.manifest.profiles[parent.profile]?.contract?.delegation.find((entry) => entry.profile === run.profile)
|
|
890
|
+
: undefined;
|
|
891
|
+
const inherited = parent ? mutation.runs[parent.id]?.deadline : deadline;
|
|
892
|
+
check(inherited !== undefined, 'IA-ENGINE-RENEWAL-UNAVAILABLE', 'Unsettled ancestor cannot be renewed');
|
|
893
|
+
const next = boundedLimits(boundedLimits({ ...grant.limits, deadline: Math.min(deadline, inherited) }, profile.contract?.limits, now), edge?.limits, now).deadline;
|
|
894
|
+
check(next > Math.max(now, run.limits.deadline) &&
|
|
895
|
+
run.depth <= grant.limits.depth &&
|
|
896
|
+
state.budget.children <= grant.limits.children, 'IA-ENGINE-RENEWAL-UNAVAILABLE', 'Renewal exceeds inherited task limits');
|
|
897
|
+
const ceiling = {
|
|
898
|
+
...retained,
|
|
899
|
+
limits: { ...retained.limits, deadline: Math.max(retained.limits.deadline, next) },
|
|
900
|
+
}, effective = narrowGrant(grant, ceiling);
|
|
901
|
+
check(effective.profiles.includes(run.profile) &&
|
|
902
|
+
effective.models.includes(profile.model) &&
|
|
903
|
+
effective.sources.includes(this.manifest.sourceDigest), 'IA-ENGINE-AUTHORITY-DENIED', 'Current authority no longer admits this task');
|
|
904
|
+
check(state.budget.retainedBytes < Math.min(state.limits.bytes, effective.limits.bytes) &&
|
|
905
|
+
(run.retainedBytes ?? 0) < run.limits.bytes, 'IA-ENGINE-LIMIT-EXCEEDED', 'Retained byte budget is exhausted');
|
|
906
|
+
mutation.runs[run.id] = { previousDeadline: run.limits.deadline, deadline: next };
|
|
907
|
+
projected.runs[run.id].limits.deadline = next;
|
|
908
|
+
// Every supported human continuation needs a new model turn. Exact request bytes are
|
|
909
|
+
// unknown until context assembly; at least one request byte accompanies the output bound.
|
|
910
|
+
this.limits(projected, effective, 'model', modelOutputTokens + 1, projected.runs[run.id]);
|
|
911
|
+
}
|
|
912
|
+
return mutation;
|
|
913
|
+
}
|
|
914
|
+
/** Read-only current renewal offer. Its exact deadline is rechecked during admission. */
|
|
915
|
+
async renewal(sessionId, principal, options = {}) {
|
|
916
|
+
const signal = options.signal, state = await this.host.store.read(sessionId), grant = await this.renewalGrant(state, principal, signal);
|
|
917
|
+
return { deadline: this.renewalPlan(state, grant).deadline };
|
|
918
|
+
}
|
|
919
|
+
/** Extends time only; never resets accounting, changes executable bindings or dispatches work. */
|
|
920
|
+
async renew(sessionId, principal, input) {
|
|
921
|
+
const request = { ...input }, owner = await this.host.store.acquire(sessionId);
|
|
922
|
+
try {
|
|
923
|
+
const state = await this.host.store.read(sessionId), grant = await this.renewalGrant(state, principal, request.signal);
|
|
924
|
+
const prior = (await this.host.store.journal(sessionId)).events.find((event) => event.id === request.commandId)?.mutation;
|
|
925
|
+
if (prior) {
|
|
926
|
+
check(prior.type === 'session.renew' &&
|
|
927
|
+
prior.expectedSequence === request.expectedSequence &&
|
|
928
|
+
prior.deadline === request.deadline, 'IA-SESSION-COMMAND-CONFLICT', 'Renewal command content changed');
|
|
929
|
+
return state;
|
|
930
|
+
}
|
|
931
|
+
check(state.sequence === request.expectedSequence, 'IA-SESSION-REVISION-CONFLICT', 'Renewal requires the current session sequence');
|
|
932
|
+
const mutation = this.renewalPlan(state, grant, request.deadline);
|
|
933
|
+
request.signal?.throwIfAborted();
|
|
934
|
+
await this.host.store.validate(owner);
|
|
935
|
+
request.signal?.throwIfAborted();
|
|
936
|
+
check(grant.expiresAt > this.now() &&
|
|
937
|
+
Math.min(grant.limits.deadline, ...Object.values(mutation.runs).map((run) => run.deadline)) > this.now(), 'IA-ENGINE-AUTHORITY-DENIED', 'Renewal authority expired before admission');
|
|
938
|
+
await this.host.store.command({ id: request.commandId, sessionId, actor: principal, expected: request.expectedSequence, mutation }, owner);
|
|
939
|
+
return this.host.store.read(sessionId);
|
|
940
|
+
}
|
|
941
|
+
finally {
|
|
942
|
+
await this.host.store.release(owner);
|
|
943
|
+
}
|
|
944
|
+
}
|
|
945
|
+
/** Bounded repair/read retry, or an exact reviewed write after proven absence. */
|
|
946
|
+
async retry(sessionId, principal, attemptId, commandId) {
|
|
947
|
+
const owner = await this.host.store.acquire(sessionId);
|
|
948
|
+
try {
|
|
949
|
+
const state = await this.host.store.read(sessionId), attempt = state.attempts[attemptId];
|
|
950
|
+
check(attempt && state.principal === principal, 'IA-ENGINE-AUTHORITY-DENIED', 'Attempt is unavailable');
|
|
951
|
+
const run = state.runs[attempt.runId], profile = this.manifest.profiles[run.profile];
|
|
952
|
+
const grant = await this.grant(principal, state, profile, run);
|
|
953
|
+
let mutation;
|
|
954
|
+
if (attempt.kind === 'model') {
|
|
955
|
+
const prior = (await this.host.store.journal(sessionId)).events.find((e) => e.id === commandId)?.mutation;
|
|
956
|
+
const details = prior?.type === 'model.repair' ? prior.details : run.wait?.details;
|
|
957
|
+
mutation = {
|
|
958
|
+
type: 'model.repair',
|
|
959
|
+
attemptId,
|
|
960
|
+
allowance: profile.contract?.repairAttempts ?? 1,
|
|
961
|
+
...(details != null ? { details } : {}),
|
|
962
|
+
};
|
|
963
|
+
}
|
|
964
|
+
else {
|
|
965
|
+
const operation = this.manifest.operations[attempt.target];
|
|
966
|
+
operationAllowed(operation, profile, grant);
|
|
967
|
+
const read = operation.recovery === 'repeatable' && operation.effects.every((e) => e === 'read');
|
|
968
|
+
const write = operation.recovery === 'reconcile' &&
|
|
969
|
+
this.host.operations[operation.handler]?.reconcile &&
|
|
970
|
+
attempt.review &&
|
|
971
|
+
['not-dispatched', 'reconciled-absent'].includes(attempt.error ?? '');
|
|
972
|
+
check(operation.digest === attempt.bindingDigest && (read || write), 'IA-ENGINE-RECOVERY-UNAVAILABLE', 'Retry needs a repeatable read or qualified proof of write absence');
|
|
973
|
+
const review = attempt.review
|
|
974
|
+
? {
|
|
975
|
+
proposalId: attempt.review.proposalId,
|
|
976
|
+
revision: attempt.review.revision,
|
|
977
|
+
digest: attempt.review.digest,
|
|
978
|
+
decisionId: attempt.review.decisionId,
|
|
979
|
+
}
|
|
980
|
+
: undefined;
|
|
981
|
+
const action = {
|
|
982
|
+
type: 'invoke',
|
|
983
|
+
operation: attempt.target,
|
|
984
|
+
input: attempt.input,
|
|
985
|
+
...(review ? { review } : {}),
|
|
986
|
+
};
|
|
987
|
+
if (write)
|
|
988
|
+
admittedReview(state, run, operation, action, grant, this.now());
|
|
989
|
+
mutation = {
|
|
990
|
+
type: 'attempt.retry',
|
|
991
|
+
attemptId,
|
|
992
|
+
action: json(action),
|
|
993
|
+
maxAttempts: profile.contract?.maxAttempts ?? 2,
|
|
994
|
+
};
|
|
995
|
+
}
|
|
996
|
+
await this.host.store.command({ id: commandId, sessionId, actor: principal, expected: state.sequence, mutation }, owner);
|
|
997
|
+
return this.host.store.read(sessionId);
|
|
998
|
+
}
|
|
999
|
+
finally {
|
|
1000
|
+
await this.host.store.release(owner);
|
|
1001
|
+
}
|
|
1002
|
+
}
|
|
1003
|
+
async reply(sessionId, principal, questionId, revision, questionDigest, answer, commandId) {
|
|
1004
|
+
const state = await this.host.store.read(sessionId), question = state.questions[questionId];
|
|
1005
|
+
check(question && question.respondent === principal, 'IA-ENGINE-AUTHORITY-DENIED', 'Question is unavailable');
|
|
1006
|
+
const run = state.runs[question.runId];
|
|
1007
|
+
await this.grant(principal, state, this.manifest.profiles[run.profile], run);
|
|
1008
|
+
await this.host.store.command({
|
|
1009
|
+
id: commandId,
|
|
1010
|
+
sessionId,
|
|
1011
|
+
actor: principal,
|
|
1012
|
+
expected: state.sequence,
|
|
1013
|
+
mutation: { type: 'question.reply', questionId, revision, digest: questionDigest, answer },
|
|
1014
|
+
});
|
|
1015
|
+
return this.host.store.read(sessionId);
|
|
1016
|
+
}
|
|
1017
|
+
async review(sessionId, principal, proposalId, revision, proposalDigest, accept, rationale, commandId) {
|
|
1018
|
+
const state = await this.host.store.read(sessionId), proposal = state.proposals[proposalId];
|
|
1019
|
+
check(proposal && (proposal.review?.reviewer ?? state.principal) === principal, 'IA-ENGINE-AUTHORITY-DENIED', 'Proposal is unavailable to this reviewer');
|
|
1020
|
+
const run = state.runs[proposal.runId], grant = await this.grant(principal, state, this.manifest.profiles[run.profile], run);
|
|
1021
|
+
if (proposal.review) {
|
|
1022
|
+
const binding = proposal.review, operation = this.manifest.operations[binding.operation];
|
|
1023
|
+
operationAllowed(operation, this.manifest.profiles[run.profile], grant);
|
|
1024
|
+
check(operation.digest === binding.bindingDigest &&
|
|
1025
|
+
binding.expiresAt > this.now() &&
|
|
1026
|
+
grant.destinations?.some((d) => digest(d) === digest(binding.destination)), 'IA-ENGINE-REVIEW-INVALID', 'Review authority, binding or destination changed');
|
|
1027
|
+
}
|
|
1028
|
+
await this.host.store.command({
|
|
1029
|
+
id: commandId,
|
|
1030
|
+
sessionId,
|
|
1031
|
+
actor: principal,
|
|
1032
|
+
expected: state.sequence,
|
|
1033
|
+
mutation: {
|
|
1034
|
+
type: 'proposal.review',
|
|
1035
|
+
proposalId,
|
|
1036
|
+
revision,
|
|
1037
|
+
digest: proposalDigest,
|
|
1038
|
+
accept,
|
|
1039
|
+
rationale,
|
|
1040
|
+
decisionId: `decision-${digest(commandId)}`,
|
|
1041
|
+
},
|
|
1042
|
+
});
|
|
1043
|
+
return this.host.store.read(sessionId);
|
|
1044
|
+
}
|
|
1045
|
+
/** IDE host supplies one structured response to the exact persisted request; no API/model credential is needed. */
|
|
1046
|
+
async submitModel(sessionId, principal, attemptId, response, commandId) {
|
|
1047
|
+
const state = await this.host.store.read(sessionId), attempt = state.attempts[attemptId];
|
|
1048
|
+
check(attempt && state.principal === principal && attempt.kind === 'model', 'IA-ENGINE-AUTHORITY-DENIED', 'Model request is unavailable');
|
|
1049
|
+
const run = state.runs[attempt.runId], profile = this.manifest.profiles[run.profile];
|
|
1050
|
+
await this.grant(principal, state, profile, run);
|
|
1051
|
+
check(response.model === profile.model &&
|
|
1052
|
+
(response.usage === null || (Number.isSafeInteger(response.usage) && response.usage >= 0)), 'IA-ENGINE-MODEL-INVALID', 'Invalid model/usage');
|
|
1053
|
+
parseAction(response.action);
|
|
1054
|
+
check(Buffer.byteLength(canonical(response)) <= 65_536, 'IA-ENGINE-MODEL-INVALID', 'Response too large');
|
|
1055
|
+
const owner = await this.host.store.acquire(sessionId);
|
|
1056
|
+
try {
|
|
1057
|
+
const journal = await this.host.store.journal(sessionId);
|
|
1058
|
+
check(journal.commands[commandId] ||
|
|
1059
|
+
(run.wait?.reason === 'external-model' &&
|
|
1060
|
+
run.wait.objectIds.includes(attemptId) &&
|
|
1061
|
+
attempt.status === 'dispatched'), 'IA-ENGINE-MODEL-INVALID', 'No external response is pending');
|
|
1062
|
+
await this.host.store.command({
|
|
1063
|
+
id: commandId,
|
|
1064
|
+
sessionId,
|
|
1065
|
+
actor: principal,
|
|
1066
|
+
expected: state.sequence,
|
|
1067
|
+
mutation: {
|
|
1068
|
+
type: 'attempt.observe',
|
|
1069
|
+
attemptId,
|
|
1070
|
+
output: json(response),
|
|
1071
|
+
effect: 'none',
|
|
1072
|
+
error: null,
|
|
1073
|
+
usage: response.usage,
|
|
1074
|
+
},
|
|
1075
|
+
}, owner);
|
|
1076
|
+
return this.host.store.read(sessionId);
|
|
1077
|
+
}
|
|
1078
|
+
finally {
|
|
1079
|
+
await this.host.store.release(owner);
|
|
1080
|
+
}
|
|
1081
|
+
}
|
|
1082
|
+
}
|
|
1083
|
+
//# sourceMappingURL=engine.js.map
|