feat(orchestrator): observe Page Data validation
This commit is contained in:
@@ -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,
|
||||
};
|
||||
|
||||
Reference in New Issue
Block a user