@hunterzhu/pulse-runtime 0.1.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/dist/context/builder.d.ts +68 -0
- package/dist/context/builder.js +127 -0
- package/dist/context/index.d.ts +2 -0
- package/dist/context/index.js +2 -0
- package/dist/context/merger.d.ts +25 -0
- package/dist/context/merger.js +125 -0
- package/dist/core/actions.d.ts +1 -0
- package/dist/core/actions.js +1 -0
- package/dist/core/errors.d.ts +8 -0
- package/dist/core/errors.js +36 -0
- package/dist/core/events.d.ts +10 -0
- package/dist/core/events.js +24 -0
- package/dist/core/factory.d.ts +35 -0
- package/dist/core/factory.js +27 -0
- package/dist/core/inbox.d.ts +119 -0
- package/dist/core/inbox.js +217 -0
- package/dist/core/mutations.d.ts +80 -0
- package/dist/core/mutations.js +127 -0
- package/dist/core/records.d.ts +1 -0
- package/dist/core/records.js +1 -0
- package/dist/core/types.d.ts +615 -0
- package/dist/core/types.js +109 -0
- package/dist/dependencies/graph.d.ts +25 -0
- package/dist/dependencies/graph.js +92 -0
- package/dist/dependencies/index.d.ts +1 -0
- package/dist/dependencies/index.js +1 -0
- package/dist/dsl/context-proxy.d.ts +20 -0
- package/dist/dsl/context-proxy.js +64 -0
- package/dist/dsl/index.d.ts +4 -0
- package/dist/dsl/index.js +4 -0
- package/dist/dsl/program.d.ts +314 -0
- package/dist/dsl/program.js +756 -0
- package/dist/dsl/session.d.ts +45 -0
- package/dist/dsl/session.js +93 -0
- package/dist/dsl/templates-index.d.ts +1 -0
- package/dist/dsl/templates-index.js +1 -0
- package/dist/dsl/templates.d.ts +85 -0
- package/dist/dsl/templates.js +110 -0
- package/dist/index.d.ts +15 -0
- package/dist/index.js +15 -0
- package/dist/lifecycle/index.d.ts +2 -0
- package/dist/lifecycle/index.js +2 -0
- package/dist/lifecycle/scopes.d.ts +38 -0
- package/dist/lifecycle/scopes.js +50 -0
- package/dist/lifecycle/watchdog.d.ts +16 -0
- package/dist/lifecycle/watchdog.js +66 -0
- package/dist/models/actions.d.ts +10 -0
- package/dist/models/actions.js +68 -0
- package/dist/models/index.d.ts +2 -0
- package/dist/models/index.js +2 -0
- package/dist/models/router.d.ts +187 -0
- package/dist/models/router.js +353 -0
- package/dist/scheduler/clock.d.ts +45 -0
- package/dist/scheduler/clock.js +92 -0
- package/dist/scheduler/decision.d.ts +72 -0
- package/dist/scheduler/decision.js +63 -0
- package/dist/scheduler/index.d.ts +6 -0
- package/dist/scheduler/index.js +6 -0
- package/dist/scheduler/locks.d.ts +18 -0
- package/dist/scheduler/locks.js +106 -0
- package/dist/scheduler/ready-queue.d.ts +32 -0
- package/dist/scheduler/ready-queue.js +40 -0
- package/dist/scheduler/runtime.d.ts +486 -0
- package/dist/scheduler/runtime.js +3445 -0
- package/dist/scheduler/telemetry.d.ts +111 -0
- package/dist/scheduler/telemetry.js +177 -0
- package/dist/scheduler/worker.d.ts +158 -0
- package/dist/scheduler/worker.js +744 -0
- package/dist/storage/artifacts.d.ts +17 -0
- package/dist/storage/artifacts.js +90 -0
- package/dist/storage/findings.d.ts +12 -0
- package/dist/storage/findings.js +70 -0
- package/dist/storage/index.d.ts +8 -0
- package/dist/storage/index.js +8 -0
- package/dist/storage/memory.d.ts +11 -0
- package/dist/storage/memory.js +21 -0
- package/dist/storage/mutation-log.d.ts +41 -0
- package/dist/storage/mutation-log.js +140 -0
- package/dist/storage/outbox.d.ts +30 -0
- package/dist/storage/outbox.js +59 -0
- package/dist/storage/persistence.d.ts +183 -0
- package/dist/storage/persistence.js +999 -0
- package/dist/storage/policy.d.ts +80 -0
- package/dist/storage/policy.js +268 -0
- package/dist/storage/session.d.ts +140 -0
- package/dist/storage/session.js +447 -0
- package/dist/tools/registry.d.ts +125 -0
- package/dist/tools/registry.js +308 -0
- package/dist/transitions/index.d.ts +2 -0
- package/dist/transitions/index.js +1 -0
- package/dist/transitions/validate.d.ts +4 -0
- package/dist/transitions/validate.js +1118 -0
- package/package.json +21 -0
|
@@ -0,0 +1,1118 @@
|
|
|
1
|
+
import { error } from '../core/errors.js';
|
|
2
|
+
import { apply } from '../core/mutations.js';
|
|
3
|
+
import { DependencyGraph } from '../dependencies/graph.js';
|
|
4
|
+
import { effectivePrivacy, enqueueControlProposal, privacyMetadataForDerivedRef, privacyRank, privacyTaintPrivacy, provenanceRefId, strictestPrivacy, validatePrivacyTaints } from '../core/types.js';
|
|
5
|
+
import { appendRuntimeEvent } from '../core/events.js';
|
|
6
|
+
import { ContextBuilder, contentHash, estimateHistoryTokens, hasUnsafePathSegment, historyPressure, ownChild, stableSerialize } from '../context/builder.js';
|
|
7
|
+
const isLocal = (value) => 'local' in value;
|
|
8
|
+
const clone = (value) => structuredClone(value);
|
|
9
|
+
const resultMetadata = (value) => ({ sizeBytes: Buffer.byteLength(stableSerialize(value), 'utf8'), contentHash: contentHash(value) });
|
|
10
|
+
const laneCopy = (lane) => ({ ...lane, resume: clone(lane.resume), context: clone(lane.context), ...(lane.visibleResultRefs === undefined ? {} : { visibleResultRefs: new Set(lane.visibleResultRefs) }), children: new Set(lane.children), ownedEffectIds: new Set(lane.ownedEffectIds), ...(lane.pendingResumeInput === undefined ? {} : { pendingResumeInput: clone(lane.pendingResumeInput) }), ...(lane.pendingControlProposals === undefined ? {} : { pendingControlProposals: clone(lane.pendingControlProposals) }), ...(lane.pendingOutcome === undefined ? {} : { pendingOutcome: clone(lane.pendingOutcome) }) });
|
|
11
|
+
function isRuntimeJsonValue(value, seen = new Set()) {
|
|
12
|
+
if (value === null || typeof value === 'string' || typeof value === 'boolean')
|
|
13
|
+
return true;
|
|
14
|
+
if (typeof value === 'number')
|
|
15
|
+
return Number.isFinite(value);
|
|
16
|
+
if (typeof value !== 'object')
|
|
17
|
+
return false;
|
|
18
|
+
if (seen.has(value))
|
|
19
|
+
return false;
|
|
20
|
+
seen.add(value);
|
|
21
|
+
const valid = Array.isArray(value)
|
|
22
|
+
? value.every((item) => isRuntimeJsonValue(item, seen))
|
|
23
|
+
: (Object.getPrototypeOf(value) === Object.prototype || Object.getPrototypeOf(value) === null) && Object.values(value).every((item) => isRuntimeJsonValue(item, seen));
|
|
24
|
+
seen.delete(value);
|
|
25
|
+
return valid;
|
|
26
|
+
}
|
|
27
|
+
function nonEmptyString(value) { return typeof value === 'string' && value.length > 0; }
|
|
28
|
+
function validProvenanceRef(value) {
|
|
29
|
+
if (nonEmptyString(value))
|
|
30
|
+
return true;
|
|
31
|
+
if (!value || typeof value !== 'object' || Array.isArray(value))
|
|
32
|
+
return false;
|
|
33
|
+
const ref = value;
|
|
34
|
+
return (ref.kind === 'result' || ref.kind === 'artifact') && nonEmptyString(ref.ref);
|
|
35
|
+
}
|
|
36
|
+
function validProvenanceRefs(value) {
|
|
37
|
+
return Array.isArray(value) && value.every(validProvenanceRef);
|
|
38
|
+
}
|
|
39
|
+
function validRuntimeError(value) {
|
|
40
|
+
if (!value || typeof value !== 'object' || Array.isArray(value))
|
|
41
|
+
return false;
|
|
42
|
+
const candidate = value;
|
|
43
|
+
return nonEmptyString(candidate.code) && typeof candidate.message === 'string' && (candidate.retryable === undefined || typeof candidate.retryable === 'boolean') && (candidate.details === undefined || isRuntimeJsonValue(candidate.details));
|
|
44
|
+
}
|
|
45
|
+
function validateContextDeltaShape(value) {
|
|
46
|
+
if (!value || typeof value !== 'object' || Array.isArray(value))
|
|
47
|
+
return 'INVALID_CONTEXT_DELTA';
|
|
48
|
+
const delta = value;
|
|
49
|
+
if (!['lane', 'global'].includes(String(delta.target)) || !Number.isInteger(delta.baseVersion) || delta.baseVersion < 0 || !Array.isArray(delta.ops))
|
|
50
|
+
return 'INVALID_CONTEXT_DELTA';
|
|
51
|
+
if (delta.sourceLaneId !== undefined && !nonEmptyString(delta.sourceLaneId))
|
|
52
|
+
return 'INVALID_CONTEXT_DELTA';
|
|
53
|
+
if (delta.privacy !== undefined && !['public', 'cloud_allowed', 'local_only'].includes(String(delta.privacy)))
|
|
54
|
+
return 'INVALID_CONTEXT_DELTA';
|
|
55
|
+
if (delta.proposal !== undefined && typeof delta.proposal !== 'boolean')
|
|
56
|
+
return 'INVALID_CONTEXT_DELTA';
|
|
57
|
+
if (delta.derivedFrom !== undefined && !validProvenanceRefs(delta.derivedFrom))
|
|
58
|
+
return 'INVALID_CONTEXT_DELTA';
|
|
59
|
+
if (delta.privacyTaints !== undefined && !Array.isArray(delta.privacyTaints))
|
|
60
|
+
return 'INVALID_CONTEXT_DELTA';
|
|
61
|
+
for (const operation of delta.ops) {
|
|
62
|
+
if (!operation || typeof operation !== 'object' || Array.isArray(operation))
|
|
63
|
+
return 'INVALID_CONTEXT_OP';
|
|
64
|
+
const op = operation;
|
|
65
|
+
if (!['set', 'append', 'remove', 'compact_history'].includes(String(op.op)))
|
|
66
|
+
return 'INVALID_CONTEXT_OP';
|
|
67
|
+
if (op.value !== undefined && !isRuntimeJsonValue(op.value))
|
|
68
|
+
return 'INVALID_CONTEXT_OP';
|
|
69
|
+
if (op.summary !== undefined && !isRuntimeJsonValue(op.summary))
|
|
70
|
+
return 'INVALID_CONTEXT_OP';
|
|
71
|
+
if (op.summaryRef !== undefined && !nonEmptyString(op.summaryRef))
|
|
72
|
+
return 'INVALID_CONTEXT_OP';
|
|
73
|
+
if (op.upToSeq !== undefined && !Number.isInteger(op.upToSeq))
|
|
74
|
+
return 'INVALID_CONTEXT_OP';
|
|
75
|
+
if (op.op === 'compact_history') {
|
|
76
|
+
if (op.path !== undefined)
|
|
77
|
+
return 'INVALID_CONTEXT_OP';
|
|
78
|
+
continue;
|
|
79
|
+
}
|
|
80
|
+
if (!Array.isArray(op.path) || op.path.length === 0 || op.path.some((part) => !nonEmptyString(part)) || hasUnsafePathSegment(op.path))
|
|
81
|
+
return 'INVALID_CONTEXT_PATH';
|
|
82
|
+
}
|
|
83
|
+
return undefined;
|
|
84
|
+
}
|
|
85
|
+
function validateControlActionShape(action) {
|
|
86
|
+
if (!action || typeof action !== 'object' || Array.isArray(action))
|
|
87
|
+
return 'INVALID_ACTION';
|
|
88
|
+
const value = action;
|
|
89
|
+
if (value.type === 'cancel_lane' && (!nonEmptyString(value.laneId) || !['SUPERSEDED', 'USER_REQUESTED', 'POLICY'].includes(String(value.reason))))
|
|
90
|
+
return 'INVALID_CANCEL_ACTION';
|
|
91
|
+
if (value.type === 'propose_cancel' && (!nonEmptyString(value.laneId) || !['SUPERSEDED', 'POLICY'].includes(String(value.reason))))
|
|
92
|
+
return 'INVALID_CANCEL_ACTION';
|
|
93
|
+
if (value.type === 'adopt_context' && !(value.version === 'latest' || (Number.isInteger(value.version) && value.version >= 0)))
|
|
94
|
+
return 'INVALID_CONTEXT_ADOPTION';
|
|
95
|
+
if (value.type === 'complete' && value.children !== undefined && !['reject_if_active', 'cancel', 'await'].includes(String(value.children)))
|
|
96
|
+
return 'INVALID_COMPLETE';
|
|
97
|
+
if (value.type === 'downgrade_privacy') {
|
|
98
|
+
if (!['human_approval', 'sanitizer'].includes(String(value.method)) || value.targetPrivacy !== 'cloud_allowed' || !nonEmptyString(value.outputRef) || !Array.isArray(value.sourceRefs) || !validProvenanceRefs(value.sourceRefs) || !isRuntimeJsonValue(value.value ?? null) || (value.summary !== undefined && !isRuntimeJsonValue(value.summary)))
|
|
99
|
+
return 'INVALID_PRIVACY_DOWNGRADE';
|
|
100
|
+
if (value.approvalRef !== undefined && !nonEmptyString(value.approvalRef))
|
|
101
|
+
return 'INVALID_PRIVACY_DOWNGRADE';
|
|
102
|
+
if (value.sanitizerId !== undefined && !nonEmptyString(value.sanitizerId))
|
|
103
|
+
return 'INVALID_PRIVACY_DOWNGRADE';
|
|
104
|
+
}
|
|
105
|
+
return undefined;
|
|
106
|
+
}
|
|
107
|
+
function validateEffectSubmission(submission) {
|
|
108
|
+
if (!submission || typeof submission !== 'object' || Array.isArray(submission))
|
|
109
|
+
return 'INVALID_EFFECT_SUBMISSION';
|
|
110
|
+
const value = submission;
|
|
111
|
+
if (!nonEmptyString(value.key))
|
|
112
|
+
return 'INVALID_EFFECT_KEY';
|
|
113
|
+
if (!['llm', 'tool', 'human', 'agent', 'timer'].includes(String(value.kind)))
|
|
114
|
+
return 'INVALID_EFFECT_KIND';
|
|
115
|
+
if (!['llm', 'tool', 'agent', 'none'].includes(String(value.concurrencyClass)))
|
|
116
|
+
return 'INVALID_EFFECT_CONCURRENCY';
|
|
117
|
+
const expectedClass = { llm: 'llm', tool: 'tool', human: 'none', agent: 'agent', timer: 'none' };
|
|
118
|
+
if (expectedClass[String(value.kind)] !== value.concurrencyClass)
|
|
119
|
+
return 'INVALID_EFFECT_CONCURRENCY';
|
|
120
|
+
if (!isRuntimeJsonValue(value.input))
|
|
121
|
+
return 'INVALID_EFFECT_INPUT';
|
|
122
|
+
if (value.derivedFrom !== undefined && (!Array.isArray(value.derivedFrom) || value.derivedFrom.some((ref) => !validProvenanceRef(ref))))
|
|
123
|
+
return 'INVALID_EFFECT_PROVENANCE';
|
|
124
|
+
if (value.wait !== undefined && typeof value.wait !== 'boolean')
|
|
125
|
+
return 'INVALID_EFFECT_WAIT';
|
|
126
|
+
if (value.privacy !== undefined && !['public', 'cloud_allowed', 'local_only'].includes(String(value.privacy)))
|
|
127
|
+
return 'INVALID_EFFECT_PRIVACY';
|
|
128
|
+
for (const field of ['priority', 'deadlineAt', 'cancelGraceMs', 'attemptTimeoutMs']) {
|
|
129
|
+
if (value[field] !== undefined && (typeof value[field] !== 'number' || !Number.isFinite(value[field]) || (['cancelGraceMs', 'attemptTimeoutMs'].includes(field) && value[field] < 0)))
|
|
130
|
+
return `INVALID_EFFECT_${field.toUpperCase()}`;
|
|
131
|
+
}
|
|
132
|
+
for (const field of ['idempotencyKey', 'toolVersion', 'toolCallId', 'llmEffectId'])
|
|
133
|
+
if (value[field] !== undefined && !nonEmptyString(value[field]))
|
|
134
|
+
return `INVALID_EFFECT_${field.toUpperCase()}`;
|
|
135
|
+
if (value.sideEffectPolicy !== undefined && !['none', 'read', 'write', 'external'].includes(String(value.sideEffectPolicy)))
|
|
136
|
+
return 'INVALID_EFFECT_SIDE_EFFECT_POLICY';
|
|
137
|
+
if (value.duplicateExecutionPolicy !== undefined && !['allow', 'forbid'].includes(String(value.duplicateExecutionPolicy)))
|
|
138
|
+
return 'INVALID_EFFECT_DUPLICATE_POLICY';
|
|
139
|
+
if (value.maxUnknownAttempts !== undefined && (!Number.isInteger(value.maxUnknownAttempts) || value.maxUnknownAttempts < 0))
|
|
140
|
+
return 'INVALID_EFFECT_UNKNOWN_ATTEMPTS';
|
|
141
|
+
if (value.retryPolicy !== undefined) {
|
|
142
|
+
if (!value.retryPolicy || typeof value.retryPolicy !== 'object' || Array.isArray(value.retryPolicy))
|
|
143
|
+
return 'INVALID_EFFECT_RETRY_POLICY';
|
|
144
|
+
const retry = value.retryPolicy;
|
|
145
|
+
if (!Number.isInteger(retry.maxAttempts) || retry.maxAttempts < 1 || typeof retry.jitter !== 'boolean' || typeof retry.initialBackoffMs !== 'number' || !Number.isFinite(retry.initialBackoffMs) || retry.initialBackoffMs < 0 || typeof retry.maxBackoffMs !== 'number' || !Number.isFinite(retry.maxBackoffMs) || retry.maxBackoffMs < retry.initialBackoffMs)
|
|
146
|
+
return 'INVALID_EFFECT_RETRY_POLICY';
|
|
147
|
+
}
|
|
148
|
+
if (value.locks !== undefined) {
|
|
149
|
+
if (!Array.isArray(value.locks) || value.locks.some((lock) => !lock || typeof lock !== 'object' || Array.isArray(lock) || !nonEmptyString(lock.resource) || !['shared', 'exclusive'].includes(String(lock.mode))))
|
|
150
|
+
return 'INVALID_EFFECT_LOCK';
|
|
151
|
+
}
|
|
152
|
+
return undefined;
|
|
153
|
+
}
|
|
154
|
+
function mergePrivacyTaints(...groups) {
|
|
155
|
+
const output = [];
|
|
156
|
+
const seen = new Set();
|
|
157
|
+
for (const group of groups)
|
|
158
|
+
for (const taint of group ?? []) {
|
|
159
|
+
const key = JSON.stringify(taint);
|
|
160
|
+
if (!seen.has(key)) {
|
|
161
|
+
seen.add(key);
|
|
162
|
+
output.push(clone(taint));
|
|
163
|
+
}
|
|
164
|
+
}
|
|
165
|
+
return output;
|
|
166
|
+
}
|
|
167
|
+
function validResume(resume) {
|
|
168
|
+
if (!resume || typeof resume !== 'object' || Array.isArray(resume))
|
|
169
|
+
return false;
|
|
170
|
+
const value = resume;
|
|
171
|
+
return nonEmptyString(value.programId) && nonEmptyString(value.programVersion) && nonEmptyString(value.step) && isRuntimeJsonValue(value.locals);
|
|
172
|
+
}
|
|
173
|
+
function descendants(state, ownerId, targetId) {
|
|
174
|
+
let current = state.lanes.get(targetId);
|
|
175
|
+
while (current?.ownerLaneId) {
|
|
176
|
+
if (current.ownerLaneId === ownerId)
|
|
177
|
+
return true;
|
|
178
|
+
current = state.lanes.get(current.ownerLaneId);
|
|
179
|
+
}
|
|
180
|
+
return false;
|
|
181
|
+
}
|
|
182
|
+
const LANE_TERMINAL_STATUSES = new Set(['succeeded', 'failed', 'cancelled']);
|
|
183
|
+
/**
|
|
184
|
+
* Cancel a Lane together with every non-terminal descendant. Cancellation is a
|
|
185
|
+
* subtree operation: without the cascade a grandchild keeps running and holding
|
|
186
|
+
* its Effects/locks after its parent has been marked cancelled. Lanes that are
|
|
187
|
+
* already closing with a result keep it (`cancelling` + `pendingOutcome`); all
|
|
188
|
+
* others move straight to `cancelled` and the Runtime cancels their owned
|
|
189
|
+
* Effects via `propagateCancelledLanes`. Returns the ids that were cancelled.
|
|
190
|
+
*/
|
|
191
|
+
function cancelLaneSubtree(state, rootId, reason, mutations, touched) {
|
|
192
|
+
const cancelled = [];
|
|
193
|
+
const queue = [rootId];
|
|
194
|
+
while (queue.length) {
|
|
195
|
+
const laneId = queue.shift();
|
|
196
|
+
if (touched.has(laneId))
|
|
197
|
+
continue;
|
|
198
|
+
touched.add(laneId);
|
|
199
|
+
const target = state.lanes.get(laneId);
|
|
200
|
+
if (!target)
|
|
201
|
+
continue;
|
|
202
|
+
for (const childId of target.children)
|
|
203
|
+
queue.push(childId);
|
|
204
|
+
if (LANE_TERMINAL_STATUSES.has(target.status))
|
|
205
|
+
continue;
|
|
206
|
+
const copy = laneCopy(target);
|
|
207
|
+
const preserveOutcome = (target.status === 'closing' || target.status === 'waiting') && target.closingResult !== undefined;
|
|
208
|
+
if (preserveOutcome)
|
|
209
|
+
copy.pendingOutcome = { status: 'succeeded', result: clone(target.closingResult.value) };
|
|
210
|
+
copy.status = preserveOutcome ? 'cancelling' : 'cancelled';
|
|
211
|
+
copy.cancelReason = reason;
|
|
212
|
+
copy.version = target.version + 1;
|
|
213
|
+
mutations.push({ op: 'setLane', laneId: target.id, record: copy });
|
|
214
|
+
mutations.push({ op: 'appendEvent', event: { type: preserveOutcome ? 'lane.cancelling' : 'lane.cancelled', laneId: target.id, data: reason } });
|
|
215
|
+
cancelled.push(target.id);
|
|
216
|
+
}
|
|
217
|
+
return cancelled;
|
|
218
|
+
}
|
|
219
|
+
function resolveTarget(value, locals) {
|
|
220
|
+
return isLocal(value) ? locals.get(value.local) : value;
|
|
221
|
+
}
|
|
222
|
+
function targetOutcome(state, target) {
|
|
223
|
+
if (target.kind === 'lane')
|
|
224
|
+
return state.lanes.get(target.id)?.status === 'succeeded' ? { status: 'succeeded' } : state.lanes.get(target.id)?.status === 'failed' ? { status: 'failed' } : state.lanes.get(target.id)?.status === 'cancelled' ? { status: 'cancelled' } : undefined;
|
|
225
|
+
return state.effects.get(target.id)?.outcome;
|
|
226
|
+
}
|
|
227
|
+
function hasDependencyCycle(state, extra) {
|
|
228
|
+
const graph = new DependencyGraph();
|
|
229
|
+
for (const wait of state.waits.values())
|
|
230
|
+
if (wait.state === 'pending')
|
|
231
|
+
for (const dependency of wait.spec.dependencies)
|
|
232
|
+
graph.add({ kind: 'lane', id: wait.laneId }, dependency.target);
|
|
233
|
+
for (const edge of extra)
|
|
234
|
+
graph.add(edge.from, edge.to, edge.kind ?? 'wait');
|
|
235
|
+
return graph.hasCycle();
|
|
236
|
+
}
|
|
237
|
+
function overlap(left, right) {
|
|
238
|
+
const a = new Set(left);
|
|
239
|
+
const b = new Set(right);
|
|
240
|
+
if (a.size === 0 && b.size === 0)
|
|
241
|
+
return 0;
|
|
242
|
+
const intersection = [...a].filter((item) => b.has(item)).length;
|
|
243
|
+
return intersection / Math.max(1, new Set([...a, ...b]).size);
|
|
244
|
+
}
|
|
245
|
+
function affinityGroups(lanes) {
|
|
246
|
+
const groups = [];
|
|
247
|
+
const join = (left, right) => {
|
|
248
|
+
if (left === right)
|
|
249
|
+
return;
|
|
250
|
+
const target = groups[left];
|
|
251
|
+
const source = groups[right];
|
|
252
|
+
for (const key of source.keys)
|
|
253
|
+
target.keys.add(key);
|
|
254
|
+
for (const signal of source.signals)
|
|
255
|
+
target.signals.add(signal);
|
|
256
|
+
groups.splice(right, 1);
|
|
257
|
+
};
|
|
258
|
+
for (let i = 0; i < lanes.length; i++) {
|
|
259
|
+
const lane = lanes[i];
|
|
260
|
+
for (let j = 0; j < i; j++) {
|
|
261
|
+
const other = lanes[j];
|
|
262
|
+
const signals = [];
|
|
263
|
+
if (lane.affinityKey && lane.affinityKey === other.affinityKey)
|
|
264
|
+
signals.push(`affinityKey:${lane.affinityKey}`);
|
|
265
|
+
const resources = (lane.resources ?? []).map((resource) => resource.resource);
|
|
266
|
+
const otherResources = (other.resources ?? []).map((resource) => resource.resource);
|
|
267
|
+
const resourceConflict = (lane.resources ?? []).some((resource) => (other.resources ?? []).some((candidate) => candidate.resource === resource.resource && (resource.mode === 'exclusive' || candidate.mode === 'exclusive')));
|
|
268
|
+
if (resourceConflict)
|
|
269
|
+
signals.push('exclusive_resource_overlap');
|
|
270
|
+
if (overlap(resources, otherResources) > 0.5 && resources.length && otherResources.length)
|
|
271
|
+
signals.push('shared_resource_overlap');
|
|
272
|
+
if (lane.toolSetId && lane.toolSetId === other.toolSetId && lane.workspacePath && other.workspacePath && (lane.workspacePath.startsWith(other.workspacePath) || other.workspacePath.startsWith(lane.workspacePath)))
|
|
273
|
+
signals.push('toolset_workspace_prefix');
|
|
274
|
+
if (overlap(lane.inputResultRefs ?? [], other.inputResultRefs ?? []) > 0.5)
|
|
275
|
+
signals.push('input_result_overlap');
|
|
276
|
+
if (!signals.length)
|
|
277
|
+
continue;
|
|
278
|
+
let left = groups.findIndex((group) => group.keys.has(other.key));
|
|
279
|
+
let right = groups.findIndex((group) => group.keys.has(lane.key));
|
|
280
|
+
if (left < 0) {
|
|
281
|
+
groups.push({ keys: new Set([other.key]), signals: new Set() });
|
|
282
|
+
left = groups.length - 1;
|
|
283
|
+
}
|
|
284
|
+
if (right < 0) {
|
|
285
|
+
groups.push({ keys: new Set([lane.key]), signals: new Set() });
|
|
286
|
+
right = groups.length - 1;
|
|
287
|
+
}
|
|
288
|
+
const group = groups[left];
|
|
289
|
+
for (const signal of signals)
|
|
290
|
+
group.signals.add(signal);
|
|
291
|
+
if (left !== right)
|
|
292
|
+
join(left, right);
|
|
293
|
+
}
|
|
294
|
+
}
|
|
295
|
+
return groups.filter((group) => group.keys.size > 1).map((group) => ({ keys: [...group.keys].sort(), signals: [...group.signals].sort() })).sort((a, b) => a.keys[0].localeCompare(b.keys[0]));
|
|
296
|
+
}
|
|
297
|
+
function sameForkProgram(left, right) {
|
|
298
|
+
return left.program.programId === right.program.programId && left.program.programVersion === right.program.programVersion && left.program.step === right.program.step && JSON.stringify(left.program.locals ?? {}) === JSON.stringify(right.program.locals ?? {});
|
|
299
|
+
}
|
|
300
|
+
function seriesOrder(members) {
|
|
301
|
+
const byKey = new Map(members.map((lane) => [lane.key, lane]));
|
|
302
|
+
const visiting = new Set();
|
|
303
|
+
const visited = new Set();
|
|
304
|
+
const ordered = [];
|
|
305
|
+
const visit = (key) => {
|
|
306
|
+
if (visited.has(key))
|
|
307
|
+
return true;
|
|
308
|
+
if (visiting.has(key))
|
|
309
|
+
return false;
|
|
310
|
+
const lane = byKey.get(key);
|
|
311
|
+
if (!lane)
|
|
312
|
+
return false;
|
|
313
|
+
visiting.add(key);
|
|
314
|
+
for (const dependency of lane.dependsOn ?? [])
|
|
315
|
+
if ('local' in dependency.target && byKey.has(dependency.target.local) && !visit(dependency.target.local))
|
|
316
|
+
return false;
|
|
317
|
+
visiting.delete(key);
|
|
318
|
+
visited.add(key);
|
|
319
|
+
ordered.push(lane);
|
|
320
|
+
return true;
|
|
321
|
+
};
|
|
322
|
+
return members.every((lane) => visit(lane.key)) ? ordered : undefined;
|
|
323
|
+
}
|
|
324
|
+
function coalesceForkAction(action) {
|
|
325
|
+
const join = action.join;
|
|
326
|
+
if (!join || (join.mode ?? 'all') !== 'all' || join.condition !== 'settled')
|
|
327
|
+
return action;
|
|
328
|
+
const byKey = new Map(action.lanes.map((lane) => [lane.key, lane]));
|
|
329
|
+
const collapsed = new Set();
|
|
330
|
+
const replacements = new Map();
|
|
331
|
+
const output = [];
|
|
332
|
+
let groupIndex = 0;
|
|
333
|
+
for (const group of affinityGroups(action.lanes)) {
|
|
334
|
+
const members = group.keys.map((key) => byKey.get(key)).filter((lane) => lane !== undefined);
|
|
335
|
+
if (members.length !== group.keys.length || members.some((lane) => lane.series !== undefined || !sameForkProgram(lane, members[0])))
|
|
336
|
+
continue;
|
|
337
|
+
if (members.some((lane) => (lane.dependsOn ?? []).some((dependency) => !('local' in dependency.target) || !group.keys.includes(dependency.target.local))))
|
|
338
|
+
continue;
|
|
339
|
+
const contextVersions = new Set(members.map((lane) => JSON.stringify(lane.contextVersion ?? 'parent')));
|
|
340
|
+
if (contextVersions.size !== 1)
|
|
341
|
+
continue;
|
|
342
|
+
const ordered = seriesOrder(members);
|
|
343
|
+
if (!ordered)
|
|
344
|
+
continue;
|
|
345
|
+
let key = `__series_coalesce_${groupIndex++}`;
|
|
346
|
+
while (byKey.has(key) || output.some((lane) => lane.key === key))
|
|
347
|
+
key = `${key}_x`;
|
|
348
|
+
const member = ordered[0];
|
|
349
|
+
const resources = new Map();
|
|
350
|
+
for (const lane of ordered)
|
|
351
|
+
for (const resource of lane.resources ?? [])
|
|
352
|
+
resources.set(resource.resource, resources.get(resource.resource) === 'exclusive' || resource.mode === 'exclusive' ? 'exclusive' : 'shared');
|
|
353
|
+
const internalDependencies = Object.fromEntries(ordered.flatMap((lane) => {
|
|
354
|
+
const dependsOn = (lane.dependsOn ?? []).filter((dependency) => 'local' in dependency.target).map((dependency) => ({ key: dependency.target.local, condition: dependency.condition }));
|
|
355
|
+
return dependsOn.length ? [[lane.key, { dependsOn }]] : [];
|
|
356
|
+
}));
|
|
357
|
+
const inputResultRefs = [...new Set(ordered.flatMap((lane) => lane.inputResultRefs ?? []))];
|
|
358
|
+
const toolSetIds = new Set(ordered.map((lane) => lane.toolSetId).filter((value) => value !== undefined));
|
|
359
|
+
const workspacePaths = new Set(ordered.map((lane) => lane.workspacePath).filter((value) => value !== undefined));
|
|
360
|
+
output.push({
|
|
361
|
+
key,
|
|
362
|
+
goal: ordered.map((lane) => `${lane.key}: ${lane.goal}`).join('\n'),
|
|
363
|
+
program: member.program,
|
|
364
|
+
...(member.priority === undefined ? {} : { priority: Math.max(...ordered.map((lane) => lane.priority ?? member.priority)) }),
|
|
365
|
+
...(member.contextVersion === undefined ? {} : { contextVersion: member.contextVersion }),
|
|
366
|
+
...(resources.size ? { resources: [...resources].map(([resource, mode]) => ({ resource, mode })) } : {}),
|
|
367
|
+
...(inputResultRefs.length ? { inputResultRefs } : {}),
|
|
368
|
+
...(toolSetIds.size === 1 ? { toolSetId: [...toolSetIds][0] } : {}),
|
|
369
|
+
...(workspacePaths.size === 1 ? { workspacePath: [...workspacePaths][0] } : {}),
|
|
370
|
+
series: { member: member.program, keys: ordered.map((lane) => lane.key), goals: Object.fromEntries(ordered.map((lane) => [lane.key, lane.goal])), ...(Object.keys(internalDependencies).length ? { members: internalDependencies } : {}), onMemberFailure: 'continue' },
|
|
371
|
+
});
|
|
372
|
+
for (const lane of ordered) {
|
|
373
|
+
collapsed.add(lane.key);
|
|
374
|
+
replacements.set(lane.key, key);
|
|
375
|
+
}
|
|
376
|
+
}
|
|
377
|
+
if (replacements.size === 0)
|
|
378
|
+
return action;
|
|
379
|
+
const aliases = Object.entries(action.joinAliases ?? Object.fromEntries(action.lanes.map((lane) => [lane.key, lane.key]))).map(([alias, laneKey]) => [alias, replacements.get(laneKey) ?? laneKey]);
|
|
380
|
+
return { ...action, lanes: [...output, ...action.lanes.filter((lane) => !collapsed.has(lane.key))], joinAliases: Object.fromEntries(aliases), affinityAck: true };
|
|
381
|
+
}
|
|
382
|
+
function validTargetRef(value) {
|
|
383
|
+
if (!value || typeof value !== 'object' || Array.isArray(value))
|
|
384
|
+
return false;
|
|
385
|
+
const target = value;
|
|
386
|
+
if (nonEmptyString(target.local))
|
|
387
|
+
return true;
|
|
388
|
+
return (target.kind === 'lane' || target.kind === 'effect') && nonEmptyString(target.id);
|
|
389
|
+
}
|
|
390
|
+
function validResourceLocks(value) {
|
|
391
|
+
return Array.isArray(value) && value.every((lock) => Boolean(lock && typeof lock === 'object' && !Array.isArray(lock) && nonEmptyString(lock.resource) && ['shared', 'exclusive'].includes(String(lock.mode))));
|
|
392
|
+
}
|
|
393
|
+
function validateForkActionShape(action) {
|
|
394
|
+
if (!action || typeof action !== 'object' || Array.isArray(action) || !Array.isArray(action.lanes))
|
|
395
|
+
return 'INVALID_FORK';
|
|
396
|
+
const value = action;
|
|
397
|
+
const lanes = value.lanes;
|
|
398
|
+
if (lanes.some((candidate) => {
|
|
399
|
+
if (!candidate || typeof candidate !== 'object' || Array.isArray(candidate))
|
|
400
|
+
return true;
|
|
401
|
+
const child = candidate;
|
|
402
|
+
if (!nonEmptyString(child.key) || typeof child.goal !== 'string' || !validResume(child.program))
|
|
403
|
+
return true;
|
|
404
|
+
if (child.priority !== undefined && (typeof child.priority !== 'number' || !Number.isFinite(child.priority)))
|
|
405
|
+
return true;
|
|
406
|
+
if (child.contextVersion !== undefined && child.contextVersion !== 'parent' && child.contextVersion !== 'latest' && (!Number.isInteger(child.contextVersion) || child.contextVersion < 0))
|
|
407
|
+
return true;
|
|
408
|
+
if (child.affinityKey !== undefined && !nonEmptyString(child.affinityKey))
|
|
409
|
+
return true;
|
|
410
|
+
if (child.resources !== undefined && !validResourceLocks(child.resources))
|
|
411
|
+
return true;
|
|
412
|
+
if (child.inputResultRefs !== undefined && (!Array.isArray(child.inputResultRefs) || child.inputResultRefs.some((ref) => !nonEmptyString(ref))))
|
|
413
|
+
return true;
|
|
414
|
+
if (child.toolSetId !== undefined && !nonEmptyString(child.toolSetId))
|
|
415
|
+
return true;
|
|
416
|
+
if (child.workspacePath !== undefined && !nonEmptyString(child.workspacePath))
|
|
417
|
+
return true;
|
|
418
|
+
if (child.dependsOn !== undefined && (!Array.isArray(child.dependsOn) || child.dependsOn.some((dependency) => !dependency || typeof dependency !== 'object' || !nonEmptyString(dependency.key) || !validTargetRef(dependency.target) || !['success', 'settled'].includes(String(dependency.condition)))))
|
|
419
|
+
return true;
|
|
420
|
+
if (child.series !== undefined) {
|
|
421
|
+
const series = child.series;
|
|
422
|
+
if (!series || typeof series !== 'object' || Array.isArray(series) || !validResume(series.member) || !Array.isArray(series.keys) || series.keys.length === 0 || series.keys.some((key) => !nonEmptyString(key)) || new Set(series.keys).size !== series.keys.length || (series.onMemberFailure !== undefined && !['continue', 'abort'].includes(String(series.onMemberFailure))))
|
|
423
|
+
return true;
|
|
424
|
+
}
|
|
425
|
+
return false;
|
|
426
|
+
}))
|
|
427
|
+
return 'INVALID_FORK_LANE';
|
|
428
|
+
if (value.affinityAck !== undefined && typeof value.affinityAck !== 'boolean')
|
|
429
|
+
return 'INVALID_FORK';
|
|
430
|
+
if (value.joinAliases !== undefined && (!value.joinAliases || typeof value.joinAliases !== 'object' || Array.isArray(value.joinAliases) || Object.entries(value.joinAliases).some(([key, laneKey]) => !nonEmptyString(key) || !nonEmptyString(laneKey))))
|
|
431
|
+
return 'INVALID_JOIN_ALIASES';
|
|
432
|
+
if (value.join !== undefined) {
|
|
433
|
+
const join = value.join;
|
|
434
|
+
if (!join || typeof join !== 'object' || Array.isArray(join) || !['success', 'settled'].includes(String(join.condition)) || (join.mode !== undefined && !['all', 'any', 'quorum'].includes(String(join.mode))) || (join.quorum !== undefined && !Number.isInteger(join.quorum)) || (join.deadlineAt !== undefined && (typeof join.deadlineAt !== 'number' || !Number.isFinite(join.deadlineAt))) || !['fail_lane', 'resume_with_error'].includes(String(join.onUnsatisfied)) || (join.onCancelled !== undefined && !['unsatisfied', 'ignore'].includes(String(join.onCancelled))))
|
|
435
|
+
return 'INVALID_FORK_JOIN';
|
|
436
|
+
}
|
|
437
|
+
return undefined;
|
|
438
|
+
}
|
|
439
|
+
function validateSubmitWaitShape(value) {
|
|
440
|
+
if (!value || typeof value !== 'object' || Array.isArray(value))
|
|
441
|
+
return 'INVALID_WAIT_DEPENDENCY';
|
|
442
|
+
const wait = value;
|
|
443
|
+
if (!['fail_lane', 'resume_with_error'].includes(String(wait.onUnsatisfied)) || (wait.onCancelled !== undefined && !['unsatisfied', 'ignore'].includes(String(wait.onCancelled))) || (wait.reason !== undefined && !['startup', 'effect', 'dependency', 'join', 'timer'].includes(String(wait.reason))) || (wait.deadlineAt !== undefined && (typeof wait.deadlineAt !== 'number' || !Number.isFinite(wait.deadlineAt))))
|
|
444
|
+
return 'INVALID_WAIT_DEPENDENCY';
|
|
445
|
+
return undefined;
|
|
446
|
+
}
|
|
447
|
+
function validateWait(state, laneId, spec, locals, newTargets, allowDuplicateTargets = false) {
|
|
448
|
+
if (!spec || typeof spec !== 'object' || Array.isArray(spec) || !Array.isArray(spec.dependencies))
|
|
449
|
+
return 'INVALID_WAIT_DEPENDENCY';
|
|
450
|
+
if (!['all', 'any', 'quorum'].includes(spec.mode) || (spec.dependencies.length === 0 && spec.mode !== 'all') || !['fail_lane', 'resume_with_error'].includes(spec.onUnsatisfied) || (spec.onCancelled !== undefined && !['unsatisfied', 'ignore'].includes(spec.onCancelled)) || (spec.reason !== undefined && !['startup', 'effect', 'dependency', 'join', 'timer'].includes(spec.reason)) || spec.dependencies.some((dependency) => !dependency || typeof dependency !== 'object' || !nonEmptyString(dependency.key) || !validTargetRef(dependency.target) || !['success', 'settled'].includes(dependency.condition) || !resolveTarget(dependency.target, newTargets.size ? newTargets : locals)))
|
|
451
|
+
return 'INVALID_WAIT_DEPENDENCY';
|
|
452
|
+
if (spec.mode === 'quorum' && (!Number.isInteger(spec.quorum) || spec.quorum < 1 || spec.quorum > spec.dependencies.length))
|
|
453
|
+
return 'INVALID_WAIT_QUORUM';
|
|
454
|
+
if (spec.mode !== 'quorum' && spec.quorum !== undefined)
|
|
455
|
+
return 'INVALID_WAIT_QUORUM';
|
|
456
|
+
if (spec.deadlineAt !== undefined && (!Number.isFinite(spec.deadlineAt) || spec.deadlineAt < state.now))
|
|
457
|
+
return 'INVALID_WAIT_DEADLINE';
|
|
458
|
+
const keys = new Set();
|
|
459
|
+
const targets = new Set();
|
|
460
|
+
for (const dependency of spec.dependencies) {
|
|
461
|
+
if (keys.has(dependency.key))
|
|
462
|
+
return 'DUPLICATE_WAIT_KEY';
|
|
463
|
+
keys.add(dependency.key);
|
|
464
|
+
const target = resolveTarget(dependency.target, newTargets.size ? newTargets : locals);
|
|
465
|
+
const targetKey = `${target.kind}:${target.id}`;
|
|
466
|
+
if (!allowDuplicateTargets && targets.has(targetKey))
|
|
467
|
+
return 'DUPLICATE_WAIT_TARGET';
|
|
468
|
+
targets.add(targetKey);
|
|
469
|
+
const isNewTarget = [...newTargets.values()].some((candidate) => candidate.kind === target.kind && candidate.id === target.id);
|
|
470
|
+
if (target.kind === 'lane' && !state.lanes.has(target.id) && !isNewTarget)
|
|
471
|
+
return 'UNKNOWN_TARGET';
|
|
472
|
+
if (target.kind === 'effect' && !state.effects.has(target.id) && !isNewTarget)
|
|
473
|
+
return 'UNKNOWN_TARGET';
|
|
474
|
+
if (target.kind === 'lane' && target.id === laneId)
|
|
475
|
+
return 'SELF_DEPENDENCY';
|
|
476
|
+
}
|
|
477
|
+
return undefined;
|
|
478
|
+
}
|
|
479
|
+
function applyContextDelta(state, lane, delta, mutations, proposalId) {
|
|
480
|
+
const base = delta.target === 'global' ? state.agents.get(lane.agentId).latestGlobalVersion : lane.context.version;
|
|
481
|
+
if (delta.baseVersion !== base)
|
|
482
|
+
return { nextVersion: base, error: 'CONTEXT_VERSION_CONFLICT' };
|
|
483
|
+
const paths = [];
|
|
484
|
+
let history = structuredClone(lane.context.history);
|
|
485
|
+
for (const op of delta.ops) {
|
|
486
|
+
if (op.op === 'compact_history') {
|
|
487
|
+
const upToSeq = op.upToSeq;
|
|
488
|
+
const summaryResult = op.summaryRef === undefined ? undefined : state.results.get(op.summaryRef);
|
|
489
|
+
if (op.summaryRef !== undefined && !summaryResult)
|
|
490
|
+
return { nextVersion: base, error: 'UNKNOWN_SUMMARY_REF' };
|
|
491
|
+
if (op.summaryRef !== undefined && !resultVisible(lane, op.summaryRef))
|
|
492
|
+
return { nextVersion: base, error: 'RESULT_NOT_VISIBLE' };
|
|
493
|
+
const summary = op.summary ?? summaryResult?.summary ?? summaryResult?.value;
|
|
494
|
+
if (delta.target !== 'lane' || upToSeq === undefined || summary === undefined || (op.summary === undefined && op.summaryRef === undefined) || !Number.isInteger(upToSeq) || upToSeq < 1)
|
|
495
|
+
return { nextVersion: base, error: 'INVALID_HISTORY_COMPACTION' };
|
|
496
|
+
if (!history.some((record) => record.seq <= upToSeq))
|
|
497
|
+
return { nextVersion: base, error: 'INVALID_HISTORY_COMPACTION' };
|
|
498
|
+
if (history.length > 0 && upToSeq > Math.max(...history.map((record) => record.seq)))
|
|
499
|
+
return { nextVersion: base, error: 'INVALID_HISTORY_COMPACTION' };
|
|
500
|
+
const compacted = history.filter((record) => record.seq <= upToSeq);
|
|
501
|
+
const summaryTaints = mergePrivacyTaints(...compacted.map((record) => record.privacyTaints), summaryResult?.privacyTaints);
|
|
502
|
+
const summaryPrivacy = effectivePrivacy(strictestPrivacy([...(compacted.map((record) => record.privacy)), delta.privacy ?? 'public', summaryResult?.privacy ?? 'public']), summaryTaints);
|
|
503
|
+
history = [{ seq: upToSeq, instruction: '[history compacted]', resultRefs: op.summaryRef === undefined ? [] : [op.summaryRef], output: clone(summary), privacy: summaryPrivacy, ...(summaryTaints.length ? { privacyTaints: summaryTaints } : {}) }, ...history.filter((record) => record.seq > upToSeq)];
|
|
504
|
+
continue;
|
|
505
|
+
}
|
|
506
|
+
if (!op.path || op.path.length === 0 || hasUnsafePathSegment(op.path))
|
|
507
|
+
return { nextVersion: base, error: 'INVALID_CONTEXT_PATH' };
|
|
508
|
+
if (op.path?.[0] === 'history')
|
|
509
|
+
return { nextVersion: base, error: 'HISTORY_IS_APPEND_ONLY' };
|
|
510
|
+
for (const existing of paths) {
|
|
511
|
+
if (op.path && (existing.every((value, index) => op.path?.[index] === value) || op.path.every((value, index) => existing[index] === value)))
|
|
512
|
+
return { nextVersion: base, error: 'CONTEXT_PATH_CONFLICT' };
|
|
513
|
+
}
|
|
514
|
+
if (op.path)
|
|
515
|
+
paths.push(op.path);
|
|
516
|
+
}
|
|
517
|
+
const source = delta.target === 'global' ? state.agents.get(lane.agentId).globalVersions.get(base) : lane.context.state;
|
|
518
|
+
const result = clone(source ?? {});
|
|
519
|
+
for (const op of delta.ops) {
|
|
520
|
+
if (op.op === 'compact_history')
|
|
521
|
+
continue;
|
|
522
|
+
const path = op.path;
|
|
523
|
+
let cursor = result;
|
|
524
|
+
for (const part of path.slice(0, -1)) {
|
|
525
|
+
const child = ownChild(cursor, part);
|
|
526
|
+
if (!child || typeof child !== 'object' || Array.isArray(child))
|
|
527
|
+
cursor[part] = {};
|
|
528
|
+
cursor = cursor[part];
|
|
529
|
+
}
|
|
530
|
+
const key = path[path.length - 1];
|
|
531
|
+
if (op.op === 'set')
|
|
532
|
+
cursor[key] = clone(op.value ?? null);
|
|
533
|
+
else if (op.op === 'remove')
|
|
534
|
+
delete cursor[key];
|
|
535
|
+
else if (op.op === 'append') {
|
|
536
|
+
const existing = ownChild(cursor, key);
|
|
537
|
+
if (!Array.isArray(existing))
|
|
538
|
+
return { nextVersion: base, error: 'APPEND_TARGET_NOT_ARRAY' };
|
|
539
|
+
existing.push(clone(op.value ?? null));
|
|
540
|
+
}
|
|
541
|
+
}
|
|
542
|
+
const nextVersion = base + 1;
|
|
543
|
+
const baseMetadata = delta.target === 'global' ? state.agents.get(lane.agentId).globalPrivacy?.get(base) ?? { privacy: 'public' } : { privacy: lane.context.privacy ?? 'public', privacyTaints: lane.context.privacyTaints };
|
|
544
|
+
const derived = derivedPrivacy(state, lane, delta.derivedFrom ?? []);
|
|
545
|
+
const propagatedTaints = mergePrivacyTaints(baseMetadata.privacyTaints, derived.privacyTaints, delta.privacyTaints);
|
|
546
|
+
const metadata = {
|
|
547
|
+
privacy: strictestPrivacy([baseMetadata.privacy, delta.privacy ?? 'public', privacyTaintPrivacy(delta.privacyTaints), derived.privacy ?? 'public']),
|
|
548
|
+
...(propagatedTaints.length ? { privacyTaints: propagatedTaints } : {}),
|
|
549
|
+
};
|
|
550
|
+
if (delta.target === 'global' && delta.proposal)
|
|
551
|
+
mutations.push({ op: 'insertMergeProposal', proposal: { id: proposalId ?? `proposal-${state.nextIds.proposal}`, agentId: lane.agentId, sourceLaneId: lane.id, baseGlobalVersion: base, delta: { ...clone(delta), sourceLaneId: lane.id }, createdAt: state.now } });
|
|
552
|
+
else if (delta.target === 'global')
|
|
553
|
+
mutations.push({ op: 'setGlobal', agentId: lane.agentId, version: nextVersion, value: result, metadata });
|
|
554
|
+
else
|
|
555
|
+
mutations.push({ op: 'setLaneContext', laneId: lane.id, version: nextVersion, value: result, metadata, ...(history.length === lane.context.history.length && history.every((record, index) => JSON.stringify(record) === JSON.stringify(lane.context.history[index])) ? {} : { history }) });
|
|
556
|
+
return { nextVersion, metadata, ...(delta.target === 'lane' ? { history } : {}) };
|
|
557
|
+
}
|
|
558
|
+
function addWait(state, lane, spec, targets, mutations, nextId) {
|
|
559
|
+
const dependencies = spec.dependencies.map((dependency) => ({ ...dependency, target: targets.get(dependency.key) }));
|
|
560
|
+
const wait = { id: nextId, laneId: lane.id, spec: { ...spec, dependencies }, state: 'pending' };
|
|
561
|
+
mutations.push({ op: 'insertWait', record: wait });
|
|
562
|
+
lane.activeWaitId = nextId;
|
|
563
|
+
lane.status = 'waiting';
|
|
564
|
+
}
|
|
565
|
+
function resultVisible(lane, ref) { return lane.visibleResultRefs === undefined || lane.visibleResultRefs.has(ref); }
|
|
566
|
+
function derivedPrivacy(state, lane, refs) {
|
|
567
|
+
const labels = [];
|
|
568
|
+
const privacyTaints = [];
|
|
569
|
+
const seenTaints = new Set();
|
|
570
|
+
for (const ref of refs) {
|
|
571
|
+
const id = provenanceRefId(ref);
|
|
572
|
+
const result = typeof ref === 'string' || ref.kind === 'result' ? state.results.get(id) : undefined;
|
|
573
|
+
if (result) {
|
|
574
|
+
if (!resultVisible(lane, id))
|
|
575
|
+
return { error: 'RESULT_NOT_VISIBLE' };
|
|
576
|
+
}
|
|
577
|
+
else if (!privacyMetadataForDerivedRef(state, lane, ref))
|
|
578
|
+
return { error: 'UNKNOWN_RESULT_REF' };
|
|
579
|
+
const metadata = privacyMetadataForDerivedRef(state, lane, ref);
|
|
580
|
+
labels.push(effectivePrivacy(metadata.privacy, metadata.privacyTaints));
|
|
581
|
+
for (const taint of metadata.privacyTaints ?? []) {
|
|
582
|
+
const value = { path: [id, ...taint.path], privacy: taint.privacy };
|
|
583
|
+
const key = JSON.stringify(value);
|
|
584
|
+
if (!seenTaints.has(key)) {
|
|
585
|
+
seenTaints.add(key);
|
|
586
|
+
privacyTaints.push(value);
|
|
587
|
+
}
|
|
588
|
+
}
|
|
589
|
+
}
|
|
590
|
+
return { privacy: strictestPrivacy(labels), ...(privacyTaints.length ? { privacyTaints } : {}) };
|
|
591
|
+
}
|
|
592
|
+
function prepareLLMInput(state, lane, submission) {
|
|
593
|
+
if (submission.kind !== 'llm' || !submission.input || typeof submission.input !== 'object' || Array.isArray(submission.input))
|
|
594
|
+
return { input: submission.input };
|
|
595
|
+
const input = submission.input;
|
|
596
|
+
if (input.request !== undefined)
|
|
597
|
+
return { input: submission.input };
|
|
598
|
+
if (typeof input.task !== 'string')
|
|
599
|
+
return { input: submission.input };
|
|
600
|
+
const instruction = typeof input.instruction === 'string' ? input.instruction : input.task;
|
|
601
|
+
if (input.inputs !== undefined && (!input.inputs || typeof input.inputs !== 'object' || Array.isArray(input.inputs)))
|
|
602
|
+
return { error: 'INVALID_LLM_INPUTS' };
|
|
603
|
+
const rawInputs = (input.inputs ?? {});
|
|
604
|
+
const readRefs = (key) => {
|
|
605
|
+
const value = rawInputs[key];
|
|
606
|
+
if (value === undefined)
|
|
607
|
+
return { refs: [] };
|
|
608
|
+
if (!Array.isArray(value) || value.some((ref) => typeof ref !== 'string' || ref.length === 0))
|
|
609
|
+
return { error: `INVALID_LLM_INPUT_REFS:${key}` };
|
|
610
|
+
return { refs: value };
|
|
611
|
+
};
|
|
612
|
+
const resultInputs = readRefs('results');
|
|
613
|
+
const findingInputs = readRefs('findings');
|
|
614
|
+
const rejectedInputs = readRefs('rejectedOutputRefs');
|
|
615
|
+
const artifactInputs = readRefs('artifacts');
|
|
616
|
+
const eventInputs = readRefs('events');
|
|
617
|
+
const inputError = resultInputs.error ?? findingInputs.error ?? rejectedInputs.error ?? artifactInputs.error ?? eventInputs.error;
|
|
618
|
+
if (inputError)
|
|
619
|
+
return { error: inputError };
|
|
620
|
+
const resultRefs = [...new Set([...(resultInputs.refs ?? []), ...(findingInputs.refs ?? []), ...(rejectedInputs.refs ?? [])])];
|
|
621
|
+
const artifactRefs = [...new Set(artifactInputs.refs ?? [])];
|
|
622
|
+
try {
|
|
623
|
+
const agent = state.agents.get(lane.agentId);
|
|
624
|
+
if (!agent)
|
|
625
|
+
return { error: 'UNKNOWN_AGENT' };
|
|
626
|
+
const projection = new ContextBuilder(state).build({ agent, lane, resultRefs, ...(artifactRefs.length ? { artifactRefs } : {}), eventIds: eventInputs.refs ?? [], instruction, ...(typeof input.system === 'string' ? { system: input.system } : {}), ...(input.policy === undefined ? {} : { policy: input.policy }), ...(input.tools === undefined ? {} : { tools: input.tools }), toolSetId: typeof input.toolSetId === 'string' ? input.toolSetId : 'default' });
|
|
627
|
+
return { input: { ...input, request: projection } };
|
|
628
|
+
}
|
|
629
|
+
catch (cause) {
|
|
630
|
+
return { error: cause instanceof Error ? cause.message : 'INVALID_LLM_CONTEXT' };
|
|
631
|
+
}
|
|
632
|
+
}
|
|
633
|
+
export function validateStep(state, laneId, output) {
|
|
634
|
+
const lane = state.lanes.get(laneId);
|
|
635
|
+
if (!lane)
|
|
636
|
+
return { rejection: error('UNKNOWN_LANE', `Lane ${laneId} does not exist`) };
|
|
637
|
+
if (!output || typeof output !== 'object' || !Array.isArray(output.actions))
|
|
638
|
+
return { rejection: error('INVALID_STEP_OUTPUT', 'Step output must contain an actions array') };
|
|
639
|
+
if (!validResume(output.next))
|
|
640
|
+
return { rejection: error('INVALID_RESUME_POINT', 'next must identify a registered program step') };
|
|
641
|
+
if (lane.status !== 'ready' && lane.status !== 'running')
|
|
642
|
+
return { rejection: error('LANE_NOT_RUNNABLE', `Lane is ${lane.status}`) };
|
|
643
|
+
const actions = output.actions;
|
|
644
|
+
const actionTypes = new Set(['submit_effects', 'fork', 'wait', 'cancel_lane', 'propose_cancel', 'adopt_context', 'downgrade_privacy', 'complete', 'fail']);
|
|
645
|
+
if (actions.some((action) => !action || typeof action !== 'object' || Array.isArray(action) || !actionTypes.has(String(action.type))))
|
|
646
|
+
return { rejection: error('INVALID_ACTION', 'Step output contains an unknown RuntimeAction') };
|
|
647
|
+
for (const action of actions) {
|
|
648
|
+
const actionShapeError = validateControlActionShape(action);
|
|
649
|
+
if (actionShapeError)
|
|
650
|
+
return { rejection: error(actionShapeError, 'RuntimeAction shape is invalid') };
|
|
651
|
+
}
|
|
652
|
+
if (output.adoptCommittedContext !== undefined && typeof output.adoptCommittedContext !== 'boolean')
|
|
653
|
+
return { rejection: error('INVALID_ADOPT_COMMITTED_CONTEXT', 'adoptCommittedContext must be a boolean') };
|
|
654
|
+
const waitSources = actions.filter((action) => action.type === 'wait' || (action.type === 'submit_effects' && Boolean(action.wait)) || (action.type === 'fork' && Boolean(action.join))).length;
|
|
655
|
+
if (waitSources > 1)
|
|
656
|
+
return { rejection: error('MULTIPLE_WAIT_SOURCES', 'a StepTransaction may have only one Wait source') };
|
|
657
|
+
const terminal = actions.filter((action) => action.type === 'complete' || action.type === 'fail');
|
|
658
|
+
if (terminal.length > 1)
|
|
659
|
+
return { rejection: error('MULTIPLE_TERMINAL_ACTIONS', 'a step may have only one terminal action') };
|
|
660
|
+
if (terminal.length && actions.length !== 1)
|
|
661
|
+
return { rejection: error('AMBIGUOUS_TERMINAL_ACTION', 'terminal actions cannot be combined with other actions in MVP') };
|
|
662
|
+
const mutations = [];
|
|
663
|
+
const workingLane = laneCopy(lane);
|
|
664
|
+
const localTargets = new Map();
|
|
665
|
+
const forkTargets = new Map();
|
|
666
|
+
const seenEffectKeys = new Set();
|
|
667
|
+
let effectCounter = state.nextIds.effect + state.effects.size;
|
|
668
|
+
let laneCounter = state.nextIds.lane + state.lanes.size;
|
|
669
|
+
let waitCounter = state.nextIds.wait + state.waits.size;
|
|
670
|
+
let resultCounter = state.nextIds.result + state.results.size;
|
|
671
|
+
let proposalCounter = state.nextIds.proposal + state.mergeProposals.size;
|
|
672
|
+
let queuedEffectCount = [...state.effects.values()].filter((effect) => effect.state === 'queued' && effect.concurrencyClass !== 'none').length;
|
|
673
|
+
const existingToolCallIds = new Set([...state.effects.values()].filter((effect) => effect.agentId === lane.agentId && effect.toolCallId !== undefined).map((effect) => effect.toolCallId).concat([...state.toolCallCorrelations.keys()]));
|
|
674
|
+
const seenToolCallIds = new Set();
|
|
675
|
+
const seenCancelTargets = new Set();
|
|
676
|
+
const cancelledSubtreeLanes = new Set();
|
|
677
|
+
if (output.contextDelta !== undefined) {
|
|
678
|
+
const deltaShapeError = validateContextDeltaShape(output.contextDelta);
|
|
679
|
+
if (deltaShapeError)
|
|
680
|
+
return { rejection: error(deltaShapeError, 'ContextDelta shape is invalid') };
|
|
681
|
+
const deltaPrivacyTaintError = validatePrivacyTaints(output.contextDelta.privacyTaints);
|
|
682
|
+
if (deltaPrivacyTaintError)
|
|
683
|
+
return { rejection: error(deltaPrivacyTaintError, 'ContextDelta privacy taints are invalid') };
|
|
684
|
+
const deltaDerived = derivedPrivacy(state, lane, output.contextDelta.derivedFrom ?? []);
|
|
685
|
+
if (deltaDerived.error)
|
|
686
|
+
return { rejection: error(deltaDerived.error, 'ContextDelta provenance references an unknown or invisible result') };
|
|
687
|
+
if (output.contextDelta.privacy !== undefined && deltaDerived.privacy !== undefined && privacyRank(output.contextDelta.privacy) < privacyRank(deltaDerived.privacy))
|
|
688
|
+
return { rejection: error('PRIVACY_DOWNGRADE_WITHOUT_PROOF', 'ContextDelta privacy cannot be broader than its sources') };
|
|
689
|
+
if (output.contextDelta.target === 'global' && !output.contextDelta.proposal && lane.ownerLaneId !== undefined)
|
|
690
|
+
return { rejection: error('GLOBAL_CONTEXT_WRITE_NOT_AUTHORIZED', 'Only the root Lane may commit Global Context directly.') };
|
|
691
|
+
if (output.contextDelta.target !== 'global' && output.contextDelta.proposal)
|
|
692
|
+
return { rejection: error('INVALID_MERGE_PROPOSAL', 'Only Global Context deltas may be proposals.') };
|
|
693
|
+
const applied = applyContextDelta(state, workingLane, output.contextDelta, mutations, output.contextDelta.proposal ? `proposal-${proposalCounter++}` : undefined);
|
|
694
|
+
if (applied.error)
|
|
695
|
+
return { rejection: error(applied.error, 'ContextDelta rejected') };
|
|
696
|
+
if (output.contextDelta.target === 'lane') {
|
|
697
|
+
const nextValue = mutations[mutations.length - 1];
|
|
698
|
+
if (nextValue?.op === 'setLaneContext')
|
|
699
|
+
workingLane.context = { ...workingLane.context, state: nextValue.value, version: applied.nextVersion, ...(nextValue.history === undefined ? {} : { history: structuredClone(nextValue.history) }), ...(nextValue.metadata === undefined ? {} : structuredClone(nextValue.metadata)) };
|
|
700
|
+
}
|
|
701
|
+
if (output.adoptCommittedContext && output.contextDelta.target !== 'global')
|
|
702
|
+
return { rejection: error('INVALID_ADOPT_COMMITTED_CONTEXT', 'adoptCommittedContext requires a global ContextDelta') };
|
|
703
|
+
if (output.contextDelta.proposal && output.adoptCommittedContext)
|
|
704
|
+
return { rejection: error('INVALID_ADOPT_COMMITTED_CONTEXT', 'proposals cannot be adopted in the same transaction') };
|
|
705
|
+
if (output.contextDelta.target === 'global' && output.adoptCommittedContext)
|
|
706
|
+
workingLane.contextSnapshotVersion = applied.nextVersion;
|
|
707
|
+
}
|
|
708
|
+
else if (output.adoptCommittedContext)
|
|
709
|
+
return { rejection: error('INVALID_ADOPT_COMMITTED_CONTEXT', 'adoptCommittedContext requires a ContextDelta') };
|
|
710
|
+
const compactRequested = output.contextDelta?.target === 'lane' && output.contextDelta.ops.some((op) => op.op === 'compact_history');
|
|
711
|
+
const nextHistoryTokens = estimateHistoryTokens(workingLane.context.history);
|
|
712
|
+
if (nextHistoryTokens > state.historyHardTokens && !compactRequested)
|
|
713
|
+
return { rejection: error('CONTEXT_TOO_LARGE', 'Lane history exceeded hardTokens and must be compacted before another Step can commit.', { historyTokens: nextHistoryTokens, softTokens: state.historySoftTokens, hardTokens: state.historyHardTokens }) };
|
|
714
|
+
const pressure = historyPressure(workingLane.context.history, state.historySoftTokens, state.historyHardTokens);
|
|
715
|
+
if (pressure)
|
|
716
|
+
workingLane.historyPressure = pressure;
|
|
717
|
+
else
|
|
718
|
+
delete workingLane.historyPressure;
|
|
719
|
+
for (const action of actions) {
|
|
720
|
+
if (action.type === 'submit_effects') {
|
|
721
|
+
if (!Array.isArray(action.effects))
|
|
722
|
+
return { rejection: error('INVALID_EFFECT_BATCH', 'submit_effects.effects must be an array') };
|
|
723
|
+
if (action.effects.length === 0)
|
|
724
|
+
return { rejection: error('EMPTY_EFFECT_BATCH', 'submit_effects requires at least one effect') };
|
|
725
|
+
if (action.wait !== undefined) {
|
|
726
|
+
const waitShapeError = validateSubmitWaitShape(action.wait);
|
|
727
|
+
if (waitShapeError)
|
|
728
|
+
return { rejection: error(waitShapeError, 'Wait rejected') };
|
|
729
|
+
}
|
|
730
|
+
for (const submission of action.effects) {
|
|
731
|
+
const submissionError = validateEffectSubmission(submission);
|
|
732
|
+
if (submissionError)
|
|
733
|
+
return { rejection: error(submissionError, 'Effect submission rejected') };
|
|
734
|
+
}
|
|
735
|
+
const newQueued = action.effects.filter((submission) => submission.concurrencyClass !== 'none').length;
|
|
736
|
+
if (queuedEffectCount + newQueued > state.maxQueuedEffects)
|
|
737
|
+
return { rejection: error('EFFECT_QUEUE_FULL', 'effect queue capacity would be exceeded') };
|
|
738
|
+
queuedEffectCount += newQueued;
|
|
739
|
+
const batchTargets = new Map();
|
|
740
|
+
for (const submission of action.effects) {
|
|
741
|
+
if (seenEffectKeys.has(submission.key))
|
|
742
|
+
return { rejection: error('DUPLICATE_EFFECT_KEY', submission.key) };
|
|
743
|
+
if (submission.locks && new Set(submission.locks.map((lock) => lock.resource)).size !== submission.locks.length)
|
|
744
|
+
return { rejection: error('DUPLICATE_EFFECT_LOCK', submission.key) };
|
|
745
|
+
const submissionDerived = derivedPrivacy(state, lane, submission.derivedFrom ?? []);
|
|
746
|
+
if (submissionDerived.error)
|
|
747
|
+
return { rejection: error(submissionDerived.error, submission.key) };
|
|
748
|
+
if (submission.toolCallId !== undefined && (existingToolCallIds.has(submission.toolCallId) || seenToolCallIds.has(submission.toolCallId)))
|
|
749
|
+
return { rejection: error('DUPLICATE_TOOL_CALL_ID', submission.toolCallId) };
|
|
750
|
+
const prepared = prepareLLMInput(state, lane, submission);
|
|
751
|
+
if (prepared.error)
|
|
752
|
+
return { rejection: error(prepared.error, submission.key) };
|
|
753
|
+
seenEffectKeys.add(submission.key);
|
|
754
|
+
if (submission.toolCallId !== undefined)
|
|
755
|
+
seenToolCallIds.add(submission.toolCallId);
|
|
756
|
+
const id = `effect-${effectCounter++}`;
|
|
757
|
+
const target = { kind: 'effect', id };
|
|
758
|
+
batchTargets.set(submission.key, target);
|
|
759
|
+
localTargets.set(submission.key, target);
|
|
760
|
+
const effect = { id, agentId: lane.agentId, ownerLaneId: lane.id, key: submission.key, kind: submission.kind, concurrencyClass: submission.concurrencyClass, input: clone(prepared.input ?? submission.input), ...(submission.derivedFrom === undefined ? {} : { derivedFrom: [...submission.derivedFrom] }), state: 'queued', attemptId: `${id}-attempt-1`, attemptNo: 1, executionState: 'local', sideEffectState: 'none', ...(submission.kind === 'llm' ? { preparation: { state: 'idle', generation: 0 } } : {}), ...(submission.priority === undefined ? {} : { schedulePriority: submission.priority }), ...(submission.deadlineAt === undefined ? {} : { deadlineAt: submission.deadlineAt }), ...(submission.cancelGraceMs === undefined ? {} : { cancelGraceMs: submission.cancelGraceMs }), ...(submission.attemptTimeoutMs === undefined ? {} : { attemptTimeoutMs: submission.attemptTimeoutMs }), ...(submission.idempotencyKey === undefined ? {} : { idempotencyKey: submission.idempotencyKey }), ...(submission.sideEffectPolicy === undefined ? {} : { sideEffectPolicy: submission.sideEffectPolicy }), ...(submission.toolVersion === undefined ? {} : { toolVersion: submission.toolVersion }), ...(submission.retryPolicy === undefined ? {} : { retryPolicy: clone(submission.retryPolicy) }), ...(submission.duplicateExecutionPolicy === undefined ? {} : { duplicateExecutionPolicy: submission.duplicateExecutionPolicy }), ...(submission.maxUnknownAttempts === undefined ? {} : { maxUnknownAttempts: submission.maxUnknownAttempts }), ...(submission.toolCallId === undefined ? {} : { toolCallId: submission.toolCallId }), ...(submission.llmEffectId === undefined ? {} : { llmEffectId: submission.llmEffectId }), ...(submission.locks === undefined ? {} : { locks: clone(submission.locks) }) };
|
|
761
|
+
mutations.push({ op: 'insertEffect', record: effect });
|
|
762
|
+
if (submission.toolCallId !== undefined)
|
|
763
|
+
mutations.push({ op: 'setToolCallCorrelation', record: { toolCallId: submission.toolCallId, llmEffectId: submission.llmEffectId ?? 'unknown', toolEffectId: id } });
|
|
764
|
+
workingLane.ownedEffectIds.add(id);
|
|
765
|
+
}
|
|
766
|
+
if (action.wait) {
|
|
767
|
+
const spec = { dependencies: action.effects.map((submission) => ({ key: submission.key, target: batchTargets.get(submission.key), condition: 'settled' })), mode: 'all', ...(action.wait.deadlineAt === undefined ? {} : { deadlineAt: action.wait.deadlineAt }), onUnsatisfied: action.wait.onUnsatisfied, ...(action.wait.onCancelled ? { onCancelled: action.wait.onCancelled } : {}), reason: action.wait.reason ?? 'effect' };
|
|
768
|
+
const waitError = validateWait(state, lane.id, spec, new Map(), batchTargets);
|
|
769
|
+
if (waitError)
|
|
770
|
+
return { rejection: error(waitError, 'Wait rejected') };
|
|
771
|
+
if (hasDependencyCycle(state, spec.dependencies.map((dependency) => ({ from: { kind: 'lane', id: lane.id }, to: dependency.target }))))
|
|
772
|
+
return { rejection: error('DEPENDENCY_CYCLE', 'Wait would create a dependency cycle') };
|
|
773
|
+
addWait(state, workingLane, spec, batchTargets, mutations, `wait-${waitCounter++}`);
|
|
774
|
+
}
|
|
775
|
+
}
|
|
776
|
+
else if (action.type === 'fork') {
|
|
777
|
+
const forkShapeError = validateForkActionShape(action);
|
|
778
|
+
if (forkShapeError)
|
|
779
|
+
return { rejection: error(forkShapeError, 'Fork rejected') };
|
|
780
|
+
const forkAction = state.forkAffinity === 'coalesce' ? coalesceForkAction(action) : action;
|
|
781
|
+
if (forkAction.lanes.length === 0)
|
|
782
|
+
return { rejection: error('EMPTY_FORK', 'fork requires at least one lane') };
|
|
783
|
+
if (state.forkAffinity === 'advise' && forkAction.affinityAck !== true) {
|
|
784
|
+
const groups = affinityGroups(forkAction.lanes);
|
|
785
|
+
if (groups.length)
|
|
786
|
+
return { rejection: error('FORK_AFFINITY_COLLAPSIBLE', 'Fork contains lanes that share a likely context affinity group.', { groups }) };
|
|
787
|
+
}
|
|
788
|
+
const agent = state.agents.get(lane.agentId);
|
|
789
|
+
const activeAgentLanes = [...state.lanes.values()].filter((candidate) => candidate.agentId === lane.agentId && !['succeeded', 'failed', 'cancelled'].includes(candidate.status)).length;
|
|
790
|
+
if (agent && activeAgentLanes + forkAction.lanes.length > agent.maxActiveLanes)
|
|
791
|
+
return { rejection: error('AGENT_LANE_LIMIT_EXCEEDED', 'agent active lane limit exceeded') };
|
|
792
|
+
const siblingTargets = new Map();
|
|
793
|
+
for (const child of forkAction.lanes) {
|
|
794
|
+
if (forkTargets.has(child.key))
|
|
795
|
+
return { rejection: error('DUPLICATE_FORK_KEY', child.key) };
|
|
796
|
+
const childId = `lane-${laneCounter++}`;
|
|
797
|
+
const target = { kind: 'lane', id: childId };
|
|
798
|
+
forkTargets.set(child.key, target);
|
|
799
|
+
siblingTargets.set(child.key, target);
|
|
800
|
+
}
|
|
801
|
+
if (state.lanes.size + forkAction.lanes.length > state.maxTotalLanes)
|
|
802
|
+
return { rejection: error('LANE_LIMIT_EXCEEDED', 'runtime lane limit exceeded') };
|
|
803
|
+
for (const child of forkAction.lanes) {
|
|
804
|
+
const target = siblingTargets.get(child.key);
|
|
805
|
+
if (child.inputResultRefs?.some((ref) => !state.results.has(ref)))
|
|
806
|
+
return { rejection: error('UNKNOWN_RESULT_REF', `fork input for ${child.key}`) };
|
|
807
|
+
if (child.series && (!child.series.keys.length || new Set(child.series.keys).size !== child.series.keys.length || !validResume(child.series.member)))
|
|
808
|
+
return { rejection: error('INVALID_SERIES_LANE', `series for ${child.key}`) };
|
|
809
|
+
const contextVersion = child.contextVersion === 'latest' ? state.agents.get(lane.agentId).latestGlobalVersion : child.contextVersion === 'parent' || child.contextVersion === undefined ? lane.contextSnapshotVersion : child.contextVersion;
|
|
810
|
+
if (!state.agents.get(lane.agentId).globalVersions.has(contextVersion))
|
|
811
|
+
return { rejection: error('UNKNOWN_CONTEXT_VERSION', String(contextVersion)) };
|
|
812
|
+
const dependencies = (child.dependsOn ?? []).map((dependency) => ({ ...dependency, target: resolveTarget(dependency.target, siblingTargets) ?? resolveTarget(dependency.target, localTargets) }));
|
|
813
|
+
if (dependencies.some((dependency) => !dependency.target))
|
|
814
|
+
return { rejection: error('UNKNOWN_TARGET', `fork dependency for ${child.key}`) };
|
|
815
|
+
const record = { id: target.id, agentId: lane.agentId, ownerLaneId: lane.id, status: dependencies.length ? 'waiting' : 'ready', version: 0, goal: child.goal, resume: clone(child.program), ...(child.series === undefined ? {} : { series: clone(child.series) }), contextSnapshotVersion: contextVersion, context: { version: 0, history: [], state: {} }, visibleResultRefs: new Set(child.inputResultRefs ?? []), children: new Set(), priority: child.priority ?? lane.priority, enqueueSeq: state.nextIds.event + laneCounter, readySince: state.now, ownedEffectIds: new Set() };
|
|
816
|
+
if (dependencies.length) {
|
|
817
|
+
const startupWait = { dependencies: dependencies, mode: 'all', onUnsatisfied: 'fail_lane', reason: 'startup' };
|
|
818
|
+
const availableTargets = new Map([...siblingTargets, ...localTargets]);
|
|
819
|
+
const startupWaitError = validateWait(state, record.id, startupWait, new Map(), availableTargets);
|
|
820
|
+
if (startupWaitError)
|
|
821
|
+
return { rejection: error(startupWaitError, `fork dependency for ${child.key}`) };
|
|
822
|
+
}
|
|
823
|
+
mutations.push({ op: 'insertLane', record });
|
|
824
|
+
workingLane.children.add(record.id);
|
|
825
|
+
if (!dependencies.length)
|
|
826
|
+
mutations.push({ op: 'appendEvent', event: { type: 'lane.ready', laneId: record.id } });
|
|
827
|
+
else {
|
|
828
|
+
const waitSpec = { dependencies: dependencies, mode: 'all', onUnsatisfied: 'fail_lane', reason: 'startup' };
|
|
829
|
+
const resolved = new Map(dependencies.map((dependency) => [dependency.key, dependency.target]));
|
|
830
|
+
addWait(state, record, waitSpec, resolved, mutations, `wait-${waitCounter++}`);
|
|
831
|
+
}
|
|
832
|
+
}
|
|
833
|
+
if (!forkAction.join) {
|
|
834
|
+
const forkEdges = forkAction.lanes.flatMap((child) => (child.dependsOn ?? []).map((dependency) => ({ from: siblingTargets.get(child.key), to: resolveTarget(dependency.target, siblingTargets) ?? resolveTarget(dependency.target, localTargets) })));
|
|
835
|
+
if (forkEdges.some((edge) => !edge.to) || hasDependencyCycle(state, forkEdges))
|
|
836
|
+
return { rejection: error('DEPENDENCY_CYCLE', 'Fork dependencies would create a cycle') };
|
|
837
|
+
}
|
|
838
|
+
if (forkAction.join) {
|
|
839
|
+
const aliases = forkAction.joinAliases === undefined ? forkAction.lanes.map((child) => [child.key, child.key]) : Object.entries(forkAction.joinAliases);
|
|
840
|
+
if (new Set(aliases.map(([key]) => key)).size !== aliases.length || aliases.some(([, laneKey]) => !siblingTargets.has(laneKey)))
|
|
841
|
+
return { rejection: error('INVALID_JOIN_ALIASES', 'join aliases must point to unique original keys and existing fork lanes') };
|
|
842
|
+
const deps = aliases.map(([key, laneKey]) => ({ key, target: siblingTargets.get(laneKey), condition: forkAction.join.condition }));
|
|
843
|
+
const joinMode = forkAction.join.mode ?? 'all';
|
|
844
|
+
const spec = { dependencies: deps, mode: joinMode, ...(forkAction.join.quorum === undefined ? {} : { quorum: forkAction.join.quorum }), ...(forkAction.join.deadlineAt === undefined ? {} : { deadlineAt: forkAction.join.deadlineAt }), onUnsatisfied: forkAction.join.onUnsatisfied, ...(forkAction.join.onCancelled ? { onCancelled: forkAction.join.onCancelled } : {}), reason: 'join' };
|
|
845
|
+
const forkEdges = forkAction.lanes.flatMap((child) => (child.dependsOn ?? []).map((dependency) => ({ from: siblingTargets.get(child.key), to: resolveTarget(dependency.target, siblingTargets) ?? resolveTarget(dependency.target, localTargets) })));
|
|
846
|
+
forkEdges.push(...deps.map((dependency) => ({ from: { kind: 'lane', id: lane.id }, to: dependency.target })));
|
|
847
|
+
if (forkEdges.some((edge) => !edge.to) || hasDependencyCycle(state, forkEdges))
|
|
848
|
+
return { rejection: error('DEPENDENCY_CYCLE', 'Fork dependencies would create a cycle') };
|
|
849
|
+
const joinTargets = new Map(deps.map((dependency) => [dependency.key, dependency.target]));
|
|
850
|
+
const waitError = validateWait(state, lane.id, spec, new Map(), joinTargets, true);
|
|
851
|
+
if (waitError)
|
|
852
|
+
return { rejection: error(waitError, 'Join rejected') };
|
|
853
|
+
addWait(state, workingLane, spec, joinTargets, mutations, `wait-${waitCounter++}`);
|
|
854
|
+
}
|
|
855
|
+
}
|
|
856
|
+
else if (action.type === 'wait') {
|
|
857
|
+
const waitError = validateWait(state, lane.id, action.spec, localTargets, new Map());
|
|
858
|
+
if (waitError)
|
|
859
|
+
return { rejection: error(waitError, 'Wait rejected') };
|
|
860
|
+
const targets = new Map();
|
|
861
|
+
for (const dependency of action.spec.dependencies) {
|
|
862
|
+
const target = resolveTarget(dependency.target, localTargets);
|
|
863
|
+
if (target)
|
|
864
|
+
targets.set(dependency.key, target);
|
|
865
|
+
}
|
|
866
|
+
if (hasDependencyCycle(state, action.spec.dependencies.map((dependency) => ({ from: { kind: 'lane', id: lane.id }, to: resolveTarget(dependency.target, localTargets) }))))
|
|
867
|
+
return { rejection: error('DEPENDENCY_CYCLE', 'Wait would create a dependency cycle') };
|
|
868
|
+
addWait(state, workingLane, action.spec, targets, mutations, `wait-${waitCounter++}`);
|
|
869
|
+
}
|
|
870
|
+
else if (action.type === 'cancel_lane') {
|
|
871
|
+
if (seenCancelTargets.has(action.laneId))
|
|
872
|
+
return { rejection: error('DUPLICATE_CANCEL_TARGET', action.laneId) };
|
|
873
|
+
seenCancelTargets.add(action.laneId);
|
|
874
|
+
if (action.laneId === lane.id || !descendants(state, lane.id, action.laneId))
|
|
875
|
+
return { rejection: error('CANCEL_NOT_OWNER', 'a Lane can only cancel its own descendants') };
|
|
876
|
+
cancelLaneSubtree(state, action.laneId, action.reason, mutations, cancelledSubtreeLanes);
|
|
877
|
+
}
|
|
878
|
+
else if (action.type === 'propose_cancel') {
|
|
879
|
+
if (seenCancelTargets.has(action.laneId))
|
|
880
|
+
return { rejection: error('DUPLICATE_CANCEL_TARGET', action.laneId) };
|
|
881
|
+
seenCancelTargets.add(action.laneId);
|
|
882
|
+
const target = state.lanes.get(action.laneId);
|
|
883
|
+
if (!target)
|
|
884
|
+
return { rejection: error('UNKNOWN_LANE', action.laneId) };
|
|
885
|
+
if (target.ownerLaneId && target.ownerLaneId !== lane.id) {
|
|
886
|
+
const owner = laneCopy(state.lanes.get(target.ownerLaneId));
|
|
887
|
+
enqueueControlProposal(owner, { type: 'cancel_lane', laneId: target.id, reason: action.reason, fromLaneId: lane.id });
|
|
888
|
+
mutations.push({ op: 'setLane', laneId: owner.id, record: { ...owner, version: owner.version + 1 } });
|
|
889
|
+
}
|
|
890
|
+
}
|
|
891
|
+
else if (action.type === 'adopt_context') {
|
|
892
|
+
if (output.contextDelta)
|
|
893
|
+
return { rejection: error('CONFLICTING_ADOPT', 'explicit adopt conflicts with adoptCommittedContext') };
|
|
894
|
+
const version = action.version === 'latest' ? state.agents.get(lane.agentId).latestGlobalVersion : action.version;
|
|
895
|
+
if (!state.agents.get(lane.agentId).globalVersions.has(version))
|
|
896
|
+
return { rejection: error('UNKNOWN_CONTEXT_VERSION', String(version)) };
|
|
897
|
+
workingLane.contextSnapshotVersion = version;
|
|
898
|
+
}
|
|
899
|
+
else if (action.type === 'downgrade_privacy') {
|
|
900
|
+
if (!nonEmptyString(action.outputRef) || !isRuntimeJsonValue(action.value ?? null) || (action.summary !== undefined && !isRuntimeJsonValue(action.summary)) || !validProvenanceRefs(action.sourceRefs))
|
|
901
|
+
return { rejection: error('INVALID_PRIVACY_DOWNGRADE', 'Privacy downgrade shape is invalid') };
|
|
902
|
+
if (!action.outputRef || state.results.has(action.outputRef))
|
|
903
|
+
return { rejection: error('INVALID_PRIVACY_OUTPUT_REF', 'downgrade_privacy requires a fresh outputRef') };
|
|
904
|
+
if (action.targetPrivacy !== 'cloud_allowed')
|
|
905
|
+
return { rejection: error('INVALID_PRIVACY_TARGET', 'Only cloud_allowed is a supported downgrade target.') };
|
|
906
|
+
if (action.sourceRefs.length === 0)
|
|
907
|
+
return { rejection: error('EMPTY_PRIVACY_SOURCES', 'downgrade_privacy requires at least one source reference.') };
|
|
908
|
+
if (action.method === 'human_approval') {
|
|
909
|
+
if (!action.approvalRef)
|
|
910
|
+
return { rejection: error('MISSING_PRIVACY_APPROVAL', 'human_approval requires approvalRef.') };
|
|
911
|
+
const approval = state.results.get(action.approvalRef);
|
|
912
|
+
if (!approval)
|
|
913
|
+
return { rejection: error('UNKNOWN_PRIVACY_APPROVAL', 'approvalRef must reference a published approval result.') };
|
|
914
|
+
if (!resultVisible(lane, action.approvalRef))
|
|
915
|
+
return { rejection: error('RESULT_NOT_VISIBLE', 'approvalRef is not visible to this Lane.') };
|
|
916
|
+
const approvalValue = approval.value;
|
|
917
|
+
const approved = approvalValue === true || (approvalValue && typeof approvalValue === 'object' && !Array.isArray(approvalValue) && approvalValue.approved === true);
|
|
918
|
+
if (!approved)
|
|
919
|
+
return { rejection: error('PRIVACY_APPROVAL_REQUIRED', 'approvalRef must contain an explicit approved=true decision.') };
|
|
920
|
+
}
|
|
921
|
+
if (action.method === 'sanitizer') {
|
|
922
|
+
if (!action.sanitizerId)
|
|
923
|
+
return { rejection: error('MISSING_PRIVACY_SANITIZER', 'sanitizer requires sanitizerId.') };
|
|
924
|
+
if (!state.trustedSanitizerIds.has(action.sanitizerId))
|
|
925
|
+
return { rejection: error('UNTRUSTED_SANITIZER', `Sanitizer ${action.sanitizerId} is not trusted by the Runtime policy.`) };
|
|
926
|
+
}
|
|
927
|
+
const sourcePrivacy = derivedPrivacy(state, lane, action.sourceRefs);
|
|
928
|
+
if (sourcePrivacy.error)
|
|
929
|
+
return { rejection: error(sourcePrivacy.error, 'Privacy downgrade references an unknown result.') };
|
|
930
|
+
const result = {
|
|
931
|
+
id: action.outputRef,
|
|
932
|
+
producer: { kind: 'lane', id: lane.id },
|
|
933
|
+
value: clone(action.value ?? null),
|
|
934
|
+
...resultMetadata(action.value ?? null),
|
|
935
|
+
storageState: 'memory',
|
|
936
|
+
pinCount: 0,
|
|
937
|
+
privacy: action.targetPrivacy,
|
|
938
|
+
derivedFrom: [...action.sourceRefs],
|
|
939
|
+
...(action.summary === undefined ? {} : { summary: clone(action.summary) }),
|
|
940
|
+
downgrade: {
|
|
941
|
+
sourceRefs: [...action.sourceRefs],
|
|
942
|
+
targetPrivacy: action.targetPrivacy,
|
|
943
|
+
method: action.method,
|
|
944
|
+
...(action.approvalRef === undefined ? {} : { approvalRef: action.approvalRef }),
|
|
945
|
+
...(action.sanitizerId === undefined ? {} : { sanitizerId: action.sanitizerId }),
|
|
946
|
+
},
|
|
947
|
+
};
|
|
948
|
+
mutations.push({ op: 'publishResult', record: result });
|
|
949
|
+
if (workingLane.visibleResultRefs)
|
|
950
|
+
workingLane.visibleResultRefs.add(action.outputRef);
|
|
951
|
+
else
|
|
952
|
+
workingLane.visibleResultRefs = new Set([action.outputRef]);
|
|
953
|
+
mutations.push({ op: 'appendEvent', event: { type: 'privacy.downgraded', laneId: lane.id, data: { outputRef: action.outputRef, sourceRefs: action.sourceRefs, method: action.method } } });
|
|
954
|
+
}
|
|
955
|
+
else if (action.type === 'complete') {
|
|
956
|
+
if (!isRuntimeJsonValue(action.result) || (action.derivedFrom !== undefined && !validProvenanceRefs(action.derivedFrom)) || (action.privacy !== undefined && !['public', 'cloud_allowed', 'local_only'].includes(action.privacy)) || (action.privacyTaints !== undefined && !Array.isArray(action.privacyTaints)))
|
|
957
|
+
return { rejection: error('INVALID_COMPLETE', 'Complete action shape is invalid') };
|
|
958
|
+
const activeChildren = [...lane.children].some((childId) => !['succeeded', 'failed', 'cancelled'].includes(state.lanes.get(childId)?.status ?? 'cancelled'));
|
|
959
|
+
if (activeChildren && (action.children ?? 'reject_if_active') === 'reject_if_active')
|
|
960
|
+
return { rejection: error('CHILDREN_STILL_ACTIVE', 'complete requires an explicit child join or cancellation') };
|
|
961
|
+
const derived = derivedPrivacy(state, lane, action.derivedFrom ?? []);
|
|
962
|
+
if (derived.error)
|
|
963
|
+
return { rejection: error(derived.error, 'Result provenance references an unknown result') };
|
|
964
|
+
const taintError = validatePrivacyTaints(action.privacyTaints);
|
|
965
|
+
if (taintError)
|
|
966
|
+
return { rejection: error(taintError, 'Result privacy taints are invalid') };
|
|
967
|
+
if (action.privacy !== undefined && derived.privacy !== undefined && privacyRank(action.privacy) < privacyRank(derived.privacy))
|
|
968
|
+
return { rejection: error('PRIVACY_DOWNGRADE_WITHOUT_PROOF', 'Result privacy cannot be broader than its sources') };
|
|
969
|
+
const propagatedTaints = mergePrivacyTaints(derived.privacyTaints, action.privacyTaints);
|
|
970
|
+
const privacy = effectivePrivacy(strictestPrivacy([derived.privacy ?? 'public', action.privacy ?? 'public']), propagatedTaints);
|
|
971
|
+
if (activeChildren && action.children === 'await') {
|
|
972
|
+
const dependencies = [...lane.children].filter((childId) => !['succeeded', 'failed', 'cancelled'].includes(state.lanes.get(childId)?.status ?? 'cancelled')).map((childId) => ({ key: childId, target: { kind: 'lane', id: childId }, condition: 'settled' }));
|
|
973
|
+
if (hasDependencyCycle(state, dependencies.map((dependency) => ({ from: { kind: 'lane', id: lane.id }, to: dependency.target, kind: 'wait' }))))
|
|
974
|
+
return { rejection: error('DEPENDENCY_CYCLE', 'closing wait would create a dependency cycle') };
|
|
975
|
+
workingLane.closingResult = { value: clone(action.result), privacy, ...(propagatedTaints.length ? { privacyTaints: propagatedTaints } : {}), ...(action.derivedFrom === undefined ? {} : { derivedFrom: [...action.derivedFrom] }) };
|
|
976
|
+
addWait(state, workingLane, { dependencies, mode: 'all', onUnsatisfied: 'resume_with_error', reason: 'join' }, new Map(dependencies.map((dependency) => [dependency.key, dependency.target])), mutations, `wait-${waitCounter++}`);
|
|
977
|
+
workingLane.status = 'waiting';
|
|
978
|
+
mutations.push({ op: 'setLane', laneId: lane.id, record: { ...workingLane, version: lane.version + 1 } });
|
|
979
|
+
mutations.push({ op: 'appendEvent', event: { type: 'lane.closing', laneId: lane.id } });
|
|
980
|
+
return { mutations };
|
|
981
|
+
}
|
|
982
|
+
if (activeChildren && action.children === 'cancel')
|
|
983
|
+
for (const childId of lane.children)
|
|
984
|
+
cancelLaneSubtree(state, childId, 'POLICY', mutations, cancelledSubtreeLanes);
|
|
985
|
+
const resultId = `result-${resultCounter++}`;
|
|
986
|
+
mutations.push({ op: 'publishResult', record: { id: resultId, producer: { kind: 'lane', id: lane.id }, value: clone(action.result), ...resultMetadata(action.result), storageState: 'memory', pinCount: 0, privacy, ...(propagatedTaints.length ? { privacyTaints: propagatedTaints } : {}), derivedFrom: [...(action.derivedFrom ?? [])] } });
|
|
987
|
+
if (workingLane.visibleResultRefs)
|
|
988
|
+
workingLane.visibleResultRefs.add(resultId);
|
|
989
|
+
else
|
|
990
|
+
workingLane.visibleResultRefs = new Set([resultId]);
|
|
991
|
+
if (lane.ownerLaneId !== undefined) {
|
|
992
|
+
const owner = state.lanes.get(lane.ownerLaneId);
|
|
993
|
+
if (owner) {
|
|
994
|
+
const ownerCopy = laneCopy(owner);
|
|
995
|
+
if (ownerCopy.visibleResultRefs)
|
|
996
|
+
ownerCopy.visibleResultRefs.add(resultId);
|
|
997
|
+
else
|
|
998
|
+
ownerCopy.visibleResultRefs = new Set([resultId]);
|
|
999
|
+
mutations.push({ op: 'setLane', laneId: owner.id, record: ownerCopy });
|
|
1000
|
+
}
|
|
1001
|
+
}
|
|
1002
|
+
workingLane.status = 'succeeded';
|
|
1003
|
+
workingLane.resultRef = resultId;
|
|
1004
|
+
mutations.push({ op: 'setLane', laneId: lane.id, record: { ...workingLane, version: lane.version + 1 } });
|
|
1005
|
+
mutations.push({ op: 'appendEvent', event: { type: 'lane.succeeded', laneId: lane.id, data: resultId } });
|
|
1006
|
+
return { mutations };
|
|
1007
|
+
}
|
|
1008
|
+
else if (action.type === 'fail') {
|
|
1009
|
+
if (!validRuntimeError(action.error) || (action.derivedFrom !== undefined && !validProvenanceRefs(action.derivedFrom)) || (action.privacy !== undefined && !['public', 'cloud_allowed', 'local_only'].includes(action.privacy)))
|
|
1010
|
+
return { rejection: error('INVALID_FAIL', 'Fail action shape is invalid') };
|
|
1011
|
+
const derived = derivedPrivacy(state, lane, action.derivedFrom ?? []);
|
|
1012
|
+
if (derived.error)
|
|
1013
|
+
return { rejection: error(derived.error, 'Failure provenance references an unknown or invisible result') };
|
|
1014
|
+
if (action.privacy !== undefined && derived.privacy !== undefined && privacyRank(action.privacy) < privacyRank(derived.privacy))
|
|
1015
|
+
return { rejection: error('PRIVACY_DOWNGRADE_WITHOUT_PROOF', 'Failure privacy cannot be broader than its sources') };
|
|
1016
|
+
const privacy = strictestPrivacy([derived.privacy ?? 'public', action.privacy ?? 'public']);
|
|
1017
|
+
workingLane.status = 'failed';
|
|
1018
|
+
workingLane.failure = { error: clone(action.error), privacy, ...(action.derivedFrom === undefined ? {} : { derivedFrom: [...action.derivedFrom] }) };
|
|
1019
|
+
mutations.push({ op: 'setLane', laneId: lane.id, record: { ...workingLane, version: lane.version + 1 } });
|
|
1020
|
+
mutations.push({ op: 'appendEvent', event: { type: 'lane.failed', laneId: lane.id, data: action.error } });
|
|
1021
|
+
return { mutations };
|
|
1022
|
+
}
|
|
1023
|
+
}
|
|
1024
|
+
workingLane.resume = clone(output.next);
|
|
1025
|
+
workingLane.version = lane.version + 1;
|
|
1026
|
+
workingLane.status = workingLane.status === 'waiting' ? 'waiting' : 'ready';
|
|
1027
|
+
mutations.push({ op: 'setLane', laneId: lane.id, record: workingLane });
|
|
1028
|
+
if (workingLane.contextSnapshotVersion !== lane.contextSnapshotVersion)
|
|
1029
|
+
mutations.push({ op: 'appendEvent', event: { type: 'lane.context_adopted', laneId: lane.id, data: workingLane.contextSnapshotVersion } });
|
|
1030
|
+
mutations.push({ op: 'appendEvent', event: { type: 'step.committed', laneId: lane.id } });
|
|
1031
|
+
return { mutations };
|
|
1032
|
+
}
|
|
1033
|
+
export function commitStep(state, laneId, output) {
|
|
1034
|
+
const result = validateStep(state, laneId, output);
|
|
1035
|
+
if ('rejection' in result)
|
|
1036
|
+
return result;
|
|
1037
|
+
apply(state, result.mutations);
|
|
1038
|
+
return result;
|
|
1039
|
+
}
|
|
1040
|
+
function requireMutations() {
|
|
1041
|
+
return { apply: (state, mutations) => {
|
|
1042
|
+
for (const mutation of mutations) {
|
|
1043
|
+
switch (mutation.op) {
|
|
1044
|
+
case 'setAgent':
|
|
1045
|
+
state.agents.set(mutation.agentId, mutation.record);
|
|
1046
|
+
break;
|
|
1047
|
+
case 'setLane':
|
|
1048
|
+
state.lanes.set(mutation.laneId, mutation.record);
|
|
1049
|
+
break;
|
|
1050
|
+
case 'setEffect':
|
|
1051
|
+
state.effects.set(mutation.effectId, mutation.record);
|
|
1052
|
+
break;
|
|
1053
|
+
case 'setWait':
|
|
1054
|
+
state.waits.set(mutation.waitId, mutation.record);
|
|
1055
|
+
break;
|
|
1056
|
+
case 'insertLane':
|
|
1057
|
+
state.lanes.set(mutation.record.id, mutation.record);
|
|
1058
|
+
break;
|
|
1059
|
+
case 'insertEffect':
|
|
1060
|
+
state.effects.set(mutation.record.id, mutation.record);
|
|
1061
|
+
break;
|
|
1062
|
+
case 'insertWait':
|
|
1063
|
+
state.waits.set(mutation.record.id, mutation.record);
|
|
1064
|
+
break;
|
|
1065
|
+
case 'publishResult': {
|
|
1066
|
+
state.results.set(mutation.record.id, mutation.record);
|
|
1067
|
+
const match = /^result-(\d+)$/.exec(mutation.record.id);
|
|
1068
|
+
if (match)
|
|
1069
|
+
state.nextIds.result = Math.max(state.nextIds.result, Number(match[1]) + 1);
|
|
1070
|
+
break;
|
|
1071
|
+
}
|
|
1072
|
+
case 'publishFinding': {
|
|
1073
|
+
state.results.set(mutation.record.id, mutation.record);
|
|
1074
|
+
const lane = state.lanes.get(mutation.record.laneId);
|
|
1075
|
+
if (lane?.visibleResultRefs)
|
|
1076
|
+
lane.visibleResultRefs.add(mutation.record.id);
|
|
1077
|
+
else if (lane)
|
|
1078
|
+
lane.visibleResultRefs = new Set([mutation.record.id]);
|
|
1079
|
+
const match = /^finding-(\d+)$/.exec(mutation.record.id);
|
|
1080
|
+
if (match)
|
|
1081
|
+
state.nextIds.result = Math.max(state.nextIds.result, Number(match[1]) + 1);
|
|
1082
|
+
break;
|
|
1083
|
+
}
|
|
1084
|
+
case 'setGlobal': {
|
|
1085
|
+
const agent = state.agents.get(mutation.agentId);
|
|
1086
|
+
agent.globalVersions.set(mutation.version, mutation.value);
|
|
1087
|
+
if (mutation.metadata) {
|
|
1088
|
+
if (!agent.globalPrivacy)
|
|
1089
|
+
agent.globalPrivacy = new Map();
|
|
1090
|
+
agent.globalPrivacy.set(mutation.version, structuredClone(mutation.metadata));
|
|
1091
|
+
}
|
|
1092
|
+
agent.latestGlobalVersion = mutation.version;
|
|
1093
|
+
break;
|
|
1094
|
+
}
|
|
1095
|
+
case 'setLaneContext': {
|
|
1096
|
+
const lane = state.lanes.get(mutation.laneId);
|
|
1097
|
+
lane.context = { ...lane.context, state: mutation.value, version: mutation.version, ...(mutation.history === undefined ? {} : { history: structuredClone(mutation.history) }), ...(mutation.metadata === undefined ? {} : structuredClone(mutation.metadata)) };
|
|
1098
|
+
break;
|
|
1099
|
+
}
|
|
1100
|
+
case 'setNextIds':
|
|
1101
|
+
state.nextIds = { ...mutation.nextIds };
|
|
1102
|
+
break;
|
|
1103
|
+
case 'appendEvent':
|
|
1104
|
+
appendRuntimeEvent(state, mutation.event);
|
|
1105
|
+
break;
|
|
1106
|
+
case 'insertMergeProposal':
|
|
1107
|
+
state.mergeProposals.set(mutation.proposal.id, mutation.proposal);
|
|
1108
|
+
break;
|
|
1109
|
+
case 'removeMergeProposal':
|
|
1110
|
+
state.mergeProposals.delete(mutation.proposalId);
|
|
1111
|
+
break;
|
|
1112
|
+
case 'setNow':
|
|
1113
|
+
state.now = mutation.now;
|
|
1114
|
+
break;
|
|
1115
|
+
}
|
|
1116
|
+
}
|
|
1117
|
+
} };
|
|
1118
|
+
}
|