Building AI Agents That Remember with Mastra and Turso Vector

Akuya EkorotAkuya Ekorot
Cover image for Building AI Agents That Remember with Mastra and Turso Vector

We’ve all been there. You start with a simple app that calls an LLM and can have simple conversations, but then you realize the LLM has a the memory of a goldfish. Or after some time, you’re hitting token limits, or older, relevant information seems to elude the LLM. No memory of what happened five minutes ago.

Then you think: "I need to add memory!" So you start researching vector databases, embedding models, and RAG pipelines. Suddenly, you're managing multiple databases, wrestling with different APIs, and your simple AI agent has become a complex distributed system.

What if I told you there's a better way? What if you could build production-ready AI agents with persistent memory, vector search, and RAG capabilities using just one database and a very clean API?

Enter Mastra and Turso! A powerful combination that's changing how developers build AI applications.

Mastra is a TypeScript AI agent framework that gives you the building blocks for production AI apps.

The best part? LibSQL, Turso’s fork of SQLite, is the default storage option for new Mastra applications. This integration means you get both traditional relational data and vector operations in one place, without the complexity of managing separate systems.

Let's build something together.

#What We're Building

By the end of this guide, you'll have a weather AI agent that:

  • Remembers past interactions efficiently
  • Uses semantic search to recall relevant context from previous interactions
  • Stores both structured data (telemetry, evals, etc.) and vector embeddings in the same database
  • Implements RAG workflows for enhanced responses

All powered by a single SQLite database with native vector capabilities.

#Setting Up Your Turso Database

First, we need a database with vector superpowers. Create an account if you don't have one.

Once you're in the dashboard:

  1. Create a new database — call it something like mastra-ai-agent
  2. Grab your credentials — you'll need the database URL and auth token
  3. Note the magic — your SQLite database now has native vector search capabilities, no extensions required

Your database URL will look like this:

libsql://mastra-ai-agent-[your-username].turso.io

And you'll get an auth token that starts with eyJ...

Keep these handy, we'll need them in the next step.

#Creating Your Mastra Project

Let's start with a fresh Mastra project. The CLI will set everything up with sensible defaults.

Choose all the defaults and when prompted, choose to include the example — we'll use this as our foundation.

npx create-mastra@latest

Now, open your .env file and add your Turso credentials:

#Turso Database Configuration

TURSO_DB_URL=libsql://your-database-url.turso.io
TURSO_AUTH_TOKEN=your_auth_token_here

#AI Model Configuration

OPENAI_API_KEY=your_openai_api_key_here

Pro tip: You can use any @ai-sdk-compatible model provider with Mastra. We'll stick with the default OpenAI for simplicity. But feel free to pick your preferred provider.

Configuring Mastra with Turso

Here's where the magic happens. Let’s start by creating a file where we will configure Turso stores src/mastra/stores.ts:

import { LibSQLStore } from '@mastra/libsql';

export const storage = new LibSQLStore({
  url: process.env.TURSO_DB_URL!,
  authToken: process.env.TURSO_AUTH_TOKEN,
});

Open src/mastra/index.ts and configure your Mastra instance to use our configured Turso store:

import { Mastra } from '@mastra/core/mastra';
import { PinoLogger } from '@mastra/loggers';
import { storage } from './stores';

import { weatherAgent } from './agents/weather-agent';

export const mastra = new Mastra({
  agents: { weatherAgent },
  storage: storage,
  logger: new PinoLogger({
    name: 'Mastra',
    level: 'info',
  }),
});

Now update your weather agent in src/mastra/agents/weather-agent.ts:

import { openai } from '@ai-sdk/openai';
import { Agent } from '@mastra/core/agent';
import { Memory } from '@mastra/memory';
import { storage } from '../stores';

export const weatherAgent = new Agent({
  name: 'Weather Agent',
  instructions: `
    You are a helpful weather assistant that provides accurate weather information.

    Your primary function is to help users get weather details for specific locations. When responding:
    - Always ask for a location if none is provided
    - If the location name isn't in English, please translate it
    - If giving a location with multiple parts (e.g. "New York, NY"), use the most relevant part
    - Include relevant details like humidity, wind conditions, and precipitation
    - Keep responses concise but informative
    - Remember user preferences from past conversations

`,
  model: openai('gpt-4o-mini'),

  memory: new Memory({
    storage: storage,
  }),
});

Notice something important: the agent's memory and the main Mastra instance can use different databases if needed. This gives you incredible flexibility for organizing your data. The storage on Mastra’s instance stores telemetry, evals, etc.

Let's test what we have so far:

npm run dev

Open http://localhost:4111 and start chatting with your agent. Behind the scenes, every conversation is being stored in your Turso database. Check your Turso dashboard – you'll see tables being created automatically as your agent learns and remembers.

#Adding Vector-Powered Memory

