feat: add pluggable workflow orchestrator controls
This commit is contained in:
@@ -0,0 +1,104 @@
|
||||
import crypto from 'node:crypto';
|
||||
|
||||
export const ORCHESTRATOR_MODE = Object.freeze({
|
||||
OFF: 'off',
|
||||
SHADOW: 'shadow',
|
||||
CANARY: 'canary',
|
||||
ACTIVE: 'active',
|
||||
});
|
||||
|
||||
export const WORKFLOW_ENGINE = Object.freeze({
|
||||
NATIVE: 'native',
|
||||
LANGGRAPH: 'langgraph',
|
||||
});
|
||||
|
||||
export const DEFAULT_ORCHESTRATED_WORKFLOWS = Object.freeze([
|
||||
'code-run-v1',
|
||||
]);
|
||||
|
||||
const ENGINE_ID_PATTERN = /^[a-z][a-z0-9_-]{0,63}$/;
|
||||
const WORKFLOW_NAME_PATTERN = /^[a-z][a-z0-9_-]{0,127}$/;
|
||||
|
||||
function normalizeIdentifier(value, pattern, fallback) {
|
||||
const normalized = String(value ?? '').trim().toLowerCase();
|
||||
return pattern.test(normalized) ? normalized : fallback;
|
||||
}
|
||||
|
||||
export function normalizeWorkflowEngineId(value, fallback = WORKFLOW_ENGINE.NATIVE) {
|
||||
return normalizeIdentifier(value, ENGINE_ID_PATTERN, fallback);
|
||||
}
|
||||
|
||||
export function normalizeWorkflowName(value, fallback = '') {
|
||||
return normalizeIdentifier(value, WORKFLOW_NAME_PATTERN, fallback);
|
||||
}
|
||||
|
||||
export function normalizeStringList(value, {
|
||||
limit = 200,
|
||||
itemLimit = 128,
|
||||
normalize = (item) => String(item ?? '').trim(),
|
||||
} = {}) {
|
||||
if (!Array.isArray(value)) return [];
|
||||
return [...new Set(value
|
||||
.map((item) => normalize(item).slice(0, itemLimit))
|
||||
.filter(Boolean))]
|
||||
.slice(0, limit);
|
||||
}
|
||||
|
||||
export function normalizeRunSpec(input = {}) {
|
||||
const source = input && typeof input === 'object' && !Array.isArray(input) ? input : {};
|
||||
const requestId = String(source.requestId ?? '').trim();
|
||||
const runId = String(source.runId ?? crypto.randomUUID()).trim();
|
||||
const workflowName = normalizeWorkflowName(
|
||||
source.workflow?.name ?? source.workflowName,
|
||||
);
|
||||
if (!requestId) throw new Error('RunSpec requires requestId');
|
||||
if (!runId) throw new Error('RunSpec requires runId');
|
||||
if (!workflowName) throw new Error('RunSpec requires workflow.name');
|
||||
return {
|
||||
version: 'orchestrator-run-v1',
|
||||
runId,
|
||||
requestId,
|
||||
workflow: {
|
||||
name: workflowName,
|
||||
version: Math.max(1, Number(source.workflow?.version ?? 1) || 1),
|
||||
},
|
||||
subject: {
|
||||
tenantId: String(source.subject?.tenantId ?? '').trim() || null,
|
||||
userId: String(source.subject?.userId ?? '').trim() || null,
|
||||
},
|
||||
input: source.input && typeof source.input === 'object' ? structuredClone(source.input) : {},
|
||||
policy: source.policy && typeof source.policy === 'object' ? structuredClone(source.policy) : {},
|
||||
limits: source.limits && typeof source.limits === 'object' ? structuredClone(source.limits) : {},
|
||||
metadata: source.metadata && typeof source.metadata === 'object'
|
||||
? structuredClone(source.metadata)
|
||||
: {},
|
||||
};
|
||||
}
|
||||
|
||||
export function buildRunEvent({
|
||||
eventId = crypto.randomUUID(),
|
||||
runId,
|
||||
sequence,
|
||||
type,
|
||||
timestamp = Date.now(),
|
||||
data = null,
|
||||
} = {}) {
|
||||
const normalizedRunId = String(runId ?? '').trim();
|
||||
const normalizedType = String(type ?? '').trim();
|
||||
if (!normalizedRunId) throw new Error('RunEvent requires runId');
|
||||
if (!normalizedType) throw new Error('RunEvent requires type');
|
||||
return {
|
||||
version: 'orchestrator-event-v1',
|
||||
eventId: String(eventId),
|
||||
runId: normalizedRunId,
|
||||
sequence: Math.max(0, Number(sequence ?? 0) || 0),
|
||||
type: normalizedType,
|
||||
timestamp: Number(timestamp) || Date.now(),
|
||||
data: data == null ? null : structuredClone(data),
|
||||
};
|
||||
}
|
||||
|
||||
export function stableRolloutBucket(value) {
|
||||
const hash = crypto.createHash('sha256').update(String(value ?? '')).digest();
|
||||
return hash.readUInt32BE(0) % 100;
|
||||
}
|
||||
Reference in New Issue
Block a user