Files
vessel/frontend/src/lib/storage/db.ts
vikingowl 9b4eeaff2a feat(agents): implement agents feature (v1)
Adds agents feature with the following capabilities:
- Agent identity: name, description
- System prompt reference from Prompt Library (promptId)
- Tool set: subset of available tools (enabledToolNames)
- Optional preferred model
- CRUD operations with IndexedDB storage (schema v7)
- Project-agent relationships (many-to-many via junction table)
- Per-chat agent selection via AgentSelector component
- Settings UI via AgentsTab in Settings page

Integration:
- Agent tools filter LLM tool calls via getToolDefinitionsForAgent()
- Agent prompt integrates with prompt resolution (priority 3)
- AgentSelector dropdown in chat UI (opens upward)

Tests:
- 22 storage layer tests
- 22 state management tests
- 7 tool integration tests
- 9 prompt resolution tests
- 14 E2E tests

Closes #7
2026-01-22 12:02:13 +01:00

479 lines
14 KiB
TypeScript

/**
* IndexedDB database setup using Dexie.js
* Provides local storage for conversations, messages, and attachments
*/
import Dexie, { type Table } from 'dexie';
/**
* Stored conversation metadata
* Uses timestamps as numbers for IndexedDB compatibility
*/
export interface StoredConversation {
id: string;
title: string;
model: string;
createdAt: number;
updatedAt: number;
isPinned: boolean;
isArchived: boolean;
messageCount: number;
syncVersion?: number;
/** Optional system prompt ID for this conversation */
systemPromptId?: string | null;
/** Optional project ID this conversation belongs to */
projectId?: string | null;
/** Optional agent ID for this conversation (determines prompt and tools) */
agentId?: string | null;
/** Auto-generated conversation summary for cross-chat context */
summary?: string | null;
/** Timestamp when summary was last updated */
summaryUpdatedAt?: number | null;
}
/**
* Conversation record with Date objects (for sync manager)
*/
export interface ConversationRecord {
id: string;
title: string;
model: string;
createdAt: Date;
updatedAt: Date;
isPinned: boolean;
isArchived: boolean;
messageCount: number;
syncVersion?: number;
/** Optional system prompt ID for this conversation */
systemPromptId?: string | null;
/** Optional project ID this conversation belongs to */
projectId?: string | null;
/** Optional agent ID for this conversation (determines prompt and tools) */
agentId?: string | null;
/** Auto-generated conversation summary for cross-chat context */
summary?: string | null;
/** Timestamp when summary was last updated */
summaryUpdatedAt?: Date | null;
}
/**
* Stored message in a conversation
* Flattened structure for efficient storage and retrieval
*/
export interface StoredMessage {
id: string;
conversationId: string;
parentId: string | null;
role: 'user' | 'assistant' | 'system' | 'tool';
content: string;
images?: string[];
toolCalls?: Array<{
id: string;
name: string;
arguments: string;
}>;
siblingIndex: number;
createdAt: number;
syncVersion?: number;
/** References to attachments stored in the attachments table */
attachmentIds?: string[];
}
/**
* Message record with Date objects (for sync manager)
*/
export interface MessageRecord {
id: string;
conversationId: string;
parentId: string | null;
role: 'user' | 'assistant' | 'system' | 'tool';
content: string;
images?: string[];
createdAt: Date;
syncVersion?: number;
}
/**
* Stored attachment for a message
* Binary data stored as Blob for efficiency
*/
export interface StoredAttachment {
id: string;
messageId: string;
mimeType: string;
data: Blob;
filename: string;
/** File size in bytes */
size: number;
/** Attachment type category */
type: 'image' | 'text' | 'pdf' | 'audio' | 'video' | 'binary';
/** Timestamp when attachment was created */
createdAt: number;
/** Cached extracted text (for text/PDF files) */
textContent?: string;
/** Whether the text content was truncated */
truncated?: boolean;
/** Whether this attachment was analyzed by the file analyzer */
analyzed?: boolean;
/** Summary from file analyzer (if analyzed) */
summary?: string;
}
/**
* Attachment metadata (without the binary data)
* Used for displaying attachment info without loading the full blob
*/
export interface AttachmentMeta {
id: string;
messageId: string;
filename: string;
mimeType: string;
size: number;
type: 'image' | 'text' | 'pdf' | 'audio' | 'video' | 'binary';
createdAt: number;
truncated?: boolean;
analyzed?: boolean;
}
/**
* Sync queue item for future backend synchronization
*/
export interface SyncQueueItem {
id: string;
entityType: 'conversation' | 'message' | 'attachment';
entityId: string;
operation: 'create' | 'update' | 'delete';
createdAt: number;
retryCount: number;
}
/**
* Knowledge base document (for RAG)
*/
export interface StoredDocument {
id: string;
name: string;
mimeType: string;
size: number;
createdAt: number;
updatedAt: number;
chunkCount: number;
embeddingModel: string;
/** Optional project ID - if set, document is project-scoped */
projectId?: string | null;
/** Embedding generation status: 'pending' | 'processing' | 'ready' | 'failed' */
embeddingStatus?: 'pending' | 'processing' | 'ready' | 'failed';
}
/**
* Document chunk with embedding (for RAG)
*/
export interface StoredChunk {
id: string;
documentId: string;
content: string;
embedding: number[];
startIndex: number;
endIndex: number;
tokenCount: number;
}
/**
* System prompt template
*/
export interface StoredPrompt {
id: string;
name: string;
content: string;
description: string;
isDefault: boolean;
createdAt: number;
updatedAt: number;
/** Capabilities this prompt is optimized for (for auto-matching) */
targetCapabilities?: string[];
}
/**
* Cached model info including embedded system prompt (from Ollama /api/show)
*/
export interface StoredModelSystemPrompt {
/** Model name (e.g., "llama3.2:8b") - Primary key */
modelName: string;
/** System prompt extracted from modelfile, null if none */
systemPrompt: string | null;
/** Model capabilities (vision, code, thinking, tools, etc.) */
capabilities: string[];
/** Timestamp when this info was fetched */
extractedAt: number;
}
/**
* User-configured model-to-prompt mapping
* Allows users to set default prompts for specific models
*/
export interface StoredModelPromptMapping {
id: string;
/** Ollama model name (e.g., "llama3.2:8b") */
modelName: string;
/** Reference to StoredPrompt.id */
promptId: string;
createdAt: number;
updatedAt: number;
}
// ============================================================================
// Project-related interfaces (v6)
// ============================================================================
/**
* Project for organizing conversations with shared context
*/
export interface StoredProject {
id: string;
name: string;
description: string;
/** Instructions injected into system prompt for all project chats */
instructions: string;
/** Hex color for UI display */
color: string;
/** Whether folder is collapsed in sidebar */
isCollapsed: boolean;
createdAt: number;
updatedAt: number;
}
/**
* Reference link attached to a project
*/
export interface StoredProjectLink {
id: string;
projectId: string;
url: string;
title: string;
description: string;
createdAt: number;
}
/**
* Chat message chunk with embedding for cross-chat RAG
* Enables searching across conversation history (project-scoped or global)
*/
export interface StoredChatChunk {
id: string;
conversationId: string;
/** Project ID for project-scoped queries, null for global conversations */
projectId: string | null;
messageId: string;
role: 'user' | 'assistant';
content: string;
embedding: number[];
createdAt: number;
}
// ============================================================================
// Agent-related interfaces (v7)
// ============================================================================
/**
* Stored agent configuration
* Agents combine identity, system prompt, and tool subset
*/
export interface StoredAgent {
id: string;
name: string;
description: string;
/** Reference to StoredPrompt.id, null for no specific prompt */
promptId: string | null;
/** Array of tool names this agent can use (subset of available tools) */
enabledToolNames: string[];
/** Optional preferred model for this agent */
preferredModel: string | null;
createdAt: number;
updatedAt: number;
}
/**
* Junction table for project-agent many-to-many relationship
* Defines which agents are available (rostered) for a project
*/
export interface StoredProjectAgent {
id: string;
projectId: string;
agentId: string;
createdAt: number;
}
/**
* Ollama WebUI database class
* Manages all local storage tables
*/
class OllamaDatabase extends Dexie {
conversations!: Table<StoredConversation>;
messages!: Table<StoredMessage>;
attachments!: Table<StoredAttachment>;
syncQueue!: Table<SyncQueueItem>;
documents!: Table<StoredDocument>;
chunks!: Table<StoredChunk>;
prompts!: Table<StoredPrompt>;
modelSystemPrompts!: Table<StoredModelSystemPrompt>;
modelPromptMappings!: Table<StoredModelPromptMapping>;
// Project-related tables (v6)
projects!: Table<StoredProject>;
projectLinks!: Table<StoredProjectLink>;
chatChunks!: Table<StoredChatChunk>;
// Agent-related tables (v7)
agents!: Table<StoredAgent>;
projectAgents!: Table<StoredProjectAgent>;
constructor() {
super('vessel');
// Version 1: Core chat functionality
this.version(1).stores({
// Primary key: id, Indexes: updatedAt, isPinned, isArchived
conversations: 'id, updatedAt, isPinned, isArchived',
// Primary key: id, Indexes: conversationId, parentId, createdAt
messages: 'id, conversationId, parentId, createdAt',
// Primary key: id, Index: messageId
attachments: 'id, messageId',
// Primary key: id, Indexes: entityType, createdAt
syncQueue: 'id, entityType, createdAt'
});
// Version 2: Knowledge base / RAG support
this.version(2).stores({
conversations: 'id, updatedAt, isPinned, isArchived',
messages: 'id, conversationId, parentId, createdAt',
attachments: 'id, messageId',
syncQueue: 'id, entityType, createdAt',
// Knowledge base documents
documents: 'id, name, createdAt, updatedAt',
// Document chunks with embeddings
chunks: 'id, documentId'
});
// Version 3: System prompts
this.version(3).stores({
conversations: 'id, updatedAt, isPinned, isArchived',
messages: 'id, conversationId, parentId, createdAt',
attachments: 'id, messageId',
syncQueue: 'id, entityType, createdAt',
documents: 'id, name, createdAt, updatedAt',
chunks: 'id, documentId',
// System prompt templates
prompts: 'id, name, isDefault, updatedAt'
});
// Version 4: Per-conversation system prompts
// Note: No schema change needed - just adding optional field to conversations
// Dexie handles this gracefully (field is undefined on old records)
this.version(4).stores({
conversations: 'id, updatedAt, isPinned, isArchived, systemPromptId',
messages: 'id, conversationId, parentId, createdAt',
attachments: 'id, messageId',
syncQueue: 'id, entityType, createdAt',
documents: 'id, name, createdAt, updatedAt',
chunks: 'id, documentId',
prompts: 'id, name, isDefault, updatedAt'
});
// Version 5: Model-specific system prompts
// Adds: cached model info (with embedded prompts) and user model-prompt mappings
this.version(5).stores({
conversations: 'id, updatedAt, isPinned, isArchived, systemPromptId',
messages: 'id, conversationId, parentId, createdAt',
attachments: 'id, messageId',
syncQueue: 'id, entityType, createdAt',
documents: 'id, name, createdAt, updatedAt',
chunks: 'id, documentId',
prompts: 'id, name, isDefault, updatedAt',
// Cached model info from Ollama /api/show (includes embedded system prompts)
modelSystemPrompts: 'modelName',
// User-configured model-to-prompt mappings
modelPromptMappings: 'id, modelName, promptId'
});
// Version 6: Projects with cross-chat context sharing
// Adds: projects, project links, chat chunks for RAG, projectId on conversations/documents
this.version(6).stores({
// Add projectId index for filtering conversations by project
conversations: 'id, updatedAt, isPinned, isArchived, systemPromptId, projectId',
messages: 'id, conversationId, parentId, createdAt',
attachments: 'id, messageId',
syncQueue: 'id, entityType, createdAt',
// Add projectId index for project-scoped document RAG
documents: 'id, name, createdAt, updatedAt, projectId',
chunks: 'id, documentId',
prompts: 'id, name, isDefault, updatedAt',
modelSystemPrompts: 'modelName',
modelPromptMappings: 'id, modelName, promptId',
// Projects for organizing conversations
projects: 'id, name, createdAt, updatedAt',
// Reference links attached to projects
projectLinks: 'id, projectId, createdAt',
// Chat message chunks for cross-conversation RAG within projects
chatChunks: 'id, conversationId, projectId, createdAt'
});
// Version 7: Agents for specialized task handling
// Adds: agents table and project-agent junction table for roster assignment
this.version(7).stores({
conversations: 'id, updatedAt, isPinned, isArchived, systemPromptId, projectId',
messages: 'id, conversationId, parentId, createdAt',
attachments: 'id, messageId',
syncQueue: 'id, entityType, createdAt',
documents: 'id, name, createdAt, updatedAt, projectId',
chunks: 'id, documentId',
prompts: 'id, name, isDefault, updatedAt',
modelSystemPrompts: 'modelName',
modelPromptMappings: 'id, modelName, promptId',
projects: 'id, name, createdAt, updatedAt',
projectLinks: 'id, projectId, createdAt',
chatChunks: 'id, conversationId, projectId, createdAt',
// Agents: indexed by id and name for lookup/sorting
agents: 'id, name, createdAt, updatedAt',
// Project-Agent junction table with compound index for efficient queries
projectAgents: 'id, projectId, agentId, [projectId+agentId]'
});
}
}
/**
* Singleton database instance
*/
export const db = new OllamaDatabase();
/**
* Result type for database operations
* Provides consistent error handling across all storage functions
*/
export type StorageResult<T> =
| { success: true; data: T }
| { success: false; error: string };
/**
* Wraps a database operation with error handling
* @param operation - Async function to execute
* @returns StorageResult with data or error
*/
export async function withErrorHandling<T>(
operation: () => Promise<T>
): Promise<StorageResult<T>> {
try {
const data = await operation();
return { success: true, data };
} catch (error) {
const message = error instanceof Error ? error.message : 'Unknown database error';
console.error('[Storage Error]', message, error);
return { success: false, error: message };
}
}
/**
* Generates a unique ID for database entities
* Uses crypto.randomUUID for guaranteed uniqueness
*/
export function generateId(): string {
return crypto.randomUUID();
}