Shared context for your agent fleet
You have more than one agent. A coding agent in your editor, a job running in the cloud, maybe something inside a third-party SaaS. Each one starts cold — re-deriving the same decisions, conventions, and constraints every session, none of them aware of what the others learned.
This tutorial builds the missing piece: one shared context store that the whole fleet reads and writes. By the end you’ll have a store that holds durable facts and a small entity graph, recall them by keyword, populate them from a raw dump with an LLM, and wire the same store into Claude Code, OpenCode, or Codex — so a fact one agent writes is instantly visible to the others.
We build it on PerSQL because the store is just a SQLite database you own: structured rows, exact recall, reachable from every surface an agent might live behind. Retrieval is lexical — FTS5 with BM25 ranking — which is how a coding agent already retrieves (it greps for tokens, it doesn’t embed). The only LLM work happens on the write path, so reads stay cheap and exact.
What you’ll build
Section titled “What you’ll build”- A context database in your namespace.
- A typed store over it (
@persql/context) — facts, tags, supersession, aentity/edgegraph. - Keyword recall, ranked.
- Write-time extraction: a raw dump becomes clean rows.
- The same store wired into a local coding agent over MCP.
Prerequisites
Section titled “Prerequisites”- Node 18+.
- A PerSQL account and a namespace token (
psql_live_…). Create one in the console under your namespace’s API tokens, or see Getting started.
npm install @persql/sdk @persql/contextexport PERSQL_TOKEN=psql_live_...Part 1 — Create the store and the schema
Section titled “Part 1 — Create the store and the schema”A context store is an ordinary database. The @persql/context SDK wraps a
database handle and manages the schema for you.
import { PerSQL } from "@persql/sdk";import { context } from "@persql/context";
const persql = new PerSQL({ token: process.env.PERSQL_TOKEN! });
// One database, shared by every agent on this project.export const ctx = context(persql.database("acme/team-context"), { source: "tutorial", // default provenance stamped on writes});
// Create the schema. Idempotent — safe to call on every boot.await ctx.init();init() lays down three things:
ctx_memory— your facts, with a porter-stemmed FTS5 index over the text.ctx_entity/ctx_edge— a lightweight graph of named things and how they relate.- triggers that keep the FTS index in sync.
Everything is plain SQL in a database you can open, query, and export like any other. There is no hidden vector index and no separate service.
Part 2 — Remember and recall
Section titled “Part 2 — Remember and recall”A fact is one durable, self-contained statement worth carrying across sessions: a decision, a convention, a constraint, an identifier.
await ctx.remember({ topic: "billing", body: "Acme is invoiced net-30; never auto-charge their card.", tags: ["billing", "invoice", "policy"],});
await ctx.remember({ topic: "auth", body: "Sessions are JWTs signed per-database; tokens last 1 hour.", tags: ["auth"],});Recall is keyword-based and BM25-ranked — most relevant first:
const hits = await ctx.recall("invoice OR payment");// → [{ body: "Acme is invoiced net-30; ...", topic: "billing", ... }]Two things make recall forgiving without any embeddings:
- Stemming. The store uses the porter stemmer, so
recall("invoice")also matches “invoicing” and “invoiced”. You search with the idea, not the exact inflection. - Token sanitization. You can paste
"invoice OR billing"or just"invoice billing"; both become a safe FTS query. Pass{ mode: "raw" }to hand-write an FTS expression yourself.
Filter and shape the result set:
await ctx.recall("auth", { limit: 3, kind: "fact" });await ctx.recent({ limit: 10 }); // newest firstawait ctx.byTag("policy"); // exact-tag lookupKeeping facts current
Section titled “Keeping facts current”Facts go stale. Rather than mutate a row in place — which would erase the history — you write the new fact and mark the old one superseded. Superseded rows drop out of recall but stay on disk for audit.
const oldId = await ctx.remember({ topic: "limits", body: "Default page size is 100 rows.",});
// Later, the limit changes:await ctx.remember({ topic: "limits", body: "Default page size is 250 rows.", supersedes: oldId,});
await ctx.recall("page size"); // → only the 250-row factEvery fact also carries a source — which agent or tool wrote it — so when ten
agents share a store you can always see provenance.
Part 3 — The entity graph
Section titled “Part 3 — The entity graph”Facts are statements; entities are the things facts are about. The store keeps a small graph so an agent can ask “what touches the billing meter?” without a full-text scan.
await ctx.link("api worker", "depends_on", "billing meter");await ctx.link("billing meter", "writes", "balance ledger");await ctx.link("console", "calls", "api worker");link creates the entities if they don’t exist (idempotent) and records the
edge. Walk the neighborhood:
const graph = await ctx.neighbors("api worker", { depth: 2 });// graph.entities → billing meter, balance ledger, console// graph.edges → the relationships among themThis is a real knowledge graph expressed in two SQL tables and queried with a recursive join — no graph database, no extra tier.
Part 4 — Write-time intelligence
Section titled “Part 4 — Write-time intelligence”So far you’ve written facts by hand. The more powerful path is to hand the store a raw dump — a meeting transcript, a design doc, a chunk of conversation — and let an LLM extract the durable facts, entities, and relationships. The model runs on the write path; recall stays a cheap, exact lookup.
@persql/context is model-agnostic: you supply the extractor, so the package
never depends on a particular LLM. The extractor returns a typed
ExtractedContext — and the exact contract (system prompt + JSON schema) is
exported from @persql/context/core so your model returns the right shape.
import { EXTRACTION_SYSTEM_PROMPT, EXTRACTION_JSON_SCHEMA } from "@persql/context/core";import Anthropic from "@anthropic-ai/sdk";
const anthropic = new Anthropic();
const ctx = context(persql.database("acme/team-context"), { source: "ingest", extract: async (raw, { hint }) => { const msg = await anthropic.messages.create({ model: "claude-sonnet-4-6", max_tokens: 1024, system: EXTRACTION_SYSTEM_PROMPT, messages: [{ role: "user", content: (hint ? `Context: ${hint}\n\n` : "") + raw }], }); const text = msg.content.find((b) => b.type === "text")?.text ?? "{}"; return JSON.parse(text); // { facts, entities, edges } },});
await ctx.init();
await ctx.rememberRaw(` Standup notes: we decided the checkout service will read prices from the pricing DB, not cache them. Pricing is owned by the payments team. Net-30 customers skip the card-on-file requirement.`);One call extracted several facts (“checkout reads prices live”, “pricing owned by payments”, “net-30 customers skip card-on-file”), the entities (checkout service, pricing DB, payments team), and the edges between them — all now recallable:
await ctx.recall("checkout pricing");await ctx.neighbors("pricing DB", { depth: 1 });If you’d rather not run the LLM yourself, the hosted Context MCP does exactly this server-side — covered next — and meters it as AI tokens, so reads on your side cost nothing extra.
Part 5 — Share the store with a coding agent
Section titled “Part 5 — Share the store with a coding agent”The point of a shared store is that agents which aren’t your SDK script can use it too. Claude Code, OpenCode, and Codex all speak MCP, so one command wires any of them to the same database.
Install the CLI and start the bridge:
npm install -g @persql/clipersql login # browser device flowpersql mcp # stdio MCP server -> context.persql.compersql mcp does two helpful things:
- It resolves the store automatically from your git remote, so every agent
working a repo shares one project store. Want a personal store that follows
you across repos instead?
persql mcp --scope user. - It injects the resolved store into every call, so the model can never write to the wrong database.
Point your tool at it. Claude Code:
claude mcp add persql-context -- persql mcpOpenCode (opencode.json):
{ "mcp": { "persql-context": { "type": "local", "command": ["persql", "mcp"] } }}Codex (~/.codex/config.toml):
[mcp_servers.persql-context]command = "persql"args = ["mcp"]Then give the agent a one-line rule (in CLAUDE.md / AGENTS.md) so it uses
the store without being told each time:
## Shared context (PerSQL)On task start, `recall` what's already known about this project. When you learna durable fact — a decision, convention, constraint, or identifier — `remember`it so other agents and future sessions see it. Recall is keyword-based; writefacts with the words you'd later search.Now the loop closes: a fact your cloud SDK agent wrote in Part 2 is
recallable inside Claude Code, and a decision Claude records mid-task is
waiting for the next agent — local, cloud, or a teammate’s machine. The hosted
MCP exposes the same surface as the SDK: recall, remember, remember_raw
(server-side extraction), recent, by_tag, link, neighbors, forget.
Part 6 — Run it for a team
Section titled “Part 6 — Run it for a team”A few notes for moving from your laptop to a shared, production store.
Scope access per agent. A third-party agent shouldn’t get raw SQL over your core materials. Mint a token scoped to just the context database, or hand it the hosted MCP (typed tools only, no SQL). Untrusted callers can also be given a read-only token so they recall but never write.
Provenance and supersession are your hygiene. With many writers, lean on the
source field to see who wrote what, and on supersedes to retire stale facts
without losing history. A periodic job can recall a topic, summarize it, write
the summary, and supersede the originals — compaction without a special feature.
Cost. Context is rows like any other data: metered on rows read, rows
written, and storage, with no per-store or per-fact fee. Recall is plain SQL, so
it’s cheap; only remember_raw extraction spends AI tokens, and only on write.
See Billing.
Limits to design around. Recall quality depends on the words in your facts — write them with the terms you’d search. FTS5 is excellent into the millions of rows, but a context store lives in one database, so it’s bounded by that database, not infinite. And recall is lexical: if you need fuzzy semantic similarity (“anything about the customer being frustrated”), layer a vector service over these rows and keep the structured store as the source of truth.
What you built
Section titled “What you built”- A shared context database with a managed schema — facts, tags, an entity graph — that you own as plain SQL.
- Keyword recall (FTS5, BM25, porter-stemmed) with supersession and provenance.
- A write-time extraction path that turns raw dumps into structured rows.
- The same store wired into a coding agent over MCP, so a fleet of agents — local, cloud, and third-party — shares one memory.
Next steps
Section titled “Next steps”- Shared agent context recipe — the same idea in ≤20 lines.
- Agent memory recipe — single-agent memory, the building block this extends.
@persql/contextreference — the full store API.