UNPKG

@sema-agent/core

Version:

Stateless, task-oriented AI agent core

254 lines (253 loc) • 10.4 kB
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 }; }