@sema-agent/core
Version:
Stateless, task-oriented AI agent core
119 lines (118 loc) • 6.63 kB
JavaScript
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).");
}
}