Tutorial: chat agent

This walkthrough builds a terminal chatbot that remembers facts across sessions. It covers harness setup, rules-based extraction, and context packing — all in a single file.

The full source is in examples/chat-agent/ in the db0 repo.

Prerequisites

  • Node.js 18+
  • An Anthropic API key (ANTHROPIC_API_KEY)

Setup

npm init -y
npm install @db0-ai/core @db0-ai/backends-sqlite @anthropic-ai/sdk

Create the harness

A harness is your entry point to db0. It binds an agent ID, session, user, backend, and profile together.

import { db0, defaultEmbeddingFn, PROFILE_CONVERSATIONAL } from "@db0-ai/core"
import { createSqliteBackend } from "@db0-ai/backends-sqlite"

const backend = await createSqliteBackend({ dbPath: "./chat-agent.sqlite" })

const harness = db0.harness({
  agentId: "chat-agent",
  sessionId: `session-${Date.now()}`,
  userId: "user",
  backend,
  embeddingFn: defaultEmbeddingFn,
  profile: PROFILE_CONVERSATIONAL,
})

PROFILE_CONVERSATIONAL is tuned for chat — it extracts user preferences, names, and stated facts, and keeps session-scope memories shorter-lived than user-scope ones.

defaultEmbeddingFn uses a deterministic hash. Swap it for an OpenAI or Gemini embedding function to get semantic search.

Load previous memories

On startup, check what the agent already knows about the user:

const existing = await harness.memory().list("user")

if (existing.length > 0) {
  console.log(`Memories from previous sessions (${existing.length}):`)
  for (const m of existing.slice(0, 5)) {
    console.log(`  - ${m.content}`)
  }
}

User-scope memories persist across sessions. Session-scope memories only last for the current session.

Pack context for the LLM

Before each LLM call, use context().pack() to assemble relevant memories into a text block that fits your token budget:

const ctx = await harness.context().pack(userMessage, { tokenBudget: 1500 })

const systemPrompt = [
  "You are a helpful assistant with persistent memory.",
  ctx.count > 0
    ? `\nRelevant memories:\n${ctx.text}`
    : "",
].filter(Boolean).join("\n")

pack() scores memories by relevance to the query, recency, and scope priority, then fills the budget greedily. You get back { text, count, estimatedTokens }.

Extract facts from the conversation

After each turn, run the extraction pipeline on both the user message and the assistant response:

const extraction = harness.extraction()

// After each turn:
const userFacts = extraction.extract(userMessage)
const assistantFacts = extraction.extract(assistantResponse)

for (const fact of [...userFacts, ...assistantFacts]) {
  await harness.context().ingest(fact.content, {
    scope: fact.scope,
    tags: fact.tags,
  })
}

Extraction is rules-based — no LLM calls. It picks up patterns like "my name is ...", "I prefer ...", "I work at ...", and assigns each fact a scope (user or session) and tags automatically.

ingest() handles deduplication and superseding. If the user says "I live in SF" and later says "I moved to NYC", the old fact gets superseded.

Run it

ANTHROPIC_API_KEY=sk-... npx tsx index.ts

First run:

Chat with an agent that remembers. Type 'quit' to exit.

you: Hi, I'm Alex. I work on backend systems at a startup.

Nice to meet you, Alex! What kind of backend systems?

(extracted 2 facts)

Second run (new session, same SQLite file):

Memories from previous sessions (2):
  - user's name is Alex
  - user works on backend systems at a startup

Chat with an agent that remembers. Type 'quit' to exit.

you: What do you remember about me?

You're Alex, and you work on backend systems at a startup!

What's happening under the hood

  1. Harness binds config and gives you memory(), context(), extraction(), state(), and log()
  2. Extraction runs pattern matching on text and returns structured facts with scope and tags
  3. Ingest writes facts to the backend, deduplicates, and supersedes stale entries
  4. Pack queries the backend for memories relevant to the current input and serializes them within a token budget

No vector database, no external services, no LLM calls for memory management. Everything runs locally in SQLite.

Next steps

  • Swap defaultEmbeddingFn for a real embedding provider to enable semantic search — see Backends
  • Try different profiles (agent-context, knowledge-base, coding-assistant)
  • Add state checkpoints to save and restore conversation branches
  • Use sub-agents to spawn child agents with shared memory