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

1

Define your schema

Call db.define() for each table. Provide column specs and a render function — or use a built-in template.

2

Define entity contexts

Call db.defineEntityContext() to declare per-entity file structures and relationships.

3

Lattice writes the tree

db.render() or db.watch() generates the complete file tree — one directory per entity.

4

Agents load what they need

Each agent reads its own CONTEXT.md or individual files. No giant global context dump.

5

Writeback closes the loop

db.defineWriteback() watches agent-written files and persists structured data back to the DB.

01

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.

TypeScript schema
typescript
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: [] },
});
Generated file tree
text
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 list

Insight: 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.

02

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.

TypeScript schema
typescript
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: [] },
});
Generated file tree
text
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.

03

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.

TypeScript schema
typescript
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'],
});
Generated file tree
text
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 index

Insight: 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.

04

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.

Writeback configuration
typescript
// 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');
The read/write cycle
text
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 state

Insight: 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.