feat(orchestrator): observe Page Data validation

This commit is contained in:
john
2026-07-25 14:16:57 +08:00
parent e02495a6c0
commit 417aa9c78b
17 changed files with 784 additions and 16 deletions
+57 -1
View File
@@ -1,4 +1,8 @@
import { buildRunEvent, normalizeRunSpec } from './contracts.mjs';
import {
buildRunEvent,
normalizePageDataValidationObservation,
normalizeRunSpec,
} from './contracts.mjs';
import {
CODE_RUN_SHADOW_WORKFLOW,
createCodeRunShadowGraph,
@@ -22,6 +26,12 @@ function graphConfig(runId) {
};
}
function validationObservationFingerprint(observation) {
if (!observation || typeof observation !== 'object') return '';
const { observedAt: _observedAt, ...stable } = observation;
return JSON.stringify(stable);
}
function projectSnapshot(runId, snapshot) {
const values = snapshot?.values ?? {};
if (!values.spec) return null;
@@ -34,6 +44,7 @@ function projectSnapshot(runId, snapshot) {
phase: values.phase ?? null,
plan: values.plan ?? null,
result: values.result ?? null,
validation: values.validation ?? null,
createdAt: values.events?.[0]?.timestamp ?? null,
updatedAt: values.events?.at?.(-1)?.timestamp ?? null,
};
@@ -136,6 +147,49 @@ export function createLangGraphOrchestratorRuntime({
);
}
async function recordValidationObservation(runId, input = {}) {
const normalizedRunId = String(runId ?? '').trim();
if (!normalizedRunId) return null;
const snapshot = await getSnapshot(normalizedRunId);
const values = snapshot?.values ?? {};
if (!values.spec) return null;
const projected = projectSnapshot(normalizedRunId, snapshot);
if (!TERMINAL_RUN_STATUSES.has(projected.status)) {
const error = new Error('Workflow run must be terminal before validation observation');
error.code = 'WORKFLOW_VALIDATION_RUN_NOT_TERMINAL';
error.status = 409;
throw error;
}
const observation = normalizePageDataValidationObservation(input);
const existing = values.validation ?? null;
if (existing?.idempotencyKey === observation.idempotencyKey) {
if (
validationObservationFingerprint(existing)
!== validationObservationFingerprint(observation)
) {
const error = new Error('Validation observation idempotency key conflicts with existing data');
error.code = 'WORKFLOW_VALIDATION_IDEMPOTENCY_CONFLICT';
error.status = 409;
throw error;
}
return projected;
}
await graph.updateState(
graphConfig(normalizedRunId),
{
validation: observation,
events: [buildRunEvent({
runId: normalizedRunId,
sequence: (values.events?.length ?? 0) + 1,
type: 'workflow_validation_observed',
data: observation,
})],
},
'finalize_run',
);
return getState(normalizedRunId);
}
async function deleteRun(runId) {
const normalizedRunId = String(runId ?? '').trim();
if (!normalizedRunId) return null;
@@ -204,6 +258,7 @@ export function createLangGraphOrchestratorRuntime({
},
getState,
recordValidationObservation,
async listEvents(runId, { after = 0 } = {}) {
const normalizedRunId = String(runId ?? '').trim();
@@ -397,4 +452,5 @@ export const orchestratorRuntimeInternals = {
graphConfig,
projectSnapshot,
TERMINAL_RUN_STATUSES,
validationObservationFingerprint,
};