@sema-agent/core
Version:
Stateless, task-oriented AI agent core
367 lines (366 loc) • 13 kB
JavaScript
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) };
}