ltcai 4.3.3 → 4.4.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.
Files changed (89) hide show
  1. package/README.md +21 -16
  2. package/docs/CHANGELOG.md +37 -0
  3. package/docs/V4_4_0_EXTRACTION_REPORT.md +239 -0
  4. package/lattice_brain/__init__.py +38 -23
  5. package/lattice_brain/_kg_common.py +11 -1
  6. package/lattice_brain/context.py +212 -2
  7. package/lattice_brain/conversations.py +234 -1
  8. package/lattice_brain/discovery.py +11 -1
  9. package/lattice_brain/documents.py +11 -1
  10. package/lattice_brain/graph/__init__.py +28 -0
  11. package/lattice_brain/graph/_kg_common.py +1123 -0
  12. package/lattice_brain/graph/curator.py +473 -0
  13. package/lattice_brain/graph/discovery.py +1455 -0
  14. package/lattice_brain/graph/documents.py +218 -0
  15. package/lattice_brain/graph/identity.py +175 -0
  16. package/lattice_brain/graph/ingest.py +644 -0
  17. package/lattice_brain/graph/network.py +205 -0
  18. package/lattice_brain/graph/projection.py +571 -0
  19. package/lattice_brain/graph/provenance.py +401 -0
  20. package/lattice_brain/graph/retrieval.py +1341 -0
  21. package/lattice_brain/graph/schema.py +640 -0
  22. package/lattice_brain/graph/store.py +237 -0
  23. package/lattice_brain/graph/write_master.py +225 -0
  24. package/lattice_brain/identity.py +11 -13
  25. package/lattice_brain/ingest.py +11 -1
  26. package/lattice_brain/ingestion.py +318 -0
  27. package/lattice_brain/memory.py +100 -1
  28. package/lattice_brain/network.py +11 -1
  29. package/lattice_brain/portability.py +431 -0
  30. package/lattice_brain/projection.py +11 -1
  31. package/lattice_brain/provenance.py +11 -1
  32. package/lattice_brain/retrieval.py +11 -1
  33. package/lattice_brain/runtime/__init__.py +32 -0
  34. package/lattice_brain/runtime/agent_runtime.py +569 -0
  35. package/lattice_brain/runtime/hooks.py +754 -0
  36. package/lattice_brain/runtime/multi_agent.py +795 -0
  37. package/lattice_brain/schema.py +11 -1
  38. package/lattice_brain/store.py +10 -2
  39. package/lattice_brain/workflow.py +461 -0
  40. package/lattice_brain/write_master.py +11 -1
  41. package/latticeai/__init__.py +1 -1
  42. package/latticeai/api/agents.py +2 -2
  43. package/latticeai/api/browser.py +1 -1
  44. package/latticeai/api/chat.py +1 -1
  45. package/latticeai/api/computer_use.py +1 -1
  46. package/latticeai/api/hooks.py +2 -2
  47. package/latticeai/api/mcp.py +1 -1
  48. package/latticeai/api/tools.py +1 -1
  49. package/latticeai/api/workflow_designer.py +2 -2
  50. package/latticeai/app_factory.py +4 -4
  51. package/latticeai/brain/__init__.py +24 -6
  52. package/latticeai/brain/_kg_common.py +11 -1117
  53. package/latticeai/brain/context.py +12 -208
  54. package/latticeai/brain/conversations.py +12 -231
  55. package/latticeai/brain/discovery.py +13 -1451
  56. package/latticeai/brain/documents.py +13 -214
  57. package/latticeai/brain/identity.py +11 -169
  58. package/latticeai/brain/ingest.py +13 -640
  59. package/latticeai/brain/memory.py +12 -97
  60. package/latticeai/brain/network.py +12 -200
  61. package/latticeai/brain/projection.py +13 -567
  62. package/latticeai/brain/provenance.py +13 -397
  63. package/latticeai/brain/retrieval.py +13 -1337
  64. package/latticeai/brain/schema.py +12 -635
  65. package/latticeai/brain/store.py +13 -233
  66. package/latticeai/brain/write_master.py +13 -221
  67. package/latticeai/core/agent.py +1 -1
  68. package/latticeai/core/agent_registry.py +2 -2
  69. package/latticeai/core/builtin_hooks.py +2 -2
  70. package/latticeai/core/graph_curator.py +6 -468
  71. package/latticeai/core/hooks.py +6 -749
  72. package/latticeai/core/marketplace.py +1 -1
  73. package/latticeai/core/multi_agent.py +6 -790
  74. package/latticeai/core/workflow_engine.py +6 -456
  75. package/latticeai/core/workspace_os.py +1 -1
  76. package/latticeai/services/agent_runtime.py +6 -564
  77. package/latticeai/services/ingestion.py +6 -313
  78. package/latticeai/services/kg_portability.py +6 -426
  79. package/latticeai/services/platform_runtime.py +3 -3
  80. package/latticeai/services/run_executor.py +1 -1
  81. package/latticeai/services/upload_service.py +1 -1
  82. package/p_reinforce.py +1 -1
  83. package/package.json +1 -1
  84. package/scripts/bump_version.py +1 -1
  85. package/scripts/wheel_smoke.py +7 -0
  86. package/src-tauri/Cargo.lock +1 -1
  87. package/src-tauri/Cargo.toml +1 -1
  88. package/src-tauri/tauri.conf.json +1 -1
  89. package/static/app/asset-manifest.json +1 -1
