import { Database } from 'bun:sqlite';
import { mkdir, writeFile, readFile, readdir } from 'node:fs/promises';
import { join } from 'node:path';
import { existsSync } from 'node:fs';
/**
* Interface definition for all storage backends in the ablation study.
*/
export interface AgentStorage {
name: string; // C0, C1, C2
init(): Promise<void>;
write(path: string, content: string): Promise<void>;
read(path: string): Promise<string | null>;
list(): Promise<string[]>; // Returns relative paths
log(entry: any): Promise<void>;
close(): void;
}
/**
* C0: Standard Filesystem Implementation.
* "What we do today"
*/
export class FileSystemStorage implements AgentStorage {
name = "C0 (Files)";
constructor(private rootDir: string) { }
async init() {
if (!existsSync(this.rootDir)) {
await mkdir(this.rootDir, { recursive: true });
}
await mkdir(join(this.rootDir, 'logs'), { recursive: true });
}
async write(path: string, content: string) {
const fullPath = join(this.rootDir, path);
// Ensure parent dir exists
const parent = fullPath.substring(0, fullPath.lastIndexOf('/'));
if (parent && !existsSync(parent)) {
await mkdir(parent, { recursive: true });
}
await writeFile(fullPath, content, 'utf8');
}
async read(path: string) {
const fullPath = join(this.rootDir, path);
if (!existsSync(fullPath)) return null;
return await readFile(fullPath, 'utf8');
}
async list() {
// Simple recursive list not implemented safely for MVP, assuming specific read patterns
// Or returning just top-level for now to satisfy interface
return await readdir(this.rootDir);
}
async log(entry: any) {
const logPath = join(this.rootDir, 'logs', 'audit.jsonl');
const line = JSON.stringify({ ts: new Date().toISOString(), ...entry }) + '\n';
await writeFile(logPath, line, { flag: 'a' });
}
close() { }
}
/**
* C1: AgentFS Blob Storage.
* "Files but inside SQLite" - tests the DB container overhead vs FS.
*/
export class AgentFSBlobStorage implements AgentStorage {
name = "C1 (AgentFS-Blob)";
private db: Database;
constructor(private dbPath: string) {
this.db = new Database(dbPath);
}
async init() {
this.db.run(`
CREATE TABLE IF NOT EXISTS files (
path TEXT PRIMARY KEY,
content TEXT,
modified_at TEXT
)
`);
this.db.run(`
CREATE TABLE IF NOT EXISTS audit_log (
id INTEGER PRIMARY KEY AUTOINCREMENT,
ts TEXT,
entry TEXT
)
`);
}
async write(path: string, content: string) {
this.db.run(
`INSERT OR REPLACE INTO files (path, content, modified_at) VALUES (?, ?, ?)`,
[path, content, new Date().toISOString()]
);
}
async read(path: string) {
const row = this.db.query(`SELECT content FROM files WHERE path = ?`).get(path) as any;
return row ? row.content : null;
}
async list() {
const rows = this.db.query(`SELECT path FROM files`).all() as any[];
return rows.map(r => r.path);
}
async log(entry: any) {
this.db.run(
`INSERT INTO audit_log (ts, entry) VALUES (?, ?)`,
[new Date().toISOString(), JSON.stringify(entry)]
);
}
close() {
this.db.close();
}
}
/**
* C2: AgentFS Native Schema.
* "Structured tables + FPF" - tests the benefit of schema.
*/
export class AgentFSSchemaStorage implements AgentStorage {
name = "C2 (AgentFS-Schema)";
private db: Database;
constructor(private dbPath: string) {
this.db = new Database(dbPath);
}
async init() {
// Enable FTS for searchability (Retrieval task)
// Using a comprehensive schema that mimics the FPF Spec
this.db.run(`
CREATE TABLE IF NOT EXISTS fpf_plan_items (
id INTEGER PRIMARY KEY AUTOINCREMENT,
kind TEXT,
status TEXT, -- planned, in-progress, done
content_json TEXT,
created_at TEXT
)
`);
// Audit trail is a first-class citizen
this.db.run(`
CREATE TABLE IF NOT EXISTS fpf_audit_trail (
id INTEGER PRIMARY KEY AUTOINCREMENT,
actor TEXT,
action TEXT,
target TEXT,
details_json TEXT,
timestamp TEXT
)
`);
// Key-Value store for misc file data (hybrid)
this.db.run(`
CREATE TABLE IF NOT EXISTS fpf_kv_store (
key TEXT PRIMARY KEY,
value TEXT,
updated_at TEXT
)
`);
}
async write(path: string, content: string) {
// In C2, if it looks like a PlanItem, we store it structured
// Otherwise we put it in KV
// Simple heuristic for the benchmark
this.db.run(
`INSERT OR REPLACE INTO fpf_kv_store (key, value, updated_at) VALUES (?, ?, ?)`,
[path, content, new Date().toISOString()]
);
}
async savePlanItem(kind: string, content: any) {
this.db.run(
`INSERT INTO fpf_plan_items (kind, status, content_json, created_at) VALUES (?, 'planned', ?, ?)`,
[kind, JSON.stringify(content), new Date().toISOString()]
);
}
async read(path: string) {
const row = this.db.query(`SELECT value FROM fpf_kv_store WHERE key = ?`).get(path) as any;
return row ? row.value : null;
}
async queryPlan(kind: string) {
const rows = this.db.query(`SELECT content_json FROM fpf_plan_items WHERE kind = ?`).all(kind) as any[];
return rows.map(r => JSON.parse(r.content_json));
}
async list() {
const kv = this.db.query(`SELECT key FROM fpf_kv_store`).all() as any[];
// Merge with structured items? For now just KV keys
return kv.map(r => r.key);
}
async log(entry: any) {
// Structured logging
this.db.run(
`INSERT INTO fpf_audit_trail (actor, action, target, details_json, timestamp) VALUES (?, ?, ?, ?, ?)`,
[
entry.actor || 'system',
entry.action || 'unknown',
entry.target || 'unknown',
JSON.stringify(entry),
new Date().toISOString()
]
);
}
close() {
this.db.close();
}
}