feat: add shadow LangGraph orchestrator runtime
This commit is contained in:
@@ -0,0 +1,128 @@
|
||||
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 eventFor(state, type, data = null) {
|
||||
return buildRunEvent({
|
||||
runId: state.spec.runId,
|
||||
sequence: state.events.length + 1,
|
||||
type,
|
||||
data,
|
||||
});
|
||||
}
|
||||
|
||||
function validateNode(state) {
|
||||
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;
|
||||
}
|
||||
if (spec.policy.executionMode !== 'observe-only' || spec.policy.sideEffectsAllowed !== false) {
|
||||
const error = new Error('Phase 2 LangGraph service accepts observe-only runs');
|
||||
error.code = 'WORKFLOW_OBSERVE_ONLY_REQUIRED';
|
||||
throw error;
|
||||
}
|
||||
return {
|
||||
spec,
|
||||
status: 'running',
|
||||
phase: 'validated',
|
||||
events: [eventFor(state, 'workflow_validated', {
|
||||
workflow: spec.workflow,
|
||||
executionMode: 'observe-only',
|
||||
})],
|
||||
};
|
||||
}
|
||||
|
||||
function planNode(state) {
|
||||
const taskType = String(state.spec.input?.taskType ?? '').trim() || 'code-change';
|
||||
const instruction = String(state.spec.input?.instruction ?? '');
|
||||
const plan = {
|
||||
taskType,
|
||||
executorAdapter: 'native-agent-run',
|
||||
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,
|
||||
stepCount: plan.steps.length,
|
||||
})],
|
||||
};
|
||||
}
|
||||
|
||||
function finalizeNode(state) {
|
||||
const result = {
|
||||
observed: true,
|
||||
executed: false,
|
||||
executorAdapter: state.plan.executorAdapter,
|
||||
taskType: state.plan.taskType,
|
||||
};
|
||||
return {
|
||||
status: 'succeeded',
|
||||
phase: 'completed',
|
||||
result,
|
||||
events: [eventFor(state, 'workflow_completed', result)],
|
||||
};
|
||||
}
|
||||
|
||||
export function createCodeRunShadowGraph({ checkpointer } = {}) {
|
||||
if (!checkpointer) throw new Error('Code run shadow graph requires a checkpointer');
|
||||
return new StateGraph(ShadowState)
|
||||
.addNode('validate_run', validateNode)
|
||||
.addNode('build_plan', planNode)
|
||||
.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,
|
||||
eventFor,
|
||||
validateNode,
|
||||
planNode,
|
||||
finalizeNode,
|
||||
};
|
||||
Reference in New Issue
Block a user