Skip to content

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:

PackageImportResponsibility
core@lota-sdk/coreRuntime factory, services, database/Redis wiring, agents, tools, workers, queues. This is the server-side package.
shared@lota-sdk/sharedTypeScript schemas, message types, constants, and tool contracts. Zero runtime dependencies. Shared between server and client.
ui@lota-sdk/uiHeadless chat helpers, tool-view rendering, and streaming utilities for client applications. Depends on shared, not core.
studio@lota-sdk/studioLota 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     --------> shared

Host 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 utilities

Runtime Lifecycle

Creation

createLotaRuntime(config) is the sole entry point. It creates an isolated runtime instance -- no module-level singletons. The factory:

  1. Validates the configuration with Zod schemas.
  2. Instantiates the SurrealDB service and Redis connection manager.
  3. Collects built-in .surql schema files from core/infrastructure/schema/.
  4. Merges plugin and host contributions (extra schema files, env keys, tools).
  5. Builds the service layer (workstream, memory, plan, attachment, activity services).
  6. Registers built-in and consumer-provided workers.
  7. Returns a LotaRuntime object.

At this point, nothing is connected yet. The runtime is a configured but inert object.

Connection

runtime.connect() performs the actual initialization:

  1. SurrealDB connection -- connects to the database server with retry logic, authenticates, and selects the lotasdk database.
  2. Schema application -- applies all collected .surql files (built-in + plugin + host extras). Schemas use IF NOT EXISTS semantics, so this is safe to run repeatedly.
  3. Redis connection -- establishes the Redis connection used by BullMQ and lease locks.
  4. 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 AbortController

Data 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 history

Identity 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:

HookWhenPurpose
buildContextBefore agent executionAssemble the full context payload
resolveAgentBefore agent executionDetermine which agent, model, and tools to use
buildExtraInstructionSectionsDuring context buildingInject additional XML sections into the system prompt
afterTurnAfter turn completesTrigger 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 function

Examples: 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 contradictions

All 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 EXISTS only. No OVERWRITE, no REMOVE, no migration DML. The schema assumes a fresh database.
  • Plugins may own additional databases, connected via runtime.connectPluginDatabases().