Appearance
Architecture
This page explains how the Lota SDK is structured, how the pieces connect, and how host applications extend the runtime.
System Overview
Host Application (your product code)
┌─────────────────────────────────────────────────────────────────┐
│ Turn Hooks · Runtime Adapters · Tool Providers · Plugins │
└───────────────────────────┬─────────────────────────────────────┘
│
createLotaRuntime()
│
┌───────────────────────────▼─────────────────────────────────────┐
│ LotaRuntime │
│ ┌─────────────┐ ┌──────────────┐ ┌─────────────────────┐ │
│ │ Services │ │ Workers │ │ Plugins / Contrib │ │
│ │ ---------- │ │ ---------- │ │ ----------------- │ │
│ │ workstream │ │ compaction │ │ env keys │ │
│ │ memory │ │ memory │ │ schema files │ │
│ │ plan │ │ skill │ │ services │ │
│ │ attachment │ │ activity │ │ tools │ │
│ │ activity │ │ host extras │ │ │ │
│ └──────┬──────┘ └──────┬───────┘ └─────────────────────┘ │
└─────────┼────────────────┼──────────────────────────────────────┘
│ │
┌─────▼────┐ ┌──────▼──────┐
│ SurrealDB │ │ Redis │
│ (lotasdk) │ │ (BullMQ) │
└─────┬────┘ └──────┬──────┘
│ │
┌─────▼────────────────▼──────┐
│ Infrastructure │
│ ┌──────────┐ ┌──────────┐ │
│ │ Bifrost │ │ S3 │ │
│ │ Gateway │ │ Storage │ │
│ └──────────┘ └──────────┘ │
└─────────────────────────────┘SurrealDB stores all persistent state: workstreams, messages, memories (with vector embeddings), execution plans, and identity records. The SDK uses a fixed database named lotasdk within the consumer-provided namespace.
Redis powers BullMQ job queues for asynchronous work (memory extraction, context compaction, skill extraction) and distributed lease locks for concurrency control.
Bifrost is the AI gateway that routes all LLM and embedding requests to configured providers with centralized key management.
S3 provides object storage for file attachments and generated documents.
Package Structure
The SDK ships as four packages, each with a distinct responsibility:
| Package | Import | Responsibility |
|---|---|---|
| core | @lota-sdk/core | Runtime factory, services, database/Redis wiring, agents, tools, workers, queues. This is the server-side package. |
| shared | @lota-sdk/shared | TypeScript schemas, message types, constants, and tool contracts. Zero runtime dependencies. Shared between server and client. |
| ui | @lota-sdk/ui | Headless chat helpers, tool-view rendering, and streaming utilities for client applications. Depends on shared, not core. |
| studio | @lota-sdk/studio | Lota Studio -- a standalone development UI with its own client and server entrypoint for inspecting and interacting with a running SDK instance. |
The dependency graph flows in one direction:
studio -> core -> shared
ui --------> sharedHost applications typically import core on the server and shared + ui on the client.
Core Internal Layout
core/src/
├── runtime/ # createLotaRuntime, context compaction, chat routing, approvals
├── db/ # SurrealDB service, cursor pagination, record-id helpers, startup
├── redis/ # Connection manager, lease locks, org-memory lock
├── ai/ # AI gateway wrappers, embedding helpers
├── bifrost/ # Bifrost model routing
├── tools/ # Built-in tool definitions (*.tool.ts), tool contract
├── services/ # Domain services (*.service.ts)
├── workers/ # BullMQ workers (*.worker.ts), runners (*.runner.ts)
│ └── utils/ # Non-worker support files (chunkers, extractors, concurrency)
├── queues/ # Queue definitions (*.queue.ts) and config
├── system-agents/ # Delegated agent factory, system agent definitions
├── config/ # Agent defaults, runtime configuration
├── utils/ # String, async, date-time, error helpers
├── storage/ # S3 client wrappers
└── document/ # Document processing utilitiesRuntime Lifecycle
Creation
createLotaRuntime(config) is the sole entry point. It creates an isolated runtime instance -- no module-level singletons. The factory:
- Validates the configuration with Zod schemas.
- Instantiates the SurrealDB service and Redis connection manager.
- Collects built-in
.surqlschema files fromcore/infrastructure/schema/. - Merges plugin and host contributions (extra schema files, env keys, tools).
- Builds the service layer (workstream, memory, plan, attachment, activity services).
- Registers built-in and consumer-provided workers.
- Returns a
LotaRuntimeobject.
At this point, nothing is connected yet. The runtime is a configured but inert object.
Connection
runtime.connect() performs the actual initialization:
- SurrealDB connection -- connects to the database server with retry logic, authenticates, and selects the
lotasdkdatabase. - Schema application -- applies all collected
.surqlfiles (built-in + plugin + host extras). Schemas useIF NOT EXISTSsemantics, so this is safe to run repeatedly. - Redis connection -- establishes the Redis connection used by BullMQ and lease locks.
- Bootstrap publication -- publishes a readiness signal so other processes (workers) can wait for schema setup to complete.
If plugins have their own databases, call runtime.connectPluginDatabases() after connect().
Shutdown
runtime.disconnect() closes SurrealDB and Redis connections and stops any active workers.
Request Flow
When a user sends a message to a workstream, the following pipeline executes:
1. User Message Arrives
│
▼
2. Pre-Turn Setup
├── Wait for any active compaction to finish
├── Register a new server run (AbortController for cancellation)
├── Set workstream.activeRunId
└── Persist the user message
│
▼
3. Context Building
├── Load message history (with compaction summary if applicable)
├── Re-sign presigned file attachment URLs
├── Retrieve pre-seeded memories (high-importance, no query needed)
├── Semantic memory search using the user's message as query
├── Load workstream state (decisions, tasks, risks, artifacts)
├── Load active execution plan state
├── Call buildExtraInstructionSections hook (host-injected context)
└── Collect upload metadata from message history
│
▼
4. Agent Resolution
├── resolveAgent hook determines which agent handles the turn
├── Returns agent config: model, reasoning profile, tools
└── Merges built-in + host + plugin tools
│
▼
5. Agent Execution (Streaming)
├── AI SDK streamText/streamObject with tool loop
├── Throttled chunk emission for natural pacing
├── Tool calls execute mid-stream
└── If a tool needs approval: stream ends with approval-requested parts
│
▼
6. Message Persistence
├── Upsert all assistant messages to the workstream
└── Update workstream.updatedAt
│
▼
7. Post-Turn Processing (Background Jobs)
├── Memory extraction (onboarding: immediate, post-onboarding: digest)
├── Context compaction assessment (if history > 70% of 200K token budget)
├── Title generation (for new workstreams with default titles)
├── Skill extraction
├── Recent activity title refinement
└── afterTurn hook (host-defined side effects)
│
▼
8. Run Cleanup
├── Unregister run from chatRunRegistry
├── Clear workstream.activeRunId
└── Dispose AbortControllerData Model
The SDK maintains several interconnected record types in SurrealDB:
organization ──┐
│ membership
user ──────────┘
│
│ owns
▼
workstream
│
├── messages[] Cursor-paginated UIMessage records
│ └── parts[] Text, reasoning, tool invocations, data
│
├── workstreamState Decisions, tasks, risks, artifacts, constraints
│
├── memoryBlock[] Per-workstream short-term notes
│
├── compactionSummary Compressed history from context compaction
│
└── execution plan runtime (0..1 active run)
├── planSpec / planNodeSpec
├── planRun / planNodeRun / planNodeAttempt
├── planArtifact / planApproval / planCheckpoint
├── planValidationIssue
└── planEvent
memory Organization- or agent-scoped facts
├── embedding 1536-dim vector (HNSW indexed)
├── scopeId Namespace: org:{id} or org:{id}:agent:{name}
└── memoryRelation[] Semantic edges (contradicts, supersedes, supports...)
recentActivity Deduplicated sidebar activity entries
recentActivityEvent Raw event historyIdentity Model
The SDK does not own the full user or organization model. It stores lightweight identity records (user, organization, membership) with just enough data for display names and access control. The host application manages the authoritative identity in its own database.
The two sides share record IDs. When you create a user in your host DB as user:alice, you upsert the same user:alice into the SDK. This lets the SDK reference host entities without cross-database joins.
Scope Model
Most SDK data is scoped to an organization. Memories have an additional scope level for agent-specific facts:
- Organization scope:
org:{orgId}-- shared facts visible to all agents. - Agent scope:
org:{orgId}:agent:{agentName}-- facts visible only to a specific agent.
Workstreams are scoped to a user within an organization.
Extension Model
The SDK is designed to be extended without forking. Host applications customize behavior through several extension points:
Turn Hooks
Hooks inject host-specific logic at defined points in the turn lifecycle:
| Hook | When | Purpose |
|---|---|---|
buildContext | Before agent execution | Assemble the full context payload |
resolveAgent | Before agent execution | Determine which agent, model, and tools to use |
buildExtraInstructionSections | During context building | Inject additional XML sections into the system prompt |
afterTurn | After turn completes | Trigger host-side effects (analytics, notifications, etc.) |
Runtime Adapters
Adapters let the SDK call back into host-owned behavior without importing host code:
SDK runtime ──adapter──> Host functionExamples: workspace providers, repository context builders, post-turn queue enqueuers, distributed lock wrappers.
Tool Providers
Additional tools merged into the agent tool registry:
typescript
toolProviders: {
myCustomTool: createMyCustomTool(),
}Plugins
Typed LotaPlugin objects that contribute services, tools, database schemas, and environment variable declarations:
typescript
pluginRuntime: {
github: githubPlugin, // contributes: services, tools, schema files
linear: linearPlugin,
}Plugins can own their own databases (connected via runtime.connectPluginDatabases()).
Extra Schema Files
Consumer-provided .surql files applied alongside built-in schemas at startup.
Extra Workers
Consumer-provided BullMQ worker factories merged onto runtime.workers.
Background Processing
The SDK offloads non-critical work to BullMQ queues processed by dedicated workers. This keeps the turn response fast while ensuring important processing happens reliably.
Turn completes
│
├──> post-chat-memory queue ────────> PostChatMemoryWorker
│ (onboarding turns) Extracts facts immediately
│
├──> regular-chat-memory-digest ────> RegularChatMemoryDigestWorker
│ (post-onboarding, 15min dedup) Batched transcript processing
│
├──> context-compaction queue ──────> ContextCompactionWorker
│ (when history > 70% of budget) Compresses old messages
│
├──> skill-extraction queue ────────> SkillExtractionWorker
│ Identifies reusable procedures
│
└──> recent-activity-title queue ───> RecentActivityTitleRefinementWorker
Refines sidebar titles
Recurring (24h cron):
└──> memory-consolidation ──────────> MemoryConsolidationWorker
Archives stale, resolves contradictionsAll workers use runtime.redis.getConnectionForBullMQ() for their Redis connection. Host-owned workers should do the same rather than importing raw Redis accessors.
Database Boundary
The SDK and host application maintain separate databases within the same SurrealDB namespace:
SurrealDB Namespace (e.g., "lota")
├── lotasdk SDK-owned database (fixed name, not configurable)
│ ├── workstream
│ ├── message
│ ├── memory
│ ├── planSpec / planNodeSpec / planRun / planNodeRun / planNodeAttempt
│ ├── planArtifact / planApproval / planCheckpoint / planValidationIssue / planEvent
│ ├── user / organization / membership
│ └── ... (all SDK tables)
│
└── your_app_db Host-owned database (your choice of name)
├── your tables
└── ...Key principles:
- The SDK always uses the database name
lotasdk. Consumers provide only the server URL, namespace, and credentials. - SDK-owned resources (workstreams, messages, memories, execution plans) live exclusively in
lotasdk. - The host application manages its own database and schema separately.
- Record IDs are shared between databases using the same-id reuse model. Pass the same record ID value to both databases for cross-referencing.
- Schema files use
IF NOT EXISTSonly. NoOVERWRITE, noREMOVE, no migration DML. The schema assumes a fresh database. - Plugins may own additional databases, connected via
runtime.connectPluginDatabases().