Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
9 changes: 9 additions & 0 deletions packages/regina-agent/README.md
Original file line number Diff line number Diff line change
Expand Up @@ -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:
Expand Down Expand Up @@ -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 |
Expand Down
2 changes: 2 additions & 0 deletions packages/regina-agent/src/schema.ts
Original file line number Diff line number Diff line change
Expand Up @@ -17,6 +17,7 @@ export interface ReginaAgentConfiguration extends NodeConfiguration {
definitionPath: string
toolsBasePath?: string
vfsDbPath?: string
fsRootPath?: string
coordinatorId?: string
instanceId?: string
apiKey?: string
Expand All @@ -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' },
Expand Down
5 changes: 3 additions & 2 deletions packages/regina-agent/src/server.ts
Original file line number Diff line number Diff line change
@@ -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'
Expand All @@ -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 {
Expand All @@ -38,7 +39,7 @@ export async function create (): Promise<FastifyInstance> {

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)
Expand Down
12 changes: 12 additions & 0 deletions packages/regina-agent/src/vfs-provider.ts
Original file line number Diff line number Diff line change
@@ -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()
}
40 changes: 40 additions & 0 deletions packages/regina-agent/test/provider.test.ts
Original file line number Diff line number Diff line change
@@ -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)
})
1 change: 1 addition & 0 deletions packages/regina-agent/test/schema.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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' },
Expand Down
26 changes: 25 additions & 1 deletion packages/regina-agent/test/session.test.ts
Original file line number Diff line number Diff line change
@@ -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 () {
Expand Down Expand Up @@ -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])
})
Loading