@sema-agent/core
Version:
Stateless, task-oriented AI agent core
254 lines (253 loc) • 10.4 kB
JavaScript
import { termSet } from "./memory-vector.js";
import { thrownText } from "./thrown-value.js";
function invalidKnob(message, code) {
const e = new Error(message);
e.code = code;
return e;
}
export function resolveFindLimit(limit) {
if (limit === Number.POSITIVE_INFINITY)
return limit;
if (!Number.isInteger(limit) || limit < 0) {
throw invalidKnob(`StrategyStore.find: limit must be a non-negative integer, or Infinity for "no cap" (got ${String(limit)})`, "config.strategy_find_limit_invalid");
}
return limit;
}
export function resolveCapacityCap(name, cap) {
if (cap === Number.POSITIVE_INFINITY) {
throw invalidKnob(`strategy store ${name} cannot be Infinity — a per-scope capacity cap can be widened but not turned off; pass a large finite integer instead`, "config.strategy_max_size_invalid");
}
if (!Number.isInteger(cap) || cap < 0) {
throw invalidKnob(`strategy store ${name} must be a non-negative integer (got ${String(cap)})`, "config.strategy_max_size_invalid");
}
return cap;
}
export const STRATEGY_ID_RE = /^[A-Za-z0-9_-]{1,64}$/;
export const MAX_STRATEGY_PROBLEM_BYTES = 8192;
export const MAX_STRATEGY_TEXT_BYTES = 4096;
export const MAX_STRATEGY_SCOPE_BYTES = 4096;
export const MAX_STRATEGY_TEACHER_MODEL_BYTES = 512;
export const MAX_STRATEGY_SIGNATURE_BYTES = 4096;
export const MAX_STRATEGY_TS_BYTES = 64;
export const MAX_STRATEGY_TS_FUTURE_MS = 24 * 60 * 60 * 1000;
export const MAX_STRATEGY_INJECTION_TOTAL_BYTES = 16384;
function invalidEntry(message, id) {
const e = new Error(message);
e.code = "strategy.entry_invalid";
if (id !== undefined)
e.strategyId = id;
return e;
}
export function storedStrategyShapeIssue(v) {
if (v === null || typeof v !== "object")
return "not an object";
const s = v;
if (typeof s.id !== "string" || !STRATEGY_ID_RE.test(s.id)) {
return `id must match [A-Za-z0-9_-]{1,64} (got ${JSON.stringify(String(s.id).slice(0, 80))})`;
}
if (typeof s.scope !== "string" || s.scope.length === 0)
return "scope must be a non-empty string";
if (Buffer.byteLength(s.scope, "utf8") > MAX_STRATEGY_SCOPE_BYTES)
return `scope exceeds ${MAX_STRATEGY_SCOPE_BYTES} bytes`;
if (typeof s.problem !== "string" || typeof s.strategy !== "string")
return "problem/strategy must be strings";
if (Buffer.byteLength(s.problem, "utf8") > MAX_STRATEGY_PROBLEM_BYTES) {
return `problem exceeds ${MAX_STRATEGY_PROBLEM_BYTES} bytes`;
}
if (Buffer.byteLength(s.strategy, "utf8") > MAX_STRATEGY_TEXT_BYTES) {
return `strategy text exceeds ${MAX_STRATEGY_TEXT_BYTES} bytes`;
}
if (typeof s.confidence !== "number" || !Number.isFinite(s.confidence) || s.confidence < 0 || s.confidence > 3) {
return `confidence must be a finite number in [0,3] (got ${String(s.confidence)})`;
}
if (typeof s.ts !== "string")
return "ts must be an ISO timestamp string";
if (Buffer.byteLength(s.ts, "utf8") > MAX_STRATEGY_TS_BYTES)
return `ts exceeds ${MAX_STRATEGY_TS_BYTES} bytes`;
if (s.teacherModel !== undefined && (typeof s.teacherModel !== "string" || Buffer.byteLength(s.teacherModel, "utf8") > MAX_STRATEGY_TEACHER_MODEL_BYTES)) {
return `teacherModel must be a string of at most ${MAX_STRATEGY_TEACHER_MODEL_BYTES} bytes when present`;
}
if (s.signature !== undefined && (typeof s.signature !== "string" || Buffer.byteLength(s.signature, "utf8") > MAX_STRATEGY_SIGNATURE_BYTES)) {
return `signature must be a string of at most ${MAX_STRATEGY_SIGNATURE_BYTES} bytes when present`;
}
if (s.origin !== undefined && s.origin !== "earned" && s.origin !== "seeded") {
return `origin must be "earned" or "seeded" when present (got ${String(s.origin)})`;
}
return null;
}
export function validateStrategyForWrite(s) {
const issue = storedStrategyShapeIssue(s);
if (issue !== null) {
throw invalidEntry(`strategy ${issue}`, typeof s.id === "string" ? s.id : undefined);
}
const t = new Date(s.ts).getTime();
if (!Number.isNaN(t) && t > Date.now() + MAX_STRATEGY_TS_FUTURE_MS) {
throw invalidEntry(`strategy ts is in the future beyond the allowed skew (${s.ts})`, s.id);
}
}
const STOP = new Set([
"the", "and", "for", "with", "from", "this", "that", "into", "your", "you", "are", "was", "were", "has",
"have", "had", "not", "but", "all", "any", "can", "use", "using", "via", "then", "than", "out", "get",
"got", "its", "it's", "their", "them", "they", "what", "when", "which", "who", "how", "why", "a", "an",
"of", "to", "in", "on", "is", "it", "or", "as", "at", "by", "be", "do", "if", "so", "no", "up", "we",
]);
const CJK_LEAD = /^[\u3040-\u30FF\u31F0-\u31FF\u3400-\u4DBF\u4E00-\u9FFF\uAC00-\uD7A3\uF900-\uFAFF]/;
function terms(s) {
return termSet(s);
}
function significant(s) {
return [...terms(s)].filter((t) => (CJK_LEAD.test(t) ? true : t.length >= 3 && !STOP.has(t)));
}
export function compileStrategyQuery(query) {
const qSig = significant(query);
if (qSig.length === 0)
return null;
const qAlnum = qSig.filter((t) => !CJK_LEAD.test(t));
return (problem) => {
const probTerms = terms(problem);
if (qSig.every((t) => probTerms.has(t)))
return true;
return qAlnum.length > 0 && qAlnum.every((t) => probTerms.has(t));
};
}
export function normalizeStrategyText(s) {
return s.trim().toLowerCase().replace(/\s+/g, " ");
}
export function strategyDedupKey(s) {
return `${normalizeStrategyText(s.problem)}\u0000${normalizeStrategyText(s.strategy)}`;
}
function ageDays(tsISO) {
const t = new Date(tsISO).getTime();
if (Number.isNaN(t)) {
return Infinity;
}
return Math.max(0, Date.now() - t) / 86_400_000;
}
export function scoreStoredStrategy(s) {
return (s.confidence + 1) * (1 / (1 + ageDays(s.ts)));
}
export class InMemoryStrategyStore {
byScope = new Map();
maxPerScope;
constructor(maxPerScope = 100) {
this.maxPerScope = resolveCapacityCap("maxPerScope", maxPerScope);
}
save(s) {
const arr = this.byScope.get(s.scope) ?? [];
const key = strategyDedupKey(s);
const dup = arr.find((e) => strategyDedupKey(e) === key);
if (dup) {
const incoming = Number.isFinite(s.confidence) ? s.confidence : undefined;
if (incoming !== undefined && incoming >= dup.confidence) {
dup.confidence = incoming;
dup.ts = s.ts;
}
dup.teacherModel = s.teacherModel ?? dup.teacherModel;
}
else {
arr.push(s);
}
if (arr.length > this.maxPerScope) {
arr.sort((a, b) => scoreStoredStrategy(b) - scoreStoredStrategy(a));
arr.length = this.maxPerScope;
}
this.byScope.set(s.scope, arr);
}
find(scope, query, limit) {
const cap = resolveFindLimit(limit);
const arr = this.byScope.get(scope);
if (!arr || arr.length === 0) {
return [];
}
const matches = compileStrategyQuery(query);
if (matches === null) {
return [];
}
return arr
.filter((s) => matches(s.problem))
.sort((a, b) => scoreStoredStrategy(b) - scoreStoredStrategy(a))
.slice(0, cap);
}
prune(scope, maxSize) {
const n = resolveCapacityCap("maxSize", maxSize);
const arr = this.byScope.get(scope);
if (arr && arr.length > n) {
arr.sort((a, b) => scoreStoredStrategy(b) - scoreStoredStrategy(a));
arr.length = n;
}
}
scopeUsage(scope) {
return { used: this.byScope.get(scope)?.length ?? 0, capacity: this.maxPerScope };
}
hasStrategy(scope, entry) {
const key = strategyDedupKey(entry);
return (this.byScope.get(scope) ?? []).some((e) => strategyDedupKey(e) === key);
}
get size() {
let n = 0;
for (const arr of this.byScope.values())
n += arr.length;
return n;
}
}
function seedRefused(message) {
const e = new Error(message);
e.code = "strategy.seed_refused";
return e;
}
export async function seedStrategies(store, scope, entries) {
if (typeof scope !== "string" || scope.length === 0) {
throw seedRefused("seedStrategies: scope must be a non-empty string");
}
const now = new Date().toISOString();
const full = [];
const ids = new Set();
const contentKeys = new Set();
for (let i = 0; i < entries.length; i++) {
const e = entries[i];
const s = { ...e, ts: e.ts ?? now, scope, origin: "seeded" };
try {
validateStrategyForWrite(s);
}
catch (err) {
throw seedRefused(`seedStrategies: entry ${i} refused, zero entries written — ${err.message}`);
}
if (ids.has(s.id)) {
throw seedRefused(`seedStrategies: duplicate id ${JSON.stringify(s.id)} within the batch, zero entries written`);
}
ids.add(s.id);
const key = strategyDedupKey(s);
if (contentKeys.has(key)) {
throw seedRefused(`seedStrategies: entry ${i} (${JSON.stringify(s.id)}) duplicates another batch entry's content, zero entries written`);
}
contentKeys.add(key);
full.push(s);
}
if (store.scopeUsage) {
const usage = await store.scopeUsage(scope);
let netNew = full.length;
if (store.hasStrategy) {
netNew = 0;
for (const s of full) {
if (!(await store.hasStrategy(scope, s)))
netNew++;
}
}
const remaining = Math.max(0, usage.capacity - usage.used);
if (netNew > remaining) {
throw seedRefused(`seedStrategies: ${netNew} net-new entries exceed the scope's remaining capacity ${remaining} ` +
`(${usage.used}/${usage.capacity} used), zero entries written — widen the capacity or trim the batch`);
}
}
let written = 0;
for (const s of full) {
try {
await store.save(s);
}
catch (err) {
return { written, failedAt: { id: s.id, error: thrownText(err) } };
}
written++;
}
return { written };
}