UNPKG

@sema-agent/core

Version:

Stateless, task-oriented AI agent core

112 lines (111 loc) • 6.43 kB
import { StoredSession, isSessionConflict } from "../session.js"; import { reconcileInterruptedSession } from "../session-reconcile.js"; import { detectConflicts, settleBranchWriteBack } from "./branch-writeback.js"; import { settleTeardownLeg } from "./teardown-bounded.js"; const RECONCILE_MAX_RETRIES = 3; export async function prepareAcquireReconcile(input) { const { sessions, spec, resume, toolEffects } = input; let acquired; let session; let conflictRef; let wakeRecovered = []; let branch; for (let attempt = 0;; attempt++) { try { acquired = await sessions.acquire(spec.sessionId, spec.requireExistingSession ? { requireExisting: true } : input.placement !== undefined ? { placement: input.placement } : undefined); } catch (err) { if (spec.requireExistingSession && err?.code === "not_found") { const e = new Error(`requireExistingSession: session "${spec.sessionId}" does not exist — refusing a silent fresh run (design/114 Phase3)` + (input.durableParkGap !== undefined ? `. Note the assembly: ${input.durableParkGap}` : "")); e.code = "resume.session_not_found"; throw e; } throw err; } conflictRef = { hit: false }; const own = { ids: undefined }; session = new StoredSession(detectConflicts(acquired.session.getStorage(), conflictRef, own)); if (!spec.sessionId) { break; } try { if (resume) { await session.getStorage().setLeafId(resume.leafId); } else if (spec.resumeAt !== undefined) { const entry = await session.getEntry(spec.resumeAt); if (!entry) { const e = new Error(`resumeAt entry "${spec.resumeAt}" not found in session "${spec.sessionId}"`); e.code = "resume_at.not_found"; throw e; } if (entry.type !== "message" && entry.type !== "custom_message") { const e = new Error(`resumeAt entry "${spec.resumeAt}" is a "${entry.type}" entry; resume-at requires a message boundary`); e.code = "resume_at.not_a_message"; throw e; } if (entry.type === "message") { const msg = entry.message; if (msg?.role === "toolResult") { const e = new Error(`resumeAt entry "${spec.resumeAt}" is a tool-result (mid-turn); resume-at requires a settled turn boundary (a user message or a finished assistant turn)`); e.code = "resume_at.not_a_message"; throw e; } if (msg?.role === "assistant" && Array.isArray(msg.content) && msg.content.some((p) => p?.type === "toolCall")) { const e = new Error(`resumeAt entry "${spec.resumeAt}" ends a turn mid-tool-call; resume-at requires a settled turn boundary`); e.code = "resume_at.not_a_message"; throw e; } } if (spec.resumeAtMode === "before") { const role = entry.type === "message" ? entry.message.role : undefined; if (role !== "user") { const kind = entry.type === "message" ? `role "${String(role)}" message` : `"${entry.type}" entry`; const e = new Error(`resumeAt entry "${spec.resumeAt}" is a ${kind}; resumeAtMode "before" supports only USER-message targets (the exclusive-rewind shape: the removed message is a user message)`); e.code = "resume_at.before_target_not_user"; throw e; } let ancestorId = entry.parentId; let sawConversationAbove = false; while (ancestorId !== null) { const ancestor = await session.getEntry(ancestorId); if (!ancestor) break; if (ancestor.type === "message" || ancestor.type === "custom_message") { sawConversationAbove = true; break; } ancestorId = ancestor.parentId; } if (!sawConversationAbove) { const e = new Error(`resumeAt entry "${spec.resumeAt}" is the session's first message; resumeAtMode "before" cannot rewind past the session root — start a NEW session instead`); e.code = "resume_at.before_root_unsupported"; throw e; } } const storage = session.getStorage(), leafBefore = await storage.getLeafId(), leafSet = spec.resumeAtMode === "before" ? entry.parentId : spec.resumeAt; await storage.setLeafId(leafSet); own.ids = new Set(); branch = { storage, leafBefore, leafSet, own }; } wakeRecovered = (await reconcileInterruptedSession(session, toolEffects, resume?.suspendedBatch)).recovered; break; } catch (err) { if (isSessionConflict(err) && attempt < RECONCILE_MAX_RETRIES) { if (sessions.forget) { const forget = sessions.forget.bind(sessions), id = spec.sessionId; await settleTeardownLeg(() => Promise.resolve(forget(id)).catch(() => undefined), "forget (acquire-reconcile re-wake leg)", (e) => input.report?.(e, id)); } branch = undefined; continue; } if (branch !== undefined) await settleBranchWriteBack(branch, acquired.sessionId, "resumeAt branch write-back (acquire-reconcile failure leg)", (e) => input.report?.(e, acquired.sessionId)); await settleTeardownLeg(() => Promise.resolve(sessions.forget ? sessions.forget(acquired.sessionId) : undefined).catch(() => undefined), "forget (acquire-reconcile failure leg)", (e) => input.report?.(e, acquired.sessionId)); throw err; } } return { acquired, session, conflictRef, wakeRecovered, branch }; }