Files
memind/services/orchestrator/code-run-shadow-graph.mjs
T

210 lines
6.0 KiB
JavaScript

import crypto from 'node:crypto';
import {
Annotation,
END,
START,
StateGraph,
} from '@langchain/langgraph';
import {
buildRunEvent,
normalizeRunSpec,
} from './contracts.mjs';
export const CODE_RUN_SHADOW_WORKFLOW = 'code-run-v1';
const ShadowState = Annotation.Root({
spec: Annotation(),
status: Annotation({
reducer: (_current, update) => update,
default: () => 'queued',
}),
phase: Annotation({
reducer: (_current, update) => update,
default: () => 'accepted',
}),
plan: Annotation({
reducer: (_current, update) => update,
default: () => null,
}),
result: Annotation({
reducer: (_current, update) => update,
default: () => null,
}),
events: Annotation({
reducer: (current, update) => [...current, ...update],
default: () => [],
}),
});
function executorIdentityForRun(runId) {
const normalizedRunId = String(runId ?? '').trim();
const jobId = `${normalizedRunId}:executor-preview`;
if (/^[a-zA-Z0-9:_-]+$/.test(normalizedRunId) && jobId.length <= 128) {
return {
jobId,
idempotencyKey: `${normalizedRunId}:build_plan:v1`,
};
}
const digest = crypto.createHash('sha256').update(normalizedRunId).digest('hex');
return {
jobId: `run_${digest}:executor-preview`,
idempotencyKey: `run_${digest}:build_plan:v1`,
};
}
function eventFor(state, type, data = null) {
return buildRunEvent({
runId: state.spec.runId,
sequence: state.events.length + 1,
type,
data,
});
}
function validateNode(state, { executionEnabled = false } = {}) {
const spec = normalizeRunSpec(state.spec);
if (spec.workflow.name !== CODE_RUN_SHADOW_WORKFLOW) {
const error = new Error(`Unsupported workflow: ${spec.workflow.name}`);
error.code = 'WORKFLOW_NOT_SUPPORTED';
throw error;
}
const observeOnly = spec.policy.executionMode === 'observe-only'
&& spec.policy.sideEffectsAllowed === false;
const active = spec.policy.executionMode === 'active'
&& spec.policy.sideEffectsAllowed === true
&& executionEnabled;
if (!observeOnly && !active) {
const error = new Error(
executionEnabled
? 'LangGraph run policy must be observe-only or explicitly active'
: 'LangGraph execution is disabled; only observe-only runs are accepted',
);
error.code = 'WORKFLOW_OBSERVE_ONLY_REQUIRED';
throw error;
}
return {
spec,
status: 'running',
phase: 'validated',
events: [eventFor(state, 'workflow_validated', {
workflow: spec.workflow,
executionMode: spec.policy.executionMode,
})],
};
}
async function planNode(state, executorGateway) {
const taskType = String(state.spec.input?.taskType ?? '').trim() || 'code-change';
const instruction = String(state.spec.input?.instruction ?? '');
const executorIdentity = executorIdentityForRun(state.spec.runId);
const active = state.spec.policy.executionMode === 'active';
const executor = String(state.spec.input?.executor ?? 'goosed').trim().toLowerCase();
const executorJob = await executorGateway.createJob({
jobId: executorIdentity.jobId,
idempotencyKey: executorIdentity.idempotencyKey,
executor,
task: {
type: taskType,
instruction,
workspaceRef: state.spec.input?.workspaceRef ?? null,
inputRefs: state.spec.input?.inputRefs ?? [],
},
subject: state.spec.subject,
authorization: {
executionAllowed: active,
actorId: state.spec.subject?.userId ?? null,
},
policy: {
sideEffectsAllowed: active && state.spec.policy.sideEffectsAllowed === true,
networkAllowed: active && state.spec.policy.networkAllowed === true,
},
controls: {
timeoutMs: state.spec.limits?.timeoutMs,
cancellationAllowed: true,
fallbackExecutor: 'aider',
maxAttempts: state.spec.limits?.maxAttempts,
},
metadata: {
source: 'langgraph-shadow-plan',
workflow: state.spec.workflow,
},
});
const plan = {
taskType,
executorAdapter: active ? 'executor-gateway' : 'native-agent-run',
executorJob: {
kind: 'executor-job',
id: executorJob.job.jobId,
executor: executorJob.job.executor,
status: executorJob.job.status,
reason: executorJob.job.reason,
durable: executorGateway.status().store.durable,
},
instructionCharacters: instruction.length,
steps: [
'accept_control_plane_run',
'project_executor_boundary',
'record_shadow_result',
],
};
return {
phase: 'planned',
plan,
events: [eventFor(state, 'workflow_planned', {
taskType,
executorAdapter: plan.executorAdapter,
executorJob: plan.executorJob,
stepCount: plan.steps.length,
})],
};
}
function finalizeNode(state) {
const queued = state.plan.executorJob.status === 'queued';
const result = {
observed: !queued,
executed: false,
executorAdapter: state.plan.executorAdapter,
taskType: state.plan.taskType,
};
return {
status: queued ? 'waiting' : 'succeeded',
phase: queued ? 'executor_queued' : 'completed',
result,
events: [eventFor(
state,
queued ? 'workflow_executor_queued' : 'workflow_completed',
result,
)],
};
}
export function createCodeRunShadowGraph({
checkpointer,
executorGateway,
executionEnabled = false,
} = {}) {
if (!checkpointer) throw new Error('Code run shadow graph requires a checkpointer');
if (!executorGateway?.createJob) {
throw new Error('Code run shadow graph requires an Executor Gateway');
}
return new StateGraph(ShadowState)
.addNode('validate_run', (state) => validateNode(state, { executionEnabled }))
.addNode('build_plan', (state) => planNode(state, executorGateway))
.addNode('finalize_run', finalizeNode)
.addEdge(START, 'validate_run')
.addEdge('validate_run', 'build_plan')
.addEdge('build_plan', 'finalize_run')
.addEdge('finalize_run', END)
.compile({ checkpointer });
}
export const codeRunShadowGraphInternals = {
ShadowState,
executorIdentityForRun,
eventFor,
validateNode,
planNode,
finalizeNode,
};