/** * RLM Artifacts — Object-level referencing for sparse context. * * Large data (DataFrames, analysis results, raw outputs) is stored on disk * and passed by reference. The LLM context holds only metadata + summary, * with actual data accessed via lazy loading in notebook/bash execution. */ import path from "path" import fs from "fs/promises" import { Global } from "@/global" import { Log } from "@/util/log" import type { RLMState } from "./state" export namespace RLMArtifacts { const log = Log.create({ service: "rlm-artifacts" }) const ARTIFACTS_DIR = path.join(Global.Path.data, "artifacts") const TTL_MS = 7 * 24 * 60 * 60 * 1000 // 7 days /** Register an artifact — writes content to disk, returns a reference. */ export async function register( sessionId: string, type: string, content: string, summary?: string, ): Promise { const id = `art-${Date.now()}-${Math.random().toString(36).slice(2, 8)}` const dir = path.join(ARTIFACTS_DIR, sessionId) await fs.mkdir(dir, { recursive: true }) const filePath = path.join(dir, `${id}.dat`) await Bun.write(filePath, content) log.info("artifact registered", { sessionId, id, type, size: content.length }) return { id, type, summary: summary ?? `${type} artifact (${content.length} bytes)`, path: filePath, } } /** Resolve an artifact — reads full content from disk (lazy loading). */ export async function resolve(sessionId: string, id: string): Promise { const filePath = path.join(ARTIFACTS_DIR, sessionId, `${id}.dat`) try { return await Bun.file(filePath).text() } catch { log.warn("artifact not found", { sessionId, id }) return null } } /** List all artifacts for a session. */ export async function list(sessionId: string): Promise { const dir = path.join(ARTIFACTS_DIR, sessionId) try { const files = await fs.readdir(dir) return files .filter((f) => f.endsWith(".dat")) .map((f) => ({ id: f.replace(".dat", ""), type: "unknown", summary: `Artifact ${f}`, path: path.join(dir, f), })) } catch { return [] } } /** Cleanup artifacts older than TTL. Called on startup. */ export async function cleanup(): Promise { try { const exists = await fs.stat(ARTIFACTS_DIR).catch(() => null) if (!exists) return const sessions = await fs.readdir(ARTIFACTS_DIR) const now = Date.now() let cleaned = 0 for (const session of sessions) { const dir = path.join(ARTIFACTS_DIR, session) const stat = await fs.stat(dir).catch(() => null) if (!stat?.isDirectory()) continue if (now - stat.mtimeMs > TTL_MS) { await fs.rm(dir, { recursive: true }) cleaned++ } } if (cleaned > 0) { log.info("cleaned old artifacts", { count: cleaned }) } } catch (e) { log.warn("artifact cleanup error", { error: e instanceof Error ? e.message : String(e) }) } } }