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
- Harness binds config and gives you
memory(),context(),extraction(),state(), andlog() - Extraction runs pattern matching on text and returns structured facts with scope and tags
- Ingest writes facts to the backend, deduplicates, and supersedes stale entries
- 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
defaultEmbeddingFnfor 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