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; }, }; }