UNPKG

@sema-agent/core

Version:

Stateless, task-oriented AI agent core

119 lines (118 loc) • 6.63 kB
import { brainToRuntime } from "../runtime.js"; import { expandTiers, roleModelIfSet } from "../roles.js"; import { isAliasModelId, openAiCompatChatSeat } from "../memory-engine/distiller.js"; import { runMemoryConsolidationDriver } from "../memory-engine/consolidation-driver.js"; import { recommendationUnconsumed } from "../memory-engine/consolidation.js"; import { asThrownError } from "../thrown-value.js"; function seatRefusal(message) { const e = new Error(message); e.code = "config.memory_consolidation_driver"; return e; } export function resolveMemoryConsolidationDriver(deps) { const seat = deps.memoryConsolidationDriver; let chat; let model; if (typeof seat?.chat === "function") { if (typeof seat.model !== "string" || seat.model.trim() === "" || isAliasModelId(seat.model)) { throw seatRefusal(`memoryConsolidationDriver.chat is a bare function, so memoryConsolidationDriver.model must carry the EXPLICIT model id it serves (got ${JSON.stringify(seat.model)}).`); } chat = seat.chat; model = seat.model; } else if (seat?.chat !== undefined) { chat = openAiCompatChatSeat(seat.chat); model = seat.chat.model; } else { const models = expandTiers(deps.models, deps.tiers); const resolved = roleModelIfSet("consolidate", {}, { models, roles: deps.roles }) ?? roleModelIfSet("summarize", {}, { models, roles: deps.roles }); if (resolved === undefined) { throw seatRefusal('memory consolidation has no model seat: configure RunnerDeps.memoryConsolidationDriver.chat (an endpoint triple or a chat function), or declare roles.consolidate / roles.summarize (a tier-expanded catalog\'s "flash" binding also satisfies the summarize hop). The chain deliberately does NOT fall back to the default main model — a whole-library distillation must not silently ride the most expensive seat.'); } if (isAliasModelId(resolved.id)) { throw seatRefusal(`the resolved consolidation model id ${JSON.stringify(resolved.id)} is an alias spelling — an alias can be re-pointed upstream and would move the run with no trace in the archive; bind an explicit id.`); } model = resolved.id; const runtime = brainToRuntime(deps.brain); chat = async (req) => { const auth = await deps.getApiKeyAndHeaders?.(resolved); const message = await runtime.completeSimple(resolved, { systemPrompt: req.system, messages: [{ role: "user", content: req.user, timestamp: Date.now() }] }, { temperature: req.temperature, maxTokens: req.maxTokens, apiKey: auth?.apiKey, headers: auth?.headers }); if (message.stopReason === "error" || message.stopReason === "aborted") { throw new Error(message.errorMessage ?? `model completion ${message.stopReason}`); } return { content: message.content .filter((c) => c.type === "text") .map((c) => c.text) .join(""), finishReason: message.stopReason, usage: { promptTokens: message.usage.input, completionTokens: message.usage.output }, model: resolved.id, }; }; } return { chat, model, ...(seat?.contract !== undefined ? { contract: seat.contract } : {}), ...(seat?.maxCycles !== undefined ? { maxCycles: seat.maxCycles } : {}), ...(seat?.mintRepairBudget !== undefined ? { mintRepairBudget: seat.mintRepairBudget } : {}), ...(deps.memoryConsolidation !== undefined ? { consolidation: deps.memoryConsolidation } : {}), }; } export function consolidationSeatResolvable(deps) { if (deps.memoryConsolidationDriver?.chat !== undefined) return true; const models = expandTiers(deps.models, deps.tiers); return (roleModelIfSet("consolidate", {}, { models, roles: deps.roles }) ?? roleModelIfSet("summarize", {}, { models, roles: deps.roles })) !== undefined; } export function autoRunConsolidationFromRecommendation(args) { const { deps, engine, scope, sessionId, onNotice, onError } = args; return (async () => { try { await runMemoryConsolidationDriver(engine, scope, { ...resolveMemoryConsolidationDriver(deps), onNotice }); } catch (err) { try { onError?.(asThrownError(err), { phase: "memory", sessionId }); } catch { } } })(); } export const AUTO_RUN_ON_RECOMMENDATION_DEFAULT = true; export function autoRunOnRecommendationEffective(deps) { const raw = deps.autoRunOnRecommendation; if (raw === true) return true; if (raw !== undefined) return false; return AUTO_RUN_ON_RECOMMENDATION_DEFAULT && deps.memoryConsolidation !== undefined && consolidationSeatResolvable(deps); } export function scopeHasUnconsumedRecommendation(engine, scope) { const row = engine.readConsolidationGate(scope); return row !== undefined && recommendationUnconsumed(row); } function autoRunRefusal(message) { const e = new Error(message); e.code = "config.auto_consolidation"; return e; } export function assertAutoConsolidationWirable(deps) { const raw = deps.autoRunOnRecommendation; if (raw === undefined) return; if (typeof raw !== "boolean") { throw autoRunRefusal(`RunnerDeps.autoRunOnRecommendation must be a boolean when present (got ${JSON.stringify(raw)}) — a non-boolean would silently read as off, arming or disarming whole-library egress by accident.`); } if (raw !== true) return; if (deps.memoryConsolidation === undefined) { throw autoRunRefusal("RunnerDeps.autoRunOnRecommendation is true but RunnerDeps.memoryConsolidation is absent — the recommendation that would trigger a run is never minted (consolidation is OFF) and the engine's verbs refuse coded, so the trigger can never fire. An armed-but-unrunnable trigger is refused, never folded to off: wire memoryConsolidation, or leave autoRunOnRecommendation unset."); } if (!consolidationSeatResolvable(deps)) { throw autoRunRefusal("RunnerDeps.autoRunOnRecommendation is true but no consolidation model seat resolves — configure RunnerDeps.memoryConsolidationDriver.chat (an endpoint triple or a chat function), or declare roles.consolidate / roles.summarize. Arming auto-run with no model to run it is refused, never folded to off (a whole-library distillation must not silently ride the default main model)."); } }