fix(agent): restore active-task routing and active-mode validation observation
Memind CI / Test, build, and release guards (push) Successful in 5m23s
Memind CI / Test, build, and release guards (push) Successful in 5m23s
Keep correction follow-ups on Agent using session transcript and active task context, and let Page Data validation reach Orchestrator when mode is active instead of requiring shadowEngine. Co-authored-by: Cursor <cursoragent@cursor.com>
This commit is contained in:
@@ -28,6 +28,19 @@ async function recordValidationWithRetry(engine, runId, observation) {
|
||||
}
|
||||
}
|
||||
|
||||
export function resolveValidationEngineSelection(selection) {
|
||||
if (selection.shadowEngine === WORKFLOW_ENGINE.LANGGRAPH) {
|
||||
return selection;
|
||||
}
|
||||
if (
|
||||
selection.engine === WORKFLOW_ENGINE.LANGGRAPH
|
||||
&& ['active', 'canary', 'shadow'].includes(String(selection.mode ?? ''))
|
||||
) {
|
||||
return selection;
|
||||
}
|
||||
return null;
|
||||
}
|
||||
|
||||
export function createWorkflowShadowObserver({
|
||||
configService,
|
||||
serviceToken = process.env.MEMIND_ORCHESTRATOR_SERVICE_TOKEN,
|
||||
@@ -66,6 +79,34 @@ export function createWorkflowShadowObserver({
|
||||
};
|
||||
}
|
||||
|
||||
async function selectValidationEngine({
|
||||
runId,
|
||||
requestId,
|
||||
userId,
|
||||
workflowName,
|
||||
}) {
|
||||
const selection = await configService.selectEngine({
|
||||
runId,
|
||||
requestId,
|
||||
userId,
|
||||
workflowName,
|
||||
});
|
||||
if (!resolveValidationEngineSelection(selection)) {
|
||||
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,
|
||||
@@ -152,7 +193,7 @@ export function createWorkflowShadowObserver({
|
||||
workflowName = 'code-run-v1',
|
||||
observation,
|
||||
} = {}) {
|
||||
const { selection, engine } = await selectShadowEngine({
|
||||
const { selection, engine } = await selectValidationEngine({
|
||||
runId,
|
||||
requestId,
|
||||
userId,
|
||||
@@ -187,5 +228,6 @@ export function createWorkflowShadowObserver({
|
||||
|
||||
export const workflowShadowObserverInternals = {
|
||||
recordValidationWithRetry,
|
||||
resolveValidationEngineSelection,
|
||||
safeError,
|
||||
};
|
||||
|
||||
@@ -3,7 +3,7 @@ import test from 'node:test';
|
||||
import { MemorySaver } from '@langchain/langgraph';
|
||||
import { createOrchestratorApp } from './app.mjs';
|
||||
import { createLangGraphOrchestratorRuntime } from './runtime.mjs';
|
||||
import { createWorkflowShadowObserver } from './shadow-observer.mjs';
|
||||
import { createWorkflowShadowObserver, resolveValidationEngineSelection } from './shadow-observer.mjs';
|
||||
|
||||
function jsonResponse(body, status = 200) {
|
||||
return new Response(JSON.stringify(body), {
|
||||
@@ -25,6 +25,77 @@ async function listen(app) {
|
||||
};
|
||||
}
|
||||
|
||||
test('resolveValidationEngineSelection accepts active-mode LangGraph engine', () => {
|
||||
assert.ok(resolveValidationEngineSelection({
|
||||
engine: 'langgraph',
|
||||
shadowEngine: null,
|
||||
mode: 'active',
|
||||
reason: 'active',
|
||||
}));
|
||||
assert.equal(resolveValidationEngineSelection({
|
||||
engine: 'native',
|
||||
shadowEngine: null,
|
||||
mode: 'active',
|
||||
reason: 'active',
|
||||
}), null);
|
||||
});
|
||||
|
||||
test('validation observer records observations in active mode without shadowEngine', async () => {
|
||||
let capturedUrl = null;
|
||||
const observer = createWorkflowShadowObserver({
|
||||
configService: {
|
||||
async selectEngine() {
|
||||
return {
|
||||
engine: 'langgraph',
|
||||
candidateEngine: 'langgraph',
|
||||
shadowEngine: null,
|
||||
fallbackEngine: 'native',
|
||||
reason: 'active',
|
||||
candidateReason: 'active',
|
||||
mode: 'active',
|
||||
configVersion: 6,
|
||||
dryRun: false,
|
||||
};
|
||||
},
|
||||
async getRuntimeState() {
|
||||
return {
|
||||
config: {
|
||||
serviceUrl: 'http://orchestrator.internal:8093',
|
||||
requestTimeoutMs: 1200,
|
||||
},
|
||||
};
|
||||
},
|
||||
},
|
||||
serviceToken: 'internal-token',
|
||||
fetchImpl: async (url) => {
|
||||
capturedUrl = url;
|
||||
return jsonResponse({
|
||||
runId: 'run-active-1',
|
||||
validation: { verdict: 'passed', kind: 'page-data-delivery' },
|
||||
});
|
||||
},
|
||||
});
|
||||
|
||||
const result = await observer.observeValidation({
|
||||
runId: 'run-active-1',
|
||||
requestId: 'request-active-1',
|
||||
userId: 'user-1',
|
||||
observation: {
|
||||
idempotencyKey: 'run-active-1:page-data-delivery:v1',
|
||||
taskType: 'page_data_dev',
|
||||
required: true,
|
||||
checks: [{ id: 'page_data_binding', status: 'passed' }],
|
||||
source: 'portal-agent-run',
|
||||
observedAt: 123,
|
||||
},
|
||||
});
|
||||
|
||||
assert.equal(result.observed, true);
|
||||
assert.equal(result.mode, 'active');
|
||||
assert.equal(result.validation.verdict, 'passed');
|
||||
assert.match(String(capturedUrl), /\/v1\/runs\/run-active-1\/validation-observations$/);
|
||||
});
|
||||
|
||||
test('shadow observer skips without creating a remote client when mode does not select shadow', async () => {
|
||||
let fetchCalls = 0;
|
||||
const observer = createWorkflowShadowObserver({
|
||||
|
||||
Reference in New Issue
Block a user