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:
| Package | Purpose |
|---|---|
@operaide/database | Core SQL operations, inserts, queries, and a virtual file system |
@operaide/vector | Vector 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,
});
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
-
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 foraktorDatabase({ id })only when there is no AppInstance context (standalone reaktors) or when an explicit, externally-managed database is required. -
Use
aktorInMemoryDatabase()during development and unit tests. It behaves like a persistent client but does not require platform infrastructure. -
Define Zod schemas for all queries and inserts. This gives you runtime validation and compile-time type inference in one step.
-
Chain operations through the client return value.
aktorInsertreturns the client specifically to support sequential composition — pass it to the next operation to enforce execution order. -
Keep vector operations in
@operaide/vector. Use@operaide/databasefor structured SQL and file storage; use@operaide/vectorwhen you need embeddings or semantic search.
Summary
| Aktor | Package | Purpose | Returns |
|---|---|---|---|
aktorAppDatabase | @operaide/database | Per-App database client; default for App-bound reaktors. Pass name for additional databases | Client |
aktorDatabase | @operaide/database | Persistent client by explicit id; for standalone reaktors or legacy databases | Client |
aktorInMemoryDatabase | @operaide/database | Ephemeral in-memory client for development and tests | Client |
aktorQuery | @operaide/database | Type-safe SQL queries with Zod validation | T[] |
aktorInsert | @operaide/database | Validated inserts | Client |
aktorExtractResultSet | @operaide/database | Convert ResultSet to JSON string | string |
aktorInitSqlarSchema | @operaide/database | Initialize the virtual file system | Client |
aktorWriteFile | @operaide/database | Write a file to the VFS | void |
aktorReadFile | @operaide/database | Read a file as binary | Uint8Array |
aktorReadTextFile | @operaide/database | Read a file as UTF-8 text | string |
aktorListDir | @operaide/database | List directory contents | string[] |
aktorStat | @operaide/database | Get file/directory metadata | FileStat |
aktorDeleteFile | @operaide/database | Delete a file from the VFS | void |
aktorInitVectorDB | @operaide/vector | Initialize the vector schema on a client | Client |
aktorVektorDatabase | @operaide/vector | Convenience 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.