138 lines
3.6 KiB
JavaScript
138 lines
3.6 KiB
JavaScript
import { normalizeRunSpec } from './contracts.mjs';
|
|
import {
|
|
CODE_RUN_SHADOW_WORKFLOW,
|
|
createCodeRunShadowGraph,
|
|
} from './code-run-shadow-graph.mjs';
|
|
|
|
function graphConfig(runId) {
|
|
return {
|
|
configurable: {
|
|
thread_id: runId,
|
|
checkpoint_ns: '',
|
|
},
|
|
};
|
|
}
|
|
|
|
function projectSnapshot(runId, snapshot) {
|
|
const values = snapshot?.values ?? {};
|
|
if (!values.spec) return null;
|
|
return {
|
|
version: 'orchestrator-state-v1',
|
|
runId,
|
|
requestId: values.spec.requestId,
|
|
workflow: values.spec.workflow,
|
|
status: values.status ?? 'unknown',
|
|
phase: values.phase ?? null,
|
|
plan: values.plan ?? null,
|
|
result: values.result ?? null,
|
|
createdAt: values.events?.[0]?.timestamp ?? null,
|
|
updatedAt: values.events?.at?.(-1)?.timestamp ?? null,
|
|
};
|
|
}
|
|
|
|
export function createLangGraphOrchestratorRuntime({
|
|
checkpointer,
|
|
checkpointKind = 'unknown',
|
|
durable = false,
|
|
checkpointProbe = null,
|
|
} = {}) {
|
|
const graph = createCodeRunShadowGraph({ checkpointer });
|
|
const probe = typeof checkpointProbe === 'function'
|
|
? checkpointProbe
|
|
: async () => {
|
|
await checkpointer.getTuple(graphConfig('__orchestrator_readiness__'));
|
|
return true;
|
|
};
|
|
|
|
async function getSnapshot(runId) {
|
|
return graph.getState(graphConfig(runId));
|
|
}
|
|
|
|
async function getState(runId) {
|
|
const normalizedRunId = String(runId ?? '').trim();
|
|
if (!normalizedRunId) return null;
|
|
return projectSnapshot(normalizedRunId, await getSnapshot(normalizedRunId));
|
|
}
|
|
|
|
function health() {
|
|
return {
|
|
status: 'ok',
|
|
service: 'memind-langgraph-orchestrator',
|
|
checkpoint: {
|
|
kind: checkpointKind,
|
|
durable: Boolean(durable),
|
|
},
|
|
execution: 'observe-only',
|
|
};
|
|
}
|
|
|
|
return {
|
|
async start(input) {
|
|
const spec = normalizeRunSpec(input);
|
|
if (spec.workflow.name !== CODE_RUN_SHADOW_WORKFLOW) {
|
|
const error = new Error(`Unsupported workflow: ${spec.workflow.name}`);
|
|
error.code = 'WORKFLOW_NOT_SUPPORTED';
|
|
error.status = 422;
|
|
throw error;
|
|
}
|
|
const existing = await getState(spec.runId);
|
|
if (existing) return existing;
|
|
await graph.invoke({
|
|
spec,
|
|
status: 'queued',
|
|
phase: 'accepted',
|
|
events: [],
|
|
}, graphConfig(spec.runId));
|
|
return getState(spec.runId);
|
|
},
|
|
|
|
getState,
|
|
|
|
async listEvents(runId, { after = 0 } = {}) {
|
|
const normalizedRunId = String(runId ?? '').trim();
|
|
const snapshot = normalizedRunId ? await getSnapshot(normalizedRunId) : null;
|
|
if (!snapshot?.values?.spec) return null;
|
|
const cursor = Math.max(0, Number(after) || 0);
|
|
const events = (snapshot.values.events ?? []).filter((event) => event.sequence > cursor);
|
|
return {
|
|
runId: normalizedRunId,
|
|
events,
|
|
nextCursor: events.at(-1)?.sequence ?? cursor,
|
|
};
|
|
},
|
|
|
|
async resume(runId) {
|
|
const state = await getState(runId);
|
|
if (!state) return null;
|
|
const error = new Error('This workflow has no interrupt point to resume');
|
|
error.code = 'WORKFLOW_NOT_INTERRUPTED';
|
|
error.status = 409;
|
|
throw error;
|
|
},
|
|
|
|
async cancel(runId) {
|
|
const state = await getState(runId);
|
|
if (!state) return null;
|
|
const error = new Error('Completed shadow observations cannot be cancelled');
|
|
error.code = 'WORKFLOW_ALREADY_TERMINAL';
|
|
error.status = 409;
|
|
throw error;
|
|
},
|
|
|
|
health,
|
|
|
|
async ready() {
|
|
await probe();
|
|
return {
|
|
...health(),
|
|
ready: true,
|
|
};
|
|
},
|
|
};
|
|
}
|
|
|
|
export const orchestratorRuntimeInternals = {
|
|
graphConfig,
|
|
projectSnapshot,
|
|
};
|