The context problem
Every AI agent session starts cold. The agent has no memory of what happened yesterday, what state the system is in, or what other agents have done.
Manual context files go stale within hours. Monolithic dumps waste tokens and dilute focus. And agent output disappears when the session ends unless you build custom persistence. Lattice solves all three.
Without Lattice
- ×Context files go stale — agents act on old state
- ×Giant context dumps waste tokens and dilute focus
- ×Agent output is lost when the session ends
- ×Custom context plumbing in every project
- ×Agents contradict themselves across sessions
With Lattice
- ✓Context files auto-sync from the database
- ✓Per-entity directories load only what's relevant
- ✓Writeback pipeline persists agent output to the DB
- ✓Declarative schema — zero custom context plumbing
- ✓Every session starts with fresh, consistent state
The pattern
Define your schema
Call db.define() for each table. Provide column specs and a render function — or use a built-in template.
Define entity contexts
Call db.defineEntityContext() to declare per-entity file structures and relationships.
Lattice writes the tree
db.render() or db.watch() generates the complete file tree — one directory per entity.
Agents load what they need
Each agent reads its own CONTEXT.md or individual files. No giant global context dump.
Writeback closes the loop
db.defineWriteback() watches agent-written files and persists structured data back to the DB.
Personal agent system
voice_notes, contacts, tasks tables
Your personal agent knows your contacts, tasks, and voice notes. But each session starts fresh — it has no idea who Alice is or what you discussed last week. Lattice keeps a structured context directory so the agent always has that history.
const db = new Lattice('./personal.db');
db.define('contacts', {
columns: {
id: 'TEXT PRIMARY KEY',
name: 'TEXT NOT NULL',
email: 'TEXT',
notes: 'TEXT',
},
render: 'default-table',
outputFile: 'contacts/CONTACTS.md',
});
db.define('tasks', {
columns: {
id: 'TEXT PRIMARY KEY',
title: 'TEXT NOT NULL',
contact_id: 'TEXT',
status: 'TEXT DEFAULT "open"',
},
render: 'default-list',
outputFile: 'TASKS.md',
});
db.defineEntityContext('contacts', {
slug: (r) => r.name.toLowerCase().replace(/\s+/g, '-'),
index: {
outputFile: 'contacts/CONTACTS.md',
render: (rows) => rows.map((r) => `- ${r.name}`).join('\n'),
},
files: {
'CONTACT.md': {
source: { type: 'self' },
render: ([r]) => `# ${r.name}\n\n${r.notes ?? ''}`,
},
'TASKS.md': {
source: { type: 'hasMany', table: 'tasks', foreignKey: 'contact_id' },
render: (rows) => rows.map((r) => `- ${r.title}`).join('\n'),
omitIfEmpty: true,
},
'NOTES.md': {
source: { type: 'hasMany', table: 'voice_notes', foreignKey: 'contact_id' },
render: (rows) => rows.map((r) => r.transcript).join('\n\n'),
omitIfEmpty: true,
},
},
combined: { outputFile: 'CONTEXT.md', exclude: [] },
});context/
├── contacts/
│ └── CONTACTS.md ← index of all contacts
├── contacts/alice/
│ ├── CONTACT.md ← Alice's record
│ ├── TASKS.md ← tasks linked to Alice
│ ├── NOTES.md ← voice notes transcripts
│ └── CONTEXT.md ← all three combined
├── contacts/bob/
│ ├── CONTACT.md
│ └── CONTEXT.md ← TASKS.md omitted (empty)
└── TASKS.md ← global task listInsight: Bob's directory has no TASKS.md or NOTES.md because omitIfEmpty: true keeps the context lean. The agent only loads files that actually contain data.
Dev tooling assistant
projects, issues, deployments tables
Your AI coding assistant manages projects across a team. It needs to know which projects exist, who owns them, what issues are open, and what's deployed — without loading the full org context into every session.
const db = new Lattice('./projects.db');
db.define('projects', {
columns: {
id: 'TEXT PRIMARY KEY',
slug: 'TEXT NOT NULL UNIQUE',
name: 'TEXT NOT NULL',
owner: 'TEXT',
status: 'TEXT DEFAULT "active"',
},
render: 'default-table',
outputFile: 'projects/PROJECTS.md',
});
db.define('issues', {
columns: {
id: 'TEXT PRIMARY KEY',
project_id: 'TEXT NOT NULL',
title: 'TEXT NOT NULL',
priority: 'INTEGER DEFAULT 2',
status: 'TEXT DEFAULT "open"',
},
render: 'default-list',
outputFile: 'ISSUES.md',
});
db.defineEntityContext('projects', {
slug: (r) => r.slug as string,
index: {
outputFile: 'projects/PROJECTS.md',
render: (rows) => rows.map((r) => `- ${r.name} (${r.status})`).join('\n'),
},
files: {
'PROJECT.md': {
source: { type: 'self' },
render: ([r]) => `# ${r.name}\nOwner: ${r.owner}\nStatus: ${r.status}`,
},
'ISSUES.md': {
source: { type: 'hasMany', table: 'issues', foreignKey: 'project_id',
orderBy: 'priority', limit: 20 },
render: (rows) => rows.map((r) => `- [${r.status}] ${r.title}`).join('\n'),
omitIfEmpty: true,
},
'DEPLOYMENTS.md': {
source: { type: 'hasMany', table: 'deployments', foreignKey: 'project_id',
orderBy: 'created_at', orderDir: 'desc', limit: 5 },
render: (rows) => rows.map((r) => `- ${r.env}: ${r.sha} (${r.status})`).join('\n'),
omitIfEmpty: true,
},
},
combined: { outputFile: 'CONTEXT.md', exclude: [] },
});context/
├── projects/
│ └── PROJECTS.md ← index of all projects
├── projects/api-service/
│ ├── PROJECT.md ← project details + owner
│ ├── ISSUES.md ← top 20 issues by priority
│ ├── DEPLOYMENTS.md ← last 5 deploys per env
│ └── CONTEXT.md ← all files combined
└── projects/web-app/
├── PROJECT.md
├── ISSUES.md
└── CONTEXT.md ← DEPLOYMENTS.md omitted (none yet)Insight: Each project directory contains only its top 20 issues and last 5 deployments. The agent working on api-service never sees web-app's issues — scoped context means less noise and fewer tokens.
Multi-agent architecture
agents, skills, projects, agent_skills tables
You're running multiple specialized agents — each with its own persona, skills, and project assignments. Lattice gives each agent a private context directory with only its relevant data, plus protected files where agents write session output.
const db = new Lattice('./agents.db');
db.define('agents', {
columns: {
id: 'TEXT PRIMARY KEY',
slug: 'TEXT NOT NULL UNIQUE',
name: 'TEXT NOT NULL',
soul: 'TEXT',
active: 'INTEGER DEFAULT 1',
},
render: 'default-table',
outputFile: 'agents/AGENTS.md',
});
db.define('skills', {
columns: {
id: 'TEXT PRIMARY KEY',
name: 'TEXT NOT NULL',
description: 'TEXT',
},
render: 'default-list',
outputFile: 'skills/SKILLS.md',
});
db.define('agent_skills', {
columns: {
agent_id: 'TEXT NOT NULL',
skill_id: 'TEXT NOT NULL',
},
tableConstraints: ['PRIMARY KEY (agent_id, skill_id)'],
primaryKey: ['agent_id', 'skill_id'],
render: 'default-table',
outputFile: 'agent_skills.md',
});
db.defineEntityContext('agents', {
slug: (r) => r.slug as string,
files: {
'AGENT.md': {
source: { type: 'self' },
render: ([r]) => `# ${r.name}\n\n${r.soul ?? ''}`,
},
'SKILLS.md': {
source: {
type: 'manyToMany',
junctionTable: 'agent_skills',
localKey: 'agent_id',
remoteKey: 'skill_id',
remoteTable: 'skills',
},
render: (rows) => rows.map((r) => `- ${r.name}: ${r.description}`).join('\n'),
omitIfEmpty: true,
},
'PROJECTS.md': {
source: { type: 'hasMany', table: 'projects', foreignKey: 'owner_agent_id' },
render: (rows) => rows.map((r) => `- ${r.name}`).join('\n'),
omitIfEmpty: true,
},
},
combined: { outputFile: 'CONTEXT.md', exclude: [] },
protectedFiles: ['SESSION.md', 'NOTES.md'],
});context/
├── agents/
│ └── AGENTS.md ← index of all agents
├── agents/forge/
│ ├── AGENT.md ← persona and soul
│ ├── SKILLS.md ← skills via junction table
│ ├── PROJECTS.md ← owned projects
│ ├── CONTEXT.md ← all files combined
│ └── SESSION.md ← agent-written (protected)
├── agents/audit/
│ ├── AGENT.md
│ ├── SKILLS.md
│ └── CONTEXT.md
└── skills/
└── SKILLS.md ← global skills indexInsight: The manyToMany source resolves skills through a junction table automatically. SESSION.md is listed in protectedFiles — Lattice will never delete it during cleanup, even if the agent stops writing to it.
Writeback pipeline
Closing the loop: agent output → database
Your agents produce output — status updates, decisions, session notes. Without writeback, that output disappears when the session ends. The writeback pipeline watches agent-written files, parses structured entries, and stores them in the database. The next session starts with that data already rendered.
// Agents write structured entries to SESSION.md.
// Lattice parses them and persists to the database.
db.defineWriteback({
file: './context/agents/*/SESSION.md',
parse(content, fromOffset) {
const newContent = content.slice(fromOffset);
const entries = parseSessionWrites(newContent);
return {
entries: entries.entries,
nextOffset: content.length,
};
},
async persist(entry, filePath) {
await db.insert('events', {
source_file: filePath,
...(entry as Record<string, unknown>),
});
},
dedupeKey: (entry) => (entry as { id: string }).id,
});
// Now db.sync() renders context AND ingests agent output:
await db.sync('./context');context/agents/forge/
├── AGENT.md ← Lattice renders (read-only)
├── SKILLS.md ← Lattice renders (read-only)
├── CONTEXT.md ← Lattice renders (read-only)
└── SESSION.md ← agent writes here ↓
SESSION.md contents:
---
type: write
op: update
table: agents
target: forge-001
reason: Completed deployment task.
---
status: idle
last_task: deploy-api
===
→ Lattice parses this → inserts into events table
→ Next render cycle includes the updated stateInsight: The dedupeKey function prevents the same entry from being processed twice. Offset tracking means Lattice only parses new content added since the last sync — no re-processing.
Ready to get started?
Install Lattice, define your schema, and have agent-ready context files in minutes. No opinions about your stack — works with any agent framework.