feat: add shadow LangGraph orchestrator runtime
This commit is contained in:
@@ -0,0 +1,113 @@
|
||||
import express from 'express';
|
||||
|
||||
function errorStatus(error) {
|
||||
if (Number.isInteger(error?.status)) return error.status;
|
||||
if (['WORKFLOW_NOT_SUPPORTED', 'WORKFLOW_OBSERVE_ONLY_REQUIRED'].includes(error?.code)) {
|
||||
return 422;
|
||||
}
|
||||
return 500;
|
||||
}
|
||||
|
||||
function authorize(serviceToken) {
|
||||
const expected = String(serviceToken ?? '').trim();
|
||||
return (request, response, next) => {
|
||||
if (!expected) return next();
|
||||
if (request.get('authorization') === `Bearer ${expected}`) return next();
|
||||
return response.status(401).json({
|
||||
error: {
|
||||
code: 'UNAUTHORIZED',
|
||||
message: 'Missing or invalid service token',
|
||||
},
|
||||
});
|
||||
};
|
||||
}
|
||||
|
||||
export function createOrchestratorApp({
|
||||
runtime,
|
||||
serviceToken = process.env.MEMIND_ORCHESTRATOR_SERVICE_TOKEN,
|
||||
} = {}) {
|
||||
if (!runtime) throw new Error('Orchestrator app requires runtime');
|
||||
const app = express();
|
||||
app.disable('x-powered-by');
|
||||
app.use(express.json({ limit: '256kb' }));
|
||||
|
||||
app.get('/health', (_request, response) => response.json(runtime.health()));
|
||||
app.get('/ready', async (_request, response) => {
|
||||
try {
|
||||
return response.json(await runtime.ready());
|
||||
} catch {
|
||||
return response.status(503).json({
|
||||
...runtime.health(),
|
||||
ready: false,
|
||||
});
|
||||
}
|
||||
});
|
||||
|
||||
app.use('/v1', authorize(serviceToken));
|
||||
|
||||
app.post('/v1/runs', async (request, response, next) => {
|
||||
try {
|
||||
response.status(202).json(await runtime.start(request.body));
|
||||
} catch (error) {
|
||||
next(error);
|
||||
}
|
||||
});
|
||||
|
||||
app.get('/v1/runs/:runId', async (request, response, next) => {
|
||||
try {
|
||||
const state = await runtime.getState(request.params.runId);
|
||||
if (!state) return response.status(404).json({ error: { code: 'RUN_NOT_FOUND' } });
|
||||
return response.json(state);
|
||||
} catch (error) {
|
||||
return next(error);
|
||||
}
|
||||
});
|
||||
|
||||
app.get('/v1/runs/:runId/events', async (request, response, next) => {
|
||||
try {
|
||||
const result = await runtime.listEvents(request.params.runId, {
|
||||
after: request.query.after,
|
||||
});
|
||||
if (!result) return response.status(404).json({ error: { code: 'RUN_NOT_FOUND' } });
|
||||
return response.json(result);
|
||||
} catch (error) {
|
||||
return next(error);
|
||||
}
|
||||
});
|
||||
|
||||
app.post('/v1/runs/:runId/resume', async (request, response, next) => {
|
||||
try {
|
||||
const state = await runtime.resume(request.params.runId, request.body);
|
||||
if (!state) return response.status(404).json({ error: { code: 'RUN_NOT_FOUND' } });
|
||||
return response.json(state);
|
||||
} catch (error) {
|
||||
return next(error);
|
||||
}
|
||||
});
|
||||
|
||||
app.post('/v1/runs/:runId/cancel', async (request, response, next) => {
|
||||
try {
|
||||
const state = await runtime.cancel(request.params.runId, request.body);
|
||||
if (!state) return response.status(404).json({ error: { code: 'RUN_NOT_FOUND' } });
|
||||
return response.json(state);
|
||||
} catch (error) {
|
||||
return next(error);
|
||||
}
|
||||
});
|
||||
|
||||
app.use((error, _request, response, _next) => {
|
||||
response.status(errorStatus(error)).json({
|
||||
error: {
|
||||
code: error?.code ?? 'ORCHESTRATOR_ERROR',
|
||||
message: error instanceof Error ? error.message : 'Orchestrator request failed',
|
||||
},
|
||||
});
|
||||
});
|
||||
|
||||
return app;
|
||||
}
|
||||
|
||||
export const orchestratorAppInternals = {
|
||||
authorize,
|
||||
errorStatus,
|
||||
};
|
||||
Reference in New Issue
Block a user