Something went wrong. Try again.
fork of prime agent with changes i want but are not upstreamable probqbly (and some fixes)
Something went wrong. Try again.
42 kB · 1100 lines
TypeScript
at main
123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463464465466467468469470471472473474475476477478479480481482483484485486487488489490491492493494495496497498499500501502503504505506507508509510511512513514515516517518519520521522523524525526527528529530531532533534535536537538539540541542543544545546547548549550551552553554555556557558559560561562563564565566567568569570571572573574575576577578579580581582583584585586587588589590591592593594595596597598599600601602603604605606607608609610611612613614615616617618619620621622623624625626627628629630631632633634635636637638639640641642643644645646647648649650651652653654655656657658659660661662663664665666667668669670671672673674675676677678679680681682683684685686687688689690691692693694695696697698699700701702703704705706707708709710711712713714715716717718719720721722723724725726727728729730731732733734735736737738739740741742743744745746747748749750751752753754755756757758759760761762763764765766767768769770771772773774775776777778779780781782783784785786787788789790791792793794795796797798799800801802803804805806807808809810811812813814815816817818819820821822823824825826827828829830831832833834835836837838839840841842843844845846847848849850851852853854855856857858859860861862863864865866867868869870871872873874875876877878879880881882883884885886887888889890891892893894895896897898899900901902903904905906907908909910911912913914915916917918919920921922923924925926927928929930931932933934935936937938939940941942943944945946947948949950951952953954955956957958959960961962963964965966967968969970971972973974975976977978979980981982983984985986987988989990991992993994995996997998999100010011002100310041005100610071008100910101011101210131014101510161017101810191020102110221023102410251026102710281029103010311032103310341035103610371038103910401041104210431044104510461047104810491050105110521053105410551056105710581059106010611062106310641065106610671068106910701071107210731074107510761077107810791080108110821083108410851086108710881089109010911092109310941095109610971098109911001101import { createHash, randomUUID } from "node:crypto";import { constants, existsSync, readdirSync, readFileSync } from "node:fs";import { access, mkdir, readdir, readFile, rename, rm, stat, writeFile } from "node:fs/promises";import os from "node:os";import path from "node:path";import { stderr, stdin } from "node:process";import { createInterface } from "node:readline/promises";import { setTimeout as sleep } from "node:timers/promises";import { fileURLToPath } from "node:url";import { getPackageDir } from "../../config.js";import { isProcessAlive, spawnHidden } from "../../utils/child-process.js";import { tryAcquireDirLock } from "../../utils/dir-lock.js";import type { PythonSkillRuntimeInfo } from "../skills.js";
const BOOTSTRAP_SCHEMA = 9;const PYTHON_VERSION = "3.11";const RUNTIME_REQUIREMENT = "prime-agent-runtime";// Serializes the kernel's user namespace so it can be revived across session// resume. Internal-only; intentionally not surfaced to the model as an import.const STATE_SNAPSHOT_REQUIREMENT = "dill";const DEFAULT_RLM_EXTRA_PACKAGES = [ { uvArg: "requests", importName: "requests", promptLabel: "requests" }, { uvArg: "httpx", importName: "httpx", promptLabel: "httpx" }, { uvArg: "pyyaml", importName: "yaml", promptLabel: "yaml (PyYAML)" }, { uvArg: "tomli", importName: "tomli", promptLabel: "tomli" }, { uvArg: "python-dotenv", importName: "dotenv", promptLabel: "dotenv (python-dotenv)" }, { uvArg: "pandas", importName: "pandas", promptLabel: "pandas" }, { uvArg: "numpy", importName: "numpy", promptLabel: "numpy" }, { uvArg: "scipy", importName: "scipy", promptLabel: "scipy" }, { uvArg: "beautifulsoup4", importName: "bs4", promptLabel: "bs4 (Beautiful Soup)" }, { uvArg: "lxml", importName: "lxml", promptLabel: "lxml" }, { uvArg: "pydantic", importName: "pydantic", promptLabel: "pydantic" }, { uvArg: "tyro", importName: "tyro", promptLabel: "tyro" },];export const DEFAULT_RLM_EXTRA_UV_ARGS = DEFAULT_RLM_EXTRA_PACKAGES.map((pkg) => pkg.uvArg);export const DEFAULT_RLM_EXTRA_IMPORT_NAMES = DEFAULT_RLM_EXTRA_PACKAGES.map((pkg) => pkg.importName);export const DEFAULT_RLM_EXTRA_IMPORT_LABELS = DEFAULT_RLM_EXTRA_PACKAGES.map((pkg) => pkg.promptLabel);const WINDOWS_PATHEXT_DEFAULT = [".COM", ".EXE", ".BAT", ".CMD"];const WINDOWS_SUPPORTED_EXECUTABLE_EXTENSIONS = new Set( WINDOWS_PATHEXT_DEFAULT.map((extension) => extension.toLowerCase()),);
export interface BatchShimInvocation { args: string[]; env: NodeJS.ProcessEnv;}
/** Build a cmd.exe invocation without embedding user-controlled values in its command string. */export function buildBatchShimInvocation( command: string, args: readonly string[], baseEnv: NodeJS.ProcessEnv, token = randomUUID().replaceAll("-", ""),): BatchShimInvocation { if (!/^[A-Za-z0-9_]+$/.test(token)) { throw new Error("Windows batch shim token contains unsupported characters"); } const values = [command, ...args]; if (values.some((value) => /["\0\r\n]/.test(value))) { throw new Error("Windows batch shim paths and arguments cannot contain quotes, NUL, or line breaks"); } const env = { ...baseEnv }; const variables = values.map((value, index) => { const name = `PRIME_AGENT_BATCH_${token}_${index}`; env[name] = value; return `"%${name}%"`; }); return { args: ["/d", "/v:off", "/s", "/c", `"${variables.join(" ")}"`], env, };}
const UV_INSTALL_COMMAND = "curl -LsSf https://astral.sh/uv/install.sh | sh";/** MCP discovery surface the kernel runtime must expose for /plugins work. */const REQUIRED_MCP_DISCOVERY_METHODS = [ "list_plugins", "search_plugins", "list_connections", "search_tools", "describe_tool",];const REQUIRED_HARNESS_METHODS = [ "create_memory", "update_memory", "delete_memory", "create_skill", "update_skill", "delete_skill", "create_subagent", "update_subagent", "delete_subagent", "create_prompt_note", "update_prompt_note", "delete_prompt_note", "record_refinement",];export const RUNTIME_READY_CHECK = `import inspect; import rlm; import agent; from agent import sessions; from rlm import McpIntegration; import rlm.mcp as mcp; from rlm.harness import HarnessEntry; _harness_methods = ${JSON.stringify(REQUIRED_HARNESS_METHODS)}; assert callable(mcp.list_tools); assert callable(mcp.call_tool); assert all(callable(getattr(mcp, _m, None)) for _m in ${JSON.stringify(REQUIRED_MCP_DISCOVERY_METHODS)}), "rlm.mcp is missing MCP discovery methods (list_plugins, search_plugins, list_connections, search_tools, describe_tool); the kernel venv needs a current prime-agent-runtime"; assert callable(rlm.spawn); assert hasattr(rlm, 'rlm'); assert callable(rlm.rlm.spawn); assert inspect.signature(rlm.spawn).parameters['name'].default is inspect.Parameter.empty; assert not hasattr(rlm, 'run'); assert not hasattr(rlm.rlm, 'run'); assert callable(rlm.host_request); assert callable(sessions.list); assert callable(sessions.get); assert callable(rlm.models); assert callable(rlm.rlm.models); assert not hasattr(rlm, 'roles'); assert not hasattr(rlm.rlm, 'roles'); assert not hasattr(rlm, 'find_models'); assert not hasattr(rlm.rlm, 'find_models'); assert 'model' in inspect.signature(rlm.spawn).parameters; assert inspect.signature(rlm.spawn).parameters['model'].default is inspect.Parameter.empty; assert 'role' not in inspect.signature(rlm.spawn).parameters; assert 'thinking' in inspect.signature(rlm.spawn).parameters; assert callable(rlm.create_session); assert callable(rlm.rlm.create_session); assert callable(rlm.progress_note); assert callable(rlm.rlm.progress_note); assert hasattr(rlm, 'harness'); assert hasattr(rlm, 'get_harness_state'); assert hasattr(rlm.rlm, 'harness'); assert hasattr(rlm.rlm, 'get_harness_state'); assert all(callable(getattr(_harness, _method, None)) for _harness in (rlm.harness, rlm.rlm.harness) for _method in _harness_methods); assert 'reference' in HarnessEntry.__dataclass_fields__; assert 'scope' in HarnessEntry.__dataclass_fields__; assert 'reference' in inspect.signature(rlm.harness.create_skill).parameters; assert 'reference' in inspect.signature(rlm.harness.update_skill).parameters; assert 'global_' in inspect.signature(rlm.harness.create_memory).parameters; assert 'global_' in inspect.signature(rlm.get_harness_state).parameters; assert not hasattr(rlm, 'background'); assert not hasattr(rlm.rlm, 'background'); from rlm.bash import BashHandle, BashResult; assert callable(rlm.bash); assert all(callable(getattr(BashHandle, _m, None)) for _m in ('tail', 'text', 'poll', 'kill')); assert callable(getattr(BashResult, 'text', None)); assert {'exit_code', 'duration', 'stdout', 'stderr'} <= set(BashResult.__dataclass_fields__); import rlm.repl as _repl; assert callable(_repl.main); assert callable(_repl.emit); assert callable(_repl.host_request); assert callable(_repl.is_active); assert _repl.PROTOCOL_VERSION == 3; assert callable(rlm.emit); assert not hasattr(rlm, 'HOST_COMM_TARGET'); assert not hasattr(mcp, 'install_shutdown_hook')`;const BOOTSTRAP_VERSION_FILE = ".bootstrap-version";export const KERNEL_VENV_LOCK_SUFFIX = ".bootstrap.lock";/** A lock this old without a readable, live owner pid is a crashed build, not a running one. */export const KERNEL_VENV_LOCK_STALE_MS = 30_000;const BOOTSTRAP_LOCK_RETRY_MS = 100;// A builder holds the lock for a whole cold install (network, wheels, editable// skills), so a waiter bounds its wait instead of failing on the first slow build.const DEFAULT_BOOTSTRAP_LOCK_WAIT_MS = 10 * 60_000;// Staged and replaced trees live beside the live venv: same filesystem, so the// promotion below is a rename, and the live path never holds a partial tree.const VENV_STAGING_SUFFIX = ".staging";const VENV_REPLACED_SUFFIX = ".replaced";const VENV_SIBLING_SWEEP_AGE_MS = 60 * 60_000;
let inFlightEnsureKernelPython: { key: string; promise: Promise<string> } | null = null;
export type KernelPythonSkill = PythonSkillRuntimeInfo;export type KernelBootstrapProgressHandler = (message: string) => void;
export interface EnsureKernelPythonOptions { pythonSkills?: readonly KernelPythonSkill[]; onProgress?: KernelBootstrapProgressHandler;}
interface BootstrapPythonSkill { importName: string; packagePath: string; pyprojectPath: string; pyprojectHash: string;}
interface BootstrapVersion { schema: number; runtime?: string; snapshot?: string; extraUvArgs?: string[]; pythonSkills?: BootstrapPythonSkill[];}
function errorMessage(error: unknown): string { return error instanceof Error ? error.message : String(error);}
function isRecord(value: unknown): value is Record<string, unknown> { return typeof value === "object" && value !== null && !Array.isArray(value);}
async function exists(filePath: string): Promise<boolean> { try { await access(filePath); return true; } catch { return false; }}
async function isExecutable(filePath: string): Promise<boolean> { try { await access(filePath, constants.X_OK); return true; } catch { return false; }}
function expandHome(filePath: string): string { if (filePath === "~") return os.homedir(); if (filePath.startsWith("~/")) return path.join(os.homedir(), filePath.slice(2)); return filePath;}
function fileContentHash(filePath: string): string { try { return `sha256:${createHash("sha256").update(readFileSync(filePath)).digest("hex")}`; } catch { return "unreadable"; }}
function normalizePythonSkills(pythonSkills: readonly KernelPythonSkill[] | undefined): BootstrapPythonSkill[] { const byKey = new Map<string, BootstrapPythonSkill>(); const addSkill = (skill: Pick<KernelPythonSkill, "importName" | "packagePath" | "pyprojectPath">): void => { const packagePath = path.resolve(skill.packagePath); const pyprojectPath = path.resolve(skill.pyprojectPath); const key = `${skill.importName}\0${packagePath}`; if (byKey.has(key)) { return; } const bootstrapSkill: BootstrapPythonSkill = { importName: skill.importName, packagePath, pyprojectPath, pyprojectHash: fileContentHash(pyprojectPath), }; byKey.set(key, bootstrapSkill); for (const dependencyName of readPythonSkillDependencyNames(bootstrapSkill)) { const siblingDependency = resolveSiblingPythonSkillDependency(bootstrapSkill, dependencyName); if (siblingDependency) { addSkill(siblingDependency); } } }; for (const skill of pythonSkills ?? []) { addSkill(skill); } return [...byKey.values()].sort((a, b) => { const packageCompare = a.packagePath.localeCompare(b.packagePath); if (packageCompare !== 0) return packageCompare; return a.importName.localeCompare(b.importName); });}
function readTomlProjectSection(pyprojectPath: string): string | undefined { try { const text = readFileSync(pyprojectPath, "utf-8"); const match = text.match(/^\s*\[project\]\s*$/m); if (!match || match.index === undefined) { return undefined; } const sectionStart = match.index + match[0].length; const rest = text.slice(sectionStart); const nextSection = rest.search(/^\s*\[/m); return nextSection >= 0 ? rest.slice(0, nextSection) : rest; } catch { return undefined; }}
function readPythonSkillProjectName(skill: BootstrapPythonSkill): string { const projectSection = readTomlProjectSection(skill.pyprojectPath); const name = projectSection?.match(/^\s*name\s*=\s*["']([^"']+)["']/m)?.[1]; return name?.trim() || skill.importName.replaceAll("_", "-");}
function parseDependencyPackageName(dependency: string): string | undefined { const withoutMarker = dependency.split(";")[0]?.trim() ?? ""; if (!withoutMarker) { return undefined; } const match = withoutMarker.match(/^([A-Za-z0-9_.-]+)/); return match?.[1]?.replaceAll("_", "-").toLowerCase();}
function findTomlArrayEnd(text: string, startIndex: number): number { let inQuote: '"' | "'" | undefined; let escaped = false; for (let index = startIndex; index < text.length; index++) { const char = text[index]; if (inQuote) { if (escaped) { escaped = false; continue; } if (char === "\\") { escaped = true; continue; } if (char === inQuote) { inQuote = undefined; } continue; } if (char === '"' || char === "'") { inQuote = char; continue; } if (char === "]") { return index; } } return -1;}
function readPythonSkillDependencyNames(skill: BootstrapPythonSkill): Set<string> { const projectSection = readTomlProjectSection(skill.pyprojectPath); if (!projectSection) { return new Set(); } const dependenciesStart = projectSection.search(/^\s*dependencies\s*=\s*\[/m); if (dependenciesStart < 0) { return new Set(); } const arrayStart = projectSection.indexOf("[", dependenciesStart); if (arrayStart < 0) { return new Set(); } const arrayEnd = findTomlArrayEnd(projectSection, arrayStart + 1); if (arrayEnd < 0) { return new Set(); } const dependenciesArray = projectSection.slice(arrayStart, arrayEnd + 1); const dependencies = new Set<string>(); const dependencyPattern = /"([^"\\]*(?:\\.[^"\\]*)*)"|'([^'\\]*(?:\\.[^'\\]*)*)'/g; for (const match of dependenciesArray.matchAll(dependencyPattern)) { const dependency = (match[1] ?? match[2] ?? "").replaceAll('\\"', '"').replaceAll("\\'", "'"); const name = parseDependencyPackageName(dependency); if (name) { dependencies.add(name); } } return dependencies;}
function resolveSiblingPythonSkillDependency( skill: BootstrapPythonSkill, dependencyName: string,): BootstrapPythonSkill | undefined { const siblingsDir = path.dirname(skill.packagePath); for (const entry of readdirSync(siblingsDir, { withFileTypes: true })) { if (!entry.isDirectory()) { continue; } const packagePath = path.join(siblingsDir, entry.name); const pyprojectPath = path.join(packagePath, "pyproject.toml"); if (!existsSync(pyprojectPath)) { continue; } const dependency: BootstrapPythonSkill = { importName: entry.name.replaceAll("-", "_"), packagePath, pyprojectPath, pyprojectHash: fileContentHash(pyprojectPath), }; if (readPythonSkillProjectName(dependency).replaceAll("_", "-").toLowerCase() === dependencyName) { return dependency; } } return undefined;}
function sortPythonSkillsForInstall(pythonSkills: readonly BootstrapPythonSkill[]): BootstrapPythonSkill[] { const byProjectName = new Map<string, BootstrapPythonSkill>(); const originalIndex = new Map<BootstrapPythonSkill, number>(); for (const [index, skill] of pythonSkills.entries()) { originalIndex.set(skill, index); byProjectName.set(readPythonSkillProjectName(skill).replaceAll("_", "-").toLowerCase(), skill); }
const dependenciesBySkill = new Map<BootstrapPythonSkill, BootstrapPythonSkill[]>(); for (const skill of pythonSkills) { dependenciesBySkill.set( skill, [...readPythonSkillDependencyNames(skill)] .map( (dependencyName) => byProjectName.get(dependencyName) ?? resolveSiblingPythonSkillDependency(skill, dependencyName), ) .filter((dependency): dependency is BootstrapPythonSkill => Boolean(dependency)), ); }
const pending = new Set(pythonSkills); const sorted: BootstrapPythonSkill[] = []; while (pending.size > 0) { let progressed = false; for (const skill of [...pending].sort((a, b) => (originalIndex.get(a) ?? 0) - (originalIndex.get(b) ?? 0))) { const dependencies = dependenciesBySkill.get(skill) ?? []; if (dependencies.some((dependency) => pending.has(dependency))) { continue; } sorted.push(skill); pending.delete(skill); progressed = true; } if (!progressed) { // Cyclic local skill dependencies cannot be topologically ordered; keep a // deterministic order and let uv surface the packaging error if needed. sorted.push(...[...pending].sort((a, b) => a.packagePath.localeCompare(b.packagePath))); break; } } return sorted;}
function formatPythonSkillInstallArgs(skill: BootstrapPythonSkill): string[] { return ["--editable", skill.packagePath];}
function ensureKernelPythonKey(pythonSkills: readonly BootstrapPythonSkill[]): string { return [ process.env.PRIME_AGENT_KERNEL_PYTHON ?? "", process.env.PRIME_AGENT_KERNEL_VENV ?? "", process.env.HOME ?? "", process.env.XDG_DATA_HOME ?? "", JSON.stringify(pythonSkills), ].join("\0");}
export function getKernelVenvDir(): string { const override = process.env.PRIME_AGENT_KERNEL_VENV; if (override) return path.resolve(expandHome(override)); return path.join(os.homedir(), ".prime", "agent", "kernel-venv");}
function getXdgKernelVenvDir(): string { const dataHome = process.env.XDG_DATA_HOME ? path.resolve(expandHome(process.env.XDG_DATA_HOME)) : path.join(os.homedir(), ".local", "share"); return path.join(dataHome, "prime", "agent", "kernel-venv");}
async function resolveWritableKernelVenvDir(): Promise<string> { const primary = getKernelVenvDir(); try { await mkdir(path.dirname(primary), { recursive: true }); return primary; } catch (primaryError) { if (process.env.PRIME_AGENT_KERNEL_VENV) { throw new Error(`couldn't create kernel venv parent directory for ${primary}: ${errorMessage(primaryError)}`); }
const fallback = getXdgKernelVenvDir(); try { await mkdir(path.dirname(fallback), { recursive: true }); return fallback; } catch (fallbackError) { throw new Error( `couldn't create kernel venv directory at ${primary} or ${fallback}; set PRIME_AGENT_KERNEL_PYTHON to a python with a current prime-agent-runtime installed. ${errorMessage(fallbackError)}`, ); } }}
function isBatchShim(command: string): boolean { return process.platform === "win32" && /\.(cmd|bat)$/i.test(command);}
function run(command: string, args: string[], options: { stdio?: "ignore" | "inherit" } = {}): Promise<void> { return new Promise((resolve, reject) => { // CPython must read UTF-8 .pth files even under a Windows legacy code page. const env = { ...process.env, ...(process.platform === "win32" ? { PYTHONUTF8: "1" } : {}) }; const batch = isBatchShim(command) ? buildBatchShimInvocation(command, args, env) : undefined; const child = spawnHidden(batch ? (process.env.ComSpec ?? "cmd.exe") : command, batch?.args ?? args, { env: batch?.env ?? env, stdio: options.stdio ?? "ignore", ...(batch ? { windowsVerbatimArguments: true } : {}), }); child.on("error", reject); child.on("exit", (code, signal) => { if (code === 0) { resolve(); return; } const reason = signal ? `signal ${signal}` : `exit code ${code}`; reject(new Error(`${command} ${args.join(" ")} failed with ${reason}`)); }); });}
async function pythonImports(python: string, moduleName: string): Promise<boolean> { try { await run(python, ["-c", `import ${moduleName}`], { stdio: "ignore" }); return true; } catch { return false; }}
async function hasPrimeAgentRuntime(python: string): Promise<boolean> { try { await run(python, ["-c", RUNTIME_READY_CHECK], { stdio: "ignore" }); return true; } catch { return false; }}
async function missingRlmExtraImportLabels(python: string): Promise<string[]> { const missing: string[] = []; for (const pkg of DEFAULT_RLM_EXTRA_PACKAGES) { if (!(await pythonImports(python, pkg.importName))) { missing.push(pkg.promptLabel); } } return missing;}
async function missingPythonSkillImportLabels( python: string, pythonSkills: readonly KernelPythonSkill[],): Promise<string[]> { const missing: string[] = []; for (const skill of pythonSkills) { if (!(await pythonImports(python, skill.importName))) { missing.push(`${skill.name} (${skill.importName})`); } } return missing;}
function reportProgress(options: EnsureKernelPythonOptions, message: string): void { if (options.onProgress) { options.onProgress(message); return; } process.stderr.write(`${message}\n`);}
function bootstrapLockDir(venv: string): string { return path.join(path.dirname(venv), `${path.basename(venv)}${KERNEL_VENV_LOCK_SUFFIX}`);}
async function lockMissingPidIsStale(lockDir: string): Promise<boolean> { try { const lockStat = await stat(lockDir); return Date.now() - lockStat.mtimeMs > KERNEL_VENV_LOCK_STALE_MS; } catch { return false; }}
export function formatBootstrapLockOwner(pid: number, bootToken: string = `${Date.now()}-${randomUUID()}`): string { return `${pid}:${bootToken}\n`;}
export function parseBootstrapLockOwner(raw: string): { pid: number; bootToken?: string } | undefined { const trimmed = raw.trim(); if (!trimmed) { return undefined; } const parts = trimmed.split(/[\s:]+/); const pid = Number.parseInt(parts[0], 10); if (!Number.isInteger(pid) || pid <= 0) { return undefined; } return { pid, bootToken: parts[1] };}
// Shared with the kernel-side bootstrap, which uses the same name to bound its own wait.function bootstrapLockWaitMs(): number { const seconds = Number.parseFloat(process.env.PRIME_AGENT_KERNEL_BOOTSTRAP_WAIT_SECONDS ?? ""); return Number.isFinite(seconds) && seconds > 0 ? seconds * 1000 : DEFAULT_BOOTSTRAP_LOCK_WAIT_MS;}
async function acquireBootstrapLock(venv: string, options: EnsureKernelPythonOptions): Promise<() => Promise<void>> { const lockDir = bootstrapLockDir(venv); const waitMs = bootstrapLockWaitMs(); await mkdir(path.dirname(lockDir), { recursive: true }); const ownerContent = formatBootstrapLockOwner(process.pid);
let deadline: number | undefined; for (;;) { const attempt = await tryAcquireDirLock( lockDir, async (ownerPid) => ownerPid === undefined ? !(await lockMissingPidIsStale(lockDir)) : isProcessAlive(ownerPid), ownerContent, ); if (attempt === "acquired") { return () => rm(lockDir, { recursive: true, force: true }); } if (attempt === "held") { if (deadline === undefined) { // Whoever holds the lock is rebuilding the venv; importing it now would // read a tree mid-install, so wait for the build instead. deadline = Date.now() + waitMs; reportProgress( options, `› another prime-agent process is building the kernel venv at ${venv}; waiting for it to finish…`, ); } else if (Date.now() >= deadline) { throw new Error( `another prime-agent process has held the kernel venv build lock at ${lockDir} for over ${Math.round(waitMs / 1000)}s. ` + "If that process is gone, remove that file and retry; otherwise wait for it to finish. " + "Set PRIME_AGENT_KERNEL_PYTHON to a Python with a current prime-agent-runtime to skip the shared venv.", ); } await sleep(BOOTSTRAP_LOCK_RETRY_MS); } }}
function venvSiblingPath(venv: string, suffix: string): string { return path.join(path.dirname(venv), `${path.basename(venv)}${suffix}-${process.pid}-${randomUUID().slice(0, 8)}`);}
/** The directory a build stages into before it is promoted onto the live path. */function kernelVenvStagingDir(venv: string): string { return venvSiblingPath(venv, VENV_STAGING_SUFFIX);}
/** Collect staged or replaced trees a crashed build left behind. */async function sweepStaleVenvSiblings(venv: string): Promise<void> { const directory = path.dirname(venv); const prefixes = [`${path.basename(venv)}${VENV_STAGING_SUFFIX}-`, `${path.basename(venv)}${VENV_REPLACED_SUFFIX}-`]; let entries: string[]; try { entries = await readdir(directory); } catch { return; } for (const name of entries) { if (!prefixes.some((prefix) => name.startsWith(prefix))) continue; const sibling = path.join(directory, name); try { const siblingStat = await stat(sibling); if (Date.now() - siblingStat.mtimeMs < VENV_SIBLING_SWEEP_AGE_MS) continue; await rm(sibling, { recursive: true, force: true }); } catch { // Litter collection only: the next build retries. } }}
/** * Move a completed staged tree onto the live venv path. rename(2) cannot replace a * non-empty directory, so the swap is two renames: the live path holds the previous * complete tree, nothing, or the new complete tree - never a partially installed one. */async function verifyStagedKernelVenv(python: string, staging: string, venv: string): Promise<void> { if (!(await hasPrimeAgentRuntime(python))) { throw new Error( `the staged kernel venv at ${staging} does not satisfy the prime-agent-runtime check; ` + `keeping the previous kernel venv at ${venv}. Re-run once the runtime install is fixed.`, ); }}
async function promoteKernelVenv(venv: string, staging: string): Promise<void> { const replaced = existsSync(venv) ? venvSiblingPath(venv, VENV_REPLACED_SUFFIX) : undefined; if (replaced) { await rename(venv, replaced); } try { await rename(staging, venv); } catch (error) { if (replaced) { await rename(replaced, venv).catch(() => undefined); } throw error; } if (replaced) { await rm(replaced, { recursive: true, force: true }).catch(() => undefined); }}
/** Try a bare command followed by supported PATHEXT extensions in the configured order. */export function windowsExecutableCandidates(name: string, pathext: string | undefined): string[] { const extensions = (pathext ?? "") .split(";") .map((ext) => ext.trim().toLowerCase()) .filter((ext) => WINDOWS_SUPPORTED_EXECUTABLE_EXTENSIONS.has(ext)); const lowerName = name.toLowerCase(); if (WINDOWS_PATHEXT_DEFAULT.some((ext) => lowerName.endsWith(ext.toLowerCase()))) { return [name]; } const seen = new Set<string>([name.toLowerCase()]); const candidates = [name]; for (const ext of extensions.length > 0 ? extensions : WINDOWS_PATHEXT_DEFAULT) { const candidate = `${name}${ext}`; if (seen.has(candidate.toLowerCase())) continue; seen.add(candidate.toLowerCase()); candidates.push(candidate); } return candidates;}
async function findExecutable(name: string): Promise<string | null> { const pathValue = process.env.PATH; if (!pathValue) return null; const candidates = process.platform === "win32" ? windowsExecutableCandidates(name, process.env.PATHEXT) : [name]; for (const dir of pathValue.split(path.delimiter)) { if (!dir) continue; for (const candidate of candidates) { const fullPath = path.join(dir, candidate); if (await isExecutable(fullPath)) return fullPath; } } return null;}
async function ensureUv(options: EnsureKernelPythonOptions): Promise<string> { const fromPath = await findExecutable("uv"); if (fromPath) return fromPath;
const localUv = path.join(os.homedir(), ".local", "bin", process.platform === "win32" ? "uv.exe" : "uv"); if (await isExecutable(localUv)) return localUv;
const shouldInstallUv = process.env.PRIME_AGENT_INSTALL_UV === "1" || (!options.onProgress && (await confirmUvInstall())); if (!shouldInstallUv) { throw new Error( `uv is required to set up the Python kernel. Install uv yourself: ${UV_INSTALL_COMMAND}, ` + "or set PRIME_AGENT_INSTALL_UV=1 to let prime-agent run that installer.", ); }
reportProgress(options, "› installing uv (one-time)…"); try { await run("sh", ["-c", UV_INSTALL_COMMAND], { stdio: options.onProgress ? "ignore" : "inherit" }); } catch (error) { throw new Error( `couldn't install uv from astral.sh; install it yourself: ${UV_INSTALL_COMMAND}, then re-run prime-agent. ${errorMessage(error)}`, ); }
if (await isExecutable(localUv)) return localUv; const installedFromPath = await findExecutable("uv"); if (installedFromPath) return installedFromPath; throw new Error("uv install completed but binary not found at ~/.local/bin/uv");}
async function confirmUvInstall(): Promise<boolean> { if (process.env.PRIME_AGENT_INSTALL_UV === "0") return false; if (!stdin.isTTY || !stderr.isTTY) return false;
const rl = createInterface({ input: stdin, output: stderr }); try { const answer = (await rl.question("Prime Agent needs uv to set up Python. Install uv from astral.sh now? [Y/n] ")) .trim() .toLowerCase(); return answer !== "n" && answer !== "no"; } finally { rl.close(); }}
async function readBootstrapVersion(venv: string): Promise<BootstrapVersion | null> { try { const raw = await readFile(path.join(venv, BOOTSTRAP_VERSION_FILE), "utf8"); const parsed: unknown = JSON.parse(raw); if (!isRecord(parsed) || typeof parsed.schema !== "number") return null; const extraUvArgs = Array.isArray(parsed.extraUvArgs) && parsed.extraUvArgs.every((v: unknown): v is string => typeof v === "string") ? (parsed.extraUvArgs as string[]) : undefined; let pythonSkills: BootstrapPythonSkill[] | undefined; if (Array.isArray(parsed.pythonSkills)) { if ( !parsed.pythonSkills.every((v: unknown): v is BootstrapPythonSkill => { if (!isRecord(v)) return false; return ( typeof v.importName === "string" && typeof v.packagePath === "string" && typeof v.pyprojectPath === "string" && typeof v.pyprojectHash === "string" ); }) ) { return null; } pythonSkills = parsed.pythonSkills as BootstrapPythonSkill[]; } return { schema: parsed.schema, runtime: typeof parsed.runtime === "string" ? parsed.runtime : undefined, snapshot: typeof parsed.snapshot === "string" ? parsed.snapshot : undefined, extraUvArgs, pythonSkills, }; } catch { return null; }}
function extraUvArgsMatch(a: string[] | undefined, b: string[] | undefined): boolean { if (a === b) return true; if (!a || !b) return false; if (a.length !== b.length) return false; return a.every((v, i) => v === b[i]);}
function pythonSkillsMatch(a: BootstrapPythonSkill[] | undefined, b: readonly BootstrapPythonSkill[]): boolean { const left = a ?? []; if (left.length !== b.length) return false; return left.every((skill, index) => { const expected = b[index]; return ( skill.importName === expected.importName && skill.packagePath === expected.packagePath && skill.pyprojectPath === expected.pyprojectPath && skill.pyprojectHash === expected.pyprojectHash ); });}
function bootstrapVersionCurrent( version: BootstrapVersion | null, runtimeIdentity: string, pythonSkills: readonly BootstrapPythonSkill[],): boolean { return ( version !== null && bootstrapBaseVersionCurrent(version, runtimeIdentity) && pythonSkillsMatch(version.pythonSkills, pythonSkills) );}
function bootstrapBaseVersionCurrent(version: BootstrapVersion | null, runtimeIdentity: string): boolean { return ( version?.schema === BOOTSTRAP_SCHEMA && version.runtime === runtimeIdentity && version.snapshot === STATE_SNAPSHOT_REQUIREMENT && extraUvArgsMatch(version.extraUvArgs, DEFAULT_RLM_EXTRA_UV_ARGS) );}
async function writeBootstrapVersion( venv: string, runtimeIdentity: string, pythonSkills: readonly BootstrapPythonSkill[],): Promise<void> { const version: BootstrapVersion = { schema: BOOTSTRAP_SCHEMA, runtime: runtimeIdentity, snapshot: STATE_SNAPSHOT_REQUIREMENT, extraUvArgs: DEFAULT_RLM_EXTRA_UV_ARGS, pythonSkills: [...pythonSkills], }; await writeFile(path.join(venv, BOOTSTRAP_VERSION_FILE), `${JSON.stringify(version)}\n`, "utf8");}
function runtimeCandidateDirs(): string[] { const moduleDir = path.dirname(fileURLToPath(import.meta.url)); // Compiled executables use a flat sidecar layout; Node packages keep sources in dist/. // Resolve both from the physical package directory, outside Bun's virtual filesystem. return [ path.join(getPackageDir(), "prime-agent-runtime"), path.join(getPackageDir(), "dist", "prime-agent-runtime"), path.resolve(moduleDir, "..", "..", "prime-agent-runtime"), path.resolve(moduleDir, "..", "..", "..", "..", "..", "prime-agent-runtime"), ];}
export async function resolveRuntimeSourceDir(): Promise<string | null> { for (const candidate of runtimeCandidateDirs()) { if (await exists(path.join(candidate, "pyproject.toml"))) { return candidate; } } return null;}
// Identity of the runtime to be installed. For a local source checkout this is a// content hash of every Python source file plus pyproject.toml, so any runtime code// or dependency change invalidates an existing venv automatically. Falls back to// the bare package name when the runtime resolves to a registry install (no local source).export async function resolveRuntimeIdentity(): Promise<string> { const sourceDir = await resolveRuntimeSourceDir(); if (!sourceDir) return RUNTIME_REQUIREMENT; return hashRuntimeSource(sourceDir);}
// Throws if the local source can't be read. A failure here must surface rather than// fall back to RUNTIME_REQUIREMENT: that constant is the registry-install identity, and// recording it for a local checkout would permanently mask later source changes.async function hashRuntimeSource(sourceDir: string): Promise<string> { const srcDir = path.join(sourceDir, "src"); const files: string[] = [path.join(sourceDir, "pyproject.toml")]; async function collect(dir: string): Promise<void> { const entries = await readdir(dir, { withFileTypes: true }); for (const entry of entries) { const full = path.join(dir, entry.name); if (entry.isDirectory()) { await collect(full); } else if (entry.isFile() && entry.name.endsWith(".py")) { files.push(full); } } } await collect(srcDir); files.sort(); const hash = createHash("sha256"); for (const file of files) { hash.update(path.relative(sourceDir, file)); hash.update("\0"); hash.update(await readFile(file)); hash.update("\0"); } return `sha256:${hash.digest("hex")}`;}
export function kernelVenvPython(venv: string, platform: NodeJS.Platform = process.platform): string { return platform === "win32" ? path.join(venv, "Scripts", "python.exe") : path.join(venv, "bin", "python");}
async function bootstrapVenv( venv: string, pythonSkills: readonly BootstrapPythonSkill[], options: EnsureKernelPythonOptions,): Promise<void> { await mkdir(path.dirname(venv), { recursive: true }); await sweepStaleVenvSiblings(venv); const uv = await ensureUv(options); const sourceDir = await resolveRuntimeSourceDir(); const runtimeRequirement = sourceDir ?? RUNTIME_REQUIREMENT; const runtimeIdentity = await resolveRuntimeIdentity();
// Build the whole tree beside the live path - runtime, extras, and every editable // Python skill - and rename it in only when the manifest is written. A kernel that // imports while this runs sees the previous complete venv, never a half-built one. const staging = kernelVenvStagingDir(venv); const python = kernelVenvPython(staging); try { await run(uv, ["python", "install", PYTHON_VERSION]); // Relocatable keeps bin/ shebangs relative to the venv, so they still resolve // after the staged tree is renamed onto the live path. await run(uv, ["venv", staging, "--python", PYTHON_VERSION, "--seed", "--relocatable"]); await run(uv, [ "pip", "install", "--python", python, runtimeRequirement, STATE_SNAPSHOT_REQUIREMENT, ...DEFAULT_RLM_EXTRA_UV_ARGS, ]); await syncPythonSkills(uv, staging, python, runtimeIdentity, pythonSkills, options); await verifyStagedKernelVenv(python, staging, venv); } catch (error) { await rm(staging, { recursive: true, force: true }).catch(() => undefined); throw error; } await promoteKernelVenv(venv, staging);}
async function syncPythonSkills( uv: string, venv: string, python: string, runtimeIdentity: string, pythonSkills: readonly BootstrapPythonSkill[], options: EnsureKernelPythonOptions,): Promise<void> { const version = await readBootstrapVersion(venv); const installedPythonSkills: BootstrapPythonSkill[] = []; const currentPythonSkills = new Map( (version?.pythonSkills ?? []).map((skill) => [`${skill.importName}\0${skill.packagePath}`, skill]), ); const pythonSkillsByProjectName = new Map( pythonSkills.map((skill) => [readPythonSkillProjectName(skill).replaceAll("_", "-").toLowerCase(), skill]), ); const dependenciesBySkill = new Map( pythonSkills.map((skill) => [ skill, [...readPythonSkillDependencyNames(skill)] .map( (dependencyName) => pythonSkillsByProjectName.get(dependencyName) ?? resolveSiblingPythonSkillDependency(skill, dependencyName), ) .filter((dependency): dependency is BootstrapPythonSkill => Boolean(dependency)), ]), );
for (const skill of sortPythonSkillsForInstall(pythonSkills)) { const existingSkill = currentPythonSkills.get(`${skill.importName}\0${skill.packagePath}`); if (existingSkill?.pyprojectPath === skill.pyprojectPath && existingSkill.pyprojectHash === skill.pyprojectHash) { installedPythonSkills.push(skill); continue; }
const localDependencies = dependenciesBySkill.get(skill) ?? []; const localDependencyArgs = localDependencies .filter((dependency) => { const installedDependency = currentPythonSkills.get(`${dependency.importName}\0${dependency.packagePath}`); const installedThisSync = installedPythonSkills.some( (installed) => installed.importName === dependency.importName && installed.packagePath === dependency.packagePath && installed.pyprojectPath === dependency.pyprojectPath && installed.pyprojectHash === dependency.pyprojectHash, ); return !( installedThisSync || (installedDependency?.pyprojectPath === dependency.pyprojectPath && installedDependency.pyprojectHash === dependency.pyprojectHash) ); }) .flatMap(formatPythonSkillInstallArgs);
try { await run(uv, [ "pip", "install", "--python", python, ...formatPythonSkillInstallArgs(skill), ...localDependencyArgs, ]); installedPythonSkills.push( skill, ...localDependencies.filter((dependency) => !installedPythonSkills.includes(dependency)), ); } catch (error) { reportProgress( options, `Warning: Python skill ${skill.importName} failed to install and will be unavailable: ${errorMessage(error)}`, ); } } await writeBootstrapVersion(venv, runtimeIdentity, installedPythonSkills);}
async function kernelBaseReady(python: string, venv: string, runtimeIdentity: string): Promise<boolean> { return ( (await hasPrimeAgentRuntime(python)) && bootstrapBaseVersionCurrent(await readBootstrapVersion(venv), runtimeIdentity) );}
async function kernelReady( python: string, venv: string, runtimeIdentity: string, pythonSkills: readonly BootstrapPythonSkill[],): Promise<boolean> { return ( (await hasPrimeAgentRuntime(python)) && bootstrapVersionCurrent(await readBootstrapVersion(venv), runtimeIdentity, pythonSkills) );}
function formatBootstrapFailure(error: unknown): Error { return new Error( `Failed to set up the Python kernel runtime. ${errorMessage(error)}\n` + "First-time setup needs internet to install uv, Python, prime-agent-runtime, and default Python packages; once set up, prime-agent runs offline. " + "An interrupted runtime upgrade needs network once more, so re-run this while online. " + "Set PRIME_AGENT_KERNEL_PYTHON to a Python with a current prime-agent-runtime and default Python packages installed to skip auto-bootstrap.", );}
async function ensureKernelPythonUncached( options: EnsureKernelPythonOptions, pythonSkills: readonly BootstrapPythonSkill[],): Promise<string> { const override = process.env.PRIME_AGENT_KERNEL_PYTHON; if (override) { const python = path.resolve(expandHome(override)); if (isBatchShim(python)) { throw new Error( `PRIME_AGENT_KERNEL_PYTHON must point directly to a Python executable, not a Windows batch shim: ${python}`, ); } const missing: string[] = []; if (!(await hasPrimeAgentRuntime(python))) { missing.push( "a current prime-agent-runtime with callable rlm.spawn, rlm.create_session, rlm.host_request, rlm.progress_note, and explicit harness CRUD methods", ); } if (missing.length === 0) { const missingExtraImports = await missingRlmExtraImportLabels(python); if (missingExtraImports.length > 0) { missing.push(`default Python packages (${missingExtraImports.join(", ")})`); } } if (missing.length === 0 && pythonSkills.length > 0) { const missingPythonSkills = await missingPythonSkillImportLabels(python, options.pythonSkills ?? []); if (missingPythonSkills.length > 0) { reportProgress( options, `Warning: Python skills unavailable in PRIME_AGENT_KERNEL_PYTHON and will be disabled: ${missingPythonSkills.join(", ")}`, ); } } if (missing.length === 0) return python; throw new Error(`PRIME_AGENT_KERNEL_PYTHON points to a Python missing ${missing.join(" and ")}: ${python}`); }
const venv = await resolveWritableKernelVenvDir(); const python = kernelVenvPython(venv); const runtimeIdentity = await resolveRuntimeIdentity(); if (await kernelReady(python, venv, runtimeIdentity, pythonSkills)) return python;
const releaseLock = await acquireBootstrapLock(venv, options); try { if (await kernelReady(python, venv, runtimeIdentity, pythonSkills)) return python; if (await kernelBaseReady(python, venv, runtimeIdentity)) { await syncPythonSkills(await ensureUv(options), venv, python, runtimeIdentity, pythonSkills, options); return python; }
reportProgress(options, "› setting up python kernel (one-time, ~30s)…"); if (existsSync(venv)) { reportProgress(options, "rebuilding kernel venv"); }
// The build stages beside the live venv and replaces it at the end, so the old // tree stays importable (and recoverable) until the new one is complete. await bootstrapVenv(venv, pythonSkills, options); } catch (error) { throw formatBootstrapFailure(error); } finally { await releaseLock().catch(() => undefined); }
reportProgress(options, "✓ ready"); return python;}
export function ensureKernelPython(options: EnsureKernelPythonOptions = {}): Promise<string> { const pythonSkills = normalizePythonSkills(options.pythonSkills); const key = ensureKernelPythonKey(pythonSkills); if (inFlightEnsureKernelPython?.key === key) return inFlightEnsureKernelPython.promise;
const promise = ensureKernelPythonUncached(options, pythonSkills).finally(() => { if (inFlightEnsureKernelPython?.promise === promise) inFlightEnsureKernelPython = null; }); inFlightEnsureKernelPython = { key, promise }; return promise;}