UNPKG

@sema-agent/core

Version:

Stateless, task-oriented AI agent core

398 lines (397 loc) • 15.9 kB
import { asAgentMessage, createCompactionSummaryMessage, createCustomMessage, } from "../harness/messages.js"; import { SessionError, isSyntheticApiErrorMessage, isValidModelChange, normalizeAnnouncedListing, normalizeCompactionStateCarrier, normalizeGitAnnouncement, normalizeReminderMark, normalizeWorkspaceState } from "../harness/types.js"; import { normalizePromptEpoch } from "../../prompt-assembly/epoch.js"; import { budgetInvokedSkillsRetention, readElidedMessages, readRetainedInvokedSkills, readUnsummarizedMessages, renderInvokedSkillsRetention, } from "../compaction/utils.js"; const RETENTION_CLAMP_DEFAULT_CHARS_PER_TOKEN = 3; const RETENTION_CLAMP_WINDOW_FRACTION = 0.5; export function buildSessionContext(pathEntries, opts) { let thinkingLevel = "off"; let model = null; let compaction = null; for (const entry of pathEntries) { if (entry.type !== "compaction") continue; const carried = normalizeCompactionStateCarrier(entry.details); if (carried.thinkingLevel !== undefined) thinkingLevel = carried.thinkingLevel; if (carried.model !== undefined) model = carried.model; } for (const entry of pathEntries) { if (entry.type === "thinking_level_change") { thinkingLevel = entry.thinkingLevel; } else if (entry.type === "model_change") { model = { provider: entry.provider, modelId: entry.modelId }; } else if (entry.type === "message" && entry.message.role === "assistant") { if (!isSyntheticApiErrorMessage(entry.message) && isValidModelChange({ provider: entry.message.provider, modelId: entry.message.model })) { model = { provider: entry.message.provider, modelId: entry.message.model }; } } else if (entry.type === "compaction") { compaction = entry; } } const messages = []; const isAssistantMessage = (m) => m.role === "assistant"; const stripAssistantUsage = (message) => { if (!isAssistantMessage(message)) { return message; } return { ...message, usage: { input: 0, output: 0, cacheRead: 0, cacheWrite: 0, totalTokens: 0, cost: { input: 0, output: 0, cacheRead: 0, cacheWrite: 0, total: 0 }, }, }; }; const appendMessage = (entry, stripUsage = false) => { if (entry.type === "message") { messages.push(stripUsage ? stripAssistantUsage(entry.message) : entry.message); } else if (entry.type === "custom_message") { messages.push(asAgentMessage(createCustomMessage(entry.customType, entry.content, entry.display, entry.details, entry.timestamp))); } }; if (compaction) { let retainedSkills = readRetainedInvokedSkills(compaction.details); if (opts?.windowTokens !== undefined && opts.windowTokens > 0 && retainedSkills.length > 0) { const cpt = opts.charsPerToken ?? RETENTION_CLAMP_DEFAULT_CHARS_PER_TOKEN; const budgetChars = Math.max(0, Math.floor(opts.windowTokens * cpt * RETENTION_CLAMP_WINDOW_FRACTION) - compaction.summary.length); retainedSkills = budgetInvokedSkillsRetention(retainedSkills, budgetChars); } const retainedSkillsBlock = renderInvokedSkillsRetention(retainedSkills); messages.push(asAgentMessage(createCompactionSummaryMessage(compaction.summary + retainedSkillsBlock, compaction.tokensBefore, compaction.timestamp, readElidedMessages(compaction.details), readUnsummarizedMessages(compaction.details)))); const compactionIdx = pathEntries.findIndex((e) => e.type === "compaction" && e.id === compaction.id); let foundFirstKept = false; for (let i = 0; i < compactionIdx; i++) { const entry = pathEntries[i]; if (entry.id === compaction.firstKeptEntryId) { foundFirstKept = true; } if (foundFirstKept) { appendMessage(entry, true); } } if (!foundFirstKept) { throw new SessionError("invalid_session", `Compaction ${compaction.id} firstKeptEntryId (${compaction.firstKeptEntryId}) not found before the compaction entry — session may be corrupt or need migration`); } for (let i = compactionIdx + 1; i < pathEntries.length; i++) { appendMessage(pathEntries[i]); } } else { for (const entry of pathEntries) { appendMessage(entry); } } return { messages, thinkingLevel, model }; } export class StoredSession { storage; constructor(storage) { this.storage = storage; } getMetadata() { return this.storage.getMetadata(); } getStorage() { return this.storage; } getLeafId() { return this.storage.getLeafId(); } getEntry(id) { return this.storage.getEntry(id); } getEntries() { return this.storage.getEntries(); } async getBranch(fromId) { const leafId = fromId ?? (await this.storage.getLeafId()); return this.storage.getPathToRoot(leafId); } async buildContext(opts) { return buildSessionContext(await this.getBranch(), opts); } getLabel(id) { return this.storage.getLabel(id); } async getSessionName() { const entries = await this.storage.findEntries("session_info"); return entries[entries.length - 1]?.name?.trim() || undefined; } async appendTypedEntry(entry) { await this.storage.appendEntry(entry, { expectedLeafId: entry.parentId }); return entry.id; } async appendMessage(message) { return this.appendTypedEntry({ type: "message", id: await this.storage.createEntryId(), parentId: await this.storage.getLeafId(), timestamp: new Date().toISOString(), message, }); } async appendThinkingLevelChange(thinkingLevel) { return this.appendTypedEntry({ type: "thinking_level_change", id: await this.storage.createEntryId(), parentId: await this.storage.getLeafId(), timestamp: new Date().toISOString(), thinkingLevel, }); } async appendModelChange(provider, modelId) { return this.appendTypedEntry({ type: "model_change", id: await this.storage.createEntryId(), parentId: await this.storage.getLeafId(), timestamp: new Date().toISOString(), provider, modelId, }); } async appendPromptEpoch(descriptor) { return this.appendTypedEntry({ type: "prompt_epoch", id: await this.storage.createEntryId(), parentId: await this.storage.getLeafId(), timestamp: new Date().toISOString(), ...descriptor, }); } async appendAnnouncedListing(snapshot) { const shaped = normalizeAnnouncedListing(snapshot); if (!shaped) { throw new SessionError("invalid_entry", "appendAnnouncedListing: snapshot is not a structurally valid announced-listing (string arrays, bounded)"); } return this.appendTypedEntry({ type: "announced_listing", id: await this.storage.createEntryId(), parentId: await this.storage.getLeafId(), timestamp: new Date().toISOString(), ...shaped, }); } async appendGitAnnouncement(state) { const shaped = normalizeGitAnnouncement(state); if (!shaped) { throw new SessionError("invalid_entry", "appendGitAnnouncement: state is not a structurally valid git announcement (closed kind, sha256 hash, bounded entryId)"); } return this.appendTypedEntry({ type: "git_announcement", id: await this.storage.createEntryId(), parentId: await this.storage.getLeafId(), timestamp: new Date().toISOString(), ...shaped, }); } async getGitAnnouncement() { const branch = await this.getBranch(); for (let i = branch.length - 1; i >= 0; i--) { const entry = branch[i]; let carried; if (entry.type === "git_announcement") { carried = normalizeGitAnnouncement(entry); } else if (entry.type === "compaction") { carried = normalizeGitAnnouncement(entry.details?.gitAnnouncement); } if (carried === undefined) continue; const announced = carried.pending !== true && carried.entryId !== undefined && gitFrameContextVisible(branch, carried.entryId); return announced ? { kind: carried.kind, hash: carried.hash, status: "announced", entryId: carried.entryId } : { kind: carried.kind, hash: carried.hash, status: "pending" }; } return undefined; } async appendWorkspaceState(state) { const shaped = normalizeWorkspaceState(state); if (!shaped) { throw new SessionError("invalid_entry", "appendWorkspaceState: state is not a structurally valid workspace-state snapshot (bounded absolute paths + a git sha)"); } return this.appendTypedEntry({ type: "workspace_state", id: await this.storage.createEntryId(), parentId: await this.storage.getLeafId(), timestamp: new Date().toISOString(), ...shaped, }); } async appendReminderMark(mark) { const shaped = normalizeReminderMark({ mark }); if (shaped === undefined) { throw new SessionError("invalid_entry", "appendReminderMark: mark is not a bounded url-safe token"); } return this.appendTypedEntry({ type: "reminder_mark", id: await this.storage.createEntryId(), parentId: await this.storage.getLeafId(), timestamp: new Date().toISOString(), mark: shaped, }); } async getReminderMark() { const branch = await this.getBranch(); for (let i = branch.length - 1; i >= 0; i--) { const entry = branch[i]; if (entry.type === "reminder_mark") { const shaped = normalizeReminderMark(entry); if (shaped !== undefined) return shaped; } } return undefined; } async getWorkspaceState() { const branch = await this.getBranch(); for (let i = branch.length - 1; i >= 0; i--) { const entry = branch[i]; if (entry.type === "workspace_state") { const shaped = normalizeWorkspaceState(entry); if (shaped) return shaped; } } return undefined; } async getAnnouncedListing() { const branch = await this.getBranch(); for (let i = branch.length - 1; i >= 0; i--) { const entry = branch[i]; if (entry.type === "announced_listing") { const shaped = normalizeAnnouncedListing(entry); if (shaped) return shaped; } else if (entry.type === "compaction") { const restated = normalizeAnnouncedListing(entry.details?.announcedListings); if (restated) return restated; } } return undefined; } async getPromptEpoch() { const branch = await this.getBranch(); for (let i = branch.length - 1; i >= 0; i--) { const entry = branch[i]; if (entry.type === "compaction") { const restated = normalizePromptEpoch(entry.details?.promptEpoch); if (restated) return restated; } else if (entry.type === "prompt_epoch") { const shaped = normalizePromptEpoch(entry); if (shaped) return shaped; } } const anchor = await this.storage.getEpochAnchor?.(); return anchor ? normalizePromptEpoch(anchor) : undefined; } async appendCompaction(summary, firstKeptEntryId, tokensBefore, details, fromHook) { const carried = await (async () => { try { const ctx = buildSessionContext(await this.getBranch()); return { thinkingLevel: ctx.thinkingLevel, ...(ctx.model !== null ? { model: ctx.model } : {}) }; } catch { return undefined; } })(); const definedOverrides = Object.fromEntries(Object.entries(details ?? {}).filter(([, v]) => v !== undefined)); const mergedDetails = carried === undefined ? details : details === undefined || (typeof details === "object" && details !== null && !Array.isArray(details)) ? { ...carried, ...definedOverrides } : details; return this.appendTypedEntry({ type: "compaction", id: await this.storage.createEntryId(), parentId: await this.storage.getLeafId(), timestamp: new Date().toISOString(), summary, firstKeptEntryId, tokensBefore, details: mergedDetails, fromHook, }); } async appendCustomEntry(customType, data) { return this.appendTypedEntry({ type: "custom", id: await this.storage.createEntryId(), parentId: await this.storage.getLeafId(), timestamp: new Date().toISOString(), customType, data, }); } async appendCustomMessageEntry(customType, content, display, details) { return this.appendTypedEntry({ type: "custom_message", id: await this.storage.createEntryId(), parentId: await this.storage.getLeafId(), timestamp: new Date().toISOString(), customType, content, display, details, }); } async appendLabel(targetId, label) { if (!(await this.storage.getEntry(targetId))) { throw new SessionError("not_found", `Entry ${targetId} not found`); } return this.appendTypedEntry({ type: "label", id: await this.storage.createEntryId(), parentId: await this.storage.getLeafId(), timestamp: new Date().toISOString(), targetId, label, }); } async appendSessionName(name) { return this.appendTypedEntry({ type: "session_info", id: await this.storage.createEntryId(), parentId: await this.storage.getLeafId(), timestamp: new Date().toISOString(), name: name.trim(), }); } } export function gitFrameContextVisible(branch, entryId) { let frameIdx = -1; let compactionIdx = -1; let firstKeptId; for (let i = 0; i < branch.length; i++) { const e = branch[i]; if (e.id === entryId) frameIdx = i; if (e.type === "compaction") { compactionIdx = i; firstKeptId = e.firstKeptEntryId; } } if (frameIdx === -1) return false; if (compactionIdx === -1 || frameIdx > compactionIdx) return true; if (firstKeptId === undefined) return false; const keptIdx = branch.findIndex((e) => e.id === firstKeptId); return keptIdx !== -1 && frameIdx >= keptIdx; }