@inventarch/session-system 0.0.0-stage → 1.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/LANGUAGE.md +56 -0
- package/LICENSE +202 -0
- package/NOTICE +4 -0
- package/README.md +2 -2
- package/SPEC.md +3 -0
- package/dist/codec.d.ts +10 -0
- package/dist/codec.d.ts.map +1 -0
- package/dist/codec.js +49 -0
- package/dist/codec.js.map +1 -0
- package/dist/index.d.ts +6 -0
- package/dist/index.d.ts.map +1 -0
- package/dist/index.js +6 -0
- package/dist/index.js.map +1 -0
- package/dist/memory.d.ts +17 -0
- package/dist/memory.d.ts.map +1 -0
- package/dist/memory.js +58 -0
- package/dist/memory.js.map +1 -0
- package/dist/native-selection.json +205 -0
- package/dist/native.ia.tgz +0 -0
- package/dist/reducer.d.ts +11 -0
- package/dist/reducer.d.ts.map +1 -0
- package/dist/reducer.js +723 -0
- package/dist/reducer.js.map +1 -0
- package/dist/sqlite.d.ts +35 -0
- package/dist/sqlite.d.ts.map +1 -0
- package/dist/sqlite.js +188 -0
- package/dist/sqlite.js.map +1 -0
- package/dist/store.d.ts +19 -0
- package/dist/store.d.ts.map +1 -0
- package/dist/store.js +97 -0
- package/dist/store.js.map +1 -0
- package/dist/system-package.json +330 -0
- package/dist/types.d.ts +411 -0
- package/dist/types.d.ts.map +1 -0
- package/dist/types.js +2 -0
- package/dist/types.js.map +1 -0
- package/package.json +78 -3
package/dist/reducer.js
ADDED
|
@@ -0,0 +1,723 @@
|
|
|
1
|
+
import { copy, digest, identifier, requireValue } from './codec.js';
|
|
2
|
+
const invariant = (condition, message) => requireValue(condition, 'IA-SESSION-TRANSITION-INVALID', message);
|
|
3
|
+
export const terminal = (run) => ['completed', 'failed', 'cancelled'].includes(run.status);
|
|
4
|
+
export function validateLimits(limits) {
|
|
5
|
+
const keys = ['steps', 'modelCalls', 'operations', 'tokens', 'children', 'depth', 'bytes', 'deadline'];
|
|
6
|
+
invariant(Object.keys(limits).length === keys.length, 'Unknown or missing limit');
|
|
7
|
+
for (const key of keys) {
|
|
8
|
+
const value = limits[key];
|
|
9
|
+
invariant(Number.isSafeInteger(value) && value >= (key === 'deadline' ? 1 : 0), `Invalid limit ${key}`);
|
|
10
|
+
}
|
|
11
|
+
}
|
|
12
|
+
function runOf(state, id) {
|
|
13
|
+
const run = state.runs[id];
|
|
14
|
+
requireValue(run, 'IA-SESSION-TRANSITION-INVALID', 'Unknown run');
|
|
15
|
+
return run;
|
|
16
|
+
}
|
|
17
|
+
export function unresolved(state, runId) {
|
|
18
|
+
return Object.values(state.attempts).filter((a) => (runId === undefined || a.runId === runId) &&
|
|
19
|
+
(a.status !== 'observed' || a.effect === 'unknown' || a.effect === 'partial'));
|
|
20
|
+
}
|
|
21
|
+
/** The only renewable shape: settled human-wait leaves and their waiting ancestors. */
|
|
22
|
+
export function humanWaitRuns(state) {
|
|
23
|
+
if (unresolved(state).length ||
|
|
24
|
+
state.controls.some((control) => !control.applied) ||
|
|
25
|
+
Object.values(state.reactions).some((reaction) => reaction.required && reaction.status === 'queued'))
|
|
26
|
+
return [];
|
|
27
|
+
const live = Object.values(state.runs).filter((run) => !terminal(run));
|
|
28
|
+
if (!live.length || Object.values(state.runs).some((run) => run.parentId && terminal(run) && !run.returned))
|
|
29
|
+
return [];
|
|
30
|
+
let humans = 0;
|
|
31
|
+
for (const run of live) {
|
|
32
|
+
if (run.status !== 'waiting' ||
|
|
33
|
+
run.pendingAction !== null ||
|
|
34
|
+
run.pauseRequested ||
|
|
35
|
+
run.cancelRequested ||
|
|
36
|
+
!run.wait?.objectIds.length)
|
|
37
|
+
return [];
|
|
38
|
+
const children = live.filter((child) => child.parentId === run.id);
|
|
39
|
+
if (run.wait.reason === 'child') {
|
|
40
|
+
if (!children.length ||
|
|
41
|
+
children.length !== run.wait.objectIds.length ||
|
|
42
|
+
children.some((child) => !run.wait.objectIds.includes(child.id)))
|
|
43
|
+
return [];
|
|
44
|
+
}
|
|
45
|
+
else if (run.wait.reason === 'input') {
|
|
46
|
+
if (children.length ||
|
|
47
|
+
!run.wait.objectIds.every((id) => state.questions[id]?.runId === run.id && ['open', 'answered'].includes(state.questions[id].status)))
|
|
48
|
+
return [];
|
|
49
|
+
humans++;
|
|
50
|
+
}
|
|
51
|
+
else if (run.wait.reason === 'review') {
|
|
52
|
+
if (children.length ||
|
|
53
|
+
!run.wait.objectIds.every((id) => state.proposals[id]?.runId === run.id &&
|
|
54
|
+
!state.proposals[id].review &&
|
|
55
|
+
['offered', 'accepted', 'rejected'].includes(state.proposals[id].status)))
|
|
56
|
+
return [];
|
|
57
|
+
humans++;
|
|
58
|
+
}
|
|
59
|
+
else
|
|
60
|
+
return [];
|
|
61
|
+
}
|
|
62
|
+
return humans ? live.sort((a, b) => a.depth - b.depth) : [];
|
|
63
|
+
}
|
|
64
|
+
function newRun(id, profile, agent, task, parentId, depth) {
|
|
65
|
+
identifier(id);
|
|
66
|
+
invariant(typeof profile === 'string' && profile.length > 0 && typeof agent === 'string' && agent.length > 0, 'Missing profile/agent');
|
|
67
|
+
return {
|
|
68
|
+
id,
|
|
69
|
+
parentId,
|
|
70
|
+
profile,
|
|
71
|
+
agent,
|
|
72
|
+
task: copy(task),
|
|
73
|
+
depth,
|
|
74
|
+
status: 'created',
|
|
75
|
+
wait: null,
|
|
76
|
+
outcome: null,
|
|
77
|
+
pendingAction: null,
|
|
78
|
+
returned: false,
|
|
79
|
+
cancelRequested: false,
|
|
80
|
+
pauseRequested: false,
|
|
81
|
+
transcript: [],
|
|
82
|
+
};
|
|
83
|
+
}
|
|
84
|
+
function settle(state, attempt, usage) {
|
|
85
|
+
invariant(usage === null || (Number.isSafeInteger(usage) && usage >= 0), 'Invalid observed usage');
|
|
86
|
+
state.budget.reservedTokens -= attempt.reservation;
|
|
87
|
+
// Unknown billing is conservatively charged, not refunded on crash/cancel.
|
|
88
|
+
state.budget.usedTokens += usage ?? attempt.reservation;
|
|
89
|
+
attempt.usage = usage;
|
|
90
|
+
}
|
|
91
|
+
function receipt(state, attempt, at, sequence, reconciles = null) {
|
|
92
|
+
return {
|
|
93
|
+
version: 1,
|
|
94
|
+
id: `${attempt.id}-${sequence}`,
|
|
95
|
+
sessionId: state.id,
|
|
96
|
+
runId: attempt.runId,
|
|
97
|
+
actionId: attempt.invocationId,
|
|
98
|
+
invocationId: attempt.invocationId,
|
|
99
|
+
attemptId: attempt.id,
|
|
100
|
+
sequence,
|
|
101
|
+
target: attempt.target,
|
|
102
|
+
bindingDigest: attempt.bindingDigest,
|
|
103
|
+
inputDigest: attempt.inputDigest,
|
|
104
|
+
authority: copy(attempt.authority),
|
|
105
|
+
effect: attempt.effect,
|
|
106
|
+
output: copy(attempt.output),
|
|
107
|
+
error: attempt.error,
|
|
108
|
+
usage: attempt.usage,
|
|
109
|
+
at,
|
|
110
|
+
reconciles,
|
|
111
|
+
...(attempt.cause ? { cause: copy(attempt.cause), actionId: attempt.cause.actionId } : {}),
|
|
112
|
+
...(attempt.review ? { review: copy(attempt.review) } : {}),
|
|
113
|
+
};
|
|
114
|
+
}
|
|
115
|
+
function idle(state, run) {
|
|
116
|
+
invariant(!terminal(run), 'Terminal runs cannot change');
|
|
117
|
+
invariant(unresolved(state, run.id).length === 0, 'Run has an unresolved attempt/effect');
|
|
118
|
+
}
|
|
119
|
+
function complete(state, run) {
|
|
120
|
+
idle(state, run);
|
|
121
|
+
invariant(!Object.values(state.runs).some((r) => r.parentId === run.id && (!terminal(r) || !r.returned)), 'A child has not returned');
|
|
122
|
+
invariant(!Object.values(state.questions).some((q) => q.runId === run.id && q.required && q.status === 'open'), 'Required question is unanswered');
|
|
123
|
+
invariant(!Object.values(state.reactions).some((r) => r.runId === run.id && r.required && r.status === 'queued'), 'Required reaction is pending');
|
|
124
|
+
}
|
|
125
|
+
/** Pure reducer. All I/O and current-authority evaluation occur outside it. */
|
|
126
|
+
export function reduce(previous, mutation, actor, at, sequence) {
|
|
127
|
+
return apply(previous ? copy(previous) : null, mutation, actor, at, sequence);
|
|
128
|
+
}
|
|
129
|
+
/** Internal fold over exclusively owned state; incoming mutation values are still copied. */
|
|
130
|
+
function apply(previous, mutation, actor, at, sequence) {
|
|
131
|
+
invariant(typeof actor === 'string' && actor.length > 0 && Number.isFinite(Date.parse(at)), 'Invalid event attribution/time');
|
|
132
|
+
if (mutation.type === 'session.create') {
|
|
133
|
+
invariant(previous === null && sequence === 1, 'Session already exists');
|
|
134
|
+
identifier(mutation.sessionId);
|
|
135
|
+
validateLimits(mutation.limits);
|
|
136
|
+
invariant(actor === mutation.principal, 'Creation principal mismatch');
|
|
137
|
+
invariant(typeof mutation.workspace === 'string' &&
|
|
138
|
+
mutation.workspace.length > 0 &&
|
|
139
|
+
mutation.manifestDigest === digest(mutation.manifest), 'Invalid workspace/manifest digest');
|
|
140
|
+
const budget = {
|
|
141
|
+
usedTokens: 0,
|
|
142
|
+
reservedTokens: 0,
|
|
143
|
+
steps: 0,
|
|
144
|
+
modelCalls: 0,
|
|
145
|
+
operations: 0,
|
|
146
|
+
children: 0,
|
|
147
|
+
retainedBytes: 0,
|
|
148
|
+
};
|
|
149
|
+
return {
|
|
150
|
+
version: 1,
|
|
151
|
+
id: mutation.sessionId,
|
|
152
|
+
principal: actor,
|
|
153
|
+
workspace: mutation.workspace,
|
|
154
|
+
manifestDigest: mutation.manifestDigest,
|
|
155
|
+
manifest: copy(mutation.manifest),
|
|
156
|
+
createdAt: at,
|
|
157
|
+
rootId: mutation.rootId,
|
|
158
|
+
sequence,
|
|
159
|
+
hash: '',
|
|
160
|
+
limits: copy(mutation.limits),
|
|
161
|
+
budget,
|
|
162
|
+
runs: {
|
|
163
|
+
[mutation.rootId]: {
|
|
164
|
+
...newRun(mutation.rootId, mutation.profile, mutation.agent, mutation.task, null, 0),
|
|
165
|
+
...(mutation.contract ? { contract: copy(mutation.contract), limits: copy(mutation.limits) } : {}),
|
|
166
|
+
...(mutation.authority ? { authority: copy(mutation.authority) } : {}),
|
|
167
|
+
},
|
|
168
|
+
},
|
|
169
|
+
attempts: {},
|
|
170
|
+
questions: {},
|
|
171
|
+
proposals: {},
|
|
172
|
+
decisions: {},
|
|
173
|
+
reactions: {},
|
|
174
|
+
controls: [],
|
|
175
|
+
receipts: [],
|
|
176
|
+
};
|
|
177
|
+
}
|
|
178
|
+
requireValue(previous, 'IA-SESSION-NOT-FOUND', 'Session does not exist');
|
|
179
|
+
const state = previous;
|
|
180
|
+
requireValue(actor === state.principal ||
|
|
181
|
+
(mutation.type === 'question.reply' && state.questions[mutation.questionId]?.respondent === actor) ||
|
|
182
|
+
(mutation.type === 'proposal.review' && state.proposals[mutation.proposalId]?.review?.reviewer === actor), 'IA-SESSION-AUTHORITY-DENIED', 'Actor does not control this session');
|
|
183
|
+
invariant(sequence === state.sequence + 1, 'Nonconsecutive sequence');
|
|
184
|
+
switch (mutation.type) {
|
|
185
|
+
case 'run.ready': {
|
|
186
|
+
const run = runOf(state, mutation.runId);
|
|
187
|
+
idle(state, run);
|
|
188
|
+
invariant(!run.cancelRequested && !run.pauseRequested, 'Run has a pending control');
|
|
189
|
+
invariant(!Object.values(state.runs).some((r) => r.parentId === run.id && (!terminal(r) || !r.returned)), 'Child work has not returned');
|
|
190
|
+
if (run.wait?.reason === 'input')
|
|
191
|
+
invariant(run.wait.objectIds.every((id) => state.questions[id]?.status === 'answered'), 'Question wait is unresolved');
|
|
192
|
+
if (run.wait?.reason === 'review')
|
|
193
|
+
invariant(run.wait.objectIds.every((id) => ['accepted', 'rejected'].includes(state.proposals[id]?.status ?? '')), 'Review wait is unresolved');
|
|
194
|
+
run.status = 'running';
|
|
195
|
+
run.wait = null;
|
|
196
|
+
break;
|
|
197
|
+
}
|
|
198
|
+
case 'attempt.prepare': {
|
|
199
|
+
const value = mutation.attempt, run = runOf(state, value.runId);
|
|
200
|
+
identifier(value.id);
|
|
201
|
+
identifier(value.invocationId);
|
|
202
|
+
invariant(run.status === 'running' && !run.cancelRequested && !run.pauseRequested, 'Run is not runnable');
|
|
203
|
+
invariant(!state.attempts[value.id] && unresolved(state).length === 0, 'Attempt exists or another effect is unresolved');
|
|
204
|
+
invariant(Date.parse(at) < state.limits.deadline, 'Session deadline exhausted');
|
|
205
|
+
invariant(value.inputDigest === digest(value.input), 'Input digest mismatch');
|
|
206
|
+
invariant(Number.isSafeInteger(value.reservation) && value.reservation >= 0, 'Invalid reservation');
|
|
207
|
+
invariant(state.budget.usedTokens + state.budget.reservedTokens + value.reservation <= state.limits.tokens, 'Token budget exhausted');
|
|
208
|
+
if (value.kind === 'model') {
|
|
209
|
+
invariant(state.budget.modelCalls < state.limits.modelCalls, 'Model call budget exhausted');
|
|
210
|
+
state.budget.modelCalls++;
|
|
211
|
+
}
|
|
212
|
+
else {
|
|
213
|
+
invariant(state.budget.operations < state.limits.operations, 'Operation budget exhausted');
|
|
214
|
+
state.budget.operations++;
|
|
215
|
+
}
|
|
216
|
+
const previousAttempts = Object.values(state.attempts).filter((a) => a.invocationId === value.invocationId);
|
|
217
|
+
const prior = previousAttempts[0];
|
|
218
|
+
invariant(!prior ||
|
|
219
|
+
(prior.inputDigest === value.inputDigest &&
|
|
220
|
+
prior.bindingDigest === value.bindingDigest &&
|
|
221
|
+
prior.target === value.target &&
|
|
222
|
+
prior.runId === value.runId), 'Invocation retry changes its contract');
|
|
223
|
+
invariant(previousAttempts.every((a) => a.effect === 'none' && a.status === 'observed'), 'An observed applied invocation cannot be repeated');
|
|
224
|
+
invariant(!prior ||
|
|
225
|
+
(digest(prior.review ?? null) === digest(value.review ?? null) &&
|
|
226
|
+
digest(prior.cause ?? null) === digest(value.cause ?? null)), 'Invocation retry changes review or causation');
|
|
227
|
+
if (value.cause) {
|
|
228
|
+
invariant(value.cause.modelAttemptId === null || state.attempts[value.cause.modelAttemptId]?.runId === run.id, 'Unknown model cause');
|
|
229
|
+
invariant(value.cause.questions.every((id) => state.questions[id]?.runId === run.id) &&
|
|
230
|
+
value.cause.proposals.every((id) => state.proposals[id]?.runId === run.id) &&
|
|
231
|
+
value.cause.decisions.every((id) => state.decisions[id]?.runId === run.id) &&
|
|
232
|
+
value.cause.receipts.every((id) => state.receipts.some((r) => r.id === id && r.runId === run.id)), 'Unknown causal reference');
|
|
233
|
+
}
|
|
234
|
+
state.budget.reservedTokens += value.reservation;
|
|
235
|
+
state.attempts[value.id] = {
|
|
236
|
+
...copy(value),
|
|
237
|
+
...(value.cause ? { preparedSequence: sequence } : {}),
|
|
238
|
+
status: 'prepared',
|
|
239
|
+
effect: 'none',
|
|
240
|
+
actionAccepted: false,
|
|
241
|
+
output: null,
|
|
242
|
+
error: null,
|
|
243
|
+
usage: null,
|
|
244
|
+
preparedAt: at,
|
|
245
|
+
observedAt: null,
|
|
246
|
+
};
|
|
247
|
+
if (value.kind !== 'model') {
|
|
248
|
+
run.pendingAction = null;
|
|
249
|
+
delete run.retryOf;
|
|
250
|
+
}
|
|
251
|
+
break;
|
|
252
|
+
}
|
|
253
|
+
case 'attempt.dispatch': {
|
|
254
|
+
const attempt = state.attempts[mutation.attemptId];
|
|
255
|
+
invariant(attempt?.status === 'prepared', 'Attempt is not prepared');
|
|
256
|
+
const a = attempt;
|
|
257
|
+
const run = runOf(state, a.runId);
|
|
258
|
+
invariant(!run.cancelRequested && !run.pauseRequested && run.status === 'running', 'Dispatch is stopped');
|
|
259
|
+
invariant(!state.controls.some((c) => !c.applied), 'Control must be applied before another dispatch');
|
|
260
|
+
a.status = 'dispatched';
|
|
261
|
+
a.effect = a.kind === 'model' ? 'none' : 'unknown';
|
|
262
|
+
break;
|
|
263
|
+
}
|
|
264
|
+
case 'attempt.abandon': {
|
|
265
|
+
const attempt = state.attempts[mutation.attemptId];
|
|
266
|
+
invariant(attempt?.status === 'prepared', 'Only undispatched preparation may be abandoned');
|
|
267
|
+
const a = attempt;
|
|
268
|
+
a.status = 'observed';
|
|
269
|
+
a.error = 'not-dispatched';
|
|
270
|
+
a.observedAt = at;
|
|
271
|
+
settle(state, a, 0);
|
|
272
|
+
state.receipts.push(receipt(state, a, at, sequence));
|
|
273
|
+
break;
|
|
274
|
+
}
|
|
275
|
+
case 'attempt.observe': {
|
|
276
|
+
const attempt = state.attempts[mutation.attemptId];
|
|
277
|
+
invariant(attempt?.status === 'dispatched', 'Attempt is not dispatched or is already observed');
|
|
278
|
+
const a = attempt;
|
|
279
|
+
invariant(['none', 'applied', 'partial', 'unknown'].includes(mutation.effect), 'Invalid effect');
|
|
280
|
+
a.status = 'observed';
|
|
281
|
+
a.output = copy(mutation.output);
|
|
282
|
+
a.effect = mutation.effect;
|
|
283
|
+
a.error = mutation.error;
|
|
284
|
+
a.observedAt = at;
|
|
285
|
+
settle(state, a, mutation.usage);
|
|
286
|
+
state.receipts.push(receipt(state, a, at, sequence, mutation.reconciles ?? null));
|
|
287
|
+
runOf(state, a.runId).transcript.push({
|
|
288
|
+
kind: a.kind,
|
|
289
|
+
target: a.target,
|
|
290
|
+
output: a.output,
|
|
291
|
+
error: a.error,
|
|
292
|
+
receipt: state.receipts.at(-1).id,
|
|
293
|
+
});
|
|
294
|
+
break;
|
|
295
|
+
}
|
|
296
|
+
case 'attempt.reconcile.start': {
|
|
297
|
+
const a = state.attempts[mutation.attemptId];
|
|
298
|
+
invariant(a?.kind === 'operation' && (a.status === 'dispatched' || a.effect === 'unknown' || a.effect === 'partial'), 'No operation needs reconciliation');
|
|
299
|
+
invariant(Number.isSafeInteger(mutation.maxAttempts) &&
|
|
300
|
+
mutation.maxAttempts > 0 &&
|
|
301
|
+
(a.reconciliations ?? 0) < mutation.maxAttempts, 'Reconciliation allowance exhausted');
|
|
302
|
+
invariant(state.budget.operations < state.limits.operations && Date.parse(at) < state.limits.deadline, 'Reconciliation budget exhausted');
|
|
303
|
+
a.reconciliations = (a.reconciliations ?? 0) + 1;
|
|
304
|
+
state.budget.operations++;
|
|
305
|
+
break;
|
|
306
|
+
}
|
|
307
|
+
case 'attempt.reconcile': {
|
|
308
|
+
const attempt = state.attempts[mutation.attemptId];
|
|
309
|
+
invariant(attempt && (attempt.status === 'dispatched' || attempt.effect === 'unknown' || attempt.effect === 'partial'), 'No unresolved attempt');
|
|
310
|
+
invariant(typeof mutation.evidence === 'string' &&
|
|
311
|
+
mutation.evidence.length > 0 &&
|
|
312
|
+
['none', 'applied', 'partial', 'unknown'].includes(mutation.effect), 'Reconciliation needs evidence');
|
|
313
|
+
const a = attempt;
|
|
314
|
+
if (a.status !== 'observed')
|
|
315
|
+
settle(state, a, null);
|
|
316
|
+
a.status = 'observed';
|
|
317
|
+
a.effect = mutation.effect;
|
|
318
|
+
a.output = copy(mutation.output);
|
|
319
|
+
a.observedAt = at;
|
|
320
|
+
if (mutation.error !== undefined)
|
|
321
|
+
a.error = mutation.error;
|
|
322
|
+
if (a.kind === 'model' && mutation.output === null)
|
|
323
|
+
a.error = 'response-unavailable';
|
|
324
|
+
state.receipts.push(receipt(state, a, at, sequence, a.id));
|
|
325
|
+
runOf(state, a.runId).transcript.push({
|
|
326
|
+
kind: 'reconciliation',
|
|
327
|
+
attemptId: a.id,
|
|
328
|
+
evidence: mutation.evidence,
|
|
329
|
+
output: a.output,
|
|
330
|
+
effect: a.effect,
|
|
331
|
+
...(mutation.error !== undefined ? { target: a.target, receipt: state.receipts.at(-1).id } : {}),
|
|
332
|
+
});
|
|
333
|
+
break;
|
|
334
|
+
}
|
|
335
|
+
case 'action.accept': {
|
|
336
|
+
const run = runOf(state, mutation.runId);
|
|
337
|
+
idle(state, run);
|
|
338
|
+
invariant(run.status === 'running' && run.pendingAction === null, 'Action is already pending');
|
|
339
|
+
const attempt = state.attempts[mutation.attemptId];
|
|
340
|
+
invariant(attempt?.kind === 'model' &&
|
|
341
|
+
attempt.status === 'observed' &&
|
|
342
|
+
attempt.runId === run.id &&
|
|
343
|
+
!attempt.actionAccepted &&
|
|
344
|
+
attempt.error === null, 'Model response is unavailable or already accepted');
|
|
345
|
+
invariant(state.budget.steps < state.limits.steps, 'Step limit exhausted');
|
|
346
|
+
state.budget.steps++;
|
|
347
|
+
attempt.actionAccepted = true;
|
|
348
|
+
if (mutation.cause)
|
|
349
|
+
run.cause = copy(mutation.cause);
|
|
350
|
+
run.pendingAction = copy(mutation.action);
|
|
351
|
+
run.transcript.push({ kind: 'action', action: copy(mutation.action) });
|
|
352
|
+
break;
|
|
353
|
+
}
|
|
354
|
+
case 'attempt.retry': {
|
|
355
|
+
const a = state.attempts[mutation.attemptId];
|
|
356
|
+
invariant(a?.kind === 'operation' && a.status === 'observed' && a.effect === 'none', 'Retry requires a settled absent operation');
|
|
357
|
+
const run = runOf(state, a.runId);
|
|
358
|
+
idle(state, run);
|
|
359
|
+
invariant(run.pendingAction === null &&
|
|
360
|
+
run.wait?.objectIds.includes(a.id) &&
|
|
361
|
+
!run.cancelRequested &&
|
|
362
|
+
!run.pauseRequested, 'Retry is not pending');
|
|
363
|
+
const attempts = Object.values(state.attempts).filter((v) => v.invocationId === a.invocationId);
|
|
364
|
+
const last = attempts.sort((x, y) => (x.preparedSequence ?? 0) - (y.preparedSequence ?? 0)).at(-1);
|
|
365
|
+
invariant(Number.isSafeInteger(mutation.maxAttempts) &&
|
|
366
|
+
mutation.maxAttempts > 0 &&
|
|
367
|
+
attempts.length < mutation.maxAttempts &&
|
|
368
|
+
last?.id === a.id, 'Retry allowance exhausted');
|
|
369
|
+
run.retryOf = a.id;
|
|
370
|
+
run.pendingAction = copy(mutation.action);
|
|
371
|
+
run.status = 'running';
|
|
372
|
+
run.wait = null;
|
|
373
|
+
break;
|
|
374
|
+
}
|
|
375
|
+
case 'model.repair': {
|
|
376
|
+
const a = state.attempts[mutation.attemptId];
|
|
377
|
+
invariant(a?.kind === 'model' && a.status === 'observed', 'Repair requires an observed model attempt');
|
|
378
|
+
const run = runOf(state, a.runId);
|
|
379
|
+
idle(state, run);
|
|
380
|
+
invariant(run.pendingAction === null &&
|
|
381
|
+
['invalid-model-action', 'model-error'].includes(run.wait?.reason ?? '') &&
|
|
382
|
+
run.wait?.objectIds.includes(a.id) &&
|
|
383
|
+
!run.cancelRequested &&
|
|
384
|
+
!run.pauseRequested, 'No model repair is pending');
|
|
385
|
+
invariant(mutation.details === undefined || digest(mutation.details) === digest(run.wait.details), 'Repair diagnostic changed');
|
|
386
|
+
invariant(Number.isSafeInteger(mutation.allowance) && (run.repairs ?? 0) < mutation.allowance, 'Repair allowance exhausted');
|
|
387
|
+
run.repairs = (run.repairs ?? 0) + 1;
|
|
388
|
+
a.actionAccepted = true;
|
|
389
|
+
run.wait = null;
|
|
390
|
+
run.status = 'running';
|
|
391
|
+
run.transcript.push({
|
|
392
|
+
kind: 'repair',
|
|
393
|
+
attemptId: a.id,
|
|
394
|
+
instruction: 'The previous model response failed. Return one valid action under the same task contract.',
|
|
395
|
+
...(mutation.details !== undefined ? { details: copy(mutation.details) } : {}),
|
|
396
|
+
});
|
|
397
|
+
break;
|
|
398
|
+
}
|
|
399
|
+
case 'action.clear': {
|
|
400
|
+
const run = runOf(state, mutation.runId);
|
|
401
|
+
invariant(!terminal(run), 'Run is terminal');
|
|
402
|
+
run.pendingAction = null;
|
|
403
|
+
break;
|
|
404
|
+
}
|
|
405
|
+
case 'run.wait': {
|
|
406
|
+
const run = runOf(state, mutation.runId);
|
|
407
|
+
invariant(!terminal(run), 'Run is terminal');
|
|
408
|
+
run.status = 'waiting';
|
|
409
|
+
run.wait = copy(mutation.wait);
|
|
410
|
+
break;
|
|
411
|
+
}
|
|
412
|
+
case 'outcome.accept': {
|
|
413
|
+
const run = runOf(state, mutation.runId);
|
|
414
|
+
idle(state, run);
|
|
415
|
+
for (const q of mutation.questions ?? []) {
|
|
416
|
+
identifier(q.id);
|
|
417
|
+
invariant(!state.questions[q.id] &&
|
|
418
|
+
q.runId === run.id &&
|
|
419
|
+
q.status === 'open' &&
|
|
420
|
+
q.revision === 1 &&
|
|
421
|
+
q.prompt.length > 0 &&
|
|
422
|
+
q.respondent.length > 0 &&
|
|
423
|
+
q.digest ===
|
|
424
|
+
digest({ prompt: q.prompt, respondent: q.respondent, choices: q.choices, revision: q.revision }), 'Invalid question');
|
|
425
|
+
state.questions[q.id] = copy(q);
|
|
426
|
+
}
|
|
427
|
+
const p = mutation.proposal;
|
|
428
|
+
if (p) {
|
|
429
|
+
identifier(p.id);
|
|
430
|
+
invariant(!state.proposals[p.id] && p.runId === run.id && p.status === 'offered' && p.digest === digest(p.candidate), 'Invalid offered proposal');
|
|
431
|
+
if (p.review)
|
|
432
|
+
invariant(p.review.candidateDigest === p.digest &&
|
|
433
|
+
p.review.reviewer.length > 0 &&
|
|
434
|
+
p.review.expiresAt > Date.parse(at), 'Invalid review contract');
|
|
435
|
+
state.proposals[p.id] = copy(p);
|
|
436
|
+
}
|
|
437
|
+
run.outcome = copy(mutation.outcome);
|
|
438
|
+
run.pendingAction = null;
|
|
439
|
+
run.transcript.push({ kind: 'outcome', outcome: copy(mutation.outcome) });
|
|
440
|
+
if (mutation.terminal) {
|
|
441
|
+
complete(state, run);
|
|
442
|
+
run.status = mutation.terminal;
|
|
443
|
+
run.wait = null;
|
|
444
|
+
}
|
|
445
|
+
else if (mutation.wait) {
|
|
446
|
+
run.status = 'waiting';
|
|
447
|
+
run.wait = copy(mutation.wait);
|
|
448
|
+
}
|
|
449
|
+
break;
|
|
450
|
+
}
|
|
451
|
+
case 'question.reply': {
|
|
452
|
+
const q = state.questions[mutation.questionId];
|
|
453
|
+
invariant(q?.status === 'open' && q.revision === mutation.revision && q.digest === mutation.digest, 'Question is stale');
|
|
454
|
+
invariant(q.respondent === actor &&
|
|
455
|
+
(!q.choices.length || (typeof mutation.answer === 'string' && q.choices.includes(mutation.answer))), 'Wrong respondent or answer choice');
|
|
456
|
+
q.answer = copy(mutation.answer);
|
|
457
|
+
q.status = 'answered';
|
|
458
|
+
runOf(state, q.runId).transcript.push({
|
|
459
|
+
kind: 'answer',
|
|
460
|
+
questionId: q.id,
|
|
461
|
+
actor,
|
|
462
|
+
answer: copy(mutation.answer),
|
|
463
|
+
});
|
|
464
|
+
break;
|
|
465
|
+
}
|
|
466
|
+
case 'proposal.review': {
|
|
467
|
+
const p = state.proposals[mutation.proposalId];
|
|
468
|
+
invariant(p?.status === 'offered' && p.revision === mutation.revision && p.digest === mutation.digest, 'Proposal is stale');
|
|
469
|
+
if (p.review)
|
|
470
|
+
invariant(p.review.reviewer === actor && p.review.expiresAt > Date.parse(at), 'Wrong reviewer or expired review');
|
|
471
|
+
identifier(mutation.decisionId);
|
|
472
|
+
invariant(!state.decisions[mutation.decisionId], 'Decision exists');
|
|
473
|
+
p.status = mutation.accept ? 'accepted' : 'rejected';
|
|
474
|
+
p.decisionId = mutation.decisionId;
|
|
475
|
+
state.decisions[mutation.decisionId] = {
|
|
476
|
+
id: mutation.decisionId,
|
|
477
|
+
runId: p.runId,
|
|
478
|
+
subject: p.id,
|
|
479
|
+
revision: p.revision,
|
|
480
|
+
choice: p.status,
|
|
481
|
+
actor,
|
|
482
|
+
rationale: mutation.rationale,
|
|
483
|
+
evidence: [p.digest],
|
|
484
|
+
...(p.review ? { review: copy(p.review), proposalDigest: p.digest } : {}),
|
|
485
|
+
};
|
|
486
|
+
runOf(state, p.runId).transcript.push({
|
|
487
|
+
kind: 'review',
|
|
488
|
+
proposalId: p.id,
|
|
489
|
+
decisionId: mutation.decisionId,
|
|
490
|
+
choice: p.status,
|
|
491
|
+
actor,
|
|
492
|
+
});
|
|
493
|
+
break;
|
|
494
|
+
}
|
|
495
|
+
case 'decision.record': {
|
|
496
|
+
const d = mutation.decision;
|
|
497
|
+
identifier(d.id);
|
|
498
|
+
invariant(!state.decisions[d.id] && d.actor === actor && d.choice.length > 0, 'Invalid attributed decision');
|
|
499
|
+
runOf(state, d.runId);
|
|
500
|
+
state.decisions[d.id] = copy(d);
|
|
501
|
+
break;
|
|
502
|
+
}
|
|
503
|
+
case 'child.create': {
|
|
504
|
+
const parent = runOf(state, mutation.parentId);
|
|
505
|
+
idle(state, parent);
|
|
506
|
+
invariant(parent.status === 'running' && !state.runs[mutation.childId], 'Parent unavailable or child exists');
|
|
507
|
+
invariant(parent.depth < state.limits.depth && state.budget.children < state.limits.children, 'Delegation limits exceeded');
|
|
508
|
+
for (let cursor = parent; cursor; cursor = cursor.parentId ? state.runs[cursor.parentId] : undefined)
|
|
509
|
+
invariant(cursor.agent !== mutation.agent, 'Ancestor agent cycle');
|
|
510
|
+
state.runs[mutation.childId] = newRun(mutation.childId, mutation.profile, mutation.agent, mutation.task, parent.id, parent.depth + 1);
|
|
511
|
+
state.budget.children++;
|
|
512
|
+
const child = state.runs[mutation.childId];
|
|
513
|
+
if (mutation.contract)
|
|
514
|
+
child.contract = copy(mutation.contract);
|
|
515
|
+
if (mutation.authority)
|
|
516
|
+
child.authority = copy(mutation.authority);
|
|
517
|
+
if (mutation.limits) {
|
|
518
|
+
validateLimits(mutation.limits);
|
|
519
|
+
child.limits = copy(mutation.limits);
|
|
520
|
+
}
|
|
521
|
+
if (mutation.cause)
|
|
522
|
+
child.cause = copy(mutation.cause);
|
|
523
|
+
parent.pendingAction = null;
|
|
524
|
+
parent.status = 'waiting';
|
|
525
|
+
parent.wait = {
|
|
526
|
+
id: mutation.childId,
|
|
527
|
+
reason: 'child',
|
|
528
|
+
objectIds: [mutation.childId],
|
|
529
|
+
continuation: 'continue',
|
|
530
|
+
details: null,
|
|
531
|
+
};
|
|
532
|
+
break;
|
|
533
|
+
}
|
|
534
|
+
case 'child.return': {
|
|
535
|
+
const child = runOf(state, mutation.childId);
|
|
536
|
+
invariant(child.parentId && terminal(child) && !child.returned, 'Child cannot return');
|
|
537
|
+
const parent = runOf(state, child.parentId);
|
|
538
|
+
invariant(!terminal(parent), 'Parent is terminal');
|
|
539
|
+
child.returned = true;
|
|
540
|
+
parent.transcript.push({ kind: 'child', childId: child.id, status: child.status, outcome: child.outcome });
|
|
541
|
+
parent.wait = null;
|
|
542
|
+
parent.status = parent.cancelRequested ? 'cancelling' : parent.pauseRequested ? 'paused' : 'running';
|
|
543
|
+
break;
|
|
544
|
+
}
|
|
545
|
+
case 'control.submit': {
|
|
546
|
+
const c = mutation.control;
|
|
547
|
+
identifier(c.id);
|
|
548
|
+
invariant(c.actor === actor && !c.applied && !state.controls.some((v) => v.id === c.id), 'Invalid control');
|
|
549
|
+
invariant(!terminal(runOf(state, c.runId)), 'Run is terminal');
|
|
550
|
+
invariant(['pause', 'cancel', 'resume'].includes(c.kind), 'Unknown control');
|
|
551
|
+
state.controls.push(copy(c));
|
|
552
|
+
break;
|
|
553
|
+
}
|
|
554
|
+
case 'control.apply': {
|
|
555
|
+
const c = state.controls.find((v) => v.id === mutation.controlId);
|
|
556
|
+
invariant(c && !c.applied, 'Unknown/applied control');
|
|
557
|
+
const root = runOf(state, c.runId);
|
|
558
|
+
const affected = (run) => {
|
|
559
|
+
for (let r = run; r; r = r.parentId ? state.runs[r.parentId] : undefined)
|
|
560
|
+
if (r.id === root.id)
|
|
561
|
+
return true;
|
|
562
|
+
return false;
|
|
563
|
+
};
|
|
564
|
+
for (const run of Object.values(state.runs)
|
|
565
|
+
.filter((r) => affected(r) && !terminal(r))
|
|
566
|
+
.sort((a, b) => b.depth - a.depth)) {
|
|
567
|
+
if (c.kind === 'resume') {
|
|
568
|
+
invariant(!run.cancelRequested, 'Cancellation cannot be resumed');
|
|
569
|
+
run.pauseRequested = false;
|
|
570
|
+
run.status = run.wait ? 'waiting' : 'running';
|
|
571
|
+
}
|
|
572
|
+
else {
|
|
573
|
+
if (c.kind === 'cancel')
|
|
574
|
+
run.cancelRequested = true;
|
|
575
|
+
else
|
|
576
|
+
run.pauseRequested = true;
|
|
577
|
+
for (const a of unresolved(state, run.id).filter((a) => a.status === 'prepared')) {
|
|
578
|
+
a.status = 'observed';
|
|
579
|
+
a.effect = 'none';
|
|
580
|
+
a.error = 'cancelled-before-dispatch';
|
|
581
|
+
a.observedAt = at;
|
|
582
|
+
settle(state, a, 0);
|
|
583
|
+
state.receipts.push(receipt(state, a, at, sequence));
|
|
584
|
+
}
|
|
585
|
+
const pending = unresolved(state, run.id), children = Object.values(state.runs).filter((child) => child.parentId === run.id && !terminal(child));
|
|
586
|
+
if (pending.length || children.length) {
|
|
587
|
+
run.status = 'waiting';
|
|
588
|
+
run.wait = {
|
|
589
|
+
id: c.id,
|
|
590
|
+
reason: pending.length ? 'unknown-effect' : 'child',
|
|
591
|
+
objectIds: [...pending.map((a) => a.id), ...children.map((child) => child.id)],
|
|
592
|
+
continuation: c.kind,
|
|
593
|
+
details: null,
|
|
594
|
+
};
|
|
595
|
+
}
|
|
596
|
+
else {
|
|
597
|
+
run.status = c.kind === 'cancel' ? 'cancelled' : 'paused';
|
|
598
|
+
if (run.wait?.reason === 'unknown-effect')
|
|
599
|
+
run.wait = null;
|
|
600
|
+
if (c.kind === 'cancel') {
|
|
601
|
+
run.pendingAction = null;
|
|
602
|
+
run.wait = null;
|
|
603
|
+
}
|
|
604
|
+
}
|
|
605
|
+
}
|
|
606
|
+
}
|
|
607
|
+
c.applied = true;
|
|
608
|
+
break;
|
|
609
|
+
}
|
|
610
|
+
case 'session.rebind': {
|
|
611
|
+
invariant(unresolved(state).length === 0 && !Object.values(state.runs).every(terminal), 'Cannot rebind unresolved/terminal work');
|
|
612
|
+
invariant(mutation.manifestDigest === digest(mutation.manifest), 'Manifest mismatch');
|
|
613
|
+
state.manifestDigest = mutation.manifestDigest;
|
|
614
|
+
state.manifest = copy(mutation.manifest);
|
|
615
|
+
break;
|
|
616
|
+
}
|
|
617
|
+
case 'session.renew': {
|
|
618
|
+
const runs = humanWaitRuns(state), now = Date.parse(at);
|
|
619
|
+
invariant(runs.length > 0 &&
|
|
620
|
+
mutation.expectedSequence === state.sequence &&
|
|
621
|
+
mutation.previousDeadline === state.limits.deadline, 'Renewal requires the exact settled human wait');
|
|
622
|
+
invariant(Number.isSafeInteger(mutation.deadline) && mutation.deadline > Math.max(state.limits.deadline, now), 'Renewal deadline must advance');
|
|
623
|
+
invariant(Object.keys(mutation.runs).length === runs.length && runs.every((run) => Object.hasOwn(mutation.runs, run.id)), 'Renewal must include every live run');
|
|
624
|
+
for (const run of runs) {
|
|
625
|
+
const value = mutation.runs[run.id];
|
|
626
|
+
invariant(run.limits &&
|
|
627
|
+
value.previousDeadline === run.limits.deadline &&
|
|
628
|
+
Number.isSafeInteger(value.deadline) &&
|
|
629
|
+
value.deadline > Math.max(run.limits.deadline, now) &&
|
|
630
|
+
value.deadline <= mutation.deadline, 'Invalid run renewal deadline');
|
|
631
|
+
if (run.parentId)
|
|
632
|
+
invariant(value.deadline <= mutation.runs[run.parentId].deadline, 'Renewal exceeds an ancestor deadline');
|
|
633
|
+
const authority = run.authority;
|
|
634
|
+
invariant(authority && typeof authority === 'object' && !Array.isArray(authority), 'Legacy authority cannot be renewed');
|
|
635
|
+
const ceiling = authority['limits'];
|
|
636
|
+
invariant(ceiling && typeof ceiling === 'object' && !Array.isArray(ceiling), 'Legacy authority limits cannot be renewed');
|
|
637
|
+
const limits = ceiling;
|
|
638
|
+
validateLimits(limits);
|
|
639
|
+
const expiresAt = authority['expiresAt'];
|
|
640
|
+
invariant(typeof expiresAt === 'number' && Number.isSafeInteger(expiresAt), 'Legacy authority expiry cannot be renewed');
|
|
641
|
+
run.limits.deadline = value.deadline;
|
|
642
|
+
limits.deadline = Math.max(limits.deadline, value.deadline);
|
|
643
|
+
authority['expiresAt'] = Math.max(expiresAt, value.deadline);
|
|
644
|
+
}
|
|
645
|
+
state.limits.deadline = mutation.deadline;
|
|
646
|
+
break;
|
|
647
|
+
}
|
|
648
|
+
case 'budget.extend': {
|
|
649
|
+
validateLimits(mutation.limits);
|
|
650
|
+
for (const key of Object.keys(state.limits))
|
|
651
|
+
invariant(mutation.limits[key] >= state.limits[key], 'Budget cannot shrink below prior ceiling');
|
|
652
|
+
state.limits = copy(mutation.limits);
|
|
653
|
+
break;
|
|
654
|
+
}
|
|
655
|
+
case 'reaction.queue': {
|
|
656
|
+
const r = mutation.reaction;
|
|
657
|
+
identifier(r.id);
|
|
658
|
+
invariant(!state.reactions[r.id] && r.status === 'queued', 'Reaction already exists');
|
|
659
|
+
if (r.required)
|
|
660
|
+
invariant(!terminal(runOf(state, r.runId)), 'Required reaction cannot attach after completion');
|
|
661
|
+
state.reactions[r.id] = copy(r);
|
|
662
|
+
break;
|
|
663
|
+
}
|
|
664
|
+
case 'reaction.settle': {
|
|
665
|
+
const r = state.reactions[mutation.reactionId];
|
|
666
|
+
invariant(r?.status === 'queued', 'Reaction already settled or absent');
|
|
667
|
+
r.status = 'settled';
|
|
668
|
+
r.result = copy(mutation.result);
|
|
669
|
+
break;
|
|
670
|
+
}
|
|
671
|
+
default:
|
|
672
|
+
invariant(false, 'Unknown mutation');
|
|
673
|
+
}
|
|
674
|
+
state.sequence = sequence;
|
|
675
|
+
return state;
|
|
676
|
+
}
|
|
677
|
+
export function replay(events, starting = null) {
|
|
678
|
+
let state = starting ? copy(starting) : null;
|
|
679
|
+
for (const event of events) {
|
|
680
|
+
const { hash, ...body } = event;
|
|
681
|
+
requireValue(event.version === (event.mutation.type === 'session.renew' ? 2 : 1), 'IA-SESSION-VERSION-UNSUPPORTED', 'Unsupported event version or mutation/version combination');
|
|
682
|
+
requireValue(event.sequence === (state?.sequence ?? 0) + 1 && event.previous === (state?.hash ?? '') && digest(body) === hash, 'IA-SESSION-STORE-CORRUPT', 'Event chain/version/digest mismatch');
|
|
683
|
+
requireValue(!state || state.id === event.sessionId, 'IA-SESSION-STORE-CORRUPT', 'Event belongs to another session');
|
|
684
|
+
state = apply(state, event.mutation, event.actor, event.at, event.sequence);
|
|
685
|
+
state.hash = hash;
|
|
686
|
+
const bytes = Buffer.byteLength(JSON.stringify(event));
|
|
687
|
+
state.budget.retainedBytes += bytes;
|
|
688
|
+
const m = event.mutation;
|
|
689
|
+
const runId = 'runId' in m
|
|
690
|
+
? m.runId
|
|
691
|
+
: m.type === 'attempt.prepare'
|
|
692
|
+
? m.attempt.runId
|
|
693
|
+
: 'attemptId' in m
|
|
694
|
+
? state.attempts[m.attemptId]?.runId
|
|
695
|
+
: 'childId' in m
|
|
696
|
+
? m.childId
|
|
697
|
+
: 'questionId' in m
|
|
698
|
+
? state.questions[m.questionId]?.runId
|
|
699
|
+
: 'proposalId' in m
|
|
700
|
+
? state.proposals[m.proposalId]?.runId
|
|
701
|
+
: state.rootId;
|
|
702
|
+
for (let run = state.runs[runId ?? state.rootId]; run; run = run.parentId ? state.runs[run.parentId] : undefined)
|
|
703
|
+
if (run.contract)
|
|
704
|
+
run.retainedBytes = (run.retainedBytes ?? 0) + bytes;
|
|
705
|
+
}
|
|
706
|
+
requireValue(state, 'IA-SESSION-NOT-FOUND', 'Session has no creation event');
|
|
707
|
+
return state;
|
|
708
|
+
}
|
|
709
|
+
export function eventFor(previous, command, at) {
|
|
710
|
+
const body = {
|
|
711
|
+
version: command.mutation.type === 'session.renew' ? 2 : 1,
|
|
712
|
+
id: command.id,
|
|
713
|
+
sequence: command.expected + 1,
|
|
714
|
+
sessionId: command.sessionId,
|
|
715
|
+
actor: command.actor,
|
|
716
|
+
at,
|
|
717
|
+
cause: command.cause ?? null,
|
|
718
|
+
mutation: copy(command.mutation),
|
|
719
|
+
previous: previous?.hash ?? '',
|
|
720
|
+
};
|
|
721
|
+
return { ...body, hash: digest(body) };
|
|
722
|
+
}
|
|
723
|
+
//# sourceMappingURL=reducer.js.map
|