Here's where things get exciting. We're going to add semantic memory to our agent, allowing it to recall relevant context from past interactions even when the exact words don't match or the full thread of previous messages is not included to the request sent to the LLM. This feature is called semanticRecall in Mastra.

Create a vector store in src/mastra/stores.ts:

import { LibSQLStore, LibSQLVector } from '@mastra/libsql';

export const storage = new LibSQLStore({
  url: process.env.TURSO_DB_URL!,
  authToken: process.env.TURSO_AUTH_TOKEN,
});

export const vector = new LibSQLVector({
  connectionUrl: process.env.TURSO_DB_URL!,
  authToken: process.env.TURSO_AUTH_TOKEN,
});

export const VECTOR_STORE_NAME = 'defaultVectorStore'; // name to identify a vector store in Mastra

Update your weather agent with vector capabilities (src/mastra/agents/weather-agent.ts):

import { openai } from '@ai-sdk/openai';
import { Agent } from '@mastra/core/agent';
import { Memory } from '@mastra/memory';
import { storage, vector } from '../stores';

export const weatherAgent = new Agent({
  name: 'Weather Agent',
  instructions: `
    You are a helpful weather assistant that provides accurate weather information.

    Your primary function is to help users get weather details for specific locations. When responding:
    - Always ask for a location if none is provided
    - If the location name isn't in English, please translate it
    - If giving a location with multiple parts (e.g. "New York, NY"), use the most relevant part
    - Include relevant details like humidity, wind conditions, and precipitation
    - Keep responses concise but informative
    - Learn from past conversations and user preferences
    - Use relevant context from previous interactions to provide better responses

`,
  model: openai('gpt-4o-mini'),

  memory: new Memory({
    storage: storage,
    vector: vector,
    embedder: openai.embedding('text-embedding-3-small'),
    options: {
      semanticRecall: true,
    },
  }),
});

Register the same vector store on the mastra instance (src/mastra/index.ts):

import { Mastra } from '@mastra/core/mastra';
import { PinoLogger } from '@mastra/loggers';
import { storage, VECTOR_STORE_NAME, vector } from './stores';

import { weatherAgent } from './agents/weather-agent';

export const mastra = new Mastra({
  agents: { weatherAgent },
  storage,
  logger: new PinoLogger({
    name: 'Mastra',
    level: 'debug',
  }),
  vectors: {
    [VECTOR_STORE_NAME]: vector, // register vector store with name
  },
});

What just happened? We added three crucial components:

  1. LibSQLVector: Handles vector operations in the same database
  2. Embedder: Converts text to vectors using OpenAI's embedding model. This is required for us to take advantage of Mastra and Turso’s vector capabilities. You may use any AI SDK-compatible embedding model of your choice, we stick to OpenAI’s embedding models for this example.
  3. Semantic recall: Allows the agent to search through past conversations for relevant context

Now when someone asks "What's the weather like for outdoor activities?", your agent can recall previous interactions about hiking, sports, or outdoor events, even if those exact words weren't used before.

#Adding Multiple Vector Stores

For more complex applications, you might want dedicated vector stores for different purposes. Here's how to set up a RAG-enabled system with multiple vector collections:

import { Mastra } from '@mastra/core/mastra';
import { PinoLogger } from '@mastra/loggers';
import { LibSQLStore, LibSQLVector } from '@mastra/libsql';

import { weatherAgent } from './agents/weather-agent';

export const mastra = new Mastra({
  agents: { weatherAgent },
  storage: new LibSQLStore({
    url: process.env.TURSO_DB_URL,
    authToken: process.env.TURSO_AUTH_TOKEN,
  }),
  logger: new PinoLogger({
    name: 'Mastra',
    level: 'info',
  }),
  vectors: {
    // For RAG workflows with external documents
    ragVectorStore: new LibSQLVector({
      connectionUrl: process.env.TURSO_DB_URL!,
      authToken: process.env.TURSO_AUTH_TOKEN,
    }),
    // For user preference learning
    preferencesStore: new LibSQLVector({
      connectionUrl: process.env.TURSO_DB_URL!,
      authToken: process.env.TURSO_AUTH_TOKEN,
    }),
  },
});

This setup gives you dedicated vector collections while keeping everything in the same database. No separate vector database required!

#Turso's Vector Superpowers

Before we dive into RAG workflows, let's appreciate what Turso brings to the table. Unlike traditional vector databases, Turso provides native vector search directly in SQLite:

  • Six vector types supported: FLOAT32, FLOAT16, FLOATB16, FLOAT8, FLOAT4, FLOAT1BIT
  • No extensions required: Vector operations work out of the box
  • Smart compression: Up to 32x size reduction with 1-bit quantization
  • Production ready: Euclidean distance, cosine similarity, and more
  • Unlimited dimensions: Up to 65,536 dimensions per vector (perfect for modern embedding models)

