@sema-agent/core
Version:
Stateless, task-oriented AI agent core
282 lines (281 loc) • 11.2 kB
JavaScript
import { selectModel } from "./select-model.js";
import { MODEL_ROLES } from "./types.js";
export { MODEL_ROLES } from "./types.js";
function catalogModel(ref, models) {
const m = models != null && (typeof models === "object" || typeof models === "function") && Object.hasOwn(models, ref) ? models[ref] : undefined;
return m || undefined;
}
function modelBindingRefusal(seat, reason, message) {
const e = new Error(message);
e.code = "config.unknown_role";
e.detail = { field: seat, reason };
return e;
}
function bindingShown(value) {
if (typeof value === "string")
return JSON.stringify(value.length > 80 ? `${value.slice(0, 80)}…` : value);
return value === null ? "null" : Array.isArray(value) ? "a list" : `a ${typeof value}`;
}
function unknownModelRefRefusal(ref, seat) {
return modelBindingRefusal(seat, "unknown_model_ref", `Unknown model ref ${bindingShown(ref)} in ${seat}. Provide it in RunnerDeps.models or pass a Model object.`);
}
export function resolveModel(ref, models) {
if (typeof ref !== "string") {
return ref;
}
const m = catalogModel(ref, models);
if (!m) {
throw new Error(`Unknown model ref "${ref}". Provide it in RunnerDeps.models or pass a Model object.`);
}
return m;
}
export const DEFAULT_TIER_ORDER = ["ultra", "max", "pro", "flash", "lite"];
export const CC_MODEL_TIER_ALIASES = {
fable: "max",
mythos: "max",
opus: "pro",
sonnet: "flash",
haiku: "lite",
};
export function resolveModelDisplayLabel(ref) {
return Object.hasOwn(CC_MODEL_TIER_ALIASES, ref) ? CC_MODEL_TIER_ALIASES[ref] : ref;
}
const TIERS_ACTIVE = Symbol.for("sema.tiersActive");
const TIER_ALIAS_VIA = Symbol.for("sema.tierAliasVia");
export function expandTiers(models, tiers) {
if (!tiers || Object.keys(tiers).length === 0)
return models;
const out = { ...models };
out[TIERS_ACTIVE] = true;
const aliasVia = new Map();
out[TIER_ALIAS_VIA] = aliasVia;
const bound = (name) => {
const b = Object.hasOwn(tiers, name) ? tiers[name] : undefined;
if (b === undefined)
return undefined;
const seat = `tiers.${name}`;
if (typeof b === "string") {
const held = catalogModel(b, models);
if (held === undefined)
throw unknownModelRefRefusal(b, seat);
return held;
}
if (typeof b !== "object" || b === null || Array.isArray(b)) {
throw modelBindingRefusal(seat, "not_a_model_ref", `${seat} is ${bindingShown(b)}, not a model reference — a tier binds a catalog model name or a Model object.`);
}
return b;
};
const ownKey = (k) => models !== undefined && Object.hasOwn(models, k) && Boolean(models[k]);
const landing = (t, self) => {
const m = bound(t);
if (!m)
return undefined;
const b = Object.hasOwn(tiers, t) ? tiers[t] : undefined;
return { model: m, via: [...(t !== self && !ownKey(t) ? [t] : []), ...(typeof b === "string" ? [b] : [])] };
};
const resolveTier = (name, self = name) => {
const direct = landing(name, self);
if (direct)
return direct;
const at = DEFAULT_TIER_ORDER.indexOf(name);
if (at === -1)
return undefined;
for (let i = at + 1; i < DEFAULT_TIER_ORDER.length; i++) {
const next = landing(DEFAULT_TIER_ORDER[i], self);
if (next)
return { model: next.model, via: [...(name !== self && !ownKey(name) ? [name] : []), ...next.via] };
}
return undefined;
};
const shadowed = (key) => Object.hasOwn(out, key) && Boolean(out[key]);
const put = (key, m, via) => {
Object.defineProperty(out, key, { value: m, writable: true, enumerable: true, configurable: true });
aliasVia.set(key, via);
};
const tierNames = new Set([...DEFAULT_TIER_ORDER, ...Object.keys(tiers)]);
for (const name of tierNames) {
if (shadowed(name))
continue;
const r = resolveTier(name);
if (r)
put(name, r.model, r.via);
}
for (const [alias, tier] of Object.entries(CC_MODEL_TIER_ALIASES)) {
if (shadowed(alias))
continue;
const r = resolveTier(tier, alias);
if (r)
put(alias, r.model, r.via);
}
if (!shadowed("best")) {
for (const name of DEFAULT_TIER_ORDER) {
const r = landing(name, "best");
if (r) {
put("best", r.model, r.via);
break;
}
}
}
return out;
}
export function tierAliasVia(word, models) {
const via = models?.[TIER_ALIAS_VIA];
return via?.get(word) ?? [];
}
export function parseModelMention(text, allowedNames) {
const allowed = new Map();
for (const n of allowedNames)
allowed.set(n.toLowerCase(), n);
const re = /(^|\s)@([A-Za-z0-9._/-]+)/g;
let m;
while ((m = re.exec(text)) !== null) {
const raw = m[2];
const trimmed = raw.replace(/[.,!?:;]+$/, "");
const canon = allowed.get(trimmed.toLowerCase());
if (canon) {
const at = m.index + m[1].length;
const end = at + 1 + trimmed.length;
const cleaned = (text.slice(0, at) + text.slice(end)).replace(/[ \t]{2,}/g, " ").trim();
return { model: canon, cleanedText: cleaned };
}
}
return { cleanedText: text.trim() };
}
export const ROLE_TIER_DEFAULTS = {
default: "pro",
summarize: "flash",
verifier: "pro",
classifier: "flash",
};
function roleTierModel(role, models) {
if (!models || !models[TIERS_ACTIVE])
return undefined;
const tier = ROLE_TIER_DEFAULTS[role];
return tier !== undefined ? models[tier] : undefined;
}
const FALLBACK = {
default: [],
summarize: ["default"],
subagent: ["default"],
verifier: ["default"],
classifier: ["summarize", "default"],
consolidate: ["summarize"],
};
function specParts(s, seat = "a role seat") {
if (s == null) {
return {};
}
if (typeof s === "string") {
return { model: s };
}
if (typeof s !== "object" && typeof s !== "function") {
throw modelBindingRefusal(seat, "not_a_model_ref", `${seat} is ${bindingShown(s)}, not a model reference — a role binds a catalog model name, a Model object or a { model, select, thinking, systemPrompt } record.`);
}
if ("id" in s && "api" in s) {
return { model: s };
}
const cfg = s;
return { model: cfg.model, select: cfg.select, thinking: cfg.thinking, systemPrompt: cfg.systemPrompt };
}
export function roleSpecNamesModel(spec) {
const parts = specParts(spec);
return parts.model !== undefined || parts.select !== undefined;
}
function toConfig(s) {
if (s == null) {
return undefined;
}
if (typeof s === "string" || ("id" in s && "api" in s)) {
return { model: s };
}
return s;
}
function mergeRoleSpec(base, over) {
const b = toConfig(base);
const o = toConfig(over);
if (!o)
return base;
if (!b)
return over;
const overHasSource = o.model !== undefined || o.select !== undefined;
return {
model: overHasSource ? o.model : b.model,
select: overHasSource ? o.select : b.select,
thinking: o.thinking ?? b.thinking,
systemPrompt: o.systemPrompt ?? b.systemPrompt,
};
}
function assertKnownRole(word, seat) {
if (typeof word === "string" && MODEL_ROLES.includes(word))
return;
const shown = typeof word === "string" ? JSON.stringify(word).slice(0, 80) : `<${word === null ? "null" : typeof word}>`;
const e = new Error(`${seat} names the model role ${shown}, which is not one of ${MODEL_ROLES.join(" / ")} — a word outside the vocabulary binds nothing and resolves nothing; bind the model on one of these roles, or name it on TaskSpec.model`);
e.code = "config.unknown_role";
throw e;
}
function mergeRoles(base, over) {
for (const key of Object.keys(base ?? {}))
assertKnownRole(key, "RunnerDeps.roles key");
for (const key of Object.keys(over ?? {}))
assertKnownRole(key, "TaskSpec.roles key");
const out = { ...base };
for (const role of Object.keys(over ?? {})) {
out[role] = mergeRoleSpec(base?.[role], over?.[role]);
}
return out;
}
function partsModel(parts, models, seat = "a role seat") {
if (parts.model) {
return resolveModel(parts.model, models);
}
if (parts.select) {
const needs = parts.select.needs;
if (needs != null && !Array.isArray(needs)) {
throw modelBindingRefusal(`${seat}.select`, "not_a_model_ref", `${seat}.select.needs is ${bindingShown(needs)}, not a list of capabilities (vision / reasoning).`);
}
const held = models != null && typeof models === "object" ? Object.values(models).filter(Boolean) : [];
return selectModel(held, parts.select);
}
return undefined;
}
export function resolveTaskModel(spec, deps) {
const start = spec.modelRole ?? "default";
assertKnownRole(start, "modelRole");
if (spec.model) {
const startParts = specParts(mergeRoles(deps.roles, spec.roles)[start], `roles.${start}`);
return {
model: resolveModel(spec.model, deps.models),
modelRef: spec.model,
thinking: spec.thinking ?? startParts.thinking,
systemPrompt: startParts.systemPrompt,
};
}
const merged = mergeRoles(deps.roles, spec.roles);
const startSystemPrompt = specParts(merged[start], `roles.${start}`).systemPrompt;
for (const role of [start, ...FALLBACK[start]]) {
const parts = specParts(merged[role], `roles.${role}`);
const fromParts = partsModel(parts, deps.models, `roles.${role}`);
const model = fromParts ?? roleTierModel(role, deps.models);
if (model) {
const modelRef = fromParts !== undefined ? (parts.model ?? model) : (ROLE_TIER_DEFAULTS[role] ?? model);
return { model, modelRef, thinking: spec.thinking ?? parts.thinking, systemPrompt: startSystemPrompt };
}
}
throw new Error(`No model for role "${start}": set TaskSpec.model, or configure RunnerDeps.roles / TaskSpec.roles["${start}"] (or a "default" role). A "select" RoleSpec also fails here if no catalog model matches.`);
}
export function roleModelIfSet(role, spec, deps) {
const merged = mergeRoles(deps.roles, spec.roles);
const seat = `roles.${role}`;
const parts = specParts(merged[role], seat);
const named = parts.model;
if (named) {
if (typeof named === "string") {
if (catalogModel(named, deps.models) === undefined)
throw unknownModelRefRefusal(named, seat);
}
else if (typeof named !== "object" || Array.isArray(named)) {
throw modelBindingRefusal(seat, "not_a_model_ref", `${seat}.model is ${bindingShown(named)}, not a model reference — a role binds a catalog model name or a Model object.`);
}
}
return partsModel(parts, deps.models, seat) ?? roleTierModel(role, deps.models);
}