UNPKG

@sema-agent/core

Version:

Stateless, task-oriented AI agent core

303 lines (302 loc) • 12.7 kB
import { InMemorySessionRepo, SessionError, uuidv7, } from "../internal/harness.js"; import { isSessionConflict } from "./session.js"; import { readMember } from "./thrown-value.js"; const DAY_MS = 24 * 60 * 60 * 1000; import { SESSION_DEFAULT_TTL_DAYS } from "../config/defaults.js"; export { SESSION_DEFAULT_TTL_DAYS }; export class TtlSessionStore { retention = "none"; durability; placements; listPlaced; repo; entries = new Map(); owners = new Map(); pending = new Map(); pendingPlacedDeletes = new Set(); deletingPlaced = new Map(); pinned = new Set(); defaultTtlMs; evictPolicy; sweepTimer; constructor(opts = {}) { this.repo = opts.repo ?? new InMemorySessionRepo(); const durability = opts.durability ?? (opts.repo === undefined ? "process-local" : undefined); if (durability !== undefined) this.durability = durability; this.defaultTtlMs = (opts.defaultTtlDays ?? SESSION_DEFAULT_TTL_DAYS) * DAY_MS; this.evictPolicy = opts.evict ?? (opts.repo ? "forget" : "delete"); if (opts.placements !== undefined) this.placements = opts.placements; const repoListPlaced = this.repo.listPlaced; if (typeof repoListPlaced === "function") { this.listPlaced = (kind, o) => repoListPlaced.call(this.repo, kind, o); } if (opts.sweepIntervalMs !== undefined && opts.sweepIntervalMs > 0) { this.sweepTimer = setInterval(() => this.sweep(), opts.sweepIntervalMs); this.sweepTimer.unref?.(); } } async acquire(sessionId, opts) { this.sweep(); if (sessionId) { const inflightPlacedDelete = this.deletingPlaced.get(sessionId); if (inflightPlacedDelete !== undefined) await inflightPlacedDelete; const existing = this.entries.get(sessionId); if (existing) { this.assertPlacementAdmission(sessionId, existing.placement, opts); existing.lastActiveAt = Date.now(); return { session: existing.session, sessionId }; } if (opts?.requireExisting) { return { session: await this.openOrCreate(sessionId, true), sessionId }; } const inflight = this.pending.get(sessionId); if (inflight) { const session = await inflight; this.assertPlacementAdmission(sessionId, this.entries.get(sessionId)?.placement, opts); return { session, sessionId }; } const p = this.openOrCreate(sessionId, false, opts?.placement); this.pending.set(sessionId, p); try { const session = await p; this.assertPlacementAdmission(sessionId, this.entries.get(sessionId)?.placement, opts); return { session, sessionId }; } finally { this.pending.delete(sessionId); } } const session = await this.createAndStore(undefined, opts?.placement); const meta = await session.getMetadata(); return { session, sessionId: meta.id }; } assertPlacementAdmission(sessionId, placement, opts) { if (placement !== undefined && opts?.requireExisting !== true && opts?.placement === undefined) { throw new SessionError("placement_refused", `session ${sessionId} belongs to the ${placement.kind} placement partition — a claim-form acquire cannot attach to it (read it with requireExisting, or address the agent through its a* handle)`); } if (opts?.placement !== undefined && opts.requireExisting !== true && this.placements?.subagent !== undefined) { const want = opts.placement; if (placement === undefined) { throw new SessionError("placement_refused", `session ${sessionId} exists OUTSIDE the ${want.kind} placement partition it was declared into — refusing to run a subagent transcript on it (a concurrent claim-form create won this id, or the repo dropped the tuple)`); } if (placement.kind !== want.kind || placement.scope !== want.scope || placement.handle !== want.handle) { throw new SessionError("placement_refused", `session ${sessionId}'s persisted placement does not match the declared one (persisted ${placement.kind}/${placement.scope ?? "-"}/${placement.handle ?? "-"} vs declared ${want.kind}/${want.scope ?? "-"}/${want.handle ?? "-"}) — refusing a cross-tuple attach`); } } } async openOrCreate(id, requireExisting = false, placement) { try { const session = await this.repo.open({ id, createdAt: "" }); const meta = await session.getMetadata(); if (meta.placement === undefined) this.pendingPlacedDeletes.delete(id); this.entries.set(id, { session, lastActiveAt: Date.now(), createdAt: meta.createdAt, ...(meta.forkedFrom !== undefined ? { forkedFrom: meta.forkedFrom } : {}), ...(meta.placement !== undefined ? { placement: meta.placement } : {}) }); return session; } catch (err) { if (!isNotFound(err) && !isSessionConflict(err)) { throw err; } if (requireExisting && isNotFound(err)) { throw err; } return this.createAndStore(id, placement); } } async createAndStore(id, placement) { const session = await this.repo.create({ ...(id !== undefined ? { id } : {}), ...(placement !== undefined ? { placement } : {}) }); const meta = await session.getMetadata(); if (meta.placement === undefined) this.pendingPlacedDeletes.delete(meta.id); this.entries.set(meta.id, { session, lastActiveAt: Date.now(), createdAt: meta.createdAt, ...(meta.forkedFrom !== undefined ? { forkedFrom: meta.forkedFrom } : {}), ...(meta.placement !== undefined ? { placement: meta.placement } : {}) }); return session; } touch(sessionId) { const e = this.entries.get(sessionId); if (e) { e.lastActiveAt = Date.now(); } } noteTaskRun(sessionId, taskId, runId) { const e = this.entries.get(sessionId); if (e) { e.lastTaskId = taskId; if (runId !== undefined) e.lastTaskRunId = runId; else delete e.lastTaskRunId; e.lastActiveAt = Date.now(); } } async list() { return [...this.entries.entries()] .filter(([, e]) => e.placement === undefined) .map(([sessionId, e]) => ({ sessionId, lastActiveAt: e.lastActiveAt, ...(e.createdAt !== undefined && e.createdAt !== "" ? { createdAt: e.createdAt } : {}), ...(e.lastTaskId !== undefined ? { lastTaskId: e.lastTaskId } : {}), ...(e.lastTaskRunId !== undefined ? { lastTaskRunId: e.lastTaskRunId } : {}), ...(e.forkedFrom !== undefined ? { forkedFrom: e.forkedFrom } : {}), })) .sort((a, b) => b.lastActiveAt - a.lastActiveAt); } async fork(sourceId, owner) { let source; try { source = await this.repo.open({ id: sourceId, createdAt: "" }); } catch (err) { if (isNotFound(err)) { return null; } throw err; } const forkOptions = await settledForkOptions(source); const forkParams = { ...forkOptions, id: uuidv7() }; if (owner != null) { forkParams.owner = owner; } const forked = await this.repo.fork(await source.getMetadata(), forkParams); const meta = await forked.getMetadata(); const inherited = this.owners.has(sourceId) ? this.owners.get(sourceId) : undefined; const forkOwner = owner ?? inherited; if (forkOwner !== undefined && !this.owners.has(meta.id)) { this.owners.set(meta.id, forkOwner); } this.entries.set(meta.id, { session: forked, lastActiveAt: Date.now(), createdAt: meta.createdAt, forkedFrom: meta.forkedFrom ?? sourceId, }); return meta.id; } async release(sessionId) { if (this.pinned.has(sessionId)) { return; } const e = this.entries.get(sessionId); if (e !== undefined && e.placement === undefined) this.pendingPlacedDeletes.delete(sessionId); let placed = e !== undefined ? e.placement !== undefined : this.pendingPlacedDeletes.has(sessionId); if (!placed && e === undefined && this.placements !== undefined) { const probe = this.repo.placementOf; if (typeof probe === "function") { try { placed = (await probe.call(this.repo, sessionId)) !== undefined; } catch { placed = false; } } } if (!e && !placed) { return; } if (placed) { this.pendingPlacedDeletes.add(sessionId); this.entries.delete(sessionId); let settleGate; this.deletingPlaced.set(sessionId, new Promise((r) => (settleGate = r))); try { const meta = e !== undefined ? await e.session.getMetadata() : { id: sessionId, createdAt: "" }; await this.repo.delete(meta); } finally { this.deletingPlaced.delete(sessionId); settleGate(); } this.pendingPlacedDeletes.delete(sessionId); this.owners.delete(sessionId); return; } this.entries.delete(sessionId); if (this.evictPolicy === "delete") { await this.repo.delete(e !== undefined ? await e.session.getMetadata() : { id: sessionId, createdAt: "" }); this.owners.delete(sessionId); } } forget(sessionId) { this.entries.delete(sessionId); } pin(sessionId) { this.pinned.add(sessionId); } unpin(sessionId) { this.pinned.delete(sessionId); } async ownerOf(sessionId) { return this.owners.has(sessionId) ? this.owners.get(sessionId) : undefined; } async register(sessionId, owner) { if (this.owners.has(sessionId)) { return; } if (owner !== null && typeof owner !== "string") { throw new TypeError(`SessionStore.register: owner must be a string (owned) or null (anonymous), got ${owner === undefined ? "undefined" : typeof owner}`); } this.owners.set(sessionId, owner); } async incarnationOf(sessionId) { let session; try { session = await this.repo.open({ id: sessionId, createdAt: "" }); } catch (err) { if (isNotFound(err)) return undefined; throw err; } return (await session.getMetadata()).incarnation; } sweep(now = Date.now()) { for (const [id, e] of this.entries) { if (this.pinned.has(id)) { continue; } if (now - e.lastActiveAt > this.defaultTtlMs) { this.entries.delete(id); if (this.evictPolicy === "delete" && e.placement === undefined) { void this.repo.delete({ id, createdAt: "" }).then(() => { this.owners.delete(id); }, () => { }); } } } } get size() { return this.entries.size; } dispose() { if (this.sweepTimer) { clearInterval(this.sweepTimer); } } } async function settledForkOptions(source) { const storage = source.getStorage(); let id = await storage.getLeafId(); while (id) { const entry = await storage.getEntry(id); if (!entry || entry.type !== "message") { return {}; } if (entry.message.role === "assistant") { const hasToolCall = Array.isArray(entry.message.content) && entry.message.content.some((c) => c.type === "toolCall"); return hasToolCall && entry.parentId ? { entryId: entry.parentId, position: "at" } : {}; } if (entry.message.role !== "toolResult") { return {}; } id = entry.parentId; } return {}; } function isNotFound(err) { return readMember(err, "code") === "not_found"; }