feat(orchestrator): enforce Page Data validation gate
This commit is contained in:
+124
-42
@@ -539,6 +539,10 @@ export function createAgentRunGateway({
|
||||
process.env.MEMIND_ORCHESTRATOR_SHADOW_MAX_QUEUE,
|
||||
100,
|
||||
),
|
||||
enforcePageDataWorkflowValidation = envFlag(
|
||||
process.env.MEMIND_ORCHESTRATOR_PAGE_DATA_VALIDATION_GATE_ENABLED,
|
||||
false,
|
||||
),
|
||||
workerIdentity = null,
|
||||
}) {
|
||||
const worker = normalizeAgentRunWorkerIdentity(workerIdentity ?? {});
|
||||
@@ -721,7 +725,7 @@ export function createAgentRunGateway({
|
||||
});
|
||||
}
|
||||
|
||||
function dispatchPageDataValidationObservation({
|
||||
function buildPageDataValidationObservationInput({
|
||||
row,
|
||||
runId,
|
||||
checks,
|
||||
@@ -729,8 +733,7 @@ export function createAgentRunGateway({
|
||||
}) {
|
||||
const userMessage = parseDbJsonColumn(row?.user_message_json, {}) ?? {};
|
||||
const runOptions = getRunOptionsFromMessage(userMessage);
|
||||
if (!isPageDataWorkflowRequested(userMessage, runOptions.taskType)) return false;
|
||||
return validationDispatcher.dispatch({
|
||||
return {
|
||||
runId,
|
||||
requestId: row?.request_id ?? null,
|
||||
userId: row?.user_id ?? null,
|
||||
@@ -747,7 +750,74 @@ export function createAgentRunGateway({
|
||||
source: 'portal-agent-run',
|
||||
observedAt: nowMs(),
|
||||
},
|
||||
});
|
||||
};
|
||||
}
|
||||
|
||||
function dispatchPageDataValidationObservation(input) {
|
||||
return validationDispatcher.dispatch(input);
|
||||
}
|
||||
|
||||
async function enforcePageDataValidationGate(input) {
|
||||
const startedAt = nowMs();
|
||||
let result = null;
|
||||
let gateError = null;
|
||||
try {
|
||||
if (typeof observeWorkflowValidation !== 'function') {
|
||||
gateError = new Error('Page Data Orchestrator validation observer is unavailable');
|
||||
gateError.code = 'WORKFLOW_VALIDATION_GATE_UNAVAILABLE';
|
||||
throw gateError;
|
||||
}
|
||||
result = await observeWorkflowValidation(input);
|
||||
const verdict = result?.validation?.verdict ?? null;
|
||||
if (!result?.observed) {
|
||||
gateError = new Error(
|
||||
`Page Data Orchestrator validation was not observed: ${result?.reason ?? 'not_selected'}`,
|
||||
);
|
||||
gateError.code = 'WORKFLOW_VALIDATION_GATE_UNAVAILABLE';
|
||||
throw gateError;
|
||||
}
|
||||
if (verdict !== 'passed') {
|
||||
gateError = new Error(
|
||||
`Page Data Orchestrator validation did not pass: ${verdict ?? 'unknown'}`,
|
||||
);
|
||||
gateError.code = verdict === 'failed'
|
||||
? 'WORKFLOW_VALIDATION_GATE_REJECTED'
|
||||
: 'WORKFLOW_VALIDATION_GATE_INCONCLUSIVE';
|
||||
throw gateError;
|
||||
}
|
||||
await appendEvent(input.runId, 'workflow_validation_gate_passed', {
|
||||
engine: result.engine ?? 'langgraph',
|
||||
mode: result.mode ?? 'shadow',
|
||||
configVersion: result.configVersion ?? null,
|
||||
kind: result.validation?.kind ?? 'page-data-delivery',
|
||||
verdict,
|
||||
latencyMs: Math.max(0, nowMs() - startedAt),
|
||||
});
|
||||
return result;
|
||||
} catch (error) {
|
||||
const resolved = gateError ?? error;
|
||||
const code = String(
|
||||
gateError?.code ?? 'WORKFLOW_VALIDATION_GATE_UNAVAILABLE',
|
||||
).slice(0, 128);
|
||||
const failure = resolved instanceof Error
|
||||
? resolved
|
||||
: new Error(String(resolved ?? 'Page Data Orchestrator validation failed'));
|
||||
await appendEvent(input.runId, 'workflow_validation_gate_failed', {
|
||||
code,
|
||||
upstreamCode: gateError
|
||||
? null
|
||||
: String(error?.code ?? 'WORKFLOW_VALIDATION_OBSERVER_FAILED').slice(0, 128),
|
||||
message: String(
|
||||
failure.message,
|
||||
).slice(0, 1000),
|
||||
verdict: result?.validation?.verdict ?? null,
|
||||
latencyMs: Math.max(0, nowMs() - startedAt),
|
||||
}).catch(() => {});
|
||||
failure.code = code;
|
||||
failure.retryable = false;
|
||||
failure.validationObservationAttempted = true;
|
||||
throw failure;
|
||||
}
|
||||
}
|
||||
|
||||
async function appendRunSnapshot(runId) {
|
||||
@@ -1662,40 +1732,46 @@ export function createAgentRunGateway({
|
||||
throw error;
|
||||
}
|
||||
}
|
||||
const validationInput = pageDataIntent
|
||||
? buildPageDataValidationObservationInput({
|
||||
row,
|
||||
runId,
|
||||
checks: [
|
||||
{ id: 'agent_run_completion', status: 'passed' },
|
||||
{
|
||||
id: 'page_data_binding',
|
||||
status: typeof syncUserPagesOnSuccess === 'function' ? 'passed' : 'skipped',
|
||||
},
|
||||
{
|
||||
id: 'page_data_storage_policy',
|
||||
status: typeof validateRunDeliverables === 'function' ? 'passed' : 'skipped',
|
||||
},
|
||||
{
|
||||
id: 'page_data_deliverable',
|
||||
status: requiresPageDeliverable ? 'passed' : 'skipped',
|
||||
},
|
||||
{
|
||||
id: 'independent_review',
|
||||
status: runOptions.reviewExecutor ? 'passed' : 'skipped',
|
||||
},
|
||||
],
|
||||
metrics: {
|
||||
pageCount: deliverables?.pageCount ?? 0,
|
||||
publicationCount: deliverables?.publicationCount ?? 0,
|
||||
},
|
||||
})
|
||||
: null;
|
||||
if (validationInput && enforcePageDataWorkflowValidation) {
|
||||
await enforcePageDataValidationGate(validationInput);
|
||||
}
|
||||
const marked = await markRun(runId, 'succeeded', {
|
||||
agent_session_id: sessionId,
|
||||
completed_at: nowMs(),
|
||||
error_message: null,
|
||||
}, { expectedStatus: 'running' });
|
||||
if (!marked) return false;
|
||||
if (pageDataIntent) {
|
||||
dispatchPageDataValidationObservation({
|
||||
row,
|
||||
runId,
|
||||
checks: [
|
||||
{ id: 'agent_run_completion', status: 'passed' },
|
||||
{
|
||||
id: 'page_data_binding',
|
||||
status: typeof syncUserPagesOnSuccess === 'function' ? 'passed' : 'skipped',
|
||||
},
|
||||
{
|
||||
id: 'page_data_storage_policy',
|
||||
status: typeof validateRunDeliverables === 'function' ? 'passed' : 'skipped',
|
||||
},
|
||||
{
|
||||
id: 'page_data_deliverable',
|
||||
status: requiresPageDeliverable ? 'passed' : 'skipped',
|
||||
},
|
||||
{
|
||||
id: 'independent_review',
|
||||
status: runOptions.reviewExecutor ? 'passed' : 'skipped',
|
||||
},
|
||||
],
|
||||
metrics: {
|
||||
pageCount: deliverables?.pageCount ?? 0,
|
||||
publicationCount: deliverables?.publicationCount ?? 0,
|
||||
},
|
||||
});
|
||||
if (validationInput && !enforcePageDataWorkflowValidation) {
|
||||
dispatchPageDataValidationObservation(validationInput);
|
||||
}
|
||||
if (typeof observePersonalMemoryOnSuccess === 'function') {
|
||||
await observePersonalMemoryOnSuccess({
|
||||
@@ -1779,16 +1855,22 @@ export function createAgentRunGateway({
|
||||
}
|
||||
: {}),
|
||||
}, { expectedStatus: 'running' });
|
||||
if (!retryable) {
|
||||
dispatchPageDataValidationObservation({
|
||||
row,
|
||||
runId,
|
||||
checks: [{
|
||||
id: pageDataFailureCheckId(err?.code),
|
||||
status: 'failed',
|
||||
codes: [String(err?.code ?? 'AGENT_RUN_FAILED')],
|
||||
}],
|
||||
});
|
||||
if (!retryable && err?.validationObservationAttempted !== true) {
|
||||
const userMessage = parseDbJsonColumn(row?.user_message_json, {}) ?? {};
|
||||
const runOptions = getRunOptionsFromMessage(userMessage);
|
||||
if (isPageDataWorkflowRequested(userMessage, runOptions.taskType)) {
|
||||
dispatchPageDataValidationObservation(
|
||||
buildPageDataValidationObservationInput({
|
||||
row,
|
||||
runId,
|
||||
checks: [{
|
||||
id: pageDataFailureCheckId(err?.code),
|
||||
status: 'failed',
|
||||
codes: [String(err?.code ?? 'AGENT_RUN_FAILED')],
|
||||
}],
|
||||
}),
|
||||
);
|
||||
}
|
||||
}
|
||||
if (retryable && autoDispatch) {
|
||||
setTimeout(() => dispatchRun(runId), retryDelaysMs[nextAttempt - 1]);
|
||||
|
||||
Reference in New Issue
Block a user