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
+90 -11
View File
@@ -1,9 +1,12 @@
import {
WORKFLOW_ENGINE,
normalizePageDataValidationObservation,
normalizeRunSpec,
} from './contracts.mjs';
import { createRemoteWorkflowEngine } from './engine-registry.mjs';
const VALIDATION_RETRY_DELAYS_MS = Object.freeze([25, 75, 225]);
function safeError(error) {
return {
code: String(error?.code ?? 'WORKFLOW_SHADOW_FAILED').slice(0, 128),
@@ -11,6 +14,20 @@ function safeError(error) {
};
}
async function recordValidationWithRetry(engine, runId, observation) {
let attempt = 0;
while (true) {
try {
return await engine.recordValidationObservation(runId, observation);
} catch (error) {
const delayMs = VALIDATION_RETRY_DELAYS_MS[attempt];
if (error?.status !== 404 || delayMs == null) throw error;
attempt += 1;
await new Promise((resolve) => setTimeout(resolve, delayMs));
}
}
}
export function createWorkflowShadowObserver({
configService,
serviceToken = process.env.MEMIND_ORCHESTRATOR_SERVICE_TOKEN,
@@ -21,14 +38,43 @@ export function createWorkflowShadowObserver({
throw new Error('Workflow shadow observer requires orchestrator config service');
}
return async function observeWorkflowRun({
async function selectShadowEngine({
runId,
requestId,
userId,
workflowName,
}) {
const selection = await configService.selectEngine({
runId,
requestId,
userId,
workflowName,
});
if (selection.shadowEngine !== WORKFLOW_ENGINE.LANGGRAPH) {
return { selection, engine: null };
}
const state = await configService.getRuntimeState();
return {
selection,
engine: createRemoteWorkflowEngine({
id: WORKFLOW_ENGINE.LANGGRAPH,
baseUrl: state.config.serviceUrl,
serviceToken,
timeoutMs: state.config.requestTimeoutMs,
fetchImpl,
}),
};
}
async function observeWorkflowRun({
runId,
requestId,
userId,
workflowName = 'code-run-v1',
taskType = null,
pageDataRequired = false,
} = {}) {
const selection = await configService.selectEngine({
const { selection, engine } = await selectShadowEngine({
runId,
requestId,
userId,
@@ -49,7 +95,7 @@ export function createWorkflowShadowObserver({
dryRun: true,
handoffAllowed: false,
} : null;
if (selection.shadowEngine !== WORKFLOW_ENGINE.LANGGRAPH) {
if (!engine) {
return {
observed: false,
reason: selection.reason,
@@ -58,14 +104,6 @@ export function createWorkflowShadowObserver({
};
}
const state = await configService.getRuntimeState();
const engine = createRemoteWorkflowEngine({
id: WORKFLOW_ENGINE.LANGGRAPH,
baseUrl: state.config.serviceUrl,
serviceToken,
timeoutMs: state.config.requestTimeoutMs,
fetchImpl,
});
const spec = normalizeRunSpec({
runId,
requestId,
@@ -75,6 +113,7 @@ export function createWorkflowShadowObserver({
instruction: '[shadow-control-plane-only]',
taskType,
toolMode: 'code',
pageDataRequired: pageDataRequired === true,
},
policy: {
executionMode: 'observe-only',
@@ -104,9 +143,49 @@ export function createWorkflowShadowObserver({
}
throw error;
}
}
observeWorkflowRun.observeValidation = async function observeWorkflowValidation({
runId,
requestId,
userId,
workflowName = 'code-run-v1',
observation,
} = {}) {
const { selection, engine } = await selectShadowEngine({
runId,
requestId,
userId,
workflowName,
});
if (!engine) {
return {
observed: false,
reason: selection.reason,
mode: selection.mode,
};
}
const normalized = normalizePageDataValidationObservation(observation);
try {
const result = await recordValidationWithRetry(engine, runId, normalized);
return {
observed: true,
engine: WORKFLOW_ENGINE.LANGGRAPH,
mode: selection.mode,
configVersion: selection.configVersion,
validation: result.validation ?? normalized,
shadowRun: result,
};
} catch (error) {
logger.warn('[orchestrator-shadow] validation observation failed:', safeError(error));
throw error;
}
};
return observeWorkflowRun;
}
export const workflowShadowObserverInternals = {
recordValidationWithRetry,
safeError,
};