@@ -0,0 +1,431 @@
1
+ """Knowledge Graph portability — local export / import / backup / restore.
2
+
3
+ The Knowledge Graph is the user's durable asset, so it must be portable without
4
+ any cloud service. Two complementary mechanisms, both fully local:
5
+
6
+ * **Logical export/import** (JSON): nodes/edges/chunks/sources/provenance with a
7
+ versioned header (schema + projection + embed-dim). Re-embeds on import, so it
8
+ is portable across machines.
9
+ * **Binary backup/restore** (ZIP): a faithful snapshot of the SQLite DB (incl.
10
+ vector embeddings) plus the blob directory, integrity-checked, for
11
+ same-machine recovery.
12
+ """
13
+
14
+ from __future__ import annotations
15
+
16
+ import hashlib
17
+ import json
18
+ import shutil
19
+ import tempfile
20
+ import zipfile
21
+ from datetime import datetime, timezone
22
+ from pathlib import Path, PurePosixPath
23
+ from typing import Any, Dict, Optional
24
+
25
+ from .archive import BrainArchivePaths, EncryptedBrainArchive
26
+ from .storage import (
27
+ DockerPostgresWizard,
28
+ PostgresEngine,
29
+ SQLiteToPostgresMigrator,
30
+ )
31
+
32
+ FORMAT = "latticeai.kg.export"
33
+ FORMAT_VERSION = 1
34
+ BACKUP_FORMAT = "latticeai.kg.backup"
35
+
36
+
37
+ def _now_iso() -> str:
38
+ return datetime.now(timezone.utc).isoformat()
39
+
40
+
41
+ def _stamp() -> str:
42
+ return _now_iso().replace(":", "").replace("-", "").replace(".", "")[:15]
43
+
44
+
45
+ def _sha256_file(path: Path) -> str:
46
+ h = hashlib.sha256()
47
+ with open(path, "rb") as fh:
48
+ for block in iter(lambda: fh.read(65536), b""):
49
+ h.update(block)
50
+ return h.hexdigest()
51
+
52
+
53
+ def _safe_zip_names(names) -> None:
54
+ for name in names:
55
+ path = PurePosixPath(name)
56
+ if path.is_absolute() or ".." in path.parts:
57
+ raise ValueError(f"Backup archive contains unsafe path: {name}")
58
+
59
+
60
+ class KGPortabilityService:
61
+ def __init__(self, *, knowledge_graph: Any, data_dir, enable_graph: bool = True, device_identity: Any = None) -> None:
62
+ self._kg = knowledge_graph
63
+ self._data_dir = Path(data_dir)
64
+ self._enable = bool(enable_graph)
65
+ self._exports_dir = self._data_dir / "workspace_exports"
66
+ # v4 sovereignty: when a DeviceIdentity is wired, exports are signed
67
+ # and imports record origin provenance. Pre-v4 unsigned bundles stay
68
+ # importable locally (origin='unsigned-legacy') — signatures are
69
+ # mandatory only on the Brain Network peer path.
70
+ self._identity = device_identity
71
+
72
+ def available(self) -> bool:
73
+ return self._enable and self._kg is not None
74
+
75
+ def _require(self) -> None:
76
+ if not self.available():
77
+ raise RuntimeError("Knowledge Graph is disabled (LATTICEAI_ENABLE_GRAPH).")
78
+
79
+ # ── logical export / import ──────────────────────────────────────────────
80
+ def export(self, *, workspace_id: Optional[str] = None) -> Dict[str, Any]:
81
+ self._require()
82
+ data = self._kg.export_graph_data(workspace_id=workspace_id)
83
+ header = {
84
+ "format": FORMAT,
85
+ "format_version": FORMAT_VERSION,
86
+ **self._kg.schema_versions(),
87
+ "exported_at": _now_iso(),
88
+ "workspace_id": workspace_id,
89
+ "counts": data.get("counts"),
90
+ }
91
+ artifact = {"header": header, **data}
92
+ if self._identity is not None:
93
+ artifact["signature"] = self._identity.sign_manifest(header)
94
+ return artifact
95
+
96
+ def export_to_file(self, path=None, *, workspace_id: Optional[str] = None) -> Dict[str, Any]:
97
+ artifact = self.export(workspace_id=workspace_id)
98
+ self._exports_dir.mkdir(parents=True, exist_ok=True)
99
+ path = Path(path) if path else self._exports_dir / f"kg-export-{_stamp()}.json"
100
+ path.write_text(json.dumps(artifact, ensure_ascii=False, indent=2), encoding="utf-8")
101
+ return {"path": str(path), "header": artifact["header"], "bytes": path.stat().st_size}
102
+
103
+ def import_data(self, artifact: Dict[str, Any], *, mode: str = "merge", dry_run: bool = False) -> Dict[str, Any]:
104
+ self._require()
105
+ if not isinstance(artifact, dict) or "nodes" not in artifact:
106
+ raise ValueError("Invalid Knowledge Graph export artifact.")
107
+ if mode not in ("merge", "replace"):
108
+ raise ValueError("mode must be 'merge' or 'replace'.")
109
+ origin = "unsigned-legacy"
110
+ signature = artifact.get("signature")
111
+ if signature:
112
+ from .graph.identity import verify_manifest
113
+
114
+ if not verify_manifest(artifact.get("header") or {}, signature):
115
+ raise ValueError("Bundle signature verification failed — refusing to import.")
116
+ origin = f"device:{signature.get('fingerprint') or 'unknown'}"
117
+ result = self._kg.import_graph_data(artifact, mode=mode, dry_run=dry_run)
118
+ result["header"] = artifact.get("header")
119
+ result["origin"] = origin
120
+ result["signed"] = bool(signature)
121
+ if not dry_run:
122
+ try:
123
+ self._kg.record_provenance(
124
+ node_id="import:" + str((artifact.get("header") or {}).get("exported_at") or _now_iso()),
125
+ source_type="bundle_import",
126
+ pipeline="kg-portability",
127
+ owner=None,
128
+ metadata={"origin": origin, "mode": mode,
129
+ "counts": (artifact.get("header") or {}).get("counts")},
130
+ )
131
+ except Exception:
132
+ pass
133
+ return result
134
+
135
+ def import_from_file(self, path, *, mode: str = "merge", dry_run: bool = False) -> Dict[str, Any]:
136
+ artifact = json.loads(Path(path).read_text(encoding="utf-8"))
137
+ return self.import_data(artifact, mode=mode, dry_run=dry_run)
138
+
139
+ # ── binary backup / restore ──────────────────────────────────────────────
140
+ def backup(self, dest_path=None) -> Dict[str, Any]:
141
+ self._require()
142
+ self._exports_dir.mkdir(parents=True, exist_ok=True)
143
+ dest = Path(dest_path) if dest_path else self._exports_dir / f"kg-backup-{_stamp()}.zip"
144
+ with tempfile.TemporaryDirectory() as tmp_s:
145
+ tmp = Path(tmp_s)
146
+ db_copy = tmp / "knowledge_graph.sqlite"
147
+ self._kg.backup_database(db_copy)
148
+ manifest = {
149
+ "format": BACKUP_FORMAT,
150
+ "format_version": FORMAT_VERSION,
151
+ **self._kg.schema_versions(),
152
+ "created_at": _now_iso(),
153
+ "db_sha256": _sha256_file(db_copy),
154
+ "has_blobs": Path(self._kg.blob_dir).exists(),
155
+ }
156
+ with zipfile.ZipFile(dest, "w", zipfile.ZIP_DEFLATED) as zf:
157
+ zf.write(db_copy, "knowledge_graph.sqlite")
158
+ zf.writestr("manifest.json", json.dumps(manifest, ensure_ascii=False, indent=2))
159
+ blob_dir = Path(self._kg.blob_dir)
160
+ if blob_dir.exists():
161
+ for f in blob_dir.rglob("*"):
162
+ if f.is_file():
163
+ zf.write(f, f"blobs/{f.relative_to(blob_dir)}")
164
+ return {"path": str(dest), "bytes": dest.stat().st_size, "manifest": manifest}
165
+
166
+ def restore(
167
+ self,
168
+ archive_path,
169
+ *,
170
+ verify: bool = True,
171
+ dry_run: bool = False,
172
+ confirm: bool = False,
173
+ ) -> Dict[str, Any]:
174
+ self._require()
175
+ archive = Path(archive_path)
176
+ if not archive.exists():
177
+ raise FileNotFoundError(f"Backup archive not found: {archive}")
178
+ if not dry_run and not confirm:
179
+ raise ValueError("Explicit confirmation is required before restoring a Knowledge Graph backup.")
180
+ with zipfile.ZipFile(archive) as zf:
181
+ names = zf.namelist()
182
+ _safe_zip_names(names)
183
+ if "knowledge_graph.sqlite" not in names:
184
+ raise ValueError("Archive is missing knowledge_graph.sqlite.")
185
+ manifest = json.loads(zf.read("manifest.json")) if "manifest.json" in names else {}
186
+ with tempfile.TemporaryDirectory() as tmp_s:
187
+ tmp = Path(tmp_s)
188
+ zf.extractall(tmp)
189
+ db_src = tmp / "knowledge_graph.sqlite"
190
+ if verify and manifest.get("db_sha256"):
191
+ if _sha256_file(db_src) != manifest["db_sha256"]:
192
+ raise ValueError("Backup integrity check failed (db sha256 mismatch).")
193
+ if dry_run:
194
+ return {
195
+ "restored": False,
196
+ "dry_run": True,
197
+ "verified": True,
198
+ "manifest": manifest,
199
+ "planned": {
200
+ "database": str(self._kg.db_path),
201
+ "blobs": str(self._kg.blob_dir),
202
+ "archive": str(archive),
203
+ },
204
+ }
205
+ db_dest = Path(self._kg.db_path)
206
+ blob_dest = Path(self._kg.blob_dir)
207
+ db_dest.parent.mkdir(parents=True, exist_ok=True)
208
+ # Drop the live DB + stale WAL/SHM siblings so the restored copy
209
+ # is authoritative (no stale journal overlaying old pages).
210
+ for sib in (db_dest, Path(str(db_dest) + "-wal"), Path(str(db_dest) + "-shm")):
211
+ if sib.exists():
212
+ sib.unlink()
213
+ shutil.copyfile(db_src, db_dest)
214
+ blob_src = tmp / "blobs"
215
+ if blob_src.exists():
216
+ if blob_dest.exists():
217
+ shutil.rmtree(blob_dest)
218
+ shutil.copytree(blob_src, blob_dest)
219
+ else:
220
+ blob_dest.mkdir(parents=True, exist_ok=True)
221
+ stats = self._kg.stats()
222
+ return {
223
+ "restored": True,
224
+ "manifest": manifest,
225
+ "nodes": sum(stats.get("nodes", {}).values()),
226
+ }
227
+
228
+ def verify_backup(self, archive_path) -> Dict[str, Any]:
229
+ archive = Path(archive_path)
230
+ if not archive.exists():
231
+ return {"ok": False, "path": str(archive), "errors": [f"Backup archive not found: {archive}"]}
232
+ try:
233
+ with zipfile.ZipFile(archive) as zf:
234
+ names = zf.namelist()
235
+ _safe_zip_names(names)
236
+ if "knowledge_graph.sqlite" not in names:
237
+ raise ValueError("Archive is missing knowledge_graph.sqlite.")
238
+ manifest = json.loads(zf.read("manifest.json")) if "manifest.json" in names else {}
239
+ with tempfile.TemporaryDirectory() as tmp_s:
240
+ tmp = Path(tmp_s)
241
+ zf.extract("knowledge_graph.sqlite", tmp)
242
+ db_src = tmp / "knowledge_graph.sqlite"
243
+ if manifest.get("db_sha256") and _sha256_file(db_src) != manifest["db_sha256"]:
244
+ raise ValueError("Backup integrity check failed (db sha256 mismatch).")
245
+ return {"ok": True, "path": str(archive), "manifest": manifest, "errors": []}
246
+ except (ValueError, zipfile.BadZipFile, OSError, json.JSONDecodeError) as exc:
247
+ return {"ok": False, "path": str(archive), "errors": [str(exc)]}
248
+
249
+ # ── encrypted .latticebrain archive ───────────────────────────────────
250
+ def encrypted_archive(self, dest_path=None, *, passphrase: str) -> Dict[str, Any]:
251
+ self._require()
252
+ self._exports_dir.mkdir(parents=True, exist_ok=True)
253
+ dest = Path(dest_path) if dest_path else self._exports_dir / f"brain-{_stamp()}.latticebrain"
254
+ metadata = {
255
+ "storage": self.storage_status().get("active", {}),
256
+ "snapshot": self.snapshot_metadata(),
257
+ "device_identity": self._identity.describe() if self._identity is not None else {},
258
+ "provenance": {"exported_at": _now_iso(), "source": "kg-portability"},
259
+ }
260
+ archive = EncryptedBrainArchive(
261
+ BrainArchivePaths(
262
+ db_path=Path(self._kg.db_path),
263
+ blob_dir=Path(self._kg.blob_dir),
264
+ data_dir=self._data_dir,
265
+ metadata=metadata,
266
+ )
267
+ )
268
+ return archive.create(dest, passphrase=passphrase)
269
+
270
+ def inspect_encrypted_archive(self, archive_path, *, passphrase: Optional[str] = None) -> Dict[str, Any]:
271
+ archive = EncryptedBrainArchive(
272
+ BrainArchivePaths(
273
+ db_path=Path(self._kg.db_path),
274
+ blob_dir=Path(self._kg.blob_dir),
275
+ data_dir=self._data_dir,
276
+ )
277
+ )
278
+ return archive.inspect(Path(archive_path), passphrase=passphrase)
279
+
280
+ def verify_encrypted_archive(self, archive_path, *, passphrase: str) -> Dict[str, Any]:
281
+ archive = EncryptedBrainArchive(
282
+ BrainArchivePaths(
283
+ db_path=Path(self._kg.db_path),
284
+ blob_dir=Path(self._kg.blob_dir),
285
+ data_dir=self._data_dir,
286
+ )
287
+ )
288
+ return archive.verify(Path(archive_path), passphrase=passphrase)
289
+
290
+ def restore_encrypted_archive(
291
+ self,
292
+ archive_path,
293
+ *,
294
+ passphrase: str,
295
+ dry_run: bool = False,
296
+ confirm: bool = False,
297
+ ) -> Dict[str, Any]:
298
+ self._require()
299
+ archive = EncryptedBrainArchive(
300
+ BrainArchivePaths(
301
+ db_path=Path(self._kg.db_path),
302
+ blob_dir=Path(self._kg.blob_dir),
303
+ data_dir=self._data_dir,
304
+ )
305
+ )
306
+ return archive.restore(
307
+ Path(archive_path),
308
+ passphrase=passphrase,
309
+ target=BrainArchivePaths(
310
+ db_path=Path(self._kg.db_path),
311
+ blob_dir=Path(self._kg.blob_dir),
312
+ data_dir=self._data_dir,
313
+ ),
314
+ dry_run=dry_run,
315
+ confirm=confirm,
316
+ )
317
+
318
+ def import_encrypted_archive(
319
+ self,
320
+ archive_path,
321
+ *,
322
+ passphrase: str,
323
+ dry_run: bool = False,
324
+ confirm: bool = False,
325
+ ) -> Dict[str, Any]:
326
+ result = self.restore_encrypted_archive(
327
+ archive_path,
328
+ passphrase=passphrase,
329
+ dry_run=dry_run,
330
+ confirm=confirm,
331
+ )
332
+ result["operation"] = "import"
333
+ return result
334
+
335
+ # ── status surface ───────────────────────────────────────────────────────
336
+ def snapshot_metadata(self) -> Dict[str, Any]:
337
+ if not self.available():
338
+ return {"available": False}
339
+ return {
340
+ "available": True,
341
+ **self._kg.schema_versions(),
342
+ "stats": self._kg.stats(),
343
+ "provenance": self._kg.provenance_stats(),
344
+ "storage": (
345
+ self._kg.storage_engine.capabilities().as_dict()
346
+ if getattr(self._kg, "storage_engine", None) is not None
347
+ else {"engine": "sqlite", "available": True}
348
+ ),
349
+ }
350
+
351
+ def storage_status(self) -> Dict[str, Any]:
352
+ if not self.available():
353
+ return {"available": False}
354
+ return {
355
+ "available": True,
356
+ "active": (
357
+ self._kg.storage_engine.capabilities().as_dict()
358
+ if getattr(self._kg, "storage_engine", None) is not None
359
+ else {"engine": "sqlite", "available": True}
360
+ ),
361
+ "postgres": PostgresEngine("", schema="lattice_brain").capabilities().as_dict(),
362
+ "backup_health": self.backup_health(),
363
+ }
364
+
365
+ def backup_health(self) -> Dict[str, Any]:
366
+ self._exports_dir.mkdir(parents=True, exist_ok=True)
367
+ backups = sorted(
368
+ [
369
+ p for p in self._exports_dir.glob("*")
370
+ if p.is_file() and (p.suffix == ".zip" or p.suffix == ".latticebrain")
371
+ ],
372
+ key=lambda p: p.stat().st_mtime,
373
+ reverse=True,
374
+ )
375
+ latest = backups[0] if backups else None
376
+ return {
377
+ "available": True,
378
+ "directory": str(self._exports_dir),
379
+ "count": len(backups),
380
+ "latest": str(latest) if latest else None,
381
+ "latest_bytes": latest.stat().st_size if latest else 0,
382
+ "encrypted_archives": sum(1 for p in backups if p.suffix == ".latticebrain"),
383
+ "zip_backups": sum(1 for p in backups if p.suffix == ".zip"),
384
+ }
385
+
386
+ def postgres_docker_setup(
387
+ self,
388
+ *,
389
+ consent: bool,
390
+ dry_run: bool = False,
391
+ port: int = 5432,
392
+ ) -> Dict[str, Any]:
393
+ wizard = DockerPostgresWizard(self._data_dir / "postgres", port=port)
394
+ return wizard.start(consent=consent, dry_run=dry_run)
395
+
396
+ def migrate_sqlite_to_postgres(
397
+ self,
398
+ *,
399
+ dsn: str,
400
+ schema: str = "lattice_brain",
401
+ dry_run: bool = True,
402
+ ) -> Dict[str, Any]:
403
+ self._require()
404
+ if not dsn:
405
+ raise ValueError("Postgres DSN is required for SQLite to Postgres migration.")
406
+ migrator = SQLiteToPostgresMigrator(
407
+ Path(self._kg.db_path),
408
+ PostgresEngine(dsn, schema=schema),
409
+ )
410
+ if dry_run:
411
+ return migrator.migrate(dry_run=True)
412
+ backup = self.backup()
413
+ verification = self.verify_backup(backup["path"])
414
+ if not verification.get("ok"):
415
+ raise RuntimeError(
416
+ "Pre-migration backup verification failed; Postgres migration was not started: "
417
+ + "; ".join(verification.get("errors") or [])
418
+ )
419
+ result = migrator.migrate(dry_run=False)
420
+ result["pre_migration_backup"] = {
421
+ "path": backup["path"],
422
+ "verified": True,
423
+ "manifest": backup.get("manifest"),
424
+ }
425
+ return result
426
+
427
+ def recent_ingestions(self, *, limit: int = 50, source_type: Optional[str] = None) -> Dict[str, Any]:
428
+ """Recent provenance records (newest first) for the ingestion-sources UI."""
429
+ if not self.available():
430
+ return {"items": [], "count": 0}
431
+ return self._kg.list_provenance(limit=limit, source_type=source_type)
@@ -1 +1,11 @@
1
- from latticeai.brain.projection import * # noqa: F401,F403
1
+ """Compatibility shim: implementation moved to lattice_brain.graph.projection.
2
+
3
+ This module aliases itself to the physical module so identity, singletons,
4
+ and monkeypatching behave as if the old flat path were the real module.
5
+ """
6
+
7
+ import sys
8
+
9
+ from .graph import projection as _impl
10
+
11
+ sys.modules[__name__] = _impl
@@ -1 +1,11 @@
1
- from latticeai.brain.provenance import * # noqa: F401,F403
1
+ """Compatibility shim: implementation moved to lattice_brain.graph.provenance.
2
+
3
+ This module aliases itself to the physical module so identity, singletons,
4
+ and monkeypatching behave as if the old flat path were the real module.
5
+ """
6
+
7
+ import sys
8
+
9
+ from .graph import provenance as _impl
10
+
11
+ sys.modules[__name__] = _impl
@@ -1 +1,11 @@
1
- from latticeai.brain.retrieval import * # noqa: F401,F403
1
+ """Compatibility shim: implementation moved to lattice_brain.graph.retrieval.
2
+
3
+ This module aliases itself to the physical module so identity, singletons,
4
+ and monkeypatching behave as if the old flat path were the real module.
5
+ """
6
+
7
+ import sys
8
+
9
+ from .graph import retrieval as _impl
10
+
11
+ sys.modules[__name__] = _impl
@@ -0,0 +1,32 @@
1
+ """Agent and hook runtime subsystem of the Brain Core.
2
+
3
+ Physically hosts the hooks registry/dispatch lifecycle, the multi-agent
4
+ orchestrator, and the agent runtime service. Lazy-loaded so importing
5
+ ``lattice_brain.runtime`` stays cheap.
6
+ """
7
+
8
+ from __future__ import annotations
9
+
10
+ __all__ = [
11
+ "AgentRuntime",
12
+ "AgentRuntimeUnavailable",
13
+ "MultiAgentOrchestrator",
14
+ "HooksRegistry",
15
+ "dispatch_tool",
16
+ ]
17
+
18
+
19
+ def __getattr__(name: str):
20
+ if name in {"AgentRuntime", "AgentRuntimeUnavailable"}:
21
+ from . import agent_runtime
22
+
23
+ return getattr(agent_runtime, name)
24
+ if name == "MultiAgentOrchestrator":
25
+ from .multi_agent import MultiAgentOrchestrator
26
+
27
+ return MultiAgentOrchestrator
28
+ if name in {"HooksRegistry", "dispatch_tool"}:
29
+ from . import hooks
30
+
31
+ return getattr(hooks, name)
32
+ raise AttributeError(name)