UNPKG

@sema-agent/core

Version:

Stateless, task-oriented AI agent core

282 lines (281 loc) • 11.2 kB
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); }