#Building RAG Workflows

Now let's add document understanding to our agent. First, install the Mastra RAG and ai package:

npm install @mastra/rag@latest ai

Create a simple RAG workflow that can ingest weather data and make it searchable.

Create the file src/mastra/workflows/embedding-workflow.ts:

import { createStep, createWorkflow } from '@mastra/core/workflows';
import { MDocument } from '@mastra/rag';
import { embedMany } from 'ai';
import { openai } from '@ai-sdk/openai';
import { z } from 'zod';
import { VECTOR_STORE_NAME } from '../stores';

const embedder = openai.embedding('text-embedding-3-small');
const VECTOR_INDEX = 'weather_docs'; // vector table name

const chunkInput = createStep({
  id: 'chunkInput',
  description: 'Chunk weather documents for embedding',
  inputSchema: z.object({
    text: z.string(),
    source: z.string().optional(),
  }),
  outputSchema: z.object({
    chunks: z.array(
      z.object({
        text: z.string(),
        metadata: z.record(z.any()),
      }),
    ),
  }),
  execute: async ({ inputData: { text, source = 'unknown' } }) => {
    const doc = MDocument.fromText(text);
    const chunks = await doc.chunk({
      strategy: 'recursive',
      size: 512,
      overlap: 50,
    });

    return {
      chunks: chunks.map((chunk) => ({
        text: chunk.text,
        metadata: {
          ...chunk.metadata,
          source,
          ingested_at: new Date().toISOString(),
        },
      })),
    };
  },
});

const generateEmbeddings = createStep({
  id: 'generateEmbeddings',
  description: 'Generate embeddings for text chunks',
  inputSchema: z.object({
    chunks: z.array(
      z.object({
        text: z.string(),
        metadata: z.record(z.any()),
      }),
    ),
  }),
  outputSchema: z.object({
    embeddings: z.array(z.array(z.number())),
    chunks: z.array(
      z.object({
        text: z.string(),
        metadata: z.record(z.any()),
      }),
    ),
  }),
  execute: async ({ inputData: { chunks } }) => {
    const { embeddings } = await embedMany({
      values: chunks.map((chunk) => chunk.text),
      model: embedder,
    });

    return { embeddings, chunks };
  },
});

const upsertVectors = createStep({
  id: 'upsertVectors',
  description: 'Store embeddings in Turso',
  inputSchema: z.object({
    embeddings: z.array(z.array(z.number())),
    chunks: z.array(
      z.object({
        text: z.string(),
        metadata: z.record(z.any()),
      }),
    ),
  }),
  outputSchema: z.object({
    success: z.boolean(),
    count: z.number(),
  }),
  execute: async ({ inputData: { embeddings, chunks }, mastra }) => {
    try {
      const vectorStore = mastra.getVector(VECTOR_STORE_NAME); // retrieve the vector store using the registered name on the mastra instance

      await vectorStore.upsert({
        indexName: VECTOR_INDEX,
        vectors: embeddings,
        metadata: chunks.map((chunk) => ({
          text: chunk.text,
          ...chunk.metadata,
        })),
      });

      return { success: true, count: embeddings.length };
    } catch (error) {
      console.error('Failed to upsert vectors:', error);
      return { success: false, count: 0 };
    }
  },
});

export const EMBEDDING_WORKFLOW_NAME = 'embeddingWorkflow';

export const embeddingWorkflow = createWorkflow({
  id: EMBEDDING_WORKFLOW_NAME,
  inputSchema: z.object({
    text: z.string().describe('Weather document content to embed'),
    source: z.string().optional().describe('Source identifier'),
  }),
  outputSchema: z.object({
    success: z.boolean(),
    count: z.number(),
  }),
  steps: [chunkInput, generateEmbeddings, upsertVectors],
})
  .then(chunkInput)
  .then(generateEmbeddings)
  .then(upsertVectors)
  .commit();

Now create a tool that lets your agent search through this knowledge base (src/mastra/tools/vector-query-tool.ts):

import { createVectorQueryTool } from '@mastra/rag';
import { openai } from '@ai-sdk/openai';
import { VECTOR_STORE_NAME } from '../stores';

const embedder = openai.embedding('text-embedding-3-small');
const VECTOR_INDEX = 'weather_docs';

export const queryVectorTool = createVectorQueryTool({
  indexName: VECTOR_INDEX,
  model: embedder,
  vectorStoreName: VECTOR_STORE_NAME, // name registered on the Mastra instance
  description: 'Search weather documentation and historical data',
});

Update your weather agent to use this tool. Note the addition of the LIBSQL_PROMPT to the agent instructions. More information on this vector store prompt can be found on Mastra’s documentation here (src/mastra/agents/weather-agent.ts):

