latticesql 3.0.0 → 3.2.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
@@ -0,0 +1,313 @@
1
+ # Example: AI Agent System
2
+
3
+ A complete example showing how to use `latticesql` as the persistent memory layer for a multi-agent AI system.
4
+
5
+ ---
6
+
7
+ ## Scenario
8
+
9
+ You have several specialised AI agents (Craft, Audit, Research, etc.). Each agent has a persistent profile and an assigned task list. You want:
10
+
11
+ - A SQLite database tracking agents, tasks, and events
12
+ - Auto-generated LLM context files so each agent can read its current state
13
+ - A writeback channel so agents can append new tasks by writing to a file
14
+ - Audit logging for all database mutations
15
+
16
+ ---
17
+
18
+ ## Project structure
19
+
20
+ ```
21
+ my-agent-system/
22
+ ├── lattice.config.yml
23
+ ├── src/
24
+ │ └── db.ts
25
+ ├── context/
26
+ │ ├── AGENTS.md
27
+ │ └── TASKS.md
28
+ ├── data/
29
+ │ └── agents.db
30
+ └── generated/
31
+ ├── types.ts
32
+ └── migration.sql
33
+ ```
34
+
35
+ ---
36
+
37
+ ## 1. Define the schema
38
+
39
+ ```yaml
40
+ # lattice.config.yml
41
+ db: ./data/agents.db
42
+
43
+ entities:
44
+ agent:
45
+ fields:
46
+ id: { type: uuid, primaryKey: true }
47
+ slug: { type: text, required: true }
48
+ name: { type: text, required: true }
49
+ role: { type: text, required: true }
50
+ status: { type: text, default: active }
51
+ model: { type: text, default: claude-sonnet-4-6 }
52
+ created_at: { type: datetime }
53
+ render: default-table
54
+ outputFile: context/AGENTS.md
55
+
56
+ task:
57
+ fields:
58
+ id: { type: uuid, primaryKey: true }
59
+ title: { type: text, required: true }
60
+ description: { type: text }
61
+ status: { type: text, default: pending }
62
+ priority: { type: integer, default: 1 }
63
+ agent_id: { type: uuid, ref: agent }
64
+ created_at: { type: datetime }
65
+ completed_at: { type: datetime }
66
+ render:
67
+ template: default-list
68
+ formatRow: '[{{status}}] {{title}} → {{agent.name}}'
69
+ outputFile: context/TASKS.md
70
+
71
+ event:
72
+ fields:
73
+ id: { type: uuid, primaryKey: true }
74
+ type: { type: text, required: true }
75
+ agent_id: { type: uuid, ref: agent }
76
+ payload: { type: text }
77
+ created_at: { type: datetime }
78
+ render:
79
+ template: default-list
80
+ formatRow: '{{created_at}} [{{type}}] {{agent.name}}'
81
+ outputFile: context/EVENTS.md
82
+ ```
83
+
84
+ ---
85
+
86
+ ## 2. Generate types and migration
87
+
88
+ ```sh
89
+ npx lattice generate
90
+ ```
91
+
92
+ Produces `generated/types.ts`:
93
+
94
+ ```ts
95
+ export interface Agent {
96
+ id: string;
97
+ slug: string;
98
+ name: string;
99
+ role: string;
100
+ status?: string;
101
+ model?: string;
102
+ created_at?: string;
103
+ }
104
+
105
+ export interface Task {
106
+ id: string;
107
+ title: string;
108
+ description?: string;
109
+ status?: string;
110
+ priority?: number;
111
+ agent_id?: string; // → agent
112
+ created_at?: string;
113
+ completed_at?: string;
114
+ }
115
+
116
+ export interface Event {
117
+ id: string;
118
+ type: string;
119
+ agent_id?: string; // → agent
120
+ payload?: string;
121
+ created_at?: string;
122
+ }
123
+ ```
124
+
125
+ ---
126
+
127
+ ## 3. Set up the database
128
+
129
+ ```ts
130
+ // src/db.ts
131
+ import { Lattice } from 'latticesql';
132
+
133
+ export const db = new Lattice({ config: './lattice.config.yml' });
134
+
135
+ db.on('audit', (event) => {
136
+ console.log(`[audit] ${event.operation} ${event.table}:${event.id} at ${event.timestamp}`);
137
+ });
138
+
139
+ db.on('render', (result) => {
140
+ console.log(`[render] Wrote ${result.filesWritten.length} file(s) in ${result.durationMs}ms`);
141
+ });
142
+ ```
143
+
144
+ ---
145
+
146
+ ## 4. Seed agents
147
+
148
+ ```ts
149
+ import { db } from './db.js';
150
+
151
+ async function seedAgents() {
152
+ await db.init();
153
+
154
+ const agents = [
155
+ { id: 'agent-craft', slug: 'craft', name: 'Craft', role: 'Software architect and implementer' },
156
+ { id: 'agent-audit', slug: 'audit', name: 'Audit', role: 'Code reviewer and security analyst' },
157
+ {
158
+ id: 'agent-research',
159
+ slug: 'research',
160
+ name: 'Research',
161
+ role: 'Technical researcher and planner',
162
+ },
163
+ ];
164
+
165
+ for (const agent of agents) {
166
+ await db.upsert('agent', {
167
+ ...agent,
168
+ status: 'active',
169
+ model: 'claude-sonnet-4-6',
170
+ created_at: new Date().toISOString(),
171
+ });
172
+ }
173
+
174
+ console.log('Agents seeded.');
175
+ }
176
+
177
+ seedAgents();
178
+ ```
179
+
180
+ ---
181
+
182
+ ## 5. Assign and complete tasks
183
+
184
+ ```ts
185
+ import { db } from './db.js';
186
+
187
+ // Create a task
188
+ const taskId = await db.insert('task', {
189
+ title: 'Add rate limiting to the API',
190
+ description: 'Implement token bucket rate limiting on /api/* routes',
191
+ status: 'pending',
192
+ priority: 2,
193
+ agent_id: 'agent-craft',
194
+ created_at: new Date().toISOString(),
195
+ });
196
+
197
+ // Query open tasks for an agent
198
+ const craftTasks = await db.query('task', {
199
+ where: { agent_id: 'agent-craft', status: 'pending' },
200
+ orderBy: 'priority',
201
+ orderDir: 'desc',
202
+ });
203
+
204
+ // Mark a task complete
205
+ await db.update('task', taskId, {
206
+ status: 'done',
207
+ completed_at: new Date().toISOString(),
208
+ });
209
+
210
+ // Count by status
211
+ const openCount = await db.count('task', { where: { status: 'pending' } });
212
+ const doneCount = await db.count('task', { where: { status: 'done' } });
213
+ console.log(`Tasks: ${openCount} open, ${doneCount} done`);
214
+ ```
215
+
216
+ ---
217
+
218
+ ## 6. Writeback: let agents append tasks
219
+
220
+ Agents can write to a shared inbox file. Lattice reads it back and inserts into the database.
221
+
222
+ ```ts
223
+ import { db } from './db.js';
224
+
225
+ // Register the writeback pipeline
226
+ db.defineWriteback({
227
+ file: './context/INBOX.md',
228
+ parse: (content, fromOffset) => {
229
+ const newContent = content.slice(fromOffset);
230
+ const entries: { title: string; agentSlug: string }[] = [];
231
+
232
+ // Parse lines like: "- TASK: [agent-slug] Task title here"
233
+ for (const line of newContent.split('\n')) {
234
+ const match = line.match(/^- TASK: \[([^\]]+)\] (.+)$/);
235
+ if (match) {
236
+ entries.push({ agentSlug: match[1]!, title: match[2]! });
237
+ }
238
+ }
239
+
240
+ return { entries, nextOffset: content.length };
241
+ },
242
+ persist: async (entry) => {
243
+ const { title, agentSlug } = entry as { title: string; agentSlug: string };
244
+ // Look up the agent by slug
245
+ const [agent] = await db.query('agent', { where: { slug: agentSlug } });
246
+ await db.insert('task', {
247
+ title,
248
+ status: 'pending',
249
+ priority: 1,
250
+ agent_id: agent?.id ?? null,
251
+ created_at: new Date().toISOString(),
252
+ });
253
+ },
254
+ dedupeKey: (entry) => {
255
+ const { title, agentSlug } = entry as { title: string; agentSlug: string };
256
+ return `${agentSlug}:${title}`;
257
+ },
258
+ });
259
+ ```
260
+
261
+ Now any agent that writes `- TASK: [craft] Refactor auth middleware` to `context/INBOX.md` will have it automatically picked up on the next sync cycle.
262
+
263
+ ---
264
+
265
+ ## 7. Start the sync loop
266
+
267
+ ```ts
268
+ import { db } from './db.js';
269
+
270
+ await db.init();
271
+
272
+ // Render once immediately:
273
+ await db.render('./context');
274
+
275
+ // Then watch with a 10-second interval:
276
+ const stop = await db.watch('./context', {
277
+ interval: 10_000,
278
+ onError: (err) => console.error('Sync error:', err),
279
+ });
280
+
281
+ // Graceful shutdown:
282
+ process.on('SIGTERM', () => {
283
+ stop();
284
+ db.close();
285
+ });
286
+ ```
287
+
288
+ ---
289
+
290
+ ## 8. What the context files look like
291
+
292
+ **`context/AGENTS.md`** (rendered by `default-table`):
293
+
294
+ ```markdown
295
+ # agent
296
+
297
+ | id | slug | name | role | status | model |
298
+ | ----------- | ----- | ----- | ------------------ | ------ | ----------------- |
299
+ | agent-craft | craft | Craft | Software architect | active | claude-sonnet-4-6 |
300
+ | agent-audit | audit | Audit | Code reviewer | active | claude-sonnet-4-6 |
301
+ ```
302
+
303
+ **`context/TASKS.md`** (rendered by `default-list` with `formatRow`):
304
+
305
+ ```markdown
306
+ # task
307
+
308
+ - [pending] Add rate limiting to the API → Craft
309
+ - [done] Write migration guide → Audit
310
+ - [pending] Research vector search options → Research
311
+ ```
312
+
313
+ These files are what the agents read at the start of each conversation — structured, compact, and automatically kept in sync with the database.
@@ -0,0 +1,366 @@
1
+ # Example: Content Management System
2
+
3
+ A complete example showing how to build a CMS with `latticesql` — authors, posts, tags, and generated context files for LLM-assisted content operations.
4
+
5
+ ---
6
+
7
+ ## Scenario
8
+
9
+ A content team wants:
10
+
11
+ - A SQLite-backed CMS storing authors, posts, and tags
12
+ - Markdown context files that LLMs can read for drafting, editing, and summarisation tasks
13
+ - A custom render function for the posts index (too complex for a built-in template)
14
+ - A multi-table view that produces one context file per author
15
+
16
+ ---
17
+
18
+ ## Project structure
19
+
20
+ ```
21
+ my-cms/
22
+ ├── lattice.config.yml
23
+ ├── src/
24
+ │ ├── db.ts
25
+ │ └── content.ts
26
+ ├── context/
27
+ │ ├── AUTHORS.md
28
+ │ ├── POSTS.md
29
+ │ └── authors/
30
+ │ ├── alice.md
31
+ │ └── bob.md
32
+ ├── data/
33
+ │ └── cms.db
34
+ └── generated/
35
+ ├── types.ts
36
+ └── migration.sql
37
+ ```
38
+
39
+ ---
40
+
41
+ ## 1. Schema
42
+
43
+ ```yaml
44
+ # lattice.config.yml
45
+ db: ./data/cms.db
46
+
47
+ entities:
48
+ author:
49
+ fields:
50
+ id: { type: uuid, primaryKey: true }
51
+ slug: { type: text, required: true }
52
+ name: { type: text, required: true }
53
+ email: { type: text, required: true }
54
+ bio: { type: text }
55
+ active: { type: boolean, default: 1 }
56
+ render: default-table
57
+ outputFile: context/AUTHORS.md
58
+
59
+ tag:
60
+ fields:
61
+ id: { type: uuid, primaryKey: true }
62
+ label: { type: text, required: true }
63
+ render: default-list
64
+ outputFile: context/TAGS.md
65
+
66
+ post:
67
+ fields:
68
+ id: { type: uuid, primaryKey: true }
69
+ slug: { type: text, required: true }
70
+ title: { type: text, required: true }
71
+ excerpt: { type: text }
72
+ status: { type: text, default: draft }
73
+ author_id: { type: uuid, ref: author }
74
+ word_count: { type: integer, default: 0 }
75
+ published_at: { type: datetime }
76
+ updated_at: { type: datetime }
77
+ render:
78
+ template: default-detail
79
+ formatRow: '**{{title}}** [{{status}}] by {{author.name}} — {{word_count}} words'
80
+ outputFile: context/POSTS.md
81
+ ```
82
+
83
+ ---
84
+
85
+ ## 2. Generate and migrate
86
+
87
+ ```sh
88
+ npx lattice generate --out src/generated
89
+ ```
90
+
91
+ Then apply the generated SQL to your database on first run.
92
+
93
+ ---
94
+
95
+ ## 3. Database and custom render
96
+
97
+ The `post` entity uses the `default-detail` template with a `formatRow` hook from the YAML config. But for the per-author context files, we need a custom multi-table view that can't be expressed in YAML:
98
+
99
+ ```ts
100
+ // src/db.ts
101
+ import { Lattice } from 'latticesql';
102
+
103
+ export const db = new Lattice({
104
+ config: './lattice.config.yml',
105
+ options: { wal: true },
106
+ });
107
+
108
+ // Per-author context files — one file per author
109
+ db.defineMulti('author-context', {
110
+ keys: () => db.query('author', { where: { active: 1 } }),
111
+
112
+ outputFile: (author) => `context/authors/${author.slug as string}.md`,
113
+
114
+ tables: ['post'],
115
+
116
+ render: (author, tables) => {
117
+ const authorPosts = (tables.post ?? [])
118
+ .filter((p) => p.author_id === author.id)
119
+ .sort((a, b) => String(b.updated_at ?? '').localeCompare(String(a.updated_at ?? '')));
120
+
121
+ const published = authorPosts.filter((p) => p.status === 'published');
122
+ const drafts = authorPosts.filter((p) => p.status === 'draft');
123
+
124
+ const lines: string[] = [
125
+ `# ${author.name as string}`,
126
+ '',
127
+ author.bio ? `${author.bio as string}` : '',
128
+ '',
129
+ `**Published:** ${published.length} posts`,
130
+ `**Drafts:** ${drafts.length} posts`,
131
+ '',
132
+ '## Recent Posts',
133
+ '',
134
+ ...published
135
+ .slice(0, 5)
136
+ .map(
137
+ (p) =>
138
+ `- **${p.title as string}** — ${(p.published_at as string) ?? 'unpublished'} (${p.word_count as number} words)`,
139
+ ),
140
+ ];
141
+
142
+ if (drafts.length > 0) {
143
+ lines.push('', '## Drafts', '');
144
+ for (const draft of drafts) {
145
+ lines.push(`- ${draft.title as string} _(${draft.word_count as number} words)_`);
146
+ }
147
+ }
148
+
149
+ return lines.filter((l) => l !== null).join('\n');
150
+ },
151
+ });
152
+
153
+ await db.init({
154
+ migrations: [
155
+ {
156
+ version: 1,
157
+ sql: 'ALTER TABLE post ADD COLUMN featured INTEGER DEFAULT 0',
158
+ },
159
+ ],
160
+ });
161
+ ```
162
+
163
+ ---
164
+
165
+ ## 4. Content operations
166
+
167
+ ```ts
168
+ // src/content.ts
169
+ import { db } from './db.js';
170
+
171
+ // --- Authors ---
172
+
173
+ export async function createAuthor(opts: {
174
+ slug: string;
175
+ name: string;
176
+ email: string;
177
+ bio?: string;
178
+ }) {
179
+ return db.insert('author', {
180
+ ...opts,
181
+ bio: opts.bio ?? null,
182
+ active: 1,
183
+ });
184
+ }
185
+
186
+ // --- Posts ---
187
+
188
+ export async function createDraft(opts: {
189
+ slug: string;
190
+ title: string;
191
+ excerpt?: string;
192
+ authorId: string;
193
+ }) {
194
+ return db.insert('post', {
195
+ ...opts,
196
+ excerpt: opts.excerpt ?? null,
197
+ status: 'draft',
198
+ author_id: opts.authorId,
199
+ word_count: 0,
200
+ updated_at: new Date().toISOString(),
201
+ });
202
+ }
203
+
204
+ export async function updatePost(
205
+ id: string,
206
+ content: { title?: string; excerpt?: string; wordCount?: number },
207
+ ) {
208
+ await db.update('post', id, {
209
+ ...(content.title ? { title: content.title } : {}),
210
+ ...(content.excerpt ? { excerpt: content.excerpt } : {}),
211
+ ...(content.wordCount ? { word_count: content.wordCount } : {}),
212
+ updated_at: new Date().toISOString(),
213
+ });
214
+ }
215
+
216
+ export async function publishPost(id: string) {
217
+ await db.update('post', id, {
218
+ status: 'published',
219
+ published_at: new Date().toISOString(),
220
+ updated_at: new Date().toISOString(),
221
+ });
222
+ }
223
+
224
+ export async function getPublishedPosts(authorId?: string) {
225
+ if (authorId) {
226
+ return db.query('post', {
227
+ where: { status: 'published', author_id: authorId },
228
+ orderBy: 'published_at',
229
+ orderDir: 'desc',
230
+ });
231
+ }
232
+ return db.query('post', {
233
+ where: { status: 'published' },
234
+ orderBy: 'published_at',
235
+ orderDir: 'desc',
236
+ });
237
+ }
238
+
239
+ export async function searchPosts(query: string) {
240
+ return db.query('post', {
241
+ filters: [
242
+ { col: 'title', op: 'like', val: `%${query}%` },
243
+ { col: 'status', op: 'ne', val: 'archived' },
244
+ ],
245
+ });
246
+ }
247
+
248
+ export async function getLongPosts(minWords = 1000) {
249
+ return db.query('post', {
250
+ filters: [
251
+ { col: 'word_count', op: 'gte', val: minWords },
252
+ { col: 'status', op: 'eq', val: 'published' },
253
+ ],
254
+ orderBy: 'word_count',
255
+ orderDir: 'desc',
256
+ });
257
+ }
258
+
259
+ // --- Tags (upsert by label) ---
260
+
261
+ export async function findOrCreateTag(label: string) {
262
+ return db.upsertBy('tag', 'label', label, {});
263
+ }
264
+
265
+ // --- Stats ---
266
+
267
+ export async function getCMSStats() {
268
+ const [totalPosts, publishedPosts, draftPosts, totalAuthors] = await Promise.all([
269
+ db.count('post'),
270
+ db.count('post', { where: { status: 'published' } }),
271
+ db.count('post', { where: { status: 'draft' } }),
272
+ db.count('author', { where: { active: 1 } }),
273
+ ]);
274
+ return { totalPosts, publishedPosts, draftPosts, totalAuthors };
275
+ }
276
+ ```
277
+
278
+ ---
279
+
280
+ ## 5. Sync loop
281
+
282
+ ```ts
283
+ import { db } from './db.js';
284
+
285
+ // Render on demand after writes:
286
+ await db.render('./context');
287
+
288
+ // Or watch with a longer interval (content changes slowly):
289
+ const stop = await db.watch('./context', {
290
+ interval: 60_000, // 1 minute
291
+ onRender: (r) => {
292
+ if (r.filesWritten.length > 0) {
293
+ console.log(`[cms] Context updated: ${r.filesWritten.join(', ')}`);
294
+ }
295
+ },
296
+ });
297
+ ```
298
+
299
+ ---
300
+
301
+ ## 6. Sample context output
302
+
303
+ **`context/POSTS.md`** (rendered by `default-detail` with `formatRow`):
304
+
305
+ ```markdown
306
+ # post
307
+
308
+ ## post-1
309
+
310
+ **How Lattice Works** [published] by Alice — 1240 words
311
+
312
+ ## post-2
313
+
314
+ **Getting Started with AI Agents** [published] by Bob — 980 words
315
+
316
+ ## post-3
317
+
318
+ **Draft: Advanced Query Patterns** [draft] by Alice — 340 words
319
+ ```
320
+
321
+ **`context/authors/alice.md`** (multi-table custom render):
322
+
323
+ ```markdown
324
+ # Alice
325
+
326
+ Senior technical writer with a focus on developer tooling.
327
+
328
+ **Published:** 12 posts
329
+ **Drafts:** 2 posts
330
+
331
+ ## Recent Posts
332
+
333
+ - **How Lattice Works** — 2026-03-15 (1240 words)
334
+ - **Building Context-Aware AI Systems** — 2026-03-01 (1800 words)
335
+
336
+ ## Drafts
337
+
338
+ - Advanced Query Patterns _(340 words)_
339
+ - Template Rendering Deep Dive _(120 words)_
340
+ ```
341
+
342
+ An LLM given access to `context/authors/alice.md` immediately knows Alice's recent work, what she's drafted, and can assist with writing, editing, or summarising without querying the database.
343
+
344
+ ---
345
+
346
+ ## 7. Using the escape hatch for raw queries
347
+
348
+ For complex queries not covered by the Lattice API — for example, joining posts to tags through a join table — use the `db.db` escape hatch:
349
+
350
+ ```ts
351
+ // Get posts with their tag labels (join table not modelled in Lattice)
352
+ const stmt = db.db.prepare(`
353
+ SELECT p.id, p.title, GROUP_CONCAT(t.label) as tags
354
+ FROM post p
355
+ LEFT JOIN post_tag pt ON pt.post_id = p.id
356
+ LEFT JOIN tag t ON t.id = pt.tag_id
357
+ WHERE p.status = 'published'
358
+ GROUP BY p.id
359
+ ORDER BY p.published_at DESC
360
+ LIMIT ?
361
+ `);
362
+
363
+ const recentWithTags = stmt.all(10) as Array<{ id: string; title: string; tags: string }>;
364
+ ```
365
+
366
+ The escape hatch bypasses Lattice sanitization and audit logging — use it only for read-only analytics queries.