Skip to main content
Version: 3.1

Agent DB

Agent DB is Operaide's persistent storage layer for Reaktors. Each App Instance gets its own SQLite database, created on first use, that you query as structured data, full-text search, or vector embeddings.

Overview

Each database is a LibSQL database (a SQLite fork with vector extensions). You interact with it through two packages:

PackagePurpose
@operaide/databaseCore SQL operations, inserts, queries, and a virtual file system
@operaide/vectorVector embeddings, semantic search, and RAG pipelines

The platform creates the database file at {dataDir}/default.sqlite inside the App Instance's exclusive storage folder. There is no shared registry, no UUID to copy, and the database is not visible to other Apps.


Getting a Database Client

aktorAppDatabase({}) returns the per-App database client. No id, no setting — the platform resolves the path from the current AppInstance context.

import { aktorAppDatabase } from '@operaide/database';

const db = aktorAppDatabase({});

This is the default for every reaktor that runs inside an App.

Multiple databases per App

An App can use more than one database. Pass name to address a named database; the file lives at {dataDir}/{name}.sqlite alongside default.sqlite. Names must match [a-zA-Z0-9_-]{1,64}.

const main = aktorAppDatabase({});                // default.sqlite
const log = aktorAppDatabase({ name: 'log' }); // log.sqlite

Use this when separate databases simplify retention, schema, or backup boundaries (for example, an append-only audit log next to the main data store).

Exceptions

Use aktorDatabase({ id }) only when there is no AppInstance context (standalone reaktors) or when the reaktor must point at an explicit, externally-managed database. The id can come from aktorSetting() for runtime configuration:

import { aktorSetting } from '@operaide/aktor';
import { aktorDatabase } from '@operaide/database';
import { z } from 'zod';

const databaseId = aktorSetting(z.string(), '<database-id>', 'Database ID');
const db = aktorDatabase({ id: databaseId });

For development and unit tests, aktorInMemoryDatabase() returns an ephemeral client that does not persist:

import { aktorInMemoryDatabase } from '@operaide/database';

const db = aktorInMemoryDatabase({ id: 'test' });

SQL Operations

Type-Safe Queries with aktorQuery

aktorQuery executes a SQL statement and validates each returned row against a Zod schema. The return type is automatically inferred from the schema.

import { aktorQuery } from '@operaide/database';
import { z } from 'zod';

const UserZ = z.object({
id: z.number(),
name: z.string(),
age: z.number(),
});

const users = aktorQuery({
client: db,
sql: 'SELECT * FROM users WHERE age > ?',
params: [18],
schema: UserZ,
});
// Type: Aktor<UserZ[]>

Parameters are passed as an array of positional values, keeping queries safe from injection.

Inserts with aktorInsert

aktorInsert validates the data against a Zod schema before executing the INSERT statement. It returns the database client, so you can chain further operations.

import { aktorInsert } from '@operaide/database';

const dbAfterInsert = aktorInsert({
client: db,
table: 'users',
data: { id: 1, name: 'alice', age: 30 },
schema: UserZ,
});

// Chain a query after the insert
const allUsers = aktorQuery({
client: dbAfterInsert,
sql: 'SELECT * FROM users',
schema: UserZ,
});

Extracting Raw Results

If you need the raw LibSQL result set as a JSON string (e.g. for forwarding to an LLM), use aktorExtractResultSet:

import { aktorExtractResultSet } from '@operaide/database';

const resultSet = aktorExtractResultSet({ resultSet: rawResult });
// Returns: JSON string of the ResultSet

File Storage (Virtual File System)

Agent DB includes a virtual file system based on the SQLar format. Files are stored as BLOBs inside the database, giving you a self-contained storage layer without external file systems.

Initialize the File System

Before using file operations, initialize the SQLar schema:

import { aktorInitSqlarSchema } from '@operaide/database';

const dbWithFs = aktorInitSqlarSchema({ client: db });

Writing and Reading Files

import { aktorWriteFile, aktorReadFile, aktorReadTextFile } from '@operaide/database';

// Write a file (string or Uint8Array)
aktorWriteFile({
client: dbWithFs,
path: '/reports/summary.md',
data: '# Monthly Summary\n\nAll metrics are on track.',
});

// Read as binary
const binary = aktorReadFile({ client: dbWithFs, path: '/reports/summary.md' });

// Read as UTF-8 text
const text = aktorReadTextFile({ client: dbWithFs, path: '/reports/summary.md' });

If a file already exists at the given path, aktorWriteFile overwrites it.

Listing and Inspecting

import { aktorListDir, aktorStat, aktorDeleteFile } from '@operaide/database';

// List directory contents
const entries = aktorListDir({ client: dbWithFs, path: '/reports' });
// Returns: ['summary.md', ...]

// Get file metadata
const info = aktorStat({ client: dbWithFs, path: '/reports/summary.md' });
// Returns: { type: 'file', size: 47, mtime: 1709654400, mode: 420 }

// Delete a file
aktorDeleteFile({ client: dbWithFs, path: '/reports/summary.md' });

For app downloads served via URL (chat attachments, tool results), see App Database Files. It uses a separate files table populated by @operaide/vector and is the recommended path for new apps that want clickable links instead of path-based reads.


Vector Database

Agent DB also serves as the storage backend for vector embeddings and semantic search. The @operaide/vector package provides an embedding pipeline and a retrieval pipeline that operate on top of the per-App database.

Initialize the vector schema (files, documents, chunks, vectors tables) on top of aktorAppDatabase:

import { aktorAppDatabase } from '@operaide/database';
import { aktorInitVectorDB } from '@operaide/vector';

