July 8th, 2026
0 reactions

Building on Vercel’s eve + Azure Cosmos DB: An Agent That Remembers

Principal Program Manager

Most “AI agent” demos forget everything the moment the process exits. That’s fine for a toy project, but useless for anything real. An agent that helps you write, triage, or support needs two things a language model alone can’t give it: durable state and the ability to recall the right context by meaning. This post shows how to build exactly that by integrating two pieces that fit together surprisingly well:

Eve — Vercel’s filesystem-first agent platform. Drop a file in agent/tools/, and it becomes a tool the model can call.

Azure Cosmos DB JavaScript SDK — the official, promise-based client for Cosmos DB NoSQL, and the piece that does the heavy lifting here. It’s a modern, fully typed TypeScript SDK

We’ll build an agent that authors a Next.js blog and keeps a long-term semantic memory in Azure Cosmos DB. Every code sample below is production-shaped, not pseudocode.

Diagram showing a user request flowing through an AI agent, working memory, search, task agents, and long-term memory storage.

Meet eve: agents as a filesystem

If you’ve built with the Vercel AI SDK, eve will feel immediately familiar it’s the same team’s take on agents, and its core idea is refreshingly literal: your agent is a folder. There’s no graph DSL to learn and no orchestration boilerplate. You describe the agent with plain files, and eve wires them into a running agent:

Diagram of an agent project folder showing agent.ts for the agent definition, instructions.md for the system prompt, a tools folder with callable TypeScript tools, and a lib folder for shared helper code.

 

  • Tools are just files. Drop a defineTool({…}) in agent/tools/ and it’s available to the model — no registration, no wiring. Inputs are described with a Zod schema, and eve generates the JSON schema the model sees.
  • Instructions are Markdown. md is your system prompt. Editing behavior means editing prose, not code.
  • Bring your own model. eve runs on the AI SDK’s provider ecosystem, so you can point it at OpenAI, Anthropic, or as we do here Azure OpenAI

agent.ts

import { createAzure } from "@ai-sdk/azure";
import { defineAgent } from "eve";
 
const azure = createAzure({
  baseURL: `${process.env.AZURE_OPENAI_ENDPOINT}/openai`,
  apiKey: process.env.AZURE_OPENAI_API_KEY,
  apiVersion: "preview",
});
 
export default defineAgent({
  model: azure.chat("gpt-4.1"), // your Azure deployment name
});
  • Runs locally, ships anywhere. eve dev gives you an interactive chat in the terminal; eve build produces a deployable server. The agent talks to the outsideworld (your blog’s API, Azure Cosmos DB) through the tools you

What eve deliberately doesn’t give you is persistence. A model call is stateless; when the process restarts, the conversation is gone. That’s the gap we fill with Azure Cosmos DB — turning a stateless agent into one that genuinely remembers.

Why Azure Cosmos DB for agent memory?

Agent memory has an awkward shape for traditional databases: you write a lot of small items, you read them back by meaning (not exact keys), and you want it fast and global. Azure Cosmos DB NoSQL fits this well:

  • Single-digit-millisecond point reads when you know the id and partition key.
  • Native vector search — store embeddings and query with VectorDistance(); no separate vector database to run.
  • Native TTL — expire ephemeral memories with zero cleanup jobs.
  • One SDK, one bill — the same container holds your app data and your vector

