From dc4bae3d0cc8632f11f25afce3640f633a3704cd Mon Sep 17 00:00:00 2001 From: Ivan Tymoshenko Date: Fri, 17 Apr 2026 17:27:49 +0200 Subject: [PATCH] feat: add fsRootPath option --- packages/regina-agent/README.md | 9 +++++ packages/regina-agent/src/schema.ts | 2 ++ packages/regina-agent/src/server.ts | 5 +-- packages/regina-agent/src/vfs-provider.ts | 12 +++++++ packages/regina-agent/test/provider.test.ts | 40 +++++++++++++++++++++ packages/regina-agent/test/schema.test.ts | 1 + packages/regina-agent/test/session.test.ts | 26 +++++++++++++- 7 files changed, 92 insertions(+), 3 deletions(-) create mode 100644 packages/regina-agent/src/vfs-provider.ts create mode 100644 packages/regina-agent/test/provider.test.ts diff --git a/packages/regina-agent/README.md b/packages/regina-agent/README.md index 4fed8b9..ccaa711 100644 --- a/packages/regina-agent/README.md +++ b/packages/regina-agent/README.md @@ -140,6 +140,14 @@ sequenceDiagram Conversations are stored as JSONL in the VFS at `/.session/messages.jsonl`. Messages are appended incrementally. On restart, the full history is restored. +The VFS backend depends on configuration: + +| Config | Provider | Persistence | +|---|---|---| +| (default) | `MemoryProvider` | In-memory only, lost on process exit | +| `vfsDbPath` | `SqliteProvider` | SQLite database file | +| `fsRootPath` | `RealFSProvider` | Real filesystem directory | + ## Context Compaction When the estimated token count exceeds a threshold (default: 100,000), older messages are automatically summarized by the model: @@ -190,6 +198,7 @@ These options are set automatically by `@platformatic/regina` when spawning an i | `definitionPath` | Path to the agent's markdown definition file | | `toolsBasePath` | Base directory for resolving tool module paths | | `vfsDbPath` | Path to the SQLite database for this instance's VFS | +| `fsRootPath` | Path to a real filesystem directory for the VFS. When set, agent files and messages are persisted directly to disk. | | `apiKey` | AI provider API key (injected from environment) | | `coordinatorId` | Parent service ID in the Watt runtime | | `instanceId` | This instance's unique identifier | diff --git a/packages/regina-agent/src/schema.ts b/packages/regina-agent/src/schema.ts index a141070..301342e 100644 --- a/packages/regina-agent/src/schema.ts +++ b/packages/regina-agent/src/schema.ts @@ -17,6 +17,7 @@ export interface ReginaAgentConfiguration extends NodeConfiguration { definitionPath: string toolsBasePath?: string vfsDbPath?: string + fsRootPath?: string coordinatorId?: string instanceId?: string apiKey?: string @@ -33,6 +34,7 @@ export const reginaAgent = { definitionPath: { type: 'string' }, toolsBasePath: { type: 'string' }, vfsDbPath: { type: 'string' }, + fsRootPath: { type: 'string' }, coordinatorId: { type: 'string' }, instanceId: { type: 'string' }, apiKey: { type: 'string' }, diff --git a/packages/regina-agent/src/server.ts b/packages/regina-agent/src/server.ts index 77afdff..988cd50 100644 --- a/packages/regina-agent/src/server.ts +++ b/packages/regina-agent/src/server.ts @@ -1,5 +1,5 @@ import { getGlobal } from '@platformatic/globals' -import { create as createVfs, MemoryProvider, SqliteProvider } from '@platformatic/vfs' +import { create as createVfs } from '@platformatic/vfs' import type { CoreMessage, StepResult, ToolSet } from 'ai' import fastify, { FastifyBaseLogger, type FastifyInstance } from 'fastify' import { Readable } from 'node:stream' @@ -14,6 +14,7 @@ import { createMetrics } from './metrics.ts' import { ReginaAgentConfiguration } from './schema.ts' import { appendMessages, loadMessages, rewriteMessages } from './session.ts' import { loadTools } from './tool-loader.ts' +import { createProvider } from './vfs-provider.ts' declare module 'fastify' { interface FastifyRequest { @@ -38,7 +39,7 @@ export async function create (): Promise { const providerSettings: ProviderSettings = { apiKey: config.apiKey, baseURL: config.baseURL } - const provider = config.vfsDbPath ? new SqliteProvider(config.vfsDbPath) : new MemoryProvider() + const provider = createProvider(config) const vfs = createVfs(provider, { moduleHooks: false }) const definition = await loadDefinition(config.definitionPath) diff --git a/packages/regina-agent/src/vfs-provider.ts b/packages/regina-agent/src/vfs-provider.ts new file mode 100644 index 0000000..99008d3 --- /dev/null +++ b/packages/regina-agent/src/vfs-provider.ts @@ -0,0 +1,12 @@ +import { MemoryProvider, RealFSProvider, SqliteProvider } from '@platformatic/vfs' +import type { ReginaAgentConfiguration } from './schema.ts' + +export function createProvider (config: ReginaAgentConfiguration['reginaAgent']) { + if (config.fsRootPath) { + return new RealFSProvider(config.fsRootPath) + } + if (config.vfsDbPath) { + return new SqliteProvider(config.vfsDbPath) + } + return new MemoryProvider() +} diff --git a/packages/regina-agent/test/provider.test.ts b/packages/regina-agent/test/provider.test.ts new file mode 100644 index 0000000..a0cc3a4 --- /dev/null +++ b/packages/regina-agent/test/provider.test.ts @@ -0,0 +1,40 @@ +import { ok } from 'node:assert' +import { mkdtemp, rm } from 'node:fs/promises' +import { tmpdir } from 'node:os' +import { join } from 'node:path' +import test from 'node:test' +import { MemoryProvider, RealFSProvider, SqliteProvider } from '@platformatic/vfs' +import { createProvider } from '../src/vfs-provider.ts' + +test('createProvider - returns MemoryProvider by default', () => { + const provider = createProvider({ definitionPath: '/test' }) + ok(provider instanceof MemoryProvider) +}) + +test('createProvider - returns SqliteProvider when vfsDbPath is set', async t => { + const dir = await mkdtemp(join(tmpdir(), 'regina-provider-')) + t.after(() => rm(dir, { recursive: true, force: true })) + + const provider = createProvider({ definitionPath: '/test', vfsDbPath: join(dir, 'test.sqlite') }) + ok(provider instanceof SqliteProvider) +}) + +test('createProvider - returns RealFSProvider when fsRootPath is set', async t => { + const dir = await mkdtemp(join(tmpdir(), 'regina-provider-')) + t.after(() => rm(dir, { recursive: true, force: true })) + + const provider = createProvider({ definitionPath: '/test', fsRootPath: dir }) + ok(provider instanceof RealFSProvider) +}) + +test('createProvider - fsRootPath takes precedence over vfsDbPath', async t => { + const dir = await mkdtemp(join(tmpdir(), 'regina-provider-')) + t.after(() => rm(dir, { recursive: true, force: true })) + + const provider = createProvider({ + definitionPath: '/test', + vfsDbPath: join(dir, 'test.sqlite'), + fsRootPath: dir + }) + ok(provider instanceof RealFSProvider) +}) diff --git a/packages/regina-agent/test/schema.test.ts b/packages/regina-agent/test/schema.test.ts index b43a8dd..3415add 100644 --- a/packages/regina-agent/test/schema.test.ts +++ b/packages/regina-agent/test/schema.test.ts @@ -36,6 +36,7 @@ test('schema - reginaAgent config shape', () => { definitionPath: { type: 'string' }, toolsBasePath: { type: 'string' }, vfsDbPath: { type: 'string' }, + fsRootPath: { type: 'string' }, coordinatorId: { type: 'string' }, instanceId: { type: 'string' }, apiKey: { type: 'string' }, diff --git a/packages/regina-agent/test/session.test.ts b/packages/regina-agent/test/session.test.ts index c32e644..633793a 100644 --- a/packages/regina-agent/test/session.test.ts +++ b/packages/regina-agent/test/session.test.ts @@ -1,7 +1,10 @@ import { deepStrictEqual, strictEqual } from 'node:assert' +import { mkdtemp, rm } from 'node:fs/promises' +import { tmpdir } from 'node:os' +import { join } from 'node:path' import test from 'node:test' import type { CoreMessage } from 'ai' -import { create as createVfs, MemoryProvider } from '@platformatic/vfs' +import { create as createVfs, MemoryProvider, RealFSProvider } from '@platformatic/vfs' import { loadMessages, appendMessages, rewriteMessages } from '../src/session.ts' function setup () { @@ -85,3 +88,24 @@ test('roundtrip - append then load preserves messages', () => { deepStrictEqual(loaded, [msg1, msg2]) }) + +test('RealFSProvider - messages persist to disk', async (t) => { + const rootPath = await mkdtemp(join(tmpdir(), 'regina-realfs-')) + t.after(() => rm(rootPath, { recursive: true, force: true })) + + const provider = new RealFSProvider(rootPath) + const vfs = createVfs(provider, { moduleHooks: false }) + + const msg1: CoreMessage = { role: 'user', content: 'hello' } + const msg2: CoreMessage = { role: 'assistant', content: 'world' } + + appendMessages(vfs, msg1, msg2) + const loaded = loadMessages(vfs) + deepStrictEqual(loaded, [msg1, msg2]) + + // Create a new VFS from the same root — messages should persist + const provider2 = new RealFSProvider(rootPath) + const vfs2 = createVfs(provider2, { moduleHooks: false }) + const reloaded = loadMessages(vfs2) + deepStrictEqual(reloaded, [msg1, msg2]) +})