UNPKG

@sema-agent/core

Version:

Stateless, task-oriented AI agent core

367 lines (366 loc) • 13 kB
export class WorkflowScriptError extends Error { code = "workflow.script_error"; constructor(message) { super(message); this.name = "WorkflowScriptError"; } } const FORBIDDEN_KEYS = new Set(["__proto__", "constructor", "prototype"]); function sliceMetaObject(source) { const decl = /export\s+const\s+meta\s*=\s*/.exec(source); if (!decl) { throw new WorkflowScriptError("workflow script must declare `export const meta = { name, description }`"); } let i = decl.index + decl[0].length; if (source[i] !== "{") { throw new WorkflowScriptError("`export const meta` must be assigned a literal object `{ ... }`"); } const start = i; let depth = 0; for (; i < source.length; i++) { const c = source[i]; if (c === '"' || c === "'" || c === "`") { i = skipString(source, i, c); continue; } if (c === "/" && source[i + 1] === "/") { i = source.indexOf("\n", i); if (i === -1) i = source.length; continue; } if (c === "/" && source[i + 1] === "*") { const end = source.indexOf("*/", i + 2); i = end === -1 ? source.length : end + 1; continue; } if (c === "{") depth++; else if (c === "}") { depth--; if (depth === 0) return { objText: source.slice(start, i + 1), declStart: decl.index, objEnd: i + 1 }; } } throw new WorkflowScriptError("`export const meta` object literal is not closed (unbalanced braces)"); } function skipString(source, start, quote) { for (let i = start + 1; i < source.length; i++) { const c = source[i]; if (c === "\\") { i++; continue; } if (c === quote) return i; } throw new WorkflowScriptError("unterminated string literal in `meta`"); } function tokenize(src) { const toks = []; let i = 0; while (i < src.length) { const c = src[i]; if (c === " " || c === "\t" || c === "\n" || c === "\r") { i++; continue; } if (c === "/" && src[i + 1] === "/") { i = src.indexOf("\n", i); if (i === -1) i = src.length; continue; } if (c === "/" && src[i + 1] === "*") { const end = src.indexOf("*/", i + 2); i = end === -1 ? src.length : end + 2; continue; } if (c === "{" || c === "}" || c === "[" || c === "]" || c === ":" || c === ",") { toks.push({ t: c }); i++; continue; } if (c === "`") { throw new WorkflowScriptError("`meta` may not contain template literals — use a plain string"); } if (c === '"' || c === "'") { const end = skipString(src, i, c); toks.push({ t: "str", v: decodeStringLiteral(src.slice(i, end + 1)) }); i = end + 1; continue; } if (c === "-" || (c >= "0" && c <= "9")) { const m = /^-?(?:0|[1-9][0-9]*)(?:\.[0-9]+)?(?:[eE][+-]?[0-9]+)?/.exec(src.slice(i)); if (!m) throw new WorkflowScriptError("invalid number literal in `meta`"); toks.push({ t: "num", v: Number(m[0]) }); i += m[0].length; continue; } const im = /^[A-Za-z_$][A-Za-z0-9_$]*/.exec(src.slice(i)); if (!im) { throw new WorkflowScriptError(`unexpected character in \`meta\`: ${JSON.stringify(c)} (only literal values are allowed)`); } toks.push({ t: "ident", v: im[0] }); i += im[0].length; } return toks; } function decodeStringLiteral(raw) { const body = raw.slice(1, -1); return body.replace(/\\(u\{[0-9a-fA-F]+\}|u[0-9a-fA-F]{4}|x[0-9a-fA-F]{2}|[\s\S])/g, (_m, esc) => { switch (esc[0]) { case "n": return "\n"; case "t": return "\t"; case "r": return "\r"; case "b": return "\b"; case "f": return "\f"; case "v": return "\v"; case "0": return "\0"; case "x": return String.fromCodePoint(parseInt(esc.slice(1), 16)); case "u": return esc[1] === "{" ? String.fromCodePoint(parseInt(esc.slice(2, -1), 16)) : String.fromCodePoint(parseInt(esc.slice(1), 16)); default: return esc; } }); } const MAX_META_DEPTH = 64; function parseTokens(toks) { let p = 0; let depth = 0; const peek = () => toks[p]; const next = () => { const t = toks[p++]; if (!t) throw new WorkflowScriptError("unexpected end of `meta` object"); return t; }; const parseValue = () => { const t = next(); switch (t.t) { case "{": return parseObject(); case "[": return parseArray(); case "str": return t.v; case "num": return t.v; case "ident": if (t.v === "true") return true; if (t.v === "false") return false; if (t.v === "null") return null; throw new WorkflowScriptError(`\`meta\` may only contain literal values — \`${t.v}\` is an identifier reference (not allowed)`); default: throw new WorkflowScriptError(`unexpected token "${t.t}" while reading a \`meta\` value`); } }; const parseObject = () => { depth++; try { if (depth > MAX_META_DEPTH) throw new WorkflowScriptError(`\`meta\` nests too deeply (max ${MAX_META_DEPTH})`); return parseObjectBody(); } finally { depth--; } }; const parseObjectBody = () => { const obj = {}; if (peek()?.t === "}") { p++; return obj; } for (;;) { const keyTok = next(); let key; if (keyTok.t === "ident") key = keyTok.v; else if (keyTok.t === "str") key = keyTok.v; else if (keyTok.t === "[") { throw new WorkflowScriptError("`meta` may not use computed keys `[ ... ]`"); } else { throw new WorkflowScriptError(`invalid \`meta\` object key (token "${keyTok.t}")`); } if (FORBIDDEN_KEYS.has(key)) { throw new WorkflowScriptError(`forbidden key in \`meta\`: ${JSON.stringify(key)}`); } const sep = next(); if (sep.t !== ":") { throw new WorkflowScriptError(`\`meta\` keys must be \`key: value\` literals (no shorthand / methods / getters) — near "${key}"`); } const value = parseValue(); Object.defineProperty(obj, key, { value, enumerable: true, writable: true, configurable: true }); const after = next(); if (after.t === "}") return obj; if (after.t !== ",") throw new WorkflowScriptError('expected "," or "}" in `meta` object'); if (peek()?.t === "}") { p++; return obj; } } }; const parseArray = () => { depth++; try { if (depth > MAX_META_DEPTH) throw new WorkflowScriptError(`\`meta\` nests too deeply (max ${MAX_META_DEPTH})`); return parseArrayBody(); } finally { depth--; } }; const parseArrayBody = () => { const arr = []; if (peek()?.t === "]") { p++; return arr; } for (;;) { arr.push(parseValue()); const after = next(); if (after.t === "]") return arr; if (after.t !== ",") throw new WorkflowScriptError('expected "," or "]" in `meta` array'); if (peek()?.t === "]") { p++; return arr; } } }; const root = parseValue(); if (p !== toks.length) { throw new WorkflowScriptError("trailing tokens after the `meta` object literal"); } return root; } export function parseWorkflowMeta(source) { const { objText } = sliceMetaObject(source); const data = parseTokens(tokenize(objText)); if (typeof data !== "object" || data === null || Array.isArray(data)) { throw new WorkflowScriptError("`meta` must be an object literal"); } const m = data; if (typeof m.name !== "string" || m.name.length === 0) { throw new WorkflowScriptError("`meta.name` is required and must be a non-empty string"); } if (typeof m.description !== "string" || m.description.length === 0) { throw new WorkflowScriptError("`meta.description` is required and must be a non-empty string"); } const meta = { name: m.name, description: m.description }; if (m.whenToUse !== undefined) { if (typeof m.whenToUse !== "string") throw new WorkflowScriptError("`meta.whenToUse` must be a string"); meta.whenToUse = m.whenToUse; } if (m.phases !== undefined) { if (!Array.isArray(m.phases)) throw new WorkflowScriptError("`meta.phases` must be an array"); meta.phases = m.phases.map((ph, idx) => { if (typeof ph !== "object" || ph === null || Array.isArray(ph)) { throw new WorkflowScriptError(`\`meta.phases[${idx}]\` must be an object`); } const phase = ph; if (typeof phase.title !== "string") { throw new WorkflowScriptError(`\`meta.phases[${idx}].title\` must be a string`); } if (phase.detail !== undefined && typeof phase.detail !== "string") { throw new WorkflowScriptError(`\`meta.phases[${idx}].detail\` must be a string`); } if (phase.model !== undefined && typeof phase.model !== "string") { throw new WorkflowScriptError(`\`meta.phases[${idx}].model\` must be a string`); } return { title: phase.title, ...(phase.detail !== undefined ? { detail: phase.detail } : {}), ...(phase.model !== undefined ? { model: phase.model } : {}), }; }); } return meta; } export function stripStringsAndComments(src) { let out = ""; let i = 0; const n = src.length; while (i < n) { const c = src[i]; const d = i + 1 < n ? src[i + 1] : ""; if (c === "/" && d === "/") { while (i < n && src[i] !== "\n") i++; continue; } if (c === "/" && d === "*") { i += 2; while (i < n && !(src[i] === "*" && src[i + 1] === "/")) i++; i += 2; continue; } if (c === '"' || c === "'") { i++; while (i < n && src[i] !== c) { if (src[i] === "\\") i++; i++; } i++; out += " "; continue; } if (c === "`") { i++; while (i < n && src[i] !== "`") { if (src[i] === "\\") { i += 2; continue; } if (src[i] === "$" && src[i + 1] === "{") { i += 2; let depth = 1; const start = i; while (i < n && depth > 0) { if (src[i] === "{") depth++; else if (src[i] === "}") depth--; if (depth > 0) i++; } out += ` ${stripStringsAndComments(src.slice(start, i))} `; i++; continue; } i++; } i++; out += " "; continue; } out += c; i++; } return out; } export function workflowScriptReadsClockOrRandom(body) { const code = stripStringsAndComments(body); return /\bDate\s*\.\s*now\b/.test(code) || /\bMath\s*\.\s*random\b/.test(code) || /\bnew\s+Date\s*\(\s*\)/.test(code); } export function splitWorkflowMeta(source) { const meta = parseWorkflowMeta(source); const { declStart, objEnd } = sliceMetaObject(source); let end = objEnd; while (end < source.length && (source[end] === ";" || source[end] === " " || source[end] === "\t")) end++; return { meta, body: source.slice(0, declStart) + source.slice(end) }; }