Step 1: A singleton client (the #1 SDK rule)

A CosmosClient owns a connection pool and routing caches. Creating one per request exhausts sockets and adds TLS latency. Create it once and reuse it:

// src/lib/cosmos.ts    
    import { CosmosClient } from "@azure/cosmos";    
    import { DefaultAzureCredential } from "@azure/identity";    
         
    type CosmosGlobal = typeof globalThis & { __client?: CosmosClient };    
         
    export function getCosmosClient(): CosmosClient {    
      const g = globalThis as CosmosGlobal;    
      if (!g.__client) {    
        g.__client = new CosmosClient({    
          endpoint: process.env.COSMOS_ENDPOINT!,    
          // Prefer Entra ID over keys. Falls back to a key only if provided.    
          aadCredentials: new DefaultAzureCredential(),    
          connectionPolicy: {    
            // Let the SDK absorb transient 429s with backoff.    
            retryOptions: { maxRetryAttemptCount: 9, maxWaitTimeInSeconds: 30 },    
          },    
        });    
      }    
      return g.__client;    
    }    

Caching on globalThis matters in dev: frameworks like Next.js reset module state on hot-reload, which would otherwise leak a new client on every save.

Step 2: Model for cheap reads

Pick a partition key that is high-cardinality and immutable, and make the item id equal to it where you can — then single-item fetches are 1 RU point reads instead of queries:

    // A blog post: id === slug, partition key is /slug.    
    const { resource: post } = await container    
      .item(slug, slug) // (id, partitionKey)    
      .read(); // ~1 RU, single-digit ms    

For lists, use continuation tokens, never OFFSET/LIMIT:

    const iterator = container.items    
      .query({ query: "SELECT c.slug, c.title FROM c WHERE c.status = 'published'" })    
      .getAsyncIterator();    
    // ...or .query(spec).fetchNext() and pass resource.continuationToken to the client.    

Step 3: Turn the container into a vector memory store

Here’s where it gets fun. To store embeddings and search them, define a vector embedding policy and a vector index at container-creation time (the policy is immutable afterward):

         
    import {    
      VectorEmbeddingDataType,    
      VectorEmbeddingDistanceFunction,    
      VectorIndexType,    
    } from "@azure/cosmos";    
         
    await database.containers.createIfNotExists({    
      id: "memory",    
      partitionKey: { paths: ["/sessionId"] }, // one conversation per partition    
      defaultTtl: -1, // TTL enabled; set per-item `ttl` for ephemeral memories    
      vectorEmbeddingPolicy: {    
        vectorEmbeddings: [    
          {    
            path: "/embedding",    
            dataType: VectorEmbeddingDataType.Float32,    
            dimensions: 1536, // text-embedding-3-small    
            distanceFunction: VectorEmbeddingDistanceFunction.Cosine,    
          },    
        ],    
      },    
      indexingPolicy: {    
        includedPaths: [{ path: "/*" }],    
        // CRITICAL: keep the raw vector out of the regular index (saves write RUs).    
        excludedPaths: [{ path: "/embedding/*" }],    
        vectorIndexes: [{ path: "/embedding", type: VectorIndexType.QuantizedFlat }],    
      },    

Storing a memory — embed the text, then create the item:

    import { randomUUID } from "node:crypto";             
    async function appendMemory(sessionId: string, content: string) {    
      const embedding = await embedText(content); // Azure OpenAI embeddings → number[]    
      await container.items.create({    
        id: randomUUID(),    
        sessionId,    
        type: "memory",    
        content,    
        embedding,    
        createdAt: new Date().toISOString(),    
      });    
    }             

Recalling by meaning — VectorDistance() with ORDER BY, a literal TOP, and a parameterized query vector (so the query plan caches):

    async function recall(sessionId: string, query: string, k = 5) {    
      const vector = await embedText(query);    
      const { resources } = await container.items    
        .query(    
          {    
            query:    
              `SELECT TOP ${k} c.content, VectorDistance(c.embedding, @vec) AS score ` +    
              `FROM c WHERE c.type = "memory" AND c.sessionId = @sessionId ` +    
              `ORDER BY VectorDistance(c.embedding, @vec)`,    
            parameters: [    
              { name: "@vec", value: vector },    
              { name: "@sessionId", value: sessionId },    
            ],    
          },    
          { partitionKey: sessionId }, // single-partition → cheap and fast    
        )    
        .fetchAll();    
      return resources; // nearest memories, most similar first    
    }    

Because we filter on the partition key, this stays a single-partition query — exactly what you want on a hot path.

Step 4: Expose it to the agent as an eve tool

eve’s model is delightfully simple: every file in `agent/tools/` is a tool. You describe its inputs with Zod, and eve hands the model a JSON schema automatically.

         
    // agent/tools/recall.ts    
    import { defineTool } from "eve/tools";    
    import { z } from "zod";    
    import { blogApiUrl, authHeaders } from "../lib/blog-client.js";    
         
    export default defineTool({    
      description:    
        "Recall the most relevant saved memories for a query using vector search.",    
      inputSchema: z.object({    
        query: z.string().min(1).describe("What to search your memory for."),    
        sessionId: z.string().optional(),    
        k: z.number().int().min(1).max(20).default(5),    
      }),    
      async execute({ query, sessionId, k }) {    
        const url = new URL(blogApiUrl("/api/memory")); // resolves BLOG_API_URL    
        url.searchParams.set("q", query);    
        url.searchParams.set("k", String(k));    
        if (sessionId) url.searchParams.set("sessionId", sessionId);    
        const res = await fetch(url, { headers: authHeaders() });    
        const { results } = await res.json();    
        return { ok: true, memories: results };    
      },    

Now the agent can decide on its own, to call recall before answering — and because the memories live in Azure Cosmos DB, they persist across sessions, deploys, and regions.

The full loop

Diagram showing an agent memory flow where an AI agent sends remember and recall requests through an authenticated API, creates text embeddings with Azure OpenAI, and stores or searches memories in Azure Cosmos DB using vector search.

That’s a complete, durable, semantically searchable memory in well under 200 lines no extra infrastructure, no second database.

Try it

The complete, runnable source for everything in this post lives in the AzureCosmosDB/eve-cosmos-memory repository clone it and follow along:

    npm install    
    npm run seed     # creates the DB, blog container, and vector memory container    
    npm run dev      # the blog (Next.js on Vercel)    
    npm run agent    # chat with the eve agent — ask it to remember and recall    

Give your agent a memory. The Azure Cosmos DB JavaScript SDK makes the durable, global, vector-searchable part almost boring which is exactly what you want from infrastructure.

About Azure Cosmos DB

Azure Cosmos DB is a fully managed and serverless NoSQL and vector database for modern app development, including AI applications. With its SLA-backed speed and availability as well as instant dynamic scalability, it is ideal for real-time NoSQL and MongoDB applications that require high performance and distributed computing over massive volumes of NoSQL and vector data.

To stay in the loop on Azure Cosmos DB updates, follow us on X, YouTube, and LinkedIn.  Join the discussion with other developers on the #nosql channel on the Microsoft Open Source Discord.

Author

Sajeetharan Sinnathurai
Principal Program Manager

Principal Product Manager passionate about empowering developers with exceptional tools and experiences. Currently part of the Azure Cosmos DB team, driving developer-focused features like JavaScript SDK, integrations, and tooling for local development etc. Interested in web development or cloud? Let’s connect!

0 comments