144 lines
4.7 KiB
JavaScript
144 lines
4.7 KiB
JavaScript
import {
|
|
WORKFLOW_ENGINE,
|
|
normalizeWorkflowEngineId,
|
|
} from './contracts.mjs';
|
|
|
|
const REQUIRED_ENGINE_METHODS = Object.freeze([
|
|
'start',
|
|
'resume',
|
|
'cancel',
|
|
'getState',
|
|
'streamEvents',
|
|
]);
|
|
|
|
function validateEngine(engine) {
|
|
if (!engine || typeof engine !== 'object') {
|
|
throw new Error('Workflow engine must be an object');
|
|
}
|
|
const id = normalizeWorkflowEngineId(engine.id, '');
|
|
if (!id) throw new Error('Workflow engine requires a valid id');
|
|
const missing = REQUIRED_ENGINE_METHODS.filter((method) => typeof engine[method] !== 'function');
|
|
if (missing.length) {
|
|
throw new Error(`Workflow engine ${id} missing methods: ${missing.join(', ')}`);
|
|
}
|
|
return id;
|
|
}
|
|
|
|
export function createWorkflowEngineRegistry(initialEngines = []) {
|
|
const engines = new Map();
|
|
|
|
function register(engine) {
|
|
const id = validateEngine(engine);
|
|
if (engines.has(id)) throw new Error(`Workflow engine already registered: ${id}`);
|
|
engines.set(id, engine);
|
|
return engine;
|
|
}
|
|
|
|
for (const engine of initialEngines) register(engine);
|
|
|
|
return {
|
|
register,
|
|
get(id) {
|
|
return engines.get(normalizeWorkflowEngineId(id, '')) ?? null;
|
|
},
|
|
has(id) {
|
|
return engines.has(normalizeWorkflowEngineId(id, ''));
|
|
},
|
|
list() {
|
|
return [...engines.values()].map((engine) => ({
|
|
id: engine.id,
|
|
label: engine.label ?? engine.id,
|
|
kind: engine.kind ?? 'plugin',
|
|
capabilities: Array.isArray(engine.capabilities) ? [...engine.capabilities] : [],
|
|
}));
|
|
},
|
|
};
|
|
}
|
|
|
|
function createHttpError(response, action) {
|
|
const error = new Error(`Workflow engine ${action} failed with HTTP ${response?.status ?? 'unknown'}`);
|
|
error.code = 'WORKFLOW_ENGINE_HTTP_ERROR';
|
|
error.status = response?.status ?? null;
|
|
return error;
|
|
}
|
|
|
|
function joinUrl(baseUrl, pathname) {
|
|
return `${String(baseUrl).replace(/\/$/, '')}/${String(pathname).replace(/^\//, '')}`;
|
|
}
|
|
|
|
export function createRemoteWorkflowEngine({
|
|
id = WORKFLOW_ENGINE.LANGGRAPH,
|
|
label = 'LangGraph',
|
|
baseUrl,
|
|
serviceToken = null,
|
|
timeoutMs = 5000,
|
|
fetchImpl = globalThis.fetch,
|
|
} = {}) {
|
|
const normalizedId = normalizeWorkflowEngineId(id, '');
|
|
const normalizedBaseUrl = String(baseUrl ?? '').trim().replace(/\/$/, '');
|
|
if (!normalizedId) throw new Error('Remote workflow engine requires a valid id');
|
|
if (!normalizedBaseUrl) throw new Error(`Remote workflow engine ${normalizedId} requires baseUrl`);
|
|
if (typeof fetchImpl !== 'function') throw new Error('Remote workflow engine requires fetch');
|
|
|
|
async function request(pathname, init = {}) {
|
|
const controller = new AbortController();
|
|
const timer = setTimeout(() => controller.abort(), Math.max(100, Number(timeoutMs) || 5000));
|
|
try {
|
|
const response = await fetchImpl(joinUrl(normalizedBaseUrl, pathname), {
|
|
...init,
|
|
headers: {
|
|
accept: 'application/json',
|
|
...(init.body ? { 'content-type': 'application/json' } : {}),
|
|
...(serviceToken ? { authorization: `Bearer ${serviceToken}` } : {}),
|
|
...(init.headers ?? {}),
|
|
},
|
|
signal: controller.signal,
|
|
});
|
|
if (!response?.ok) throw createHttpError(response, pathname);
|
|
return response.json();
|
|
} finally {
|
|
clearTimeout(timer);
|
|
}
|
|
}
|
|
|
|
return {
|
|
id: normalizedId,
|
|
label,
|
|
kind: 'remote',
|
|
capabilities: ['durable-execution', 'interrupt', 'streaming', 'executor-observability'],
|
|
async start(spec) {
|
|
return request('/v1/runs', { method: 'POST', body: JSON.stringify(spec) });
|
|
},
|
|
async resume(runId, input) {
|
|
return request(`/v1/runs/${encodeURIComponent(runId)}/resume`, {
|
|
method: 'POST',
|
|
body: JSON.stringify(input ?? {}),
|
|
});
|
|
},
|
|
async cancel(runId, input = {}) {
|
|
return request(`/v1/runs/${encodeURIComponent(runId)}/cancel`, {
|
|
method: 'POST',
|
|
body: JSON.stringify(input),
|
|
});
|
|
},
|
|
async getState(runId) {
|
|
return request(`/v1/runs/${encodeURIComponent(runId)}`);
|
|
},
|
|
async *streamEvents(runId, cursor = null) {
|
|
const query = cursor == null ? '' : `?after=${encodeURIComponent(cursor)}`;
|
|
const result = await request(`/v1/runs/${encodeURIComponent(runId)}/events${query}`);
|
|
for (const event of Array.isArray(result?.events) ? result.events : []) yield event;
|
|
},
|
|
async getExecutorJob(jobId) {
|
|
return request(`/v1/executor-jobs/${encodeURIComponent(jobId)}`);
|
|
},
|
|
async *streamExecutorJobEvents(jobId, cursor = null) {
|
|
const query = cursor == null ? '' : `?after=${encodeURIComponent(cursor)}`;
|
|
const result = await request(
|
|
`/v1/executor-jobs/${encodeURIComponent(jobId)}/events${query}`,
|
|
);
|
|
for (const event of Array.isArray(result?.events) ? result.events : []) yield event;
|
|
},
|
|
};
|
|
}
|