import { openai } from '@ai-sdk/openai';
import { Agent } from '@mastra/core/agent';
import { Memory } from '@mastra/memory';
import { storage, vectorStore } from '../stores';
import { LIBSQL_PROMPT } from '@mastra/libsql';
import { queryVectorTool } from '../tools/vector-query-tool';

export const weatherAgent = new Agent({
  name: 'Weather Agent',
  instructions: `
    You are a helpful weather assistant with access to historical weather data and documentation.

    Your capabilities include:
    - Providing current weather information for any location
    - Searching historical weather patterns and climate data
    - Learning from past conversations to provide personalized responses
    - Using relevant context from your knowledge base to enhance answers

    When responding:
    - Always ask for a location if none is provided
    - Use the vector search tool to find relevant historical data when appropriate
    - Include relevant details like humidity, wind conditions, and precipitation
    - Reference your sources when using historical data
    - Keep responses concise but informative

    ${LIBSQL_PROMPT}

`,
  model: openai('gpt-4o-mini'),

  memory: new Memory({
    storage: storage,
    vector: vector,
    embedder: openai.embedding('text-embedding-3-small'),
    options: {
      semanticRecall: true,
    },
  }),

  tools: { queryVectorTool },
});

Finally, let’s register our workflow to our Mastra instance (src/mastra/index.ts):

import { Mastra } from '@mastra/core/mastra';
import { PinoLogger } from '@mastra/loggers';
import { storage, VECTOR_STORE_NAME, vectorStore } from './stores';

import { weatherAgent } from './agents/weather-agent';
import { embeddingWorkflow } from './workflows/embedding-workflow';

export const mastra = new Mastra({
  agents: { weatherAgent },
  storage,
  logger: new PinoLogger({
    name: 'Mastra',
    level: 'debug',
  }),
  vectors: {
    [VECTOR_STORE_NAME]: vectorStore, // using the name of the vector
  },
  workflows: {
    embeddingWorkflow, // register the workflow to the Mastra instance
  },
});

#Testing Your AI Agent

Let's see everything in action! First, let's add some weather knowledge to our system. We can do this with a script, or a custom API endpoint on our Mastra server. We will go with a script just to keep things simple. Let’s create a file at the root of our project (ingest-weather.ts):

import { mastra } from './src/mastra';
import { EMBEDDING_WORKFLOW_NAME } from './src/mastra/workflows/embedding-workflow';

const weatherData = `
Climate patterns in San Francisco show that summers are typically dry with temperatures ranging from 60-70°F,
while winters are mild and wet with temperatures rarely dropping below 40°F. The city experiences significant
microclimates due to its geography, with areas like the Mission District being warmer and sunnier than
the Richmond District. Fog is common during summer months, particularly in the afternoons.

New York City has a humid subtropical climate with hot, humid summers (70-85°F) and cold winters (30-45°F).
The city experiences four distinct seasons with significant precipitation throughout the year. Hurricane season
can bring severe weather from June through November. Central Park often serves as a temperature reference point
for the city.
`;

async function ingestWeatherData() {
  const workflowRun = mastra.getWorkflow(EMBEDDING_WORKFLOW_NAME).createRun();

  const result = await workflowRun.start({
    inputData: {
      text: weatherData,
      source: 'climate_database',
    },
  });

  console.log('Ingested weather data:', result);
}

ingestWeatherData();

We can run this script to run our workflow and add the weather data. Note we have the --env-file flag to include all the environment variables we configured:

npx tsx --env-file=.env ingest-weather.ts

Now start your development server:

npm run dev

Open http://localhost:4111 and try these conversations. Watch how the queryVectorTool is used and the relevant context that is retrieved:

  1. "Tell me about San Francisco's climate patterns"
  2. "I'm planning a summer trip to SF, what should I expect?"
  3. "What about New York weather for outdoor activities?"

#Wrapping Up!

You've built an AI agent that can:

  • ✅ Remember conversations better with semantic recall
  • ✅ Search through historical data and documents
  • ✅ Store both structured and vector data in a single database

But this is just the beginning.

#Why This Combination Works

The Mastra + Turso combination solves a fundamental problem in AI development: complexity creep. Instead of managing multiple databases, APIs, and services, you get:

  • One database for all your data needs
  • TypeScript-first development with excellent tooling
  • Production-ready scaling from day one
  • Vector operations without the vector database tax
  • SQL superpowers for complex queries and reporting

Most importantly, you can focus on building great AI experiences instead of wrestling with infrastructure.

#Ready to Build?

The future of AI development is about simplicity and power in equal measure. With Mastra and Turso, you're not just building an AI agent – you're building a foundation that can grow with your ambitions.

Start with the weather agent we built today, then expand it into whatever vision you have. The tools are ready, the integration is seamless, and the only limit is your imagination.

Ready to get started? Check out the Mastra documentation and grab your Turso Database today.

Happy building! 🚀

scarf