p-layers 0.1.0__tar.gz

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.
p_layers-0.1.0/LICENSE ADDED
@@ -0,0 +1,21 @@
1
+ MIT License
2
+
3
+ Copyright (c) 2026 humanerd-drew
4
+
5
+ Permission is hereby granted, free of charge, to any person obtaining a copy
6
+ of this software and associated documentation files (the "Software"), to deal
7
+ in the Software without restriction, including without limitation the rights
8
+ to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
9
+ copies of the Software, and to permit persons to whom the Software is
10
+ furnished to do so, subject to the following conditions:
11
+
12
+ The above copyright notice and this permission notice shall be included in all
13
+ copies or substantial portions of the Software.
14
+
15
+ THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
16
+ IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
17
+ FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
18
+ AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
19
+ LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
20
+ OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
21
+ SOFTWARE.
@@ -0,0 +1,13 @@
1
+ Metadata-Version: 2.4
2
+ Name: p-layers
3
+ Version: 0.1.0
4
+ Summary: 7 governance layers for AI agent memory — P0-P6 with KnowledgeDB, MCP server, and wiki-compile
5
+ Project-URL: Home, https://github.com/humanerd-drew/p-layer
6
+ Requires-Python: >=3.10
7
+ License-File: LICENSE
8
+ Requires-Dist: mcp>=1.0
9
+ Provides-Extra: pg
10
+ Requires-Dist: psycopg2-binary>=2.9; extra == "pg"
11
+ Requires-Dist: pgvector>=0.3; extra == "pg"
12
+ Requires-Dist: numpy>=1.24; extra == "pg"
13
+ Dynamic: license-file
@@ -0,0 +1,185 @@
1
+ # P-Layer
2
+
3
+ **7 layers. 1 memory. Zero chaos.**
4
+
5
+ Organize your AI agent's memory into governance layers — from immutable rules (P0) to incident retrospectives (P6). Each layer has a contract: who can write, when to query, how to maintain.
6
+
7
+ ```python
8
+ pip install p-layers # PyPI: p-layers, import: p_layer
9
+ python3 -m p_layer.mcp.server # starts MCP server with SQLite (zero config)
10
+ ```
11
+
12
+ ## The 7 Layers
13
+
14
+ | Layer | Name | Purpose | Query | Write Access |
15
+ |-------|------|---------|-------|-------------|
16
+ | **P0** | brainstem | Immutable rules | Every session start | system only |
17
+ | **P1** | limbic | Identity & persona | Session start + output | human only |
18
+ | **P2** | hippocampus | Raw session archive | **Last resort** | append-only |
19
+ | **P3** | sensors | Tool integrations | When debugging | system + cron |
20
+ | **P4** | cortex | Skills & growth | Skill selection | agent + manual |
21
+ | **P5** | ego | **Compiled wiki** | **1st priority** | auto-generated |
22
+ | **P6** | prefrontal | Incidents & RCA | During RCA | agent + manual |
23
+
24
+ ## Real-world flow
25
+
26
+ An AI assistant discovers a bug in the build pipeline:
27
+
28
+ 1. **P6** — Writes an incident report with timeline + root cause
29
+ 2. **P0** — If the root cause was a rule violation, proposes a P0 amendment
30
+ 3. **`knowledge_recall`** — When similar symptoms appear weeks later, the MCP server surfaces the incident ranked by confidence + freshness + serendipity
31
+ 4. **`wiki_compile.py`** — End of day, all incidents + fixes are compiled into P5 wiki pages
32
+ 5. **Query routing** — Next session, the compiled knowledge is found instantly (P5 first, P2 last)
33
+
34
+ The same bug never happens twice — not because the agent remembers, but because the governance layer learned.
35
+
36
+ ## MCP Server
37
+
38
+ 7 tools, all included:
39
+
40
+ | Tool | What it does |
41
+ |------|-------------|
42
+ | `knowledge_remember` | Store a fact with confidence, TTL, version label |
43
+ | `knowledge_recall` | Ranked search — confidence + freshness + 5% serendipity |
44
+ | `knowledge_forget` | Soft-delete (supersede, never destroy) |
45
+ | `knowledge_update` | Update by ID — old version is superseded, history preserved |
46
+ | `knowledge_memory-stats` | Entry counts by layer |
47
+ | `knowledge_snapshot-create` | Freeze current state under a version label |
48
+ | `knowledge_snapshot-rollback` | Supersede entries created after a snapshot |
49
+
50
+ ### MCP client configuration
51
+
52
+ **opencode** (`opencode.jsonc`):
53
+ ```json
54
+ {
55
+ "mcp": {
56
+ "p-layer": {
57
+ "type": "local",
58
+ "command": ["python3", "-m", "p_layer.mcp.server"],
59
+ "env": { "KNOWLEDGE_PG_DSN": "{env:KNOWLEDGE_PG_DSN}" },
60
+ "enabled": true
61
+ }
62
+ }
63
+ }
64
+ ```
65
+
66
+ **Claude Desktop** (`claude_desktop_config.json`):
67
+ ```json
68
+ {
69
+ "mcpServers": {
70
+ "p-layer": {
71
+ "command": "python3",
72
+ "args": ["-m", "p_layer.mcp.server"],
73
+ "env": { "KNOWLEDGE_PG_DSN": "" }
74
+ }
75
+ }
76
+ }
77
+ ```
78
+
79
+ ## Architecture
80
+
81
+ ```
82
+ P0-brainstem (rules) ─────────── governs all layer write permissions
83
+ P1-limbic (persona) ──────────── defines agent voice
84
+
85
+ P2-hippocampus (raw data) ──────────────┤
86
+ │ │
87
+ ├──→ sessions/ (append-only logs) │
88
+ ├──→ memories/ (extracted entries) │
89
+ └──→ knowledge/ (ingested artifacts) │
90
+
91
+ P3-sensors ──→ MCP configs ──→ P_LAYER KNOWLEDGEDB ←── P4-cortex skill index
92
+ (Pg + SQLite) │
93
+ │ │
94
+ ┌──────────────────────────┤ │
95
+ ▼ ▼ ▼
96
+ knowledge_recall P5-ego/wiki/compiled/ P6-prefrontal
97
+ (ranked FTS + vector) (auto-generated daily) (incidents + RCA)
98
+
99
+ wiki_lint.py
100
+ (broken link check)
101
+ ```
102
+
103
+ ### Backend selection
104
+
105
+ | Variable | Effect |
106
+ |----------|--------|
107
+ | `KNOWLEDGE_PG_DSN` unset | SQLite mode (`.knowledge/knowledge.db`) |
108
+ | `KNOWLEDGE_PG_DSN=dbname=...` | PostgreSQL primary, SQLite fallback |
109
+ | `KNOWLEDGE_DB_DIR=/path` | Custom SQLite directory |
110
+
111
+ ## Query Routing (priority order)
112
+
113
+ When an agent searches for information:
114
+
115
+ ```
116
+ 1. P5-ego/wiki/compiled/ ← compiled wiki (check FIRST)
117
+ 2. P5-ego/memory/ ← saved preferences
118
+ 3. P2-hippocampus/knowledge/ ← raw ingested knowledge
119
+ 4. P2-hippocampus/memories/ ← raw session memory
120
+ 5. P2-hippocampus/sessions/ ← raw session logs (LAST resort)
121
+ 6. KnowledgeDB (SQLite/Pg) ← cross-cut fallback
122
+ ```
123
+
124
+ Steps 1-2 should cover ~80% of queries. Steps 3+ are gaps → next wiki-compile cycle.
125
+
126
+ ## Using p-layers in your project
127
+
128
+ The `p-layers/` directory contains the canonical governance contracts. Each map to a runtime directory in your project:
129
+
130
+ ```
131
+ your-project/
132
+ ├── p-layers/ ← contract docs (canonical, read-only)
133
+ │ ├── P0-brainstem/README.md
134
+ │ └── ...
135
+ ├── P2-hippocampus/ ← runtime data (your sessions, archives)
136
+ │ └── sessions/
137
+ ├── P5-ego/
138
+ │ └── wiki/compiled/ ← auto-generated by wiki_compile.py
139
+ └── P6-prefrontal/
140
+ └── incidents/ ← your incident reports
141
+ ```
142
+
143
+ **Copy** → `cp -r p-layers/ your-project/` (you own them, customize freely)\
144
+ **Submodule** → `git submodule add <url>` (stay in sync) \
145
+ **Refer** → point your agent's init workflow at `p-layers/P0-brainstem/README.md`
146
+
147
+ ## Scripts
148
+
149
+ | Script | Purpose |
150
+ |--------|---------|
151
+ | `scripts/ontology_setup.py` | Initialize entity type hierarchy + relation constraints |
152
+ | `scripts/seed_knowledge_db.py` | Bootstrap knowledge.db with schema + seeds |
153
+ | `scripts/ingest_fact.py` | Insert a single fact from CLI |
154
+ | `scripts/ingest_instructions.py` | Batch-ingest .md files into KnowledgeDB |
155
+ | `scripts/inference.py` | Transitive closure, backtrace, contradiction detection |
156
+ | `scripts/wiki_compile.py` | KnowledgeDB → Markdown wiki pages + INDEX.json |
157
+ | `scripts/wiki_lint.py` | Broken link detection, INDEX consistency |
158
+
159
+ ## Ontology Layer
160
+
161
+ 24 entity types across 6 root categories:
162
+
163
+ ```
164
+ artifact → doc, code, project
165
+ agent → persona, tool, script, skill
166
+ decision → pattern, preference
167
+ event → incident, session
168
+ knowledge → concept, paper, reference
169
+ meta → category, _task, fact
170
+ ```
171
+
172
+ Relation constraints enforce type safety at insert time:
173
+
174
+ | Relation | Source → Target |
175
+ |----------|----------------|
176
+ | `depends_on` | any → tool/script/skill |
177
+ | `fixed_by` | incident → pattern/decision |
178
+ | `caused` | decision/pattern → incident |
179
+ | `led_to` | decision → decision |
180
+ | `cites` | paper → paper |
181
+ | `contradicts` | decision/pattern → decision/pattern |
182
+
183
+ ## License
184
+
185
+ MIT
@@ -0,0 +1 @@
1
+ """p-layer — Knowledge governance layers for AI agents."""
@@ -0,0 +1,5 @@
1
+ """p-layer core — KnowledgeDB with PostgreSQL (primary) + SQLite (fallback)."""
2
+
3
+ from .db import KnowledgeDB as KnowledgeDB
4
+ from .memory import recall_ranked as recall_ranked
5
+ from .ontology import ONTOLOGY as ONTOLOGY
@@ -0,0 +1,412 @@
1
+ """
2
+ KnowledgeDB — Unified PgVector + SQLite fallback access.
3
+
4
+ Single entry point for all database operations.
5
+ Layer-aware: P0 (rules) through P6 (incidents).
6
+
7
+ Usage:
8
+ db = KnowledgeDB()
9
+ db.insert(layer='P5', type='knowledge', content='...')
10
+ results = db.search('query string', layers=['P5', 'P6'])
11
+ """
12
+
13
+ import hashlib
14
+ import json
15
+ import logging
16
+ import os
17
+ import sqlite3
18
+ import time
19
+ from contextlib import contextmanager
20
+ from datetime import datetime, timezone
21
+ from pathlib import Path
22
+ from typing import Any, Optional
23
+
24
+ try:
25
+ import numpy as np
26
+ import psycopg2
27
+ from pgvector.psycopg2 import register_vector
28
+ HAS_PG = True
29
+ except ImportError:
30
+ np = None
31
+ psycopg2 = None
32
+ register_vector = None
33
+ HAS_PG = False
34
+
35
+ logger = logging.getLogger(__name__)
36
+
37
+ DEFAULT_DB_DIR = os.environ.get("KNOWLEDGE_DB_DIR", str(Path.cwd() / ".knowledge"))
38
+ PG_DSN = os.environ.get("KNOWLEDGE_PG_DSN", "")
39
+
40
+ LAYER_AUTHORITY = {
41
+ "P0": 100, "P1": 80, "P2": 60, "P3": 50, "P4": 40, "P5": 30, "P6": 20,
42
+ }
43
+
44
+ LAYER_WRITERS = {
45
+ "P0": frozenset({"system"}),
46
+ "P1": frozenset({"system"}),
47
+ "P2": frozenset({"system", "gateway", "cron"}),
48
+ "P3": frozenset({"system", "gateway", "cron"}),
49
+ "P4": frozenset({"system", "cron", "agent", "manual"}),
50
+ "P5": frozenset({"system", "cron", "agent", "manual", "tool"}),
51
+ "P6": frozenset({"system", "cron", "agent", "manual", "tool"}),
52
+ }
53
+
54
+
55
+ class DatabaseError(Exception):
56
+ pass
57
+
58
+
59
+ class WriteDenied(DatabaseError):
60
+ pass
61
+
62
+
63
+ class ConnectionError(DatabaseError):
64
+ pass
65
+
66
+
67
+ class KnowledgeDB:
68
+ """Unified database access with Pg primary + SQLite fallback."""
69
+
70
+ def __init__(self, mode: str = "auto", dsn: str = None,
71
+ db_dir: str = None):
72
+ self.mode = mode
73
+ self.dsn = dsn or PG_DSN
74
+ self.db_dir = Path(db_dir or DEFAULT_DB_DIR)
75
+ self._pg_conn = None
76
+ self._sqlite_conn = None
77
+ if HAS_PG:
78
+ self._connect_pg()
79
+
80
+ def _connect_pg(self):
81
+ if self.mode == "sqlite" or not self.dsn:
82
+ return
83
+ try:
84
+ conn = psycopg2.connect(self.dsn)
85
+ conn.set_session(autocommit=True)
86
+ register_vector(conn)
87
+ self._pg_conn = conn
88
+ except Exception as e:
89
+ if self.mode == "pg":
90
+ raise ConnectionError(f"Pg connection failed: {e}")
91
+ logger.warning("Pg unavailable, using SQLite fallback: %s", e)
92
+ self.mode = "sqlite"
93
+
94
+ def _connect_sqlite(self):
95
+ if self._sqlite_conn is None:
96
+ path = self.db_dir / "knowledge.db"
97
+ path.parent.mkdir(parents=True, exist_ok=True)
98
+ self._sqlite_conn = sqlite3.connect(str(path))
99
+ self._sqlite_conn.row_factory = sqlite3.Row
100
+ return self._sqlite_conn
101
+
102
+ def set_mode(self, mode: str):
103
+ if mode not in ("auto", "pg", "sqlite"):
104
+ raise ValueError(f"Invalid mode: {mode}")
105
+ self.mode = mode
106
+ if mode == "pg" and self._pg_conn is None:
107
+ self._connect_pg()
108
+ if mode == "sqlite" and self._sqlite_conn is None:
109
+ self._connect_sqlite()
110
+
111
+ @property
112
+ def available(self) -> bool:
113
+ if self.mode == "sqlite":
114
+ return True
115
+ if self._pg_conn is None:
116
+ return False
117
+ try:
118
+ cur = self._pg_conn.cursor()
119
+ cur.execute("SELECT 1")
120
+ cur.close()
121
+ return True
122
+ except Exception:
123
+ return False
124
+
125
+ def _check_layer_write(self, layer: str, who: str):
126
+ allowed = LAYER_WRITERS.get(layer, frozenset())
127
+ who_prefix = who.split(":")[0] if ":" in who else who
128
+ if who_prefix not in allowed and who not in allowed:
129
+ raise WriteDenied(
130
+ f"Layer {layer} does not allow writes from '{who}'. "
131
+ f"Allowed: {sorted(allowed)}"
132
+ )
133
+
134
+ def insert(self, layer: str, type: str, content: str,
135
+ who: str = "system", source: str = "",
136
+ source_path: str = "", embedding: list = None,
137
+ created_at: str = None) -> dict:
138
+ if layer not in LAYER_AUTHORITY:
139
+ raise ValueError(f"Invalid layer: {layer}. Must be P0-P6")
140
+ self._check_layer_write(layer, who)
141
+
142
+ data = {
143
+ "who": who, "type": type, "layer": layer,
144
+ "authority": LAYER_AUTHORITY[layer],
145
+ "content": content[:100000],
146
+ "source": source, "source_path": source_path,
147
+ }
148
+ if created_at:
149
+ data["created_at"] = created_at
150
+
151
+ if self.mode != "sqlite" and self._pg_conn:
152
+ try:
153
+ return self._pg_insert(data, embedding)
154
+ except Exception as e:
155
+ logger.error("Pg insert failed, trying fallback: %s", e)
156
+ if self.mode == "pg":
157
+ raise
158
+
159
+ return self._sqlite_insert(data)
160
+
161
+ def _pg_insert(self, data: dict, embedding: list = None) -> dict:
162
+ cur = self._pg_conn.cursor()
163
+ cols = list(data.keys())
164
+ placeholders = ["%s"] * len(cols)
165
+ values = [data[k] for k in cols]
166
+
167
+ if embedding is not None:
168
+ cols.append("embedding")
169
+ placeholders.append("%s")
170
+ values.append(np.array(embedding, dtype=np.float32))
171
+
172
+ cur.execute(
173
+ f"INSERT INTO entries ({', '.join(cols)}) "
174
+ f"VALUES ({', '.join(placeholders)}) "
175
+ f"RETURNING id, who, type, layer, authority, created_at",
176
+ values
177
+ )
178
+ row = cur.fetchone()
179
+ if not row:
180
+ raise DatabaseError("INSERT RETURNING returned no rows")
181
+ return {
182
+ "id": row[0], "who": row[1], "type": row[2],
183
+ "layer": row[3], "authority": row[4],
184
+ "created_at": row[5].isoformat() if row[5] else None,
185
+ }
186
+
187
+ def _sqlite_insert(self, data: dict) -> dict:
188
+ conn = self._connect_sqlite()
189
+ conn.execute(
190
+ "INSERT INTO memory_fts (fact, type) VALUES (?, ?)",
191
+ (data["content"], data["type"])
192
+ )
193
+ conn.commit()
194
+ return {"id": conn.execute("SELECT last_insert_rowid()").fetchone()[0], **data}
195
+
196
+ def search(self, query: str, layers: list = None,
197
+ limit: int = 20, who: str = None) -> list:
198
+ if self.mode != "sqlite" and self._pg_conn:
199
+ try:
200
+ return self._pg_search(query, layers, limit, who)
201
+ except Exception as e:
202
+ logger.error("Pg search failed, fallback: %s", e)
203
+ if self.mode == "pg":
204
+ raise
205
+ return self._sqlite_search(query, limit)
206
+
207
+ def _pg_search(self, query: str, layers: list = None,
208
+ limit: int = 20, who: str = None) -> list:
209
+ cur = self._pg_conn.cursor()
210
+ conditions = []
211
+ params = []
212
+
213
+ if layers:
214
+ placeholders = ", ".join(f"'{l}'" for l in layers)
215
+ conditions.append(f"layer IN ({placeholders})")
216
+ if who:
217
+ conditions.append("who = %s")
218
+ params.append(who)
219
+ if query:
220
+ conditions.append("tsv @@ plainto_tsquery('english', %s)")
221
+ params.append(query)
222
+
223
+ where = " AND ".join(conditions) if conditions else "TRUE"
224
+ sql = f"""
225
+ SELECT id, who, type, layer, authority,
226
+ substring(content, 1, 200) as content_preview,
227
+ created_at,
228
+ ts_rank(tsv, plainto_tsquery('english', %s)) as rank
229
+ FROM entries
230
+ WHERE {where}
231
+ ORDER BY authority DESC, rank DESC, created_at DESC
232
+ LIMIT {limit}
233
+ """
234
+ all_params = [query] + params if query else params
235
+ cur.execute(sql, all_params if params else [query] if query else [])
236
+
237
+ results = []
238
+ for r in cur.fetchall():
239
+ results.append({
240
+ "id": r[0], "who": r[1], "type": r[2],
241
+ "layer": r[3], "authority": r[4],
242
+ "content": r[5], "created_at": r[6].isoformat() if r[6] else None,
243
+ })
244
+ return results
245
+
246
+ def _sqlite_search(self, query: str, limit: int = 20) -> list:
247
+ conn = self._connect_sqlite()
248
+ try:
249
+ rows = conn.execute(
250
+ "SELECT rowid, fact, type FROM memory_fts "
251
+ "WHERE memory_fts MATCH ? LIMIT ?",
252
+ (query, limit)
253
+ ).fetchall()
254
+ return [{"id": r[0], "content": r[1], "type": r[2], "layer": "P5"} for r in rows]
255
+ except sqlite3.OperationalError:
256
+ return []
257
+
258
+ def vector_search(self, embedding: list, layers: list = None,
259
+ limit: int = 10) -> list:
260
+ if self.mode == "sqlite" or self._pg_conn is None:
261
+ return []
262
+ cur = self._pg_conn.cursor()
263
+ vec = np.array(embedding, dtype=np.float32)
264
+ layer_filter = ""
265
+ if layers:
266
+ placeholders = ", ".join(f"'{l}'" for l in layers)
267
+ layer_filter = f"AND layer IN ({placeholders})"
268
+
269
+ cur.execute(
270
+ f"""SELECT id, who, type, layer, authority,
271
+ substring(content, 1, 200) as preview,
272
+ created_at,
273
+ 1 - (embedding <=> %s) as similarity
274
+ FROM entries WHERE embedding IS NOT NULL {layer_filter}
275
+ ORDER BY embedding <=> %s LIMIT {limit}""",
276
+ (vec, vec)
277
+ )
278
+ results = []
279
+ for r in cur.fetchall():
280
+ results.append({
281
+ "id": r[0], "who": r[1], "type": r[2],
282
+ "layer": r[3], "authority": r[4],
283
+ "content": r[5], "created_at": r[6].isoformat() if r[6] else None,
284
+ "similarity": round(r[7], 4),
285
+ })
286
+ return results
287
+
288
+ def hybrid_search(self, query: str, embedding: list,
289
+ layers: list = None, limit: int = 10) -> list:
290
+ if self.mode == "sqlite" or self._pg_conn is None:
291
+ return self._sqlite_search(query, limit)
292
+ cur = self._pg_conn.cursor()
293
+ vec = np.array(embedding, dtype=np.float32)
294
+ layer_filter = ""
295
+ if layers:
296
+ placeholders = ", ".join(f"'{l}'" for l in layers)
297
+ layer_filter = f"AND layer IN ({placeholders})"
298
+
299
+ cur.execute(
300
+ f"""SELECT id, who, type, layer, authority,
301
+ substring(content, 1, 200) as preview, created_at,
302
+ ts_rank(tsv, plainto_tsquery('english', %s)) as fts_score,
303
+ 1 - (embedding <=> %s) as vec_score,
304
+ (ts_rank(tsv, plainto_tsquery('english', %s)) * 0.3 +
305
+ (1 - (embedding <=> %s)) * 0.7) as combined
306
+ FROM entries
307
+ WHERE (tsv @@ plainto_tsquery('english', %s) OR embedding IS NOT NULL)
308
+ {layer_filter}
309
+ ORDER BY combined DESC LIMIT {limit}""",
310
+ (query, vec, query, vec, query)
311
+ )
312
+ results = []
313
+ for r in cur.fetchall():
314
+ results.append({
315
+ "id": r[0], "who": r[1], "type": r[2],
316
+ "layer": r[3], "authority": r[4],
317
+ "content": r[5], "created_at": r[6].isoformat() if r[6] else None,
318
+ "combined_score": round(r[9], 4),
319
+ })
320
+ return results
321
+
322
+ def get_layer(self, layer: str, limit: int = 50) -> list:
323
+ return self.search("", layers=[layer], limit=limit)
324
+
325
+ def get_layer_count(self, layer: str = None) -> dict:
326
+ if self.mode == "sqlite" or self._pg_conn is None:
327
+ return {"sqlite": True}
328
+ cur = self._pg_conn.cursor()
329
+ if layer:
330
+ cur.execute("SELECT layer, COUNT(*) FROM entries WHERE layer = %s GROUP BY layer", (layer,))
331
+ else:
332
+ cur.execute("SELECT layer, COUNT(*) FROM entries GROUP BY layer ORDER BY layer")
333
+ return {r[0]: r[1] for r in cur.fetchall()}
334
+
335
+ def graph_query(self, entity_label: str, depth: int = 2) -> dict:
336
+ if self.mode == "sqlite" or self._pg_conn is None:
337
+ return {"nodes": [], "edges": []}
338
+ cur = self._pg_conn.cursor()
339
+ cur.execute("SELECT id, label, entity_type FROM entities WHERE label = %s", (entity_label,))
340
+ start = cur.fetchone()
341
+ if not start:
342
+ return {"nodes": [], "edges": []}
343
+
344
+ nodes = {start[0]: {"id": start[0], "label": start[1], "type": start[2]}}
345
+ edges = []
346
+ visited = {start[0]}
347
+ current = {start[0]}
348
+
349
+ for _ in range(depth):
350
+ if not current:
351
+ break
352
+ placeholders = ", ".join(str(i) for i in current)
353
+ cur.execute(
354
+ f"""SELECT r.id, r.source_id, r.target_id, r.rel_type,
355
+ s.label as s_label, s.entity_type as s_type,
356
+ t.label as t_label, t.entity_type as t_type
357
+ FROM relations r
358
+ JOIN entities s ON r.source_id = s.id
359
+ JOIN entities t ON r.target_id = t.id
360
+ WHERE r.source_id IN ({placeholders})
361
+ OR r.target_id IN ({placeholders})"""
362
+ )
363
+ new_ids = set()
364
+ for r in cur.fetchall():
365
+ edges.append({"id": r[0], "source": r[1], "target": r[2], "type": r[3]})
366
+ if r[1] not in visited:
367
+ nodes[r[1]] = {"id": r[1], "label": r[4], "type": r[5]}
368
+ new_ids.add(r[1])
369
+ visited.add(r[1])
370
+ if r[2] not in visited:
371
+ nodes[r[2]] = {"id": r[2], "label": r[6], "type": r[7]}
372
+ new_ids.add(r[2])
373
+ visited.add(r[2])
374
+ current = new_ids
375
+ return {"nodes": list(nodes.values()), "edges": edges}
376
+
377
+ def health_check(self) -> dict:
378
+ result = {
379
+ "mode": self.mode,
380
+ "pg_available": self._pg_conn is not None and self.available,
381
+ "timestamp": datetime.now(timezone.utc).isoformat(),
382
+ }
383
+ try:
384
+ result["counts"] = self.get_layer_count()
385
+ except Exception as e:
386
+ result["counts_error"] = str(e)
387
+ try:
388
+ test = self.insert(
389
+ layer="P6", type="health_check",
390
+ content=f"[HEALTH_CHECK] {time.time()}",
391
+ who="system:health"
392
+ )
393
+ result["write_test"] = "PASS"
394
+ if self._pg_conn:
395
+ cur = self._pg_conn.cursor()
396
+ cur.execute("DELETE FROM entries WHERE id = %s", (test["id"],))
397
+ cur.close()
398
+ except Exception as e:
399
+ result["write_test"] = f"FAIL: {e}"
400
+ return result
401
+
402
+ def close(self):
403
+ if self._pg_conn:
404
+ self._pg_conn.close()
405
+ if self._sqlite_conn:
406
+ self._sqlite_conn.close()
407
+
408
+ def __enter__(self):
409
+ return self
410
+
411
+ def __exit__(self, *args):
412
+ self.close()
@@ -0,0 +1,114 @@
1
+ """Memory business logic — ranked recall, serendipity, TTL management."""
2
+
3
+ import logging
4
+ import random
5
+ from datetime import datetime, timezone
6
+
7
+ _SERENDIPITY_CHANCE = 0.05
8
+ _DEFAULT_TTL = {"fact": 90, "decision": 180, "pattern": 30, "incident": 365}
9
+
10
+ logger = logging.getLogger(__name__)
11
+
12
+
13
+ def recall_ranked(db, query: str, limit: int = 10, layers: list = None,
14
+ serendipity: bool = True) -> list:
15
+ """Recall with confidence-weighted ranking + serendipity.
16
+
17
+ Falls back to standard search when Pg is unavailable (SQLite mode).
18
+ """
19
+ has_pg = getattr(db, '_pg_conn', None) is not None
20
+
21
+ if not has_pg:
22
+ results = db.search(query, layers=layers or ["P5", "P6"], limit=limit)
23
+ return list(results) if results else []
24
+
25
+ cur = db._pg_conn.cursor()
26
+
27
+ where_parts = []
28
+ params = []
29
+ if layers:
30
+ placeholders = ", ".join("%s" for _ in layers)
31
+ where_parts.append(f"layer IN ({placeholders})")
32
+ params.extend(layers)
33
+
34
+ if query.strip():
35
+ where_parts.append("tsv @@ plainto_tsquery('english', %s)")
36
+ params.append(query)
37
+
38
+ where_sql = " AND ".join(where_parts) if where_parts else "TRUE"
39
+ ts_query = query.strip() or "."
40
+ rank_expr = "ts_rank(tsv, plainto_tsquery('english', %s))"
41
+
42
+ sql = f"""
43
+ SELECT id, content, type, layer, confidence, ttl_days,
44
+ access_count, last_accessed_at, version_id, created_at
45
+ FROM entries
46
+ WHERE {where_sql}
47
+ AND (superseded_by IS NULL)
48
+ ORDER BY
49
+ ({rank_expr} * (0.5 + confidence * 0.5) *
50
+ (1.0 + GREATEST(0.0, 1.0 - EXTRACT(EPOCH FROM (NOW() - created_at)) / 86400.0 /
51
+ NULLIF(NULLIF(ttl_days, 0), 0)) * 0.3)) DESC
52
+ LIMIT %s
53
+ """
54
+ params.append(ts_query)
55
+ params.append(limit)
56
+
57
+ try:
58
+ cur.execute(sql, params)
59
+ rows = cur.fetchall()
60
+ except Exception as e:
61
+ logger.warning("Recall ranked query failed, falling back: %s", e)
62
+ rows = db.search(query, layers=layers or ["P5", "P6"], limit=limit)
63
+ rows = list(rows) if rows else []
64
+
65
+ seen_ids = []
66
+ results = []
67
+ for row in rows:
68
+ if isinstance(row, dict):
69
+ results.append(row)
70
+ if "id" in row:
71
+ seen_ids.append(row["id"])
72
+ continue
73
+ entry = {
74
+ "id": row[0], "content": row[1], "type": row[2],
75
+ "layer": row[3], "confidence": row[4], "ttl_days": row[5],
76
+ "access_count": row[6], "version_id": row[8],
77
+ }
78
+ seen_ids.append(row[0])
79
+ results.append(entry)
80
+
81
+ if seen_ids and has_pg:
82
+ try:
83
+ cur.execute(
84
+ "UPDATE entries SET access_count = access_count + 1, "
85
+ "last_accessed_at = NOW() WHERE id = ANY(%s)",
86
+ (seen_ids,)
87
+ )
88
+ db._pg_conn.commit()
89
+ except Exception:
90
+ db._pg_conn.rollback()
91
+
92
+ if serendipity and random.random() < _SERENDIPITY_CHANCE and seen_ids:
93
+ try:
94
+ cur.execute(
95
+ "SELECT id, content, type, layer, confidence, ttl_days, "
96
+ "access_count, version_id FROM entries "
97
+ "WHERE id != ALL(%s) AND (superseded_by IS NULL) "
98
+ "AND (access_count = 0 OR last_accessed_at IS NULL "
99
+ "OR last_accessed_at < NOW() - INTERVAL '60 days') "
100
+ "ORDER BY RANDOM() LIMIT 1",
101
+ (seen_ids,)
102
+ )
103
+ wild = cur.fetchone()
104
+ if wild:
105
+ results.append({
106
+ "id": wild[0], "content": wild[1], "type": wild[2],
107
+ "layer": wild[3], "confidence": wild[4], "ttl_days": wild[5],
108
+ "access_count": wild[6], "version_id": wild[7],
109
+ "_serendipity": True,
110
+ })
111
+ except Exception:
112
+ pass
113
+
114
+ return results
@@ -0,0 +1,49 @@
1
+ """Ontology type hierarchy and relation constraints.
2
+
3
+ Reusable across both PostgreSQL and SQLite backends.
4
+ """
5
+
6
+ TYPE_HIERARCHY = {
7
+ "artifact": None,
8
+ "doc": "artifact",
9
+ "code": "artifact",
10
+ "project": "artifact",
11
+ "agent": None,
12
+ "persona": "agent",
13
+ "tool": "agent",
14
+ "script": "agent",
15
+ "skill": "agent",
16
+ "decision": None,
17
+ "pattern": "decision",
18
+ "preference": "decision",
19
+ "event": None,
20
+ "incident": "event",
21
+ "session": "event",
22
+ "knowledge": None,
23
+ "concept": "knowledge",
24
+ "paper": "knowledge",
25
+ "reference": "knowledge",
26
+ "meta": None,
27
+ "category": "meta",
28
+ "_task": "meta",
29
+ "fact": "meta",
30
+ }
31
+
32
+ RELATION_CONSTRAINTS = {
33
+ "depends_on": (None, ("tool", "script", "skill")),
34
+ "fixed_by": (("incident",), ("pattern", "decision")),
35
+ "caused": (("decision", "pattern"), ("incident",)),
36
+ "led_to": (("decision", "pattern", "preference"), ("decision", "pattern", "preference")),
37
+ "implements": (("script",), ("pattern", "decision")),
38
+ "contradicts": (("decision", "pattern", "preference"), ("decision", "pattern", "preference")),
39
+ "cites": (("paper",), ("paper",)),
40
+ "references": None,
41
+ "relates_to": None,
42
+ "subtype_of": None,
43
+ "belongs_to": None,
44
+ }
45
+
46
+ ONTOLOGY = {
47
+ "type_hierarchy": TYPE_HIERARCHY,
48
+ "relation_constraints": RELATION_CONSTRAINTS,
49
+ }
File without changes
@@ -0,0 +1,335 @@
1
+ #!/usr/bin/env python3
2
+ """MCP Agent Memory Server — PgVector-backed with SQLite fallback.
3
+
4
+ Tools:
5
+ knowledge_remember — store a fact
6
+ knowledge_recall — search with confidence ranking + serendipity
7
+ knowledge_forget — supersede (soft-delete)
8
+ knowledge_update — update by ID (bumps version)
9
+ knowledge_memory-stats — stats
10
+ knowledge_snapshot-create — snapshot current entries
11
+ knowledge_snapshot-rollback — rollback to a snapshot
12
+ """
13
+
14
+ import json
15
+ import sys
16
+ from datetime import datetime, timezone
17
+
18
+ from mcp.server import Server
19
+ from mcp.server.stdio import stdio_server
20
+ from mcp.types import Tool, TextContent
21
+
22
+ from p_layer.core.db import KnowledgeDB
23
+ from p_layer.core.memory import recall_ranked, _DEFAULT_TTL
24
+
25
+ server = Server("knowledge-system")
26
+
27
+
28
+ def get_db():
29
+ return KnowledgeDB()
30
+
31
+
32
+ def _has_pg(db) -> bool:
33
+ return db._pg_conn is not None
34
+
35
+
36
+ @server.list_tools()
37
+ async def list_tools() -> list[Tool]:
38
+ return [
39
+ Tool(
40
+ name="knowledge_remember",
41
+ description="Store a fact, decision, pattern, or incident into persistent memory",
42
+ inputSchema={
43
+ "type": "object",
44
+ "properties": {
45
+ "fact": {"type": "string", "description": "The content to remember"},
46
+ "type": {
47
+ "type": "string",
48
+ "enum": ["fact", "decision", "pattern", "incident"],
49
+ "default": "fact",
50
+ },
51
+ "confidence": {
52
+ "type": "number", "default": 1.0,
53
+ "description": "Confidence score 0.0-1.0",
54
+ },
55
+ "ttl_days": {
56
+ "type": "integer",
57
+ "description": "Days before deprioritization (default: fact=90, decision=180, pattern=30, incident=365)",
58
+ },
59
+ "version_id": {
60
+ "type": "string", "default": "latest",
61
+ "description": "Version label for grouping",
62
+ },
63
+ },
64
+ "required": ["fact"],
65
+ },
66
+ ),
67
+ Tool(
68
+ name="knowledge_recall",
69
+ description="Search persistent memory — ranked by relevance, confidence, and freshness",
70
+ inputSchema={
71
+ "type": "object",
72
+ "properties": {
73
+ "query": {"type": "string", "description": "Search query"},
74
+ "limit": {"type": "integer", "default": 10},
75
+ "serendipity": {
76
+ "type": "boolean", "default": True,
77
+ "description": "Include a wildcard low-ranked entry",
78
+ },
79
+ },
80
+ "required": ["query"],
81
+ },
82
+ ),
83
+ Tool(
84
+ name="knowledge_forget",
85
+ description="Supersede a memory entry (soft-delete)",
86
+ inputSchema={
87
+ "type": "object",
88
+ "properties": {
89
+ "id": {"type": "integer", "description": "Entry ID to forget"},
90
+ "reason": {"type": "string", "description": "Optional reason"},
91
+ },
92
+ "required": ["id"],
93
+ },
94
+ ),
95
+ Tool(
96
+ name="knowledge_memory-stats",
97
+ description="Show memory database statistics",
98
+ inputSchema={"type": "object", "properties": {}},
99
+ ),
100
+ Tool(
101
+ name="knowledge_update",
102
+ description="Update a memory entry — bumps version, old entry is superseded",
103
+ inputSchema={
104
+ "type": "object",
105
+ "properties": {
106
+ "id": {"type": "integer", "description": "Entry ID"},
107
+ "fact": {"type": "string", "description": "New content"},
108
+ "type": {
109
+ "type": "string",
110
+ "enum": ["fact", "decision", "pattern", "incident"],
111
+ },
112
+ "confidence": {"type": "number", "description": "Updated confidence 0.0-1.0"},
113
+ },
114
+ "required": ["id"],
115
+ },
116
+ ),
117
+ Tool(
118
+ name="knowledge_snapshot-create",
119
+ description="Create a snapshot of current entries under a version label",
120
+ inputSchema={
121
+ "type": "object",
122
+ "properties": {
123
+ "version_id": {"type": "string", "description": "e.g. 'v1', 'v2.1'"},
124
+ "label": {"type": "string", "description": "Human-readable description"},
125
+ },
126
+ "required": ["version_id"],
127
+ },
128
+ ),
129
+ Tool(
130
+ name="knowledge_snapshot-rollback",
131
+ description="Rollback to a snapshot — marks newer entries as superseded",
132
+ inputSchema={
133
+ "type": "object",
134
+ "properties": {
135
+ "version_id": {"type": "string", "description": "Snapshot version to rollback to"},
136
+ },
137
+ "required": ["version_id"],
138
+ },
139
+ ),
140
+ ]
141
+
142
+
143
+ @server.call_tool()
144
+ async def call_tool(name: str, arguments: dict) -> list[TextContent]:
145
+ db = get_db()
146
+
147
+ if name == "knowledge_remember":
148
+ fact = arguments["fact"]
149
+ typ = arguments.get("type", "fact")
150
+ confidence = max(0.0, min(1.0, float(arguments.get("confidence", 1.0))))
151
+ ttl = arguments.get("ttl_days", _DEFAULT_TTL.get(typ, 90))
152
+ version_id = arguments.get("version_id", "latest")
153
+
154
+ result = db.insert(
155
+ layer="P6", type=typ, content=fact,
156
+ who="tool:knowledge-system",
157
+ source="knowledge_remember",
158
+ )
159
+
160
+ if _has_pg(db) and isinstance(result, dict) and result.get("id"):
161
+ try:
162
+ cur = db._pg_conn.cursor()
163
+ cur.execute(
164
+ "UPDATE entries SET confidence = %s, ttl_days = %s, version_id = %s WHERE id = %s",
165
+ (confidence, ttl, version_id, result["id"]),
166
+ )
167
+ db._pg_conn.commit()
168
+ except Exception:
169
+ db._pg_conn.rollback()
170
+
171
+ return [TextContent(type="text", text=str(result))]
172
+
173
+ elif name == "knowledge_recall":
174
+ query = arguments["query"]
175
+ limit = int(arguments.get("limit", 10))
176
+ serendipity = arguments.get("serendipity", True)
177
+
178
+ if not query.strip():
179
+ results = db.search("", layers=["P5", "P6"], limit=limit)
180
+ else:
181
+ results = recall_ranked(db, query, limit=limit, layers=["P5", "P6"], serendipity=serendipity)
182
+ return [TextContent(type="text", text=str(results))]
183
+
184
+ elif name == "knowledge_forget":
185
+ mem_id = int(arguments["id"])
186
+ reason = arguments.get("reason", "")
187
+
188
+ if _has_pg(db):
189
+ cur = db._pg_conn.cursor()
190
+ cur.execute(
191
+ "UPDATE entries SET superseded_by = id, confidence = GREATEST(confidence, 0.0), "
192
+ "updated_at = NOW() WHERE id = %s",
193
+ (mem_id,),
194
+ )
195
+ db._pg_conn.commit()
196
+ msg = {"superseded": cur.rowcount > 0, "id": mem_id}
197
+ if reason:
198
+ msg["reason"] = reason
199
+ return [TextContent(type="text", text=str(msg))]
200
+
201
+ cur = db._connect_sqlite().execute(
202
+ "DELETE FROM memory_fts WHERE rowid = ?", (mem_id,)
203
+ )
204
+ return [TextContent(type="text", text=str({"deleted": cur.rowcount > 0}))]
205
+
206
+ elif name == "knowledge_memory-stats":
207
+ counts = db.get_layer_count()
208
+ if _has_pg(db):
209
+ cur = db._pg_conn.cursor()
210
+ cur.execute("SELECT COUNT(*) FROM entries WHERE superseded_by IS NULL")
211
+ active = cur.fetchone()[0]
212
+ cur.execute("SELECT COUNT(*) FROM entries WHERE superseded_by IS NOT NULL")
213
+ superseded = cur.fetchone()[0]
214
+ else:
215
+ active = sum(counts.values())
216
+ superseded = 0
217
+ return [TextContent(type="text", text=str({
218
+ "total": sum(counts.values()) if isinstance(counts, dict) else 0,
219
+ "active": active,
220
+ "superseded": superseded,
221
+ "byLayer": counts,
222
+ }))]
223
+
224
+ elif name == "knowledge_update":
225
+ mem_id = int(arguments["id"])
226
+ fact = arguments.get("fact")
227
+ typ = arguments.get("type")
228
+ confidence = arguments.get("confidence")
229
+
230
+ if _has_pg(db):
231
+ cur = db._pg_conn.cursor()
232
+ if fact:
233
+ cur.execute(
234
+ "UPDATE entries SET superseded_by = id, updated_at = NOW() WHERE id = %s",
235
+ (mem_id,),
236
+ )
237
+ new_result = db.insert(
238
+ layer="P6", type=typ or "fact", content=fact,
239
+ who="tool:knowledge-system", source="knowledge_update",
240
+ )
241
+ if isinstance(new_result, dict) and new_result.get("id"):
242
+ cur.execute(
243
+ "UPDATE entries SET version_id = 'v2' WHERE id = %s",
244
+ (new_result["id"],),
245
+ )
246
+ db._pg_conn.commit()
247
+ return [TextContent(type="text", text=str({
248
+ "updated": True, "superseded_id": mem_id,
249
+ "new_id": new_result.get("id") if isinstance(new_result, dict) else None,
250
+ }))]
251
+
252
+ updates = []
253
+ params = []
254
+ if confidence is not None:
255
+ updates.append("confidence = %s")
256
+ params.append(max(0.0, min(1.0, float(confidence))))
257
+ if typ:
258
+ updates.append("type = %s")
259
+ params.append(typ)
260
+ if updates:
261
+ params.append(mem_id)
262
+ cur.execute(
263
+ f"UPDATE entries SET {', '.join(updates)}, updated_at = NOW() WHERE id = %s",
264
+ params,
265
+ )
266
+ db._pg_conn.commit()
267
+ return [TextContent(type="text", text=str({"updated": cur.rowcount > 0}))]
268
+
269
+ return [TextContent(type="text", text=str({"updated": False}))]
270
+
271
+ elif name == "knowledge_snapshot-create":
272
+ version_id = arguments["version_id"]
273
+ label = arguments.get("label", "")
274
+
275
+ if not _has_pg(db):
276
+ return [TextContent(type="text", text=str({"error": "PgVector required for snapshots"}))]
277
+
278
+ cur = db._pg_conn.cursor()
279
+ cur.execute("SELECT ARRAY_AGG(id) FROM entries WHERE superseded_by IS NULL")
280
+ entry_ids = cur.fetchone()[0] or []
281
+
282
+ cur.execute(
283
+ "INSERT INTO memory_snapshots (version_id, label, entry_ids) "
284
+ "VALUES (%s, %s, %s) "
285
+ "ON CONFLICT (version_id) DO UPDATE SET label = EXCLUDED.label, "
286
+ "entry_ids = EXCLUDED.entry_ids, created_at = NOW()",
287
+ (version_id, label, entry_ids),
288
+ )
289
+ db._pg_conn.commit()
290
+ return [TextContent(type="text", text=str({
291
+ "version_id": version_id, "entries_snapshot": len(entry_ids),
292
+ "created_at": datetime.now(timezone.utc).isoformat(),
293
+ }))]
294
+
295
+ elif name == "knowledge_snapshot-rollback":
296
+ version_id = arguments["version_id"]
297
+
298
+ if not _has_pg(db):
299
+ return [TextContent(type="text", text=str({"error": "PgVector required for snapshots"}))]
300
+
301
+ cur = db._pg_conn.cursor()
302
+ cur.execute(
303
+ "SELECT entry_ids, created_at FROM memory_snapshots WHERE version_id = %s",
304
+ (version_id,),
305
+ )
306
+ row = cur.fetchone()
307
+ if not row:
308
+ return [TextContent(type="text", text=str({"error": f"Snapshot '{version_id}' not found"}))]
309
+
310
+ snapshot_ids, snapshot_at = row
311
+ cur.execute(
312
+ "UPDATE entries SET superseded_by = id, updated_at = NOW() "
313
+ "WHERE id != ALL(%s) AND created_at > %s AND superseded_by IS NULL",
314
+ (snapshot_ids, snapshot_at),
315
+ )
316
+ affected = cur.rowcount
317
+ db._pg_conn.commit()
318
+
319
+ return [TextContent(type="text", text=str({
320
+ "version_id": version_id, "entries_rolled_forward": affected,
321
+ "snapshot_entries": len(snapshot_ids),
322
+ "note": "Entries superseded, not deleted. Recall ranking deprioritizes them.",
323
+ }))]
324
+
325
+ raise ValueError(f"Unknown tool: {name}")
326
+
327
+
328
+ async def main():
329
+ async with stdio_server() as (read_stream, write_stream):
330
+ await server.run(read_stream, write_stream, server.create_initialization_options())
331
+
332
+
333
+ if __name__ == "__main__":
334
+ import asyncio
335
+ asyncio.run(main())
@@ -0,0 +1,13 @@
1
+ Metadata-Version: 2.4
2
+ Name: p-layers
3
+ Version: 0.1.0
4
+ Summary: 7 governance layers for AI agent memory — P0-P6 with KnowledgeDB, MCP server, and wiki-compile
5
+ Project-URL: Home, https://github.com/humanerd-drew/p-layer
6
+ Requires-Python: >=3.10
7
+ License-File: LICENSE
8
+ Requires-Dist: mcp>=1.0
9
+ Provides-Extra: pg
10
+ Requires-Dist: psycopg2-binary>=2.9; extra == "pg"
11
+ Requires-Dist: pgvector>=0.3; extra == "pg"
12
+ Requires-Dist: numpy>=1.24; extra == "pg"
13
+ Dynamic: license-file
@@ -0,0 +1,16 @@
1
+ LICENSE
2
+ README.md
3
+ pyproject.toml
4
+ p_layer/__init__.py
5
+ p_layer/core/__init__.py
6
+ p_layer/core/db.py
7
+ p_layer/core/memory.py
8
+ p_layer/core/ontology.py
9
+ p_layer/mcp/__init__.py
10
+ p_layer/mcp/server.py
11
+ p_layers.egg-info/PKG-INFO
12
+ p_layers.egg-info/SOURCES.txt
13
+ p_layers.egg-info/dependency_links.txt
14
+ p_layers.egg-info/entry_points.txt
15
+ p_layers.egg-info/requires.txt
16
+ p_layers.egg-info/top_level.txt
@@ -0,0 +1,2 @@
1
+ [console_scripts]
2
+ p-layer-mcp = p_layer.mcp.server:main
@@ -0,0 +1,6 @@
1
+ mcp>=1.0
2
+
3
+ [pg]
4
+ psycopg2-binary>=2.9
5
+ pgvector>=0.3
6
+ numpy>=1.24
@@ -0,0 +1 @@
1
+ p_layer
@@ -0,0 +1,27 @@
1
+ [project]
2
+ name = "p-layers"
3
+ version = "0.1.0"
4
+ description = "7 governance layers for AI agent memory — P0-P6 with KnowledgeDB, MCP server, and wiki-compile"
5
+ requires-python = ">=3.10"
6
+ dependencies = [
7
+ "mcp>=1.0",
8
+ ]
9
+ [project.optional-dependencies]
10
+ pg = [
11
+ "psycopg2-binary>=2.9",
12
+ "pgvector>=0.3",
13
+ "numpy>=1.24",
14
+ ]
15
+
16
+ [project.urls]
17
+ Home = "https://github.com/humanerd-drew/p-layer"
18
+
19
+ [project.scripts]
20
+ p-layer-mcp = "p_layer.mcp.server:main"
21
+
22
+ [build-system]
23
+ requires = ["setuptools>=68"]
24
+ build-backend = "setuptools.build_meta"
25
+
26
+ [tool.setuptools.packages.find]
27
+ include = ["p_layer*"]
@@ -0,0 +1,4 @@
1
+ [egg_info]
2
+ tag_build =
3
+ tag_date = 0
4
+