UNPKG

@sema-agent/core

Version:

Stateless, task-oriented AI agent core

223 lines (222 loc) • 7.9 kB
import { selectModel } from "./select-model.js"; export { MODEL_ROLES } from "./types.js"; export function resolveModel(ref, models) { if (typeof ref !== "string") { return ref; } const m = models !== undefined && Object.hasOwn(models, ref) ? models[ref] : undefined; 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; return b === undefined ? undefined : resolveModel(b, models); }; 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", advisor: DEFAULT_TIER_ORDER[0], 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"], team: ["default"], synthesize: ["team", "default"], advisor: ["default"], verifier: ["default"], classifier: ["summarize", "default"], consolidate: ["summarize"], }; function specParts(s) { if (s == null) { return {}; } if (typeof s === "string") { return { model: s }; } 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 mergeRoles(base, over) { const out = { ...base }; for (const role of Object.keys(over ?? {})) { out[role] = mergeRoleSpec(base?.[role], over?.[role]); } return out; } function partsModel(parts, models) { if (parts.model) { return resolveModel(parts.model, models); } if (parts.select) { return selectModel(models ?? {}, parts.select); } return undefined; } export function resolveTaskModel(spec, deps) { if (spec.model) { const start = spec.modelRole ?? "default"; const startParts = specParts(mergeRoles(deps.roles, spec.roles)[start]); return { model: resolveModel(spec.model, deps.models), modelRef: spec.model, thinking: spec.thinking ?? startParts.thinking, systemPrompt: startParts.systemPrompt, }; } const start = spec.modelRole ?? "default"; const merged = mergeRoles(deps.roles, spec.roles); const startSystemPrompt = specParts(merged[start]).systemPrompt; for (const role of [start, ...FALLBACK[start]]) { const parts = specParts(merged[role]); const fromParts = partsModel(parts, deps.models); 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); return partsModel(specParts(merged[role]), deps.models) ?? roleTierModel(role, deps.models); }