const vectorDb = aktorInitVectorDB({ client: aktorAppDatabase({}) });

aktorVektorDatabase() is a convenience wrapper that runs the same initializer, but currently still binds to the legacy explicit-id form via aktorSetting(). Prefer the manual pattern above for new apps.

For full details on embedding, chunking, retrieval, and RAG patterns, see the Vectorization Functions chapter.


Full Example: Database-Backed Reaktor

This Reaktor creates a table, inserts a row, and queries the result — a minimal pattern for structured data storage:

import { aktorConst, createAktorComposition, registerReaktorDefinition } from '@operaide/aktor';
import { aktorAppDatabase, aktorInsert, aktorQuery } from '@operaide/database';
import { z } from 'zod';

const UserZ = z.object({
id: z.number(),
name: z.string(),
age: z.number(),
});

const aktorDbOrmExample = createAktorComposition('aktorDbOrmExample', () => {
const db = aktorAppDatabase({});

const dbAfterInsert = aktorInsert({
client: db,
table: 'users',
data: aktorConst({ id: 1, name: 'max', age: 42 }),
schema: UserZ,
});

return aktorQuery({
client: dbAfterInsert,
sql: 'SELECT * FROM users',
schema: UserZ,
});
});

registerReaktorDefinition({
reaktorDefinitionId: 'db-reaktor-orm',
label: 'Database ORM Example',
description: 'Insert and query data with type-safe schemas',
inputSchema: z.object({}),
outputSchema: UserZ.array(),
aktor: aktorDbOrmExample,
});
note

The table must already exist in the database. You can create tables using raw SQL via the LibSQL client during initialization, or manage the schema through the platform UI.

Full Example: AI Chat with Database Tools

This pattern exposes database operations as LLM tools, letting the AI decide when to query the database during a conversation:

import {
aktorAICall,
aktorAISettingProviderModel,
aktorPatchMessages,
aktorToolSet,
aktorToTool,
registerChatReaktorDefinition,
} from '@operaide/ai';
import { aktorConst, createAktorFunction, createAktorComposition } from '@operaide/aktor';
import { aktorAppDatabase } from '@operaide/database';
import { z } from 'zod';
import type { LlmOptions } from '@operaide/ai';

const aktorQueryDatabase = createAktorFunction(
'aktorQueryDatabase',
async ({ client, sql }: { client: any; sql: string }) => {
if (!sql) return [];
const result = await client.execute(sql);
return result.rows;
}
);

const aktorDatabaseChat = createAktorComposition('aktorDatabaseChat', ({ messages }) => {
const db = aktorAppDatabase({});

const queryTool = aktorToTool({
aktor: aktorQueryDatabase,
description: 'Execute a read-only SQL query against the database.',
parameters: z.object({
sql: z.string().describe('The SQL SELECT query to execute'),
}),
dependencies: { client: db },
});

const tools = aktorToolSet({ queryTool });

return aktorAICall({
messages: aktorPatchMessages({
messages,
system: aktorConst(
'You are a database assistant. Use the queryTool to answer questions about the data.'
),
}),
providerModel: aktorAISettingProviderModel(),
tools,
llmOptions: aktorConst<LlmOptions>({ max_steps: 10 }),
});
});

registerChatReaktorDefinition({
reaktorDefinitionId: 'database-chat',
label: 'Database Chat',
description: 'Chat with your database using natural language',
aktor: aktorDatabaseChat,
});

Best Practices

  1. Default to aktorAppDatabase({}) for any reaktor that runs inside an App. The per-App database is exclusive to one AppInstance and needs no configuration. Reach for aktorDatabase({ id }) only when there is no AppInstance context (standalone reaktors) or when an explicit, externally-managed database is required.

  2. Use aktorInMemoryDatabase() during development and unit tests. It behaves like a persistent client but does not require platform infrastructure.

  3. Define Zod schemas for all queries and inserts. This gives you runtime validation and compile-time type inference in one step.

  4. Chain operations through the client return value. aktorInsert returns the client specifically to support sequential composition — pass it to the next operation to enforce execution order.

  5. Keep vector operations in @operaide/vector. Use @operaide/database for structured SQL and file storage; use @operaide/vector when you need embeddings or semantic search.


Summary

AktorPackagePurposeReturns
aktorAppDatabase@operaide/databasePer-App database client; default for App-bound reaktors. Pass name for additional databasesClient
aktorDatabase@operaide/databasePersistent client by explicit id; for standalone reaktors or legacy databasesClient
aktorInMemoryDatabase@operaide/databaseEphemeral in-memory client for development and testsClient
aktorQuery@operaide/databaseType-safe SQL queries with Zod validationT[]
aktorInsert@operaide/databaseValidated insertsClient
aktorExtractResultSet@operaide/databaseConvert ResultSet to JSON stringstring
aktorInitSqlarSchema@operaide/databaseInitialize the virtual file systemClient
aktorWriteFile@operaide/databaseWrite a file to the VFSvoid
aktorReadFile@operaide/databaseRead a file as binaryUint8Array
aktorReadTextFile@operaide/databaseRead a file as UTF-8 textstring
aktorListDir@operaide/databaseList directory contentsstring[]
aktorStat@operaide/databaseGet file/directory metadataFileStat
aktorDeleteFile@operaide/databaseDelete a file from the VFSvoid
aktorInitVectorDB@operaide/vectorInitialize the vector schema on a clientClient
aktorVektorDatabase@operaide/vectorConvenience wrapper that initializes the schema and binds to a setting-based id (legacy form)Client

For vector embeddings and semantic search, see Vectorization Functions. For AI model integration, see LLM Calls.