blun-king-cli 9.1.565 → 9.1.566
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/LIESMICH.txt +2 -2
- package/README.md +1 -1
- package/agent-spine-plugin/.claude-plugin/marketplace.json +1 -1
- package/agent-spine-plugin/.claude-plugin/plugin.json +1 -1
- package/agent-spine-plugin/.codex-plugin/plugin.json +1 -1
- package/agent-spine-plugin/README.md +13 -1
- package/agent-spine-plugin/blun.plugin.json +1 -1
- package/agent-spine-plugin/hooks/version.json +1 -1
- package/agent-spine-plugin/package.json +1 -1
- package/agent-spine-plugin/src/hook.js +40 -44
- package/agent-spine-plugin/src/index.js +3 -2
- package/agent-spine-plugin/src/lib/gateway-runtime.js +531 -27
- package/agent-spine-plugin/src/lib/selfstarter.js +20 -1
- package/agent-spine-plugin/src/lib/source-roots.js +3 -2
- package/agent-spine-plugin/src/version.js +1 -1
- package/agent-spine-plugin/src/worker.js +12 -4
- package/bin/native-runtime-cache.cjs +76 -0
- package/blun.mjs +10 -0
- package/package.json +1 -1
|
@@ -17,6 +17,9 @@ export const GATEWAY_RUNTIME_SCHEMA = "agentspine.gateway-runtime/v1";
|
|
|
17
17
|
export const GATEWAY_EVENT_SCHEMA = "agentspine.gateway-event/v1";
|
|
18
18
|
export const GOAL_PLAN_SCHEMA = "agentspine.goal-plan/v1";
|
|
19
19
|
export const KNOWLEDGE_GAP_SCHEMA = "agentspine.knowledge-gap/v1";
|
|
20
|
+
export const EXECUTION_OUTCOME_SCHEMA = "agentspine.execution-outcome/v1";
|
|
21
|
+
export const EXECUTION_ATTEMPT_SCHEMA = "agentspine.execution-attempt/v1";
|
|
22
|
+
export const STRATEGY_TRANSFER_PROOF_SCHEMA = "agentspine.strategy-transfer-proof/v1";
|
|
20
23
|
|
|
21
24
|
const CONFIRMATION = "local-owner-confirmed";
|
|
22
25
|
const MAX_BYTES = 8 * 1024 * 1024;
|
|
@@ -33,6 +36,7 @@ const SECRET_KEY_RE = /"(?:api[-_ ]?key|token|password|secret|credential)"\s*:/i
|
|
|
33
36
|
const AUTHORITY_RE = /\b(?:permission|rights?|roles?|owner|trusted|delegat|authorized|approval|production|payment|spending|tool capability|send capability)\b/i;
|
|
34
37
|
const HEALTH_VALUES = new Set(["stopped", "running", "unknown", "healthy", "degraded", "failed"]);
|
|
35
38
|
const KNOWLEDGE_EVIDENCE = new Set(["owner-input", "objective-observation"]);
|
|
39
|
+
const METRIC_OPERATORS = new Set(["gte", "lte", "eq"]);
|
|
36
40
|
|
|
37
41
|
function emptyPolicy(root) {
|
|
38
42
|
return { schema: GATEWAY_POLICY_SCHEMA, root, revision: 0, enabled: false, killSwitch: false, goals: [], history: [] };
|
|
@@ -136,8 +140,358 @@ function validKnowledgeGap(gap) {
|
|
|
136
140
|
return gap.resolutionDigest === sha256(JSON.stringify(knowledgeGapResolutionMaterial(gap)));
|
|
137
141
|
}
|
|
138
142
|
|
|
143
|
+
function executionDecisionMaterial(execution) {
|
|
144
|
+
return {
|
|
145
|
+
requiredCapabilities: execution.requiredCapabilities, strategies: execution.strategies,
|
|
146
|
+
verification: execution.verification, selectedStrategyId: execution.selectedStrategyId,
|
|
147
|
+
...(execution.explorationMaxAttempts === undefined ? {} : {
|
|
148
|
+
explorationMaxAttempts: execution.explorationMaxAttempts,
|
|
149
|
+
explorationOrder: execution.explorationOrder
|
|
150
|
+
}),
|
|
151
|
+
...(execution.transferKey === undefined ? {} : {
|
|
152
|
+
transferKey: execution.transferKey, transferMaxAgeDays: execution.transferMaxAgeDays,
|
|
153
|
+
transferProof: execution.transferProof
|
|
154
|
+
}),
|
|
155
|
+
authority: "context-only-decision"
|
|
156
|
+
};
|
|
157
|
+
}
|
|
158
|
+
|
|
159
|
+
function sufficientExecutionStrategies(execution) {
|
|
160
|
+
const required = new Set(execution.requiredCapabilities);
|
|
161
|
+
return execution.strategies.filter((strategy) => strategy.capabilities.every((capability) => ID_RE.test(capability))
|
|
162
|
+
&& [...required].every((capability) => strategy.capabilities.includes(capability)))
|
|
163
|
+
.sort((left, right) => left.risk - right.risk || left.cost - right.cost
|
|
164
|
+
|| left.strategyId.localeCompare(right.strategyId));
|
|
165
|
+
}
|
|
166
|
+
|
|
167
|
+
function selectedExecutionStrategy(execution) {
|
|
168
|
+
const sufficient = sufficientExecutionStrategies(execution);
|
|
169
|
+
const minimumRisk = sufficient[0]?.risk;
|
|
170
|
+
const transferred = execution.transferProof && sufficient.find((strategy) =>
|
|
171
|
+
strategy.strategyId === execution.transferProof.strategyId && strategy.risk === minimumRisk);
|
|
172
|
+
return transferred || sufficient[0] || null;
|
|
173
|
+
}
|
|
174
|
+
|
|
175
|
+
function expectedExplorationOrder(execution) {
|
|
176
|
+
const sufficient = sufficientExecutionStrategies(execution);
|
|
177
|
+
const selected = selectedExecutionStrategy(execution);
|
|
178
|
+
if (!selected) return [];
|
|
179
|
+
return [selected, ...sufficient.filter((strategy) => strategy.risk === selected.risk
|
|
180
|
+
&& strategy.strategyId !== selected.strategyId)].map((strategy) => strategy.strategyId);
|
|
181
|
+
}
|
|
182
|
+
|
|
183
|
+
function strategyTransferProofMaterial(proof) {
|
|
184
|
+
return {
|
|
185
|
+
transferKey: proof.transferKey, strategyId: proof.strategyId, maxAgeDays: proof.maxAgeDays,
|
|
186
|
+
evidence: proof.evidence, authority: "context-only-transfer"
|
|
187
|
+
};
|
|
188
|
+
}
|
|
189
|
+
|
|
190
|
+
function validStrategyTransferProof(proof) {
|
|
191
|
+
if (!(proof && proof.schema === STRATEGY_TRANSFER_PROOF_SCHEMA
|
|
192
|
+
&& ID_RE.test(proof.proofId || "") && ID_RE.test(proof.transferKey || "")
|
|
193
|
+
&& ID_RE.test(proof.strategyId || "") && Number.isInteger(proof.maxAgeDays)
|
|
194
|
+
&& proof.maxAgeDays >= 1 && proof.maxAgeDays <= 90 && Array.isArray(proof.evidence)
|
|
195
|
+
&& proof.evidence.length >= 2 && proof.evidence.length <= 8
|
|
196
|
+
&& new Set(proof.evidence.map((item) => item.sourceGoalId)).size === proof.evidence.length
|
|
197
|
+
&& new Set(proof.evidence.map((item) => item.sourceDigest)).size === proof.evidence.length
|
|
198
|
+
&& proof.evidence.every((item) => item && ID_RE.test(item.sourceGoalId || "")
|
|
199
|
+
&& ID_RE.test(item.sourceStepId || "") && ID_RE.test(item.outcomeId || "")
|
|
200
|
+
&& /^[a-f0-9]{64}$/.test(item.outcomeDigest || "")
|
|
201
|
+
&& /^[a-f0-9]{64}$/.test(item.sourceDigest || "")
|
|
202
|
+
&& Number.isFinite(new Date(item.completedAt).getTime()))
|
|
203
|
+
&& /^[a-f0-9]{64}$/.test(proof.proofDigest || "")
|
|
204
|
+
&& proof.authority === "context-only-transfer")) return false;
|
|
205
|
+
const digest = sha256(JSON.stringify(strategyTransferProofMaterial(proof)));
|
|
206
|
+
return proof.proofDigest === digest && proof.proofId === "strategy-transfer:" + digest.slice(0, 32);
|
|
207
|
+
}
|
|
208
|
+
|
|
209
|
+
function sameStrategy(left, right) {
|
|
210
|
+
return JSON.stringify(left) === JSON.stringify(right);
|
|
211
|
+
}
|
|
212
|
+
|
|
213
|
+
function sameVerification(left, right) {
|
|
214
|
+
return JSON.stringify(left) === JSON.stringify(right);
|
|
215
|
+
}
|
|
216
|
+
|
|
217
|
+
function collectStrategyTransferEvidence(goals, execution, strategy, scope) {
|
|
218
|
+
if (!execution.transferKey) return { evidence: [], regressed: false };
|
|
219
|
+
const before = new Date(scope.before);
|
|
220
|
+
const cutoff = new Date(before.getTime() - execution.transferMaxAgeDays * 86400000);
|
|
221
|
+
const evidence = []; let regressed = false;
|
|
222
|
+
for (const goal of goals) {
|
|
223
|
+
if (goal.goalId === scope.goalId || goal.projectId !== scope.projectId || goal.groupId !== scope.groupId || !goal.plan) continue;
|
|
224
|
+
for (const step of goal.plan.steps) {
|
|
225
|
+
const prior = step.execution;
|
|
226
|
+
const completedAt = step.completedAt && new Date(step.completedAt);
|
|
227
|
+
const updatedAt = new Date(step.updatedAt);
|
|
228
|
+
if (!prior || prior.transferKey !== execution.transferKey
|
|
229
|
+
|| !sameVerification(prior.verification, execution.verification)
|
|
230
|
+
|| !sameStrategy(prior.strategies.find((item) => item.strategyId === strategy.strategyId), strategy)
|
|
231
|
+
|| updatedAt > before) continue;
|
|
232
|
+
for (const outcome of step.executionOutcomes || []) {
|
|
233
|
+
if (outcome.strategyId !== strategy.strategyId) continue;
|
|
234
|
+
const observedAt = new Date(outcome.observedAt);
|
|
235
|
+
if (observedAt < cutoff || observedAt > before || observedAt < new Date(goal.createdAt)) continue;
|
|
236
|
+
if (!outcome.passed || outcome.blockingDefect) { regressed = true; continue; }
|
|
237
|
+
if (step.status !== "completed" || !completedAt || completedAt < cutoff || completedAt > before
|
|
238
|
+
|| observedAt > completedAt) continue;
|
|
239
|
+
evidence.push({ sourceGoalId: goal.goalId, sourceStepId: step.stepId, outcomeId: outcome.outcomeId,
|
|
240
|
+
outcomeDigest: outcome.digest, sourceDigest: outcome.sourceDigest, completedAt: step.completedAt });
|
|
241
|
+
}
|
|
242
|
+
}
|
|
243
|
+
}
|
|
244
|
+
const unique = [];
|
|
245
|
+
for (const item of evidence.sort((left, right) => left.completedAt.localeCompare(right.completedAt)
|
|
246
|
+
|| left.outcomeId.localeCompare(right.outcomeId))) {
|
|
247
|
+
if (!unique.some((entry) => entry.sourceGoalId === item.sourceGoalId || entry.sourceDigest === item.sourceDigest)) unique.push(item);
|
|
248
|
+
}
|
|
249
|
+
return { evidence: unique.slice(-8), regressed };
|
|
250
|
+
}
|
|
251
|
+
|
|
252
|
+
function createStrategyTransferProof(goals, execution, scope) {
|
|
253
|
+
if (!execution.transferKey) return null;
|
|
254
|
+
const required = new Set(execution.requiredCapabilities);
|
|
255
|
+
const sufficient = execution.strategies.filter((strategy) =>
|
|
256
|
+
[...required].every((capability) => strategy.capabilities.includes(capability)));
|
|
257
|
+
const minimumRisk = Math.min(...sufficient.map((strategy) => strategy.risk));
|
|
258
|
+
const proven = sufficient.filter((strategy) => strategy.risk === minimumRisk).map((strategy) => ({
|
|
259
|
+
strategy, ...collectStrategyTransferEvidence(goals, execution, strategy, scope)
|
|
260
|
+
})).filter((item) => !item.regressed && item.evidence.length >= 2)
|
|
261
|
+
.sort((left, right) => right.evidence.length - left.evidence.length
|
|
262
|
+
|| left.strategy.cost - right.strategy.cost || left.strategy.strategyId.localeCompare(right.strategy.strategyId));
|
|
263
|
+
if (!proven.length) return null;
|
|
264
|
+
const proof = { schema: STRATEGY_TRANSFER_PROOF_SCHEMA, proofId: null,
|
|
265
|
+
transferKey: execution.transferKey, strategyId: proven[0].strategy.strategyId,
|
|
266
|
+
maxAgeDays: execution.transferMaxAgeDays, evidence: proven[0].evidence,
|
|
267
|
+
proofDigest: null, authority: "context-only-transfer" };
|
|
268
|
+
proof.proofDigest = sha256(JSON.stringify(strategyTransferProofMaterial(proof)));
|
|
269
|
+
proof.proofId = "strategy-transfer:" + proof.proofDigest.slice(0, 32);
|
|
270
|
+
return proof;
|
|
271
|
+
}
|
|
272
|
+
|
|
273
|
+
function validGoalTransferProofs(goal, goals) {
|
|
274
|
+
if (!goal.plan) return true;
|
|
275
|
+
return goal.plan.steps.every((step) => {
|
|
276
|
+
if (!step.execution?.transferProof) return true;
|
|
277
|
+
const expected = createStrategyTransferProof(goals, step.execution, {
|
|
278
|
+
goalId: goal.goalId, projectId: goal.projectId, groupId: goal.groupId, before: goal.createdAt
|
|
279
|
+
});
|
|
280
|
+
return JSON.stringify(expected) === JSON.stringify(step.execution.transferProof);
|
|
281
|
+
});
|
|
282
|
+
}
|
|
283
|
+
|
|
284
|
+
function validExecutionDecision(execution) {
|
|
285
|
+
if (!(execution && execution.authority === "context-only-decision"
|
|
286
|
+
&& Array.isArray(execution.requiredCapabilities) && execution.requiredCapabilities.length > 0
|
|
287
|
+
&& execution.requiredCapabilities.length <= 16
|
|
288
|
+
&& new Set(execution.requiredCapabilities).size === execution.requiredCapabilities.length
|
|
289
|
+
&& execution.requiredCapabilities.every((capability) => ID_RE.test(capability || ""))
|
|
290
|
+
&& Array.isArray(execution.strategies) && execution.strategies.length >= 2 && execution.strategies.length <= 8
|
|
291
|
+
&& new Set(execution.strategies.map((strategy) => strategy?.strategyId)).size === execution.strategies.length
|
|
292
|
+
&& execution.strategies.every((strategy) => strategy && ID_RE.test(strategy.strategyId || "")
|
|
293
|
+
&& Array.isArray(strategy.capabilities) && strategy.capabilities.length > 0 && strategy.capabilities.length <= 16
|
|
294
|
+
&& new Set(strategy.capabilities).size === strategy.capabilities.length
|
|
295
|
+
&& strategy.capabilities.every((capability) => ID_RE.test(capability || ""))
|
|
296
|
+
&& Number.isInteger(strategy.risk) && strategy.risk >= 0 && strategy.risk <= 100
|
|
297
|
+
&& Number.isInteger(strategy.cost) && strategy.cost >= 0 && strategy.cost <= 100)
|
|
298
|
+
&& execution.verification && ID_RE.test(execution.verification.evaluatorId || "")
|
|
299
|
+
&& ID_RE.test(execution.verification.metric || "") && METRIC_OPERATORS.has(execution.verification.operator)
|
|
300
|
+
&& Number.isFinite(execution.verification.threshold)
|
|
301
|
+
&& Number.isInteger(execution.verification.minCases) && execution.verification.minCases >= 1
|
|
302
|
+
&& execution.verification.minCases <= 100000 && ID_RE.test(execution.selectedStrategyId || "")
|
|
303
|
+
&& (execution.explorationMaxAttempts === undefined || (Number.isInteger(execution.explorationMaxAttempts)
|
|
304
|
+
&& execution.explorationMaxAttempts >= 2 && execution.explorationMaxAttempts <= 4
|
|
305
|
+
&& Array.isArray(execution.explorationOrder)
|
|
306
|
+
&& execution.explorationOrder.length === execution.explorationMaxAttempts
|
|
307
|
+
&& new Set(execution.explorationOrder).size === execution.explorationOrder.length
|
|
308
|
+
&& execution.explorationOrder.every((strategyId) => ID_RE.test(strategyId || ""))))
|
|
309
|
+
&& (execution.transferKey === undefined || (ID_RE.test(execution.transferKey || "")
|
|
310
|
+
&& Number.isInteger(execution.transferMaxAgeDays) && execution.transferMaxAgeDays >= 1
|
|
311
|
+
&& execution.transferMaxAgeDays <= 90
|
|
312
|
+
&& (execution.transferProof === null || (validStrategyTransferProof(execution.transferProof)
|
|
313
|
+
&& execution.transferProof.transferKey === execution.transferKey
|
|
314
|
+
&& execution.transferProof.maxAgeDays === execution.transferMaxAgeDays))))
|
|
315
|
+
&& /^[a-f0-9]{64}$/.test(execution.decisionDigest || ""))) return false;
|
|
316
|
+
const selected = selectedExecutionStrategy(execution);
|
|
317
|
+
return selected?.strategyId === execution.selectedStrategyId
|
|
318
|
+
&& (execution.explorationMaxAttempts === undefined
|
|
319
|
+
|| JSON.stringify(execution.explorationOrder)
|
|
320
|
+
=== JSON.stringify(expectedExplorationOrder(execution).slice(0, execution.explorationMaxAttempts)))
|
|
321
|
+
&& execution.decisionDigest === sha256(JSON.stringify(executionDecisionMaterial(execution)));
|
|
322
|
+
}
|
|
323
|
+
|
|
324
|
+
function createExecutionDecision(value, field, transferContext) {
|
|
325
|
+
if (value === null || value === undefined) return null;
|
|
326
|
+
const execution = {
|
|
327
|
+
requiredCapabilities: Array.isArray(value.requiredCapabilities)
|
|
328
|
+
? value.requiredCapabilities.map((capability) => exactId(capability, `${field}.requiredCapabilities`)) : [],
|
|
329
|
+
strategies: Array.isArray(value.strategies) ? value.strategies.map((strategy, index) => ({
|
|
330
|
+
strategyId: exactId(strategy?.strategyId, `${field}.strategies[${index}].strategyId`),
|
|
331
|
+
capabilities: Array.isArray(strategy?.capabilities)
|
|
332
|
+
? strategy.capabilities.map((capability) => exactId(capability, `${field}.strategies[${index}].capabilities`)) : [],
|
|
333
|
+
risk: Number(strategy?.risk), cost: Number(strategy?.cost)
|
|
334
|
+
})) : [],
|
|
335
|
+
verification: {
|
|
336
|
+
evaluatorId: exactId(value.verification?.evaluatorId, `${field}.verification.evaluatorId`),
|
|
337
|
+
metric: exactId(value.verification?.metric, `${field}.verification.metric`),
|
|
338
|
+
operator: value.verification?.operator,
|
|
339
|
+
threshold: Number(value.verification?.threshold), minCases: Number(value.verification?.minCases)
|
|
340
|
+
},
|
|
341
|
+
selectedStrategyId: null, decisionDigest: null, authority: "context-only-decision"
|
|
342
|
+
};
|
|
343
|
+
if (value.transfer !== undefined && value.transfer !== null) {
|
|
344
|
+
execution.transferKey = exactId(value.transfer?.transferKey, `${field}.transfer.transferKey`);
|
|
345
|
+
execution.transferMaxAgeDays = Number(value.transfer?.maxAgeDays);
|
|
346
|
+
execution.transferProof = null;
|
|
347
|
+
execution.transferProof = createStrategyTransferProof(transferContext.goals, execution, transferContext.scope);
|
|
348
|
+
}
|
|
349
|
+
execution.selectedStrategyId = selectedExecutionStrategy(execution)?.strategyId || null;
|
|
350
|
+
if (value.exploration !== undefined && value.exploration !== null) {
|
|
351
|
+
execution.explorationMaxAttempts = Number(value.exploration?.maxAttempts);
|
|
352
|
+
execution.explorationOrder = expectedExplorationOrder(execution).slice(0, execution.explorationMaxAttempts);
|
|
353
|
+
}
|
|
354
|
+
execution.decisionDigest = sha256(JSON.stringify(executionDecisionMaterial(execution)));
|
|
355
|
+
if (!validExecutionDecision(execution)) {
|
|
356
|
+
throw new Error(`${field} requires 2-8 bounded strategies and one objective verification gate`);
|
|
357
|
+
}
|
|
358
|
+
return execution;
|
|
359
|
+
}
|
|
360
|
+
|
|
361
|
+
function executionOutcomeMaterial(outcome) {
|
|
362
|
+
return {
|
|
363
|
+
queueId: outcome.queueId, decisionDigest: outcome.decisionDigest, strategyId: outcome.strategyId,
|
|
364
|
+
...(outcome.attempt === undefined ? {} : {
|
|
365
|
+
attempt: outcome.attempt, previousOutcomeDigest: outcome.previousOutcomeDigest
|
|
366
|
+
}),
|
|
367
|
+
capabilitiesUsed: outcome.capabilitiesUsed, evaluatorId: outcome.evaluatorId, metric: outcome.metric,
|
|
368
|
+
value: outcome.value, cases: outcome.cases, blockingDefect: outcome.blockingDefect,
|
|
369
|
+
sourceDigest: outcome.sourceDigest, observedAt: outcome.observedAt, passed: outcome.passed,
|
|
370
|
+
authority: "objective-evidence-only"
|
|
371
|
+
};
|
|
372
|
+
}
|
|
373
|
+
|
|
374
|
+
function currentExecutionAttempt(execution, outcomes = []) {
|
|
375
|
+
if (execution.explorationMaxAttempts === undefined) return null;
|
|
376
|
+
if (outcomes.length >= execution.explorationMaxAttempts || outcomes.some((outcome) => outcome.passed || outcome.blockingDefect)) {
|
|
377
|
+
return null;
|
|
378
|
+
}
|
|
379
|
+
return {
|
|
380
|
+
schema: EXECUTION_ATTEMPT_SCHEMA,
|
|
381
|
+
attempt: outcomes.length + 1, maxAttempts: execution.explorationMaxAttempts,
|
|
382
|
+
strategyId: execution.explorationOrder[outcomes.length],
|
|
383
|
+
previousOutcomeDigest: outcomes.at(-1)?.digest || null,
|
|
384
|
+
decisionDigest: execution.decisionDigest, authority: "context-only-attempt"
|
|
385
|
+
};
|
|
386
|
+
}
|
|
387
|
+
|
|
388
|
+
export function executionAttemptForStep(step) {
|
|
389
|
+
if (!step?.execution || !Array.isArray(step.executionOutcomes)) return null;
|
|
390
|
+
return currentExecutionAttempt(step.execution, step.executionOutcomes);
|
|
391
|
+
}
|
|
392
|
+
|
|
393
|
+
function metricPassed(operator, value, threshold) {
|
|
394
|
+
if (operator === "gte") return value >= threshold;
|
|
395
|
+
if (operator === "lte") return value <= threshold;
|
|
396
|
+
return value === threshold;
|
|
397
|
+
}
|
|
398
|
+
|
|
399
|
+
function validExecutionOutcome(outcome, execution, expectedStrategyId = execution.selectedStrategyId,
|
|
400
|
+
expectedAttempt = null, previousOutcomeDigest = null) {
|
|
401
|
+
if (!(outcome && outcome.schema === EXECUTION_OUTCOME_SCHEMA && ID_RE.test(outcome.outcomeId || "")
|
|
402
|
+
&& ID_RE.test(outcome.queueId || "") && outcome.decisionDigest === execution.decisionDigest
|
|
403
|
+
&& outcome.strategyId === expectedStrategyId
|
|
404
|
+
&& (execution.explorationMaxAttempts === undefined
|
|
405
|
+
? outcome.attempt === undefined && outcome.previousOutcomeDigest === undefined
|
|
406
|
+
: outcome.attempt === expectedAttempt && outcome.previousOutcomeDigest === previousOutcomeDigest)
|
|
407
|
+
&& Array.isArray(outcome.capabilitiesUsed)
|
|
408
|
+
&& outcome.capabilitiesUsed.length <= 16 && new Set(outcome.capabilitiesUsed).size === outcome.capabilitiesUsed.length
|
|
409
|
+
&& outcome.capabilitiesUsed.every((capability) => ID_RE.test(capability || ""))
|
|
410
|
+
&& outcome.evaluatorId === execution.verification.evaluatorId && outcome.metric === execution.verification.metric
|
|
411
|
+
&& Number.isFinite(outcome.value) && Number.isInteger(outcome.cases) && outcome.cases >= 0
|
|
412
|
+
&& typeof outcome.blockingDefect === "boolean" && /^[a-f0-9]{64}$/.test(outcome.sourceDigest || "")
|
|
413
|
+
&& Number.isFinite(new Date(outcome.observedAt).getTime()) && typeof outcome.passed === "boolean"
|
|
414
|
+
&& /^[a-f0-9]{64}$/.test(outcome.digest || "") && outcome.authority === "objective-evidence-only")) return false;
|
|
415
|
+
const strategy = execution.strategies.find((item) => item.strategyId === expectedStrategyId);
|
|
416
|
+
const used = new Set(outcome.capabilitiesUsed);
|
|
417
|
+
const expectedPass = execution.requiredCapabilities.every((capability) => used.has(capability))
|
|
418
|
+
&& outcome.capabilitiesUsed.every((capability) => strategy.capabilities.includes(capability))
|
|
419
|
+
&& outcome.cases >= execution.verification.minCases && !outcome.blockingDefect
|
|
420
|
+
&& metricPassed(execution.verification.operator, outcome.value, execution.verification.threshold);
|
|
421
|
+
const digest = sha256(JSON.stringify(executionOutcomeMaterial(outcome)));
|
|
422
|
+
return outcome.passed === expectedPass && outcome.digest === digest
|
|
423
|
+
&& outcome.outcomeId === "execution-outcome:" + digest.slice(0, 32);
|
|
424
|
+
}
|
|
425
|
+
|
|
426
|
+
function validExecutionOutcomeSequence(execution, outcomes) {
|
|
427
|
+
if (execution.explorationMaxAttempts === undefined) {
|
|
428
|
+
return outcomes.every((outcome) => validExecutionOutcome(outcome, execution));
|
|
429
|
+
}
|
|
430
|
+
if (outcomes.length > execution.explorationMaxAttempts
|
|
431
|
+
|| new Set(outcomes.map((outcome) => outcome.sourceDigest)).size !== outcomes.length) return false;
|
|
432
|
+
let previous = null;
|
|
433
|
+
for (let index = 0; index < outcomes.length; index += 1) {
|
|
434
|
+
const outcome = outcomes[index];
|
|
435
|
+
if (!validExecutionOutcome(outcome, execution, execution.explorationOrder[index], index + 1, previous)) return false;
|
|
436
|
+
if (index < outcomes.length - 1 && (outcome.passed || outcome.blockingDefect)) return false;
|
|
437
|
+
previous = outcome.digest;
|
|
438
|
+
}
|
|
439
|
+
return true;
|
|
440
|
+
}
|
|
441
|
+
|
|
442
|
+
function reviewExecutionResult(execution, priorOutcomes, queueId, report, now) {
|
|
443
|
+
const attempt = currentExecutionAttempt(execution, priorOutcomes);
|
|
444
|
+
const expectedStrategyId = attempt?.strategyId || execution.selectedStrategyId;
|
|
445
|
+
if (!report || report.strategyId !== expectedStrategyId || !Array.isArray(report.capabilitiesUsed)
|
|
446
|
+
|| !report.outcome || report.outcome.evaluatorId !== execution.verification.evaluatorId
|
|
447
|
+
|| report.outcome.metric !== execution.verification.metric) {
|
|
448
|
+
return { passed: false, outcome: null, reason: "Objective execution evidence is missing or invalid." };
|
|
449
|
+
}
|
|
450
|
+
let outcome;
|
|
451
|
+
try {
|
|
452
|
+
outcome = {
|
|
453
|
+
schema: EXECUTION_OUTCOME_SCHEMA, outcomeId: null, queueId,
|
|
454
|
+
decisionDigest: execution.decisionDigest, strategyId: report.strategyId,
|
|
455
|
+
...(attempt === null ? {} : { attempt: attempt.attempt, previousOutcomeDigest: attempt.previousOutcomeDigest }),
|
|
456
|
+
capabilitiesUsed: report.capabilitiesUsed.map((capability) => exactId(capability, "execution.capabilitiesUsed")),
|
|
457
|
+
evaluatorId: report.outcome.evaluatorId, metric: report.outcome.metric,
|
|
458
|
+
value: Number(report.outcome.value), cases: Number(report.outcome.cases),
|
|
459
|
+
blockingDefect: report.outcome.blockingDefect === true, sourceDigest: String(report.outcome.sourceDigest || ""),
|
|
460
|
+
observedAt: timestamp(report.outcome.observedAt || now), passed: false, digest: null,
|
|
461
|
+
authority: "objective-evidence-only"
|
|
462
|
+
};
|
|
463
|
+
if (new Date(outcome.observedAt) > new Date(now)
|
|
464
|
+
|| (attempt && priorOutcomes.some((prior) => prior.sourceDigest === outcome.sourceDigest))) {
|
|
465
|
+
return { passed: false, outcome: null, reason: "Objective execution evidence is missing or invalid." };
|
|
466
|
+
}
|
|
467
|
+
const strategy = execution.strategies.find((item) => item.strategyId === expectedStrategyId);
|
|
468
|
+
const used = new Set(outcome.capabilitiesUsed);
|
|
469
|
+
outcome.passed = execution.requiredCapabilities.every((capability) => used.has(capability))
|
|
470
|
+
&& outcome.capabilitiesUsed.every((capability) => strategy.capabilities.includes(capability))
|
|
471
|
+
&& outcome.cases >= execution.verification.minCases && !outcome.blockingDefect
|
|
472
|
+
&& metricPassed(execution.verification.operator, outcome.value, execution.verification.threshold);
|
|
473
|
+
outcome.digest = sha256(JSON.stringify(executionOutcomeMaterial(outcome)));
|
|
474
|
+
outcome.outcomeId = "execution-outcome:" + outcome.digest.slice(0, 32);
|
|
475
|
+
} catch {
|
|
476
|
+
return { passed: false, outcome: null, reason: "Objective execution evidence is missing or invalid." };
|
|
477
|
+
}
|
|
478
|
+
if (!validExecutionOutcome(outcome, execution, expectedStrategyId, attempt?.attempt ?? null,
|
|
479
|
+
attempt?.previousOutcomeDigest ?? null)
|
|
480
|
+
|| !validExecutionOutcomeSequence(execution, [...priorOutcomes, outcome])) {
|
|
481
|
+
return { passed: false, outcome: null, reason: "Objective execution evidence is missing or invalid." };
|
|
482
|
+
}
|
|
483
|
+
const nextAttempt = !outcome.passed && !outcome.blockingDefect
|
|
484
|
+
? currentExecutionAttempt(execution, [...priorOutcomes, outcome]) : null;
|
|
485
|
+
return { passed: outcome.passed, outcome, nextAttempt,
|
|
486
|
+
reason: outcome.passed ? null : "The objective execution gate did not pass." };
|
|
487
|
+
}
|
|
488
|
+
|
|
139
489
|
function planDefinitionMaterial(steps) {
|
|
140
|
-
return steps.map(({ stepId,
|
|
490
|
+
return steps.map(({ stepId, agentId, resources, execution, title, successCriterion, dependsOn }) => ({
|
|
491
|
+
stepId, ...(agentId === undefined ? {} : { agentId }),
|
|
492
|
+
...(resources === undefined ? {} : { resources }), ...(execution === undefined ? {} : { execution }),
|
|
493
|
+
title, successCriterion, dependsOn
|
|
494
|
+
}));
|
|
141
495
|
}
|
|
142
496
|
|
|
143
497
|
function validGoalPlan(plan) {
|
|
@@ -149,6 +503,14 @@ function validGoalPlan(plan) {
|
|
|
149
503
|
if (ids.size !== plan.steps.length || ids.has(undefined)) return false;
|
|
150
504
|
for (const step of plan.steps) {
|
|
151
505
|
if (!(ID_RE.test(step.stepId || "") && typeof step.title === "string" && step.title.length > 0 && step.title.length <= 500
|
|
506
|
+
&& (step.agentId === undefined || ID_RE.test(step.agentId || ""))
|
|
507
|
+
&& (step.resources === undefined || (Array.isArray(step.resources) && step.resources.length <= 16
|
|
508
|
+
&& new Set(step.resources).size === step.resources.length && step.resources.every((resource) => ID_RE.test(resource || ""))))
|
|
509
|
+
&& (step.execution === undefined || step.execution === null || validExecutionDecision(step.execution))
|
|
510
|
+
&& (step.executionOutcomes === undefined || (Array.isArray(step.executionOutcomes) && step.executionOutcomes.length <= 8
|
|
511
|
+
&& new Set(step.executionOutcomes.map((outcome) => outcome.outcomeId)).size === step.executionOutcomes.length
|
|
512
|
+
&& (step.executionOutcomes.length === 0 || (step.execution
|
|
513
|
+
&& validExecutionOutcomeSequence(step.execution, step.executionOutcomes)))))
|
|
152
514
|
&& typeof step.successCriterion === "string" && step.successCriterion.length > 0 && step.successCriterion.length <= 1000
|
|
153
515
|
&& Array.isArray(step.dependsOn) && new Set(step.dependsOn).size === step.dependsOn.length
|
|
154
516
|
&& step.dependsOn.every((dependency) => ids.has(dependency) && dependency !== step.stepId)
|
|
@@ -161,7 +523,8 @@ function validGoalPlan(plan) {
|
|
|
161
523
|
&& new Set(step.knowledgeGaps.map((gap) => gap.gapId)).size === step.knowledgeGaps.length
|
|
162
524
|
&& step.knowledgeGaps.every((gap) => validKnowledgeGap(gap)
|
|
163
525
|
&& gap.goalStepId === step.stepId && gap.planDefinitionsDigest === plan.definitionsDigest)))
|
|
164
|
-
&& Number.isFinite(new Date(step.updatedAt).getTime())
|
|
526
|
+
&& Number.isFinite(new Date(step.updatedAt).getTime())
|
|
527
|
+
&& (step.executionOutcomes || []).every((outcome) => new Date(outcome.observedAt) <= new Date(step.updatedAt)))) return false;
|
|
165
528
|
}
|
|
166
529
|
const visiting = new Set(); const visited = new Set();
|
|
167
530
|
const visit = (stepId) => {
|
|
@@ -181,6 +544,8 @@ function validGoalPlan(plan) {
|
|
|
181
544
|
if (plan.steps.some((step) => (step.knowledgeGaps || []).filter((gap) => gap.status === "open").length > 1)) return false;
|
|
182
545
|
if (plan.steps.some((step) => (step.knowledgeGaps || []).some((gap) => gap.status === "open")
|
|
183
546
|
&& step.status !== "blocked")) return false;
|
|
547
|
+
if (plan.steps.some((step) => step.execution && step.status === "completed"
|
|
548
|
+
&& !(step.executionOutcomes || []).some((outcome) => outcome.passed))) return false;
|
|
184
549
|
if (plan.definitionsDigest !== sha256(JSON.stringify(planDefinitionMaterial(plan.steps)))) return false;
|
|
185
550
|
const current = plan.steps.filter((step) => ["active", "blocked"].includes(step.status));
|
|
186
551
|
return current.length <= 1 && (plan.currentStepId === null
|
|
@@ -195,15 +560,19 @@ function activateNextPlanStep(plan, now) {
|
|
|
195
560
|
return next;
|
|
196
561
|
}
|
|
197
562
|
|
|
198
|
-
function createGoalPlan(steps, now) {
|
|
563
|
+
function createGoalPlan(steps, now, defaultAgentId, transferContext) {
|
|
199
564
|
if (!Array.isArray(steps) || steps.length === 0 || steps.length > 32) throw new Error("goal plan requires 1-32 steps");
|
|
200
565
|
const normalized = steps.map((step, index) => ({
|
|
201
566
|
stepId: exactId(step?.stepId ?? step?.id, `steps[${index}].stepId`),
|
|
567
|
+
agentId: exactId(step?.agentId ?? defaultAgentId, `steps[${index}].agentId`),
|
|
568
|
+
resources: Array.isArray(step?.resources)
|
|
569
|
+
? step.resources.map((resource) => exactId(resource, `steps[${index}].resources`)) : [],
|
|
570
|
+
execution: createExecutionDecision(step?.execution, `steps[${index}].execution`, transferContext),
|
|
202
571
|
title: safeText(step?.title, `steps[${index}].title`, 500),
|
|
203
572
|
successCriterion: safeText(step?.successCriterion, `steps[${index}].successCriterion`),
|
|
204
573
|
dependsOn: Array.isArray(step?.dependsOn) ? step.dependsOn.map((dependency) => exactId(dependency, `steps[${index}].dependsOn`)) : [],
|
|
205
574
|
status: "pending", checkpoint: null, blocker: null, completedAt: null, completedByQueueId: null,
|
|
206
|
-
knowledgeGaps: [], updatedAt: now
|
|
575
|
+
knowledgeGaps: [], executionOutcomes: [], updatedAt: now
|
|
207
576
|
}));
|
|
208
577
|
const plan = { schema: GOAL_PLAN_SCHEMA, revision: 0, currentStepId: null, steps: normalized,
|
|
209
578
|
definitionsDigest: sha256(JSON.stringify(planDefinitionMaterial(normalized))), authority: "context-only-plan" };
|
|
@@ -216,6 +585,31 @@ function currentPlanStep(goal) {
|
|
|
216
585
|
return goal.plan?.steps.find((step) => step.stepId === goal.plan.currentStepId) || null;
|
|
217
586
|
}
|
|
218
587
|
|
|
588
|
+
function planStepAgentId(goal, step) {
|
|
589
|
+
return step?.agentId ?? goal.agentId;
|
|
590
|
+
}
|
|
591
|
+
|
|
592
|
+
function planStepResources(step) {
|
|
593
|
+
return Array.isArray(step?.resources) ? step.resources : [];
|
|
594
|
+
}
|
|
595
|
+
|
|
596
|
+
function queuePlanStep(policy, item) {
|
|
597
|
+
if (!item.goalStepId) return null;
|
|
598
|
+
return policy.goals.find((goal) => goal.goalId === item.goalId)?.plan?.steps
|
|
599
|
+
.find((step) => step.stepId === item.goalStepId) || null;
|
|
600
|
+
}
|
|
601
|
+
|
|
602
|
+
function conflictingResources(policy, candidate, leased) {
|
|
603
|
+
const wanted = new Set(planStepResources(queuePlanStep(policy, candidate)));
|
|
604
|
+
if (!wanted.size || candidate.projectId !== leased.projectId || candidate.groupId !== leased.groupId) return [];
|
|
605
|
+
return planStepResources(queuePlanStep(policy, leased)).filter((resource) => wanted.has(resource));
|
|
606
|
+
}
|
|
607
|
+
|
|
608
|
+
function effectiveQueuePriority(policy, item) {
|
|
609
|
+
if (!item.goalId) return item.priority;
|
|
610
|
+
return policy.goals.find((goal) => goal.goalId === item.goalId)?.priority ?? item.priority;
|
|
611
|
+
}
|
|
612
|
+
|
|
219
613
|
function createKnowledgeGap(goal, step, queueId, request, now) {
|
|
220
614
|
const gap = {
|
|
221
615
|
schema: KNOWLEDGE_GAP_SCHEMA, gapId: null, goalId: goal.goalId, goalStepId: step.stepId,
|
|
@@ -244,8 +638,8 @@ function planQueueKey(goalId, stepId, phase, suffix = "") {
|
|
|
244
638
|
|
|
245
639
|
function newGoalQueue(goal, step, kind, key, current, availableAt = current) {
|
|
246
640
|
return { queueId: "gateway-queue:" + sha256(key).slice(0, 32), dedupeKey: key, kind,
|
|
247
|
-
agentId: goal
|
|
248
|
-
goalStepId: step?.stepId || null, channelEventId: null, priority:
|
|
641
|
+
agentId: planStepAgentId(goal, step), projectId: goal.projectId, groupId: goal.groupId, goalId: goal.goalId,
|
|
642
|
+
goalStepId: step?.stepId || null, channelEventId: null, priority: goal.priority, status: "pending",
|
|
249
643
|
attempts: 0, lease: null, availableAt, createdAt: current, updatedAt: current,
|
|
250
644
|
completedAt: null, lastError: null, authority: "execution-state-only" };
|
|
251
645
|
}
|
|
@@ -345,6 +739,7 @@ function normalizePolicy(value, root) {
|
|
|
345
739
|
if (!value || value.schema !== GATEWAY_POLICY_SCHEMA || value.root !== root
|
|
346
740
|
|| !Number.isInteger(value.revision) || typeof value.enabled !== "boolean" || typeof value.killSwitch !== "boolean"
|
|
347
741
|
|| !Array.isArray(value.goals) || !Array.isArray(value.history) || value.goals.some((item) => !validGoal(item))
|
|
742
|
+
|| value.goals.some((item) => !validGoalTransferProofs(item, value.goals))
|
|
348
743
|
|| value.history.some((item) => !validPolicyHistory(item))) {
|
|
349
744
|
throw new Error("gateway policy is invalid; autonomous runtime is disabled");
|
|
350
745
|
}
|
|
@@ -471,14 +866,25 @@ export async function assignGoal({ root = process.cwd(), goalId, agentId, ownerS
|
|
|
471
866
|
readJson(paths.gatewayRuntimePath, paths.catalog.root, normalizeRuntime, emptyRuntime),
|
|
472
867
|
loadPersonaRuntime(paths.catalog.root, paths.catalog)
|
|
473
868
|
]);
|
|
474
|
-
|
|
475
|
-
|
|
869
|
+
goalId = exactId(goalId, "goalId"); agentId = exactId(agentId, "agentId");
|
|
870
|
+
projectId = exactId(projectId, "projectId"); groupId = exactId(groupId, "groupId", true);
|
|
871
|
+
const leadIdentity = assertActivePersona(personas.policy, personas.runtime, agentId, projectId, groupId);
|
|
476
872
|
const createdAt = timestamp(now);
|
|
477
873
|
const active = policy.goals.find((item) => item.agentId === agentId && item.status === "active" && item.goalId !== goalId);
|
|
478
874
|
if (active) throw new Error("an agent may have only one active focused goal");
|
|
479
|
-
const plan = steps === null ? null : createGoalPlan(steps, createdAt
|
|
875
|
+
const plan = steps === null ? null : createGoalPlan(steps, createdAt, agentId, {
|
|
876
|
+
goals: policy.goals, scope: { goalId, projectId, groupId, before: createdAt }
|
|
877
|
+
});
|
|
878
|
+
if (plan) {
|
|
879
|
+
for (const stepAgentId of new Set(plan.steps.map((step) => step.agentId))) {
|
|
880
|
+
const stepIdentity = assertActivePersona(personas.policy, personas.runtime, stepAgentId, projectId, groupId);
|
|
881
|
+
if (stepIdentity.binding.tenantId !== leadIdentity.binding.tenantId) {
|
|
882
|
+
throw new Error("goal-plan team members must share the authenticated tenant and exact project group");
|
|
883
|
+
}
|
|
884
|
+
}
|
|
885
|
+
}
|
|
480
886
|
const firstStep = plan ? plan.steps.find((step) => step.stepId === plan.currentStepId) : null;
|
|
481
|
-
const goal = { goalId
|
|
887
|
+
const goal = { goalId, agentId, ownerSubjectId: exactId(ownerSubjectId, "ownerSubjectId"),
|
|
482
888
|
projectId, groupId, priority: Number(priority), successCriterion: safeText(successCriterion, "successCriterion"),
|
|
483
889
|
nextSafeStep: safeText(firstStep?.title || nextSafeStep, "nextSafeStep"), deadline: deadline === null ? null : timestamp(deadline),
|
|
484
890
|
status: "active", checkpoint: null, heartbeatAt: null, blocker: null, createdAt, updatedAt: createdAt,
|
|
@@ -496,6 +902,10 @@ export async function assignGoal({ root = process.cwd(), goalId, agentId, ownerS
|
|
|
496
902
|
if ((blockedStep.knowledgeGaps || []).some((gap) => gap.status === "open")) {
|
|
497
903
|
throw new Error("blocked goal plan has an open knowledge gap; resolve it with goal-clarify");
|
|
498
904
|
}
|
|
905
|
+
if (blockedStep.execution?.explorationMaxAttempts !== undefined
|
|
906
|
+
&& currentExecutionAttempt(blockedStep.execution, blockedStep.executionOutcomes || []) === null) {
|
|
907
|
+
throw new Error("bounded exploration is exhausted or stopped by a blocking defect; assign a new goal ID");
|
|
908
|
+
}
|
|
499
909
|
policy.history.push({ kind: "goal", at: createdAt, value: structuredClone(previous), authority: "authenticated-goal-policy" });
|
|
500
910
|
blockedStep.status = "active"; blockedStep.blocker = null; blockedStep.updatedAt = createdAt;
|
|
501
911
|
previous.status = "active"; previous.blocker = null; previous.updatedAt = createdAt; previous.plan.revision += 1;
|
|
@@ -640,11 +1050,32 @@ export async function reconcileGateway({ root = process.cwd(), now = new Date()
|
|
|
640
1050
|
outbox.status = "delivery-unknown"; outbox.updatedAt = current;
|
|
641
1051
|
appendReceipt(runtime, "delivery-unknown", outbox.outboxId, current, { reason: "crash-during-send" });
|
|
642
1052
|
}
|
|
1053
|
+
let policyChanged = false;
|
|
643
1054
|
if (policy.enabled && !policy.killSwitch) {
|
|
644
1055
|
for (const goal of policy.goals.filter((item) => item.status === "active" && item.plan)) {
|
|
645
1056
|
const step = currentPlanStep(goal);
|
|
646
1057
|
if (!step) throw new Error("active goal plan has no current step");
|
|
1058
|
+
try {
|
|
1059
|
+
assertActivePersona(personas.policy, personas.runtime, planStepAgentId(goal, step), goal.projectId, goal.groupId);
|
|
1060
|
+
} catch {
|
|
1061
|
+
policy.history.push({ kind: "goal", at: current, value: structuredClone(goal), authority: "authenticated-goal-policy" });
|
|
1062
|
+
const blocker = "Assigned team member is unavailable in this exact project group.";
|
|
1063
|
+
step.status = "blocked"; step.blocker = blocker; step.updatedAt = current;
|
|
1064
|
+
goal.status = "blocked"; goal.blocker = blocker; goal.updatedAt = current; goal.plan.revision += 1;
|
|
1065
|
+
for (const queued of runtime.queue.filter((item) => item.goalId === goal.goalId
|
|
1066
|
+
&& item.goalStepId === step.stepId && ["pending", "leased"].includes(item.status))) {
|
|
1067
|
+
preserve(runtime, "queue", queued, "step-agent-unavailable", current);
|
|
1068
|
+
queued.status = "cancelled"; queued.lease = null; queued.completedAt = current; queued.updatedAt = current;
|
|
1069
|
+
for (const lane of runtime.lanes.filter((item) => item.queueId === queued.queueId && item.status === "leased")) {
|
|
1070
|
+
lane.status = "expired"; lane.updatedAt = current;
|
|
1071
|
+
}
|
|
1072
|
+
appendReceipt(runtime, "step-agent-unavailable", queued.queueId, current, { goalStepId: step.stepId });
|
|
1073
|
+
}
|
|
1074
|
+
policy.revision += 1; policyChanged = true;
|
|
1075
|
+
continue;
|
|
1076
|
+
}
|
|
647
1077
|
const runnable = runtime.queue.some((item) => item.goalId === goal.goalId && item.goalStepId === step.stepId
|
|
1078
|
+
&& item.agentId === planStepAgentId(goal, step)
|
|
648
1079
|
&& ["pending", "leased", "awaiting-delivery"].includes(item.status));
|
|
649
1080
|
if (!runnable) {
|
|
650
1081
|
const key = planQueueKey(goal.goalId, step.stepId, "recovery", String(goal.plan.revision));
|
|
@@ -657,13 +1088,15 @@ export async function reconcileGateway({ root = process.cwd(), now = new Date()
|
|
|
657
1088
|
}
|
|
658
1089
|
for (const goal of policy.goals.filter((item) => item.status === "active" && item.deadline
|
|
659
1090
|
&& new Date(item.deadline) <= new Date(current))) {
|
|
660
|
-
|
|
1091
|
+
const step = currentPlanStep(goal);
|
|
1092
|
+
const deadlineAgentId = planStepAgentId(goal, step);
|
|
1093
|
+
try { assertActivePersona(personas.policy, personas.runtime, deadlineAgentId, goal.projectId, goal.groupId); }
|
|
661
1094
|
catch { continue; }
|
|
662
1095
|
const key = "goal:" + goal.goalId + ":deadline:" + goal.deadline;
|
|
663
1096
|
if (!runtime.queue.some((item) => item.dedupeKey === key)) runtime.queue.push({
|
|
664
1097
|
queueId: "gateway-queue:" + sha256(key).slice(0, 32), dedupeKey: key, kind: "deadline",
|
|
665
|
-
agentId:
|
|
666
|
-
goalStepId:
|
|
1098
|
+
agentId: deadlineAgentId, projectId: goal.projectId, groupId: goal.groupId, goalId: goal.goalId,
|
|
1099
|
+
goalStepId: step?.stepId || null, channelEventId: null, priority: PRIORITY.deadline, status: "pending", attempts: 0, lease: null,
|
|
667
1100
|
availableAt: current, createdAt: current, updatedAt: current, completedAt: null, lastError: null,
|
|
668
1101
|
authority: "execution-state-only"
|
|
669
1102
|
});
|
|
@@ -691,7 +1124,10 @@ export async function reconcileGateway({ root = process.cwd(), now = new Date()
|
|
|
691
1124
|
runtime.health.gateway = policy.enabled && !policy.killSwitch ? "running" : "stopped";
|
|
692
1125
|
runtime.health.scheduler = "healthy"; runtime.health.queue = "healthy"; runtime.health.lastReconciledAt = current;
|
|
693
1126
|
runtime.revision += 1;
|
|
694
|
-
await
|
|
1127
|
+
if (policyChanged) await Promise.all([
|
|
1128
|
+
writeJson(paths.gatewayPolicyPath, policy), writeJson(paths.gatewayRuntimePath, runtime)
|
|
1129
|
+
]);
|
|
1130
|
+
else await writeJson(paths.gatewayRuntimePath, runtime);
|
|
695
1131
|
return { policy, runtime, recovered: true };
|
|
696
1132
|
});
|
|
697
1133
|
}
|
|
@@ -710,7 +1146,8 @@ export async function claimGatewayWork({ root = process.cwd(), workerId, leaseSe
|
|
|
710
1146
|
if (!Number.isInteger(seconds) || seconds < 15 || seconds > 900) throw new Error("leaseSeconds must be 15-900");
|
|
711
1147
|
const items = runtime.queue.filter((item) => item.status === "pending" && new Date(item.availableAt) <= new Date(current)
|
|
712
1148
|
&& !currentLane(runtime, item.agentId));
|
|
713
|
-
items.sort((a, b) => b
|
|
1149
|
+
items.sort((a, b) => effectiveQueuePriority(policy, b) - effectiveQueuePriority(policy, a)
|
|
1150
|
+
|| a.createdAt.localeCompare(b.createdAt) || a.queueId.localeCompare(b.queueId));
|
|
714
1151
|
let item = null; let revoked = false;
|
|
715
1152
|
for (const candidate of items) {
|
|
716
1153
|
try {
|
|
@@ -725,7 +1162,8 @@ export async function claimGatewayWork({ root = process.cwd(), workerId, leaseSe
|
|
|
725
1162
|
if (candidate.goalStepId) {
|
|
726
1163
|
const goal = policy.goals.find((entry) => entry.goalId === candidate.goalId);
|
|
727
1164
|
const step = goal && currentPlanStep(goal);
|
|
728
|
-
if (!goal?.plan || goal.status !== "active" || step?.stepId !== candidate.goalStepId || step.status !== "active"
|
|
1165
|
+
if (!goal?.plan || goal.status !== "active" || step?.stepId !== candidate.goalStepId || step.status !== "active"
|
|
1166
|
+
|| candidate.agentId !== planStepAgentId(goal, step)) {
|
|
729
1167
|
preserve(runtime, "queue", candidate, "plan-step-stale", current);
|
|
730
1168
|
candidate.status = "cancelled"; candidate.completedAt = current; candidate.updatedAt = current;
|
|
731
1169
|
appendReceipt(runtime, "plan-step-stale", candidate.queueId, current, { goalStepId: candidate.goalStepId });
|
|
@@ -733,6 +1171,9 @@ export async function claimGatewayWork({ root = process.cwd(), workerId, leaseSe
|
|
|
733
1171
|
continue;
|
|
734
1172
|
}
|
|
735
1173
|
}
|
|
1174
|
+
const resourceConflict = runtime.queue.some((leased) => leased.status === "leased"
|
|
1175
|
+
&& leased.queueId !== candidate.queueId && conflictingResources(policy, candidate, leased).length > 0);
|
|
1176
|
+
if (resourceConflict) continue;
|
|
736
1177
|
item = candidate;
|
|
737
1178
|
break;
|
|
738
1179
|
}
|
|
@@ -778,9 +1219,10 @@ export async function completeGatewayRun({ root = process.cwd(), queueId, worker
|
|
|
778
1219
|
const lane = runtime.lanes.find((entry) => entry.queueId === item.queueId && entry.workerId === workerId && entry.status === "leased");
|
|
779
1220
|
if (!lane) throw new Error("agent lane lease is missing");
|
|
780
1221
|
const boundGoal = item.goalId ? policy.goals.find((entry) => entry.goalId === item.goalId) : null;
|
|
1222
|
+
const boundStep = item.goalStepId ? boundGoal && currentPlanStep(boundGoal) : null;
|
|
781
1223
|
if (item.goalStepId) {
|
|
782
|
-
|
|
783
|
-
|
|
1224
|
+
if (!boundGoal?.plan || boundGoal.status !== "active" || boundStep?.stepId !== item.goalStepId
|
|
1225
|
+
|| boundStep.status !== "active" || item.agentId !== planStepAgentId(boundGoal, boundStep)) {
|
|
784
1226
|
throw new Error("run completion is not bound to the current active goal step");
|
|
785
1227
|
}
|
|
786
1228
|
}
|
|
@@ -792,8 +1234,14 @@ export async function completeGatewayRun({ root = process.cwd(), queueId, worker
|
|
|
792
1234
|
if (knowledgeGapRequest && (result?.blocked || result?.completed)) {
|
|
793
1235
|
throw new Error("knowledge gap result cannot also complete or generically block a step");
|
|
794
1236
|
}
|
|
1237
|
+
let executionReview = boundStep?.execution && result?.completed
|
|
1238
|
+
? reviewExecutionResult(boundStep.execution, boundStep.executionOutcomes || [],
|
|
1239
|
+
item.queueId, result.execution, current) : null;
|
|
1240
|
+
if (executionReview && (boundStep.executionOutcomes || []).length >= 8) {
|
|
1241
|
+
executionReview = { passed: false, outcome: null, reason: "Execution outcome history is full." };
|
|
1242
|
+
}
|
|
795
1243
|
const text = result?.text ? safeText(result.text, "result.text", 16000) : null;
|
|
796
|
-
let clarification = null;
|
|
1244
|
+
let clarification = null; let exploration = null;
|
|
797
1245
|
preserve(runtime, "queue", item, "run-completed", current);
|
|
798
1246
|
if (item.channelEventId) {
|
|
799
1247
|
if (!text) throw new Error("a channel obligation requires a non-empty response");
|
|
@@ -814,13 +1262,15 @@ export async function completeGatewayRun({ root = process.cwd(), queueId, worker
|
|
|
814
1262
|
}
|
|
815
1263
|
item.status = "awaiting-delivery";
|
|
816
1264
|
} else {
|
|
817
|
-
item.status = result?.blocked || knowledgeGapRequest
|
|
1265
|
+
item.status = result?.blocked || knowledgeGapRequest || (executionReview && !executionReview.passed)
|
|
1266
|
+
? "blocked" : "completed"; item.completedAt = current;
|
|
818
1267
|
const goal = boundGoal;
|
|
819
1268
|
if (goal) {
|
|
820
1269
|
policy.history.push({ kind: "goal", at: current, value: structuredClone(goal), authority: "authenticated-goal-policy" });
|
|
821
1270
|
const checkpoint = result?.checkpoint === undefined ? goal.checkpoint : safeCheckpoint(result.checkpoint);
|
|
822
1271
|
goal.checkpoint = checkpoint; goal.heartbeatAt = current;
|
|
823
|
-
goal.blocker = result?.blocked ? safeText(result.blocker || "Run blocked.", "blocker", 500)
|
|
1272
|
+
goal.blocker = result?.blocked ? safeText(result.blocker || "Run blocked.", "blocker", 500)
|
|
1273
|
+
: executionReview && !executionReview.passed ? executionReview.reason : null;
|
|
824
1274
|
if (goal.plan && item.goalStepId) {
|
|
825
1275
|
const step = currentPlanStep(goal);
|
|
826
1276
|
step.checkpoint = checkpoint; step.updatedAt = current;
|
|
@@ -847,7 +1297,38 @@ export async function completeGatewayRun({ root = process.cwd(), queueId, worker
|
|
|
847
1297
|
goal.plan.revision += 1;
|
|
848
1298
|
} else if (result?.blocked) {
|
|
849
1299
|
step.status = "blocked"; step.blocker = goal.blocker; goal.status = "blocked";
|
|
1300
|
+
} else if (executionReview && !executionReview.passed) {
|
|
1301
|
+
if (executionReview.outcome) step.executionOutcomes.push(executionReview.outcome);
|
|
1302
|
+
if (executionReview.outcome && executionReview.nextAttempt) {
|
|
1303
|
+
item.status = "completed"; step.status = "active"; step.blocker = null;
|
|
1304
|
+
goal.status = "active"; goal.blocker = null; goal.nextSafeStep = step.title;
|
|
1305
|
+
goal.plan.revision += 1;
|
|
1306
|
+
const key = planQueueKey(goal.goalId, step.stepId, "explore",
|
|
1307
|
+
executionReview.outcome.digest.slice(0, 20));
|
|
1308
|
+
let queued = runtime.queue.find((entry) => entry.dedupeKey === key);
|
|
1309
|
+
if (!queued) {
|
|
1310
|
+
queued = newGoalQueue(goal, step, "follow-up", key, current);
|
|
1311
|
+
runtime.queue.push(queued);
|
|
1312
|
+
}
|
|
1313
|
+
exploration = { queueId: queued.queueId, ...executionReview.nextAttempt };
|
|
1314
|
+
appendReceipt(runtime, "execution-exploration-continued", queued.queueId, current, {
|
|
1315
|
+
goalStepId: step.stepId, failedOutcomeId: executionReview.outcome.outcomeId,
|
|
1316
|
+
attempt: executionReview.nextAttempt.attempt,
|
|
1317
|
+
strategyId: executionReview.nextAttempt.strategyId
|
|
1318
|
+
});
|
|
1319
|
+
} else {
|
|
1320
|
+
step.status = "blocked"; step.blocker = goal.blocker; goal.status = "blocked";
|
|
1321
|
+
appendReceipt(runtime, executionReview.outcome ? "execution-gate-failed" : "execution-proof-invalid",
|
|
1322
|
+
item.queueId, current, { goalStepId: step.stepId,
|
|
1323
|
+
outcomeId: executionReview.outcome?.outcomeId || null });
|
|
1324
|
+
}
|
|
850
1325
|
} else if (result?.completed) {
|
|
1326
|
+
if (executionReview?.outcome) {
|
|
1327
|
+
step.executionOutcomes.push(executionReview.outcome);
|
|
1328
|
+
appendReceipt(runtime, "execution-gate-passed", item.queueId, current, {
|
|
1329
|
+
goalStepId: step.stepId, outcomeId: executionReview.outcome.outcomeId
|
|
1330
|
+
});
|
|
1331
|
+
}
|
|
851
1332
|
step.status = "completed"; step.completedAt = current; step.completedByQueueId = item.queueId; step.blocker = null;
|
|
852
1333
|
goal.plan.currentStepId = null; goal.plan.revision += 1;
|
|
853
1334
|
const next = activateNextPlanStep(goal.plan, current);
|
|
@@ -877,8 +1358,8 @@ export async function completeGatewayRun({ root = process.cwd(), queueId, worker
|
|
|
877
1358
|
: "goal:" + goal.goalId + ":follow-up:" + checkpointDigest;
|
|
878
1359
|
if (!runtime.queue.some((entry) => entry.dedupeKey === key)) runtime.queue.push({
|
|
879
1360
|
queueId: "gateway-queue:" + sha256(key).slice(0, 32), dedupeKey: key, kind: "follow-up",
|
|
880
|
-
agentId: goal
|
|
881
|
-
goalStepId: step?.stepId || null, channelEventId: null, priority:
|
|
1361
|
+
agentId: planStepAgentId(goal, step), projectId: goal.projectId, groupId: goal.groupId, goalId: goal.goalId,
|
|
1362
|
+
goalStepId: step?.stepId || null, channelEventId: null, priority: goal.priority, status: "pending", attempts: 0, lease: null,
|
|
882
1363
|
availableAt: new Date(new Date(current).getTime() + 60000).toISOString(), createdAt: current, updatedAt: current,
|
|
883
1364
|
completedAt: null, lastError: null, authority: "execution-state-only"
|
|
884
1365
|
});
|
|
@@ -889,7 +1370,8 @@ export async function completeGatewayRun({ root = process.cwd(), queueId, worker
|
|
|
889
1370
|
runtime.health.host = "healthy"; runtime.health.worker = "healthy"; runtime.health.lastTickAt = current;
|
|
890
1371
|
appendReceipt(runtime, "run-terminal", item.queueId, current, { status: item.status, goalStepId: item.goalStepId || null }); runtime.revision += 1;
|
|
891
1372
|
await Promise.all([writeJson(paths.gatewayPolicyPath, policy), writeJson(paths.gatewayRuntimePath, runtime)]);
|
|
892
|
-
return { item, outbox: runtime.outbox.find((entry) => entry.queueId === item.queueId) || null,
|
|
1373
|
+
return { item, outbox: runtime.outbox.find((entry) => entry.queueId === item.queueId) || null,
|
|
1374
|
+
clarification, exploration, executionReview };
|
|
893
1375
|
});
|
|
894
1376
|
}
|
|
895
1377
|
|
|
@@ -1047,6 +1529,7 @@ export function gatewayRuntimeFindings(policy, runtime) {
|
|
|
1047
1529
|
const goalIds = new Set();
|
|
1048
1530
|
for (const goal of policy.goals) {
|
|
1049
1531
|
if (!validGoal(goal)) findings.push("invalid-goal:" + (goal?.goalId || "unknown"));
|
|
1532
|
+
else if (!validGoalTransferProofs(goal, policy.goals)) findings.push("invalid-strategy-transfer:" + goal.goalId);
|
|
1050
1533
|
if (goalIds.has(goal.goalId)) findings.push("duplicate-goal:" + goal.goalId);
|
|
1051
1534
|
goalIds.add(goal.goalId);
|
|
1052
1535
|
if (goal.status === "active" && active.has(goal.agentId)) findings.push("multiple-active-goals:" + goal.agentId);
|
|
@@ -1061,7 +1544,9 @@ export function gatewayRuntimeFindings(policy, runtime) {
|
|
|
1061
1544
|
queueIds.add(item.queueId); dedupeKeys.add(item.dedupeKey);
|
|
1062
1545
|
if (item.goalStepId) {
|
|
1063
1546
|
const goal = policy.goals.find((entry) => entry.goalId === item.goalId);
|
|
1064
|
-
|
|
1547
|
+
const step = goal?.plan?.steps.find((entry) => entry.stepId === item.goalStepId);
|
|
1548
|
+
if (!step) findings.push("orphan-goal-step:" + item.queueId);
|
|
1549
|
+
else if (item.agentId !== planStepAgentId(goal, step)) findings.push("goal-step-agent-mismatch:" + item.queueId);
|
|
1065
1550
|
}
|
|
1066
1551
|
}
|
|
1067
1552
|
const outboxIds = new Set(); const idempotencyKeys = new Set();
|
|
@@ -1108,12 +1593,31 @@ export async function gatewayContext({ root = process.cwd(), agentId = null } =
|
|
|
1108
1593
|
const { policy, runtime } = await loadGatewayRuntime(root);
|
|
1109
1594
|
const findings = gatewayRuntimeFindings(policy, runtime);
|
|
1110
1595
|
if (findings.length) throw new Error("gateway runtime failed closed: " + findings.join(", "));
|
|
1111
|
-
const
|
|
1112
|
-
const
|
|
1596
|
+
const exactAgentId = agentId === null ? null : exactId(agentId, "agentId");
|
|
1597
|
+
const goals = policy.goals.filter((item) => exactAgentId === null || item.agentId === exactAgentId
|
|
1598
|
+
|| item.plan?.steps.some((step) => planStepAgentId(item, step) === exactAgentId));
|
|
1599
|
+
const executionAttempts = goals.map((goal) => {
|
|
1600
|
+
const step = currentPlanStep(goal); const attempt = executionAttemptForStep(step);
|
|
1601
|
+
return attempt && (exactAgentId === null || planStepAgentId(goal, step) === exactAgentId)
|
|
1602
|
+
? { goalId: goal.goalId, goalStepId: step.stepId, ...attempt } : null;
|
|
1603
|
+
}).filter(Boolean);
|
|
1604
|
+
const queue = runtime.queue.filter((item) => exactAgentId === null || item.agentId === exactAgentId);
|
|
1113
1605
|
const queueIds = new Set(queue.map((item) => item.queueId));
|
|
1606
|
+
const leased = runtime.queue.filter((item) => item.status === "leased");
|
|
1607
|
+
const resourceWaits = queue.filter((item) => item.status === "pending").map((item) => {
|
|
1608
|
+
const blockers = leased.map((entry) => ({ entry, resources: conflictingResources(policy, item, entry) }))
|
|
1609
|
+
.filter(({ resources }) => resources.length > 0);
|
|
1610
|
+
return blockers.length ? {
|
|
1611
|
+
queueId: item.queueId,
|
|
1612
|
+
resources: [...new Set(blockers.flatMap(({ resources }) => resources))].sort(),
|
|
1613
|
+
blockedByQueueIds: blockers.map(({ entry }) => entry.queueId).sort(),
|
|
1614
|
+
authority: "execution-state-only"
|
|
1615
|
+
} : null;
|
|
1616
|
+
}).filter(Boolean);
|
|
1114
1617
|
return { schema: "agentspine.gateway-context/v1", enabled: policy.enabled, killSwitch: policy.killSwitch,
|
|
1115
1618
|
goals: structuredClone(goals), queue: structuredClone(queue),
|
|
1116
1619
|
outbox: structuredClone(runtime.outbox.filter((item) => queueIds.has(item.queueId))),
|
|
1620
|
+
resourceWaits, executionAttempts,
|
|
1117
1621
|
health: structuredClone(runtime.health), healthFindings: gatewayHealthFindings(policy, runtime),
|
|
1118
1622
|
authority: "execution-state-only" };
|
|
1119
1623
|
}
|