ltcai 9.9.9 → 10.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.
Files changed (228) hide show
  1. package/README.md +51 -38
  2. package/docs/CHANGELOG.md +197 -0
  3. package/docs/COMMUNITY_AND_PLUGINS.md +1 -1
  4. package/docs/DEVELOPMENT.md +1 -1
  5. package/docs/HYBRID_CLOUD_KG_STREAMING.md +101 -0
  6. package/docs/ONBOARDING.md +1 -1
  7. package/docs/OPERATIONS.md +1 -1
  8. package/docs/TRUST_MODEL.md +1 -1
  9. package/docs/WHY_LATTICE.md +1 -1
  10. package/docs/kg-schema.md +1 -1
  11. package/lattice_brain/__init__.py +1 -1
  12. package/lattice_brain/archive.py +5 -4
  13. package/lattice_brain/conversations.py +14 -3
  14. package/lattice_brain/embeddings.py +12 -2
  15. package/lattice_brain/graph/_kg_common.py +5 -5
  16. package/lattice_brain/graph/_kg_fsutil.py +4 -3
  17. package/lattice_brain/graph/discovery.py +4 -3
  18. package/lattice_brain/graph/discovery_index.py +0 -1
  19. package/lattice_brain/graph/documents.py +3 -2
  20. package/lattice_brain/graph/fusion.py +4 -0
  21. package/lattice_brain/graph/ingest.py +12 -2
  22. package/lattice_brain/graph/projection.py +3 -2
  23. package/lattice_brain/graph/provenance.py +5 -3
  24. package/lattice_brain/graph/rerank.py +4 -1
  25. package/lattice_brain/graph/retrieval.py +0 -1
  26. package/lattice_brain/graph/retrieval_docgen.py +0 -1
  27. package/lattice_brain/graph/retrieval_reads.py +0 -1
  28. package/lattice_brain/graph/retrieval_vector.py +0 -1
  29. package/lattice_brain/graph/schema.py +4 -3
  30. package/lattice_brain/graph/store.py +18 -4
  31. package/lattice_brain/graph/write_master.py +46 -1
  32. package/lattice_brain/ingestion.py +4 -1
  33. package/lattice_brain/portability.py +5 -2
  34. package/lattice_brain/quality.py +12 -5
  35. package/lattice_brain/quiet.py +43 -0
  36. package/lattice_brain/runtime/agent_runtime.py +12 -8
  37. package/lattice_brain/runtime/hooks.py +2 -1
  38. package/lattice_brain/runtime/multi_agent.py +2 -3
  39. package/lattice_brain/sensitivity.py +94 -0
  40. package/lattice_brain/storage/base.py +30 -2
  41. package/lattice_brain/storage/migration.py +3 -2
  42. package/lattice_brain/storage/postgres.py +2 -2
  43. package/lattice_brain/storage/sqlite.py +6 -4
  44. package/lattice_brain/workflow.py +4 -2
  45. package/latticeai/__init__.py +1 -1
  46. package/latticeai/api/admin.py +1 -1
  47. package/latticeai/api/agents.py +4 -2
  48. package/latticeai/api/auth.py +5 -1
  49. package/latticeai/api/automation_intelligence.py +2 -1
  50. package/latticeai/api/browser.py +3 -2
  51. package/latticeai/api/chat.py +28 -17
  52. package/latticeai/api/chat_agent_http.py +22 -8
  53. package/latticeai/api/chat_contracts.py +4 -0
  54. package/latticeai/api/chat_documents.py +6 -2
  55. package/latticeai/api/chat_hybrid.py +82 -0
  56. package/latticeai/api/computer_use.py +8 -3
  57. package/latticeai/api/knowledge_graph.py +1 -1
  58. package/latticeai/api/mcp.py +4 -4
  59. package/latticeai/api/models.py +5 -2
  60. package/latticeai/api/network_boundary.py +220 -0
  61. package/latticeai/api/permissions.py +0 -1
  62. package/latticeai/api/realtime.py +1 -1
  63. package/latticeai/api/security_dashboard.py +1 -1
  64. package/latticeai/api/setup.py +16 -3
  65. package/latticeai/api/static_routes.py +2 -1
  66. package/latticeai/api/tools.py +12 -8
  67. package/latticeai/api/voice_capture.py +3 -1
  68. package/latticeai/api/workflow_designer.py +1 -1
  69. package/latticeai/api/workspace.py +1 -2
  70. package/latticeai/app_factory.py +131 -78
  71. package/latticeai/cli/entrypoint.py +6 -4
  72. package/latticeai/core/agent.py +55 -495
  73. package/latticeai/core/agent_eval.py +2 -2
  74. package/latticeai/core/agent_helpers.py +493 -0
  75. package/latticeai/core/agent_prompts.py +0 -1
  76. package/latticeai/core/agent_registry.py +3 -1
  77. package/latticeai/core/agent_state.py +41 -0
  78. package/latticeai/core/audit.py +1 -1
  79. package/latticeai/core/builtin_hooks.py +2 -1
  80. package/latticeai/core/config.py +0 -1
  81. package/latticeai/core/embedding_providers.py +12 -1
  82. package/latticeai/core/file_generation.py +3 -0
  83. package/latticeai/core/invitations.py +4 -1
  84. package/latticeai/core/io_utils.py +3 -1
  85. package/latticeai/core/legacy_compatibility.py +1 -2
  86. package/latticeai/core/local_embeddings.py +12 -1
  87. package/latticeai/core/marketplace.py +1 -2
  88. package/latticeai/core/mcp_registry.py +0 -1
  89. package/latticeai/core/model_compat.py +1 -1
  90. package/latticeai/core/model_resolution.py +1 -1
  91. package/latticeai/core/network_boundary.py +168 -0
  92. package/latticeai/core/oidc.py +3 -0
  93. package/latticeai/core/permission_mode.py +6 -6
  94. package/latticeai/core/plugins.py +3 -2
  95. package/latticeai/core/policy.py +0 -1
  96. package/latticeai/core/quiet.py +84 -0
  97. package/latticeai/core/realtime.py +3 -2
  98. package/latticeai/core/run_store.py +4 -2
  99. package/latticeai/core/security.py +4 -0
  100. package/latticeai/core/users.py +5 -3
  101. package/latticeai/core/workspace_graph_trace.py +3 -0
  102. package/latticeai/core/workspace_os.py +65 -273
  103. package/latticeai/core/workspace_os_constants.py +126 -0
  104. package/latticeai/core/workspace_os_state.py +180 -0
  105. package/latticeai/core/workspace_os_utils.py +3 -1
  106. package/latticeai/core/workspace_permissions.py +1 -0
  107. package/latticeai/core/workspace_snapshots.py +3 -1
  108. package/latticeai/core/workspace_timeline.py +3 -1
  109. package/latticeai/integrations/telegram_bot.py +25 -16
  110. package/latticeai/models/router.py +6 -3
  111. package/latticeai/runtime/access_runtime.py +3 -1
  112. package/latticeai/runtime/audit_runtime.py +3 -2
  113. package/latticeai/runtime/chat_wiring.py +4 -1
  114. package/latticeai/runtime/history_runtime.py +1 -1
  115. package/latticeai/runtime/lifespan_runtime.py +3 -1
  116. package/latticeai/runtime/network_boundary_wiring.py +124 -0
  117. package/latticeai/runtime/persistence_runtime.py +3 -1
  118. package/latticeai/runtime/router_registration.py +11 -1
  119. package/latticeai/services/architecture_readiness.py +1 -2
  120. package/latticeai/services/change_proposals.py +2 -1
  121. package/latticeai/services/cloud_egress_audit.py +85 -0
  122. package/latticeai/services/cloud_extraction.py +129 -0
  123. package/latticeai/services/cloud_streaming.py +266 -0
  124. package/latticeai/services/cloud_token_guard.py +84 -0
  125. package/latticeai/services/command_center.py +2 -1
  126. package/latticeai/services/folder_watch.py +3 -0
  127. package/latticeai/services/funnel_metrics.py +3 -1
  128. package/latticeai/services/hybrid_chat.py +265 -0
  129. package/latticeai/services/hybrid_context.py +228 -0
  130. package/latticeai/services/hybrid_policy.py +178 -0
  131. package/latticeai/services/memory_service.py +1 -1
  132. package/latticeai/services/model_catalog.py +10 -1
  133. package/latticeai/services/model_engines.py +35 -14
  134. package/latticeai/services/model_loading.py +3 -2
  135. package/latticeai/services/model_runtime.py +68 -17
  136. package/latticeai/services/multimodal_streaming.py +123 -0
  137. package/latticeai/services/network_boundary_service.py +154 -0
  138. package/latticeai/services/openai_compatible_adapter.py +100 -0
  139. package/latticeai/services/p_reinforce.py +4 -0
  140. package/latticeai/services/platform_runtime.py +9 -4
  141. package/latticeai/services/process_audit.py +0 -1
  142. package/latticeai/services/product_readiness.py +1 -1
  143. package/latticeai/services/run_executor.py +3 -2
  144. package/latticeai/services/search_service.py +1 -4
  145. package/latticeai/services/setup_detection.py +2 -1
  146. package/latticeai/services/tool_dispatch.py +7 -2
  147. package/latticeai/services/upload_service.py +2 -1
  148. package/latticeai/setup/auto_setup.py +10 -7
  149. package/latticeai/setup/wizard.py +15 -11
  150. package/latticeai/tools/__init__.py +3 -3
  151. package/latticeai/tools/commands.py +7 -7
  152. package/latticeai/tools/computer.py +3 -2
  153. package/latticeai/tools/documents.py +10 -9
  154. package/latticeai/tools/filesystem.py +7 -4
  155. package/latticeai/tools/knowledge.py +2 -0
  156. package/latticeai/tools/local_files.py +1 -1
  157. package/latticeai/tools/network.py +2 -1
  158. package/package.json +6 -4
  159. package/scripts/bench_agent_smoke.py +0 -1
  160. package/scripts/bench_models.py +0 -1
  161. package/scripts/brain_quality_eval.py +7 -2
  162. package/scripts/bump_version.py +2 -1
  163. package/scripts/check_current_release_docs.mjs +1 -1
  164. package/scripts/migrate_brain_storage.py +5 -1
  165. package/scripts/profile_kg.py +2 -7
  166. package/scripts/verify_hf_model_registry.py +2 -2
  167. package/src-tauri/Cargo.lock +1 -1
  168. package/src-tauri/Cargo.toml +1 -1
  169. package/src-tauri/tauri.conf.json +1 -1
  170. package/static/app/asset-manifest.json +37 -37
  171. package/static/app/assets/Act-CbdGD-2i.js +1 -0
  172. package/static/app/assets/AdminConsole-LgCkXpnf.js +1 -0
  173. package/static/app/assets/Brain-CoPGJI1L.js +321 -0
  174. package/static/app/assets/BrainHome-gVnaxSwp.js +2 -0
  175. package/static/app/assets/BrainSignals-ChxAYHtj.js +1 -0
  176. package/static/app/assets/Capture-DBtgkHZg.js +1 -0
  177. package/static/app/assets/CommandPalette-Ovtv5I0Y.js +1 -0
  178. package/static/app/assets/Library-CBia2Fvm.js +1 -0
  179. package/static/app/assets/LivingBrain-CNz-6NSd.js +1 -0
  180. package/static/app/assets/{ProductFlow-cB3EFinZ.js → ProductFlow-ePX-Y73J.js} +1 -1
  181. package/static/app/assets/ReviewCard-C4mpvkwH.js +3 -0
  182. package/static/app/assets/System-BvWNK1zc.js +1 -0
  183. package/static/app/assets/activity-CiauPV_3.js +1 -0
  184. package/static/app/assets/{bot-DtKkLfXj.js → bot-Dwct-Q1x.js} +1 -1
  185. package/static/app/assets/brain-BKDW1F0T.js +1 -0
  186. package/static/app/assets/{button-BTxZDMFJ.js → button-CCB9Kuw7.js} +1 -1
  187. package/static/app/assets/{circle-pause--qxF9-EO.js → circle-pause-DNa8WHC5.js} +1 -1
  188. package/static/app/assets/{circle-play-89x3S6jm.js → circle-play-8jmxn5mf.js} +1 -1
  189. package/static/app/assets/{cpu-DTUI63vo.js → cpu-mxvF3V1I.js} +1 -1
  190. package/static/app/assets/{download-EJS5_O0f.js → download--SmCcx0p.js} +1 -1
  191. package/static/app/assets/{folder-open-BL9zYTnc.js → folder-open-D4Wy5roo.js} +1 -1
  192. package/static/app/assets/{hard-drive-BFnwi1Lu.js → hard-drive-BwQcSPlS.js} +1 -1
  193. package/static/app/assets/index-CQWdDU3z.css +2 -0
  194. package/static/app/assets/index-DDV2YZwM.js +10 -0
  195. package/static/app/assets/input-DFhSjmLS.js +1 -0
  196. package/static/app/assets/{network-zKa3jAwl.js → network-Bts7VO94.js} +1 -1
  197. package/static/app/assets/primitives-BuSMEJY8.js +1 -0
  198. package/static/app/assets/search-DZzxhWaQ.js +1 -0
  199. package/static/app/assets/{shield-alert-CpHeCsaF.js → shield-alert-BfTO6X_G.js} +1 -1
  200. package/static/app/assets/textarea-BjctW1oh.js +1 -0
  201. package/static/app/assets/{useFocusTrap-58TNCONM.js → useFocusTrap-CE43-LrA.js} +1 -1
  202. package/static/app/assets/{useQuery-CcVlXlBd.js → useQuery-Bv3ejLL5.js} +1 -1
  203. package/static/app/assets/{users-C0cFMj6o.js → users-CsNqLZAj.js} +1 -1
  204. package/static/app/assets/utils-KFFdVG_t.js +7 -0
  205. package/static/app/assets/workspace-wdCvdyPF.js +1 -0
  206. package/static/app/index.html +4 -4
  207. package/static/sw.js +1 -1
  208. package/static/app/assets/Act-BFbpl2_Y.js +0 -1
  209. package/static/app/assets/AdminConsole-DaZPha9Y.js +0 -1
  210. package/static/app/assets/Brain-DGrV0heZ.js +0 -321
  211. package/static/app/assets/BrainHome-Re6Zxh_W.js +0 -2
  212. package/static/app/assets/BrainSignals-BQ11jDpJ.js +0 -1
  213. package/static/app/assets/Capture-DRaf1zLH.js +0 -1
  214. package/static/app/assets/CommandPalette-DAKPm4uJ.js +0 -1
  215. package/static/app/assets/LanguageSwitcher-BgTwHoWv.js +0 -1
  216. package/static/app/assets/Library-BVtrSt12.js +0 -1
  217. package/static/app/assets/LivingBrain-7glOmK0R.js +0 -1
  218. package/static/app/assets/ReviewCard-CShhHS-w.js +0 -3
  219. package/static/app/assets/System-B4nQDy8a.js +0 -1
  220. package/static/app/assets/brain-wVV63_1z.js +0 -1
  221. package/static/app/assets/index-BFtDYI6l.js +0 -10
  222. package/static/app/assets/index-BY9bpxyx.css +0 -2
  223. package/static/app/assets/input-DEz5UjBz.js +0 -1
  224. package/static/app/assets/primitives-jNrZm2O9.js +0 -1
  225. package/static/app/assets/search-ku6-_6wY.js +0 -1
  226. package/static/app/assets/textarea-DpcQkn75.js +0 -1
  227. package/static/app/assets/utils-DYf7pqrR.js +0 -7
  228. package/static/app/assets/workspace-CjthHsLN.js +0 -1
@@ -0,0 +1,94 @@
1
+ """Which local sources are never allowed to leave the machine.
2
+
3
+ This lives in Brain Core rather than in the app layer because it is a property
4
+ of the *data*, not of any transport: a node stamped never-leaves stays that way
5
+ whether it is read by the cloud path, an export, or a future surface nobody has
6
+ written yet. Brain Core cannot import from ``latticeai`` (see
7
+ ``tests/unit/test_import_guard.py``), so the app-layer network boundary imports
8
+ this, not the other way round.
9
+
10
+ Deliberately narrow. These are locations whose *entire purpose* is to hold
11
+ secrets, so a match is a fact about the path rather than a guess about content.
12
+ A broader heuristic — scanning text for things that look like keys — would
13
+ produce false confidence in both directions: it would miss secrets in unusual
14
+ shapes and quarantine ordinary notes that merely discuss credentials.
15
+ """
16
+
17
+ from __future__ import annotations
18
+
19
+ from typing import Any, Optional
20
+
21
+ # Path fragments checked case-insensitively against the POSIX form of the path.
22
+ SENSITIVE_PATH_FRAGMENTS: tuple = (
23
+ "/.ssh/",
24
+ "/.gnupg/",
25
+ "/.aws/",
26
+ "/.kube/",
27
+ "/.docker/config.json",
28
+ "/.netrc",
29
+ "/.npmrc",
30
+ "/.pypirc",
31
+ "/.git-credentials",
32
+ "id_rsa",
33
+ "id_ed25519",
34
+ ".pem",
35
+ ".p12",
36
+ ".pfx",
37
+ ".keystore",
38
+ )
39
+
40
+ # Exact filenames (in any directory) that are secret-bearing by convention.
41
+ SENSITIVE_FILENAMES: frozenset = frozenset({
42
+ ".env",
43
+ ".env.local",
44
+ ".env.production",
45
+ ".env.development",
46
+ "credentials",
47
+ "secrets.yaml",
48
+ "secrets.yml",
49
+ "secrets.json",
50
+ })
51
+
52
+ #: Metadata key stamped on a node that must never leave the machine.
53
+ LOCAL_ONLY_FLAG = "local_only"
54
+ #: Companion key holding the human-readable reason.
55
+ LOCAL_ONLY_REASON = "local_only_reason"
56
+
57
+
58
+ def sensitive_reason_for_path(path: Any) -> Optional[str]:
59
+ """Return why ``path`` is never-leaves, or ``None`` if it is ordinary."""
60
+ if not path:
61
+ return None
62
+ lowered = str(path).replace("\\", "/").lower()
63
+ name = lowered.rsplit("/", 1)[-1]
64
+ if name in SENSITIVE_FILENAMES:
65
+ return f"{name!r} is a secret-bearing filename"
66
+ for fragment in SENSITIVE_PATH_FRAGMENTS:
67
+ if fragment in lowered:
68
+ return f"path contains {fragment!r}"
69
+ return None
70
+
71
+
72
+ def stamp_sensitivity(metadata: dict, path: Any) -> Optional[str]:
73
+ """Stamp never-leaves onto ``metadata`` when ``path`` warrants it.
74
+
75
+ Returns the reason it stamped, or ``None``. Never clears an existing flag —
76
+ a user or another rule may have set it for a reason this function cannot
77
+ see, and downgrading a never-leaves marker is not a decision code should
78
+ make on its own.
79
+ """
80
+ reason = sensitive_reason_for_path(path)
81
+ if reason:
82
+ metadata[LOCAL_ONLY_FLAG] = True
83
+ metadata.setdefault(LOCAL_ONLY_REASON, reason)
84
+ return reason
85
+
86
+
87
+ __all__ = [
88
+ "SENSITIVE_PATH_FRAGMENTS",
89
+ "SENSITIVE_FILENAMES",
90
+ "LOCAL_ONLY_FLAG",
91
+ "LOCAL_ONLY_REASON",
92
+ "sensitive_reason_for_path",
93
+ "stamp_sensitivity",
94
+ ]
@@ -3,9 +3,10 @@
3
3
  from __future__ import annotations
4
4
 
5
5
  from abc import ABC, abstractmethod
6
+ from contextlib import contextmanager
6
7
  from dataclasses import dataclass, field
7
8
  from pathlib import Path
8
- from typing import Any, Dict, Optional
9
+ from typing import Any, Dict, Iterator, Optional
9
10
 
10
11
 
11
12
  class StorageUnavailable(RuntimeError):
@@ -58,7 +59,34 @@ class StorageEngine(ABC):
58
59
 
59
60
  @abstractmethod
60
61
  def connect(self) -> Any:
61
- """Return a DB-API-like connection for this engine."""
62
+ """Return a DB-API-like connection for this engine.
63
+
64
+ The caller owns the connection and must close it. Prefer
65
+ :meth:`session`, which does that for you — a bare ``connect()`` is only
66
+ correct when the statement cannot run inside a transaction (``VACUUM``)
67
+ or when the connection must outlive one block.
68
+ """
69
+
70
+ @contextmanager
71
+ def session(self) -> Iterator[Any]:
72
+ """Transactional connection that is always closed.
73
+
74
+ ``with sqlite3.connect(...) as conn`` commits or rolls back — it does
75
+ **not** close. Every site that relied on that leaked a file descriptor
76
+ until the connection was garbage collected, which CPython usually did
77
+ promptly enough to hide it. Anything holding a frame alive (a coverage
78
+ tracer, a profiler, a logged traceback) delays that collection and the
79
+ descriptors accumulate until the process hits ``EMFILE``.
80
+
81
+ This yields inside ``with conn`` so commit/rollback semantics are
82
+ unchanged, then closes in ``finally``.
83
+ """
84
+ conn = self.connect()
85
+ try:
86
+ with conn:
87
+ yield conn
88
+ finally:
89
+ conn.close()
62
90
 
63
91
  @abstractmethod
64
92
  def backup(self, destination: Path) -> Dict[str, Any]:
@@ -3,6 +3,7 @@
3
3
  from __future__ import annotations
4
4
 
5
5
  import sqlite3
6
+ from contextlib import closing
6
7
  from dataclasses import dataclass
7
8
  from pathlib import Path
8
9
  from typing import Any, Dict, List
@@ -52,7 +53,7 @@ class SQLiteToPostgresMigrator:
52
53
  def plan(self) -> Dict[str, Any]:
53
54
  if not self.sqlite_path.exists():
54
55
  raise FileNotFoundError(f"SQLite brain database not found: {self.sqlite_path}")
55
- with sqlite3.connect(str(self.sqlite_path)) as conn:
56
+ with closing(sqlite3.connect(str(self.sqlite_path))) as conn, conn:
56
57
  conn.row_factory = sqlite3.Row
57
58
  table_names = [
58
59
  row["name"]
@@ -119,7 +120,7 @@ class SQLiteToPostgresMigrator:
119
120
  schema = _quote_ident(self.target.config.schema)
120
121
  copied: Dict[str, int] = {}
121
122
  self.target.initialize()
122
- with sqlite3.connect(str(self.sqlite_path)) as src, self.target.connect() as dst:
123
+ with closing(sqlite3.connect(str(self.sqlite_path))) as src, self.target.session() as dst:
123
124
  src.row_factory = sqlite3.Row
124
125
  with dst.cursor() as cur:
125
126
  for table in plan["tables"]:
@@ -45,7 +45,7 @@ class PostgresEngine(StorageEngine):
45
45
 
46
46
  def initialize(self) -> Dict[str, Any]:
47
47
  schema = _quote_ident(self.config.schema)
48
- with self.connect() as conn:
48
+ with self.session() as conn:
49
49
  with conn.cursor() as cur:
50
50
  cur.execute("CREATE EXTENSION IF NOT EXISTS vector")
51
51
  cur.execute(f"CREATE SCHEMA IF NOT EXISTS {schema}")
@@ -85,7 +85,7 @@ class PostgresEngine(StorageEngine):
85
85
 
86
86
  def capabilities(self) -> StorageCapabilities:
87
87
  try:
88
- with self.connect() as conn:
88
+ with self.session() as conn:
89
89
  with conn.cursor() as cur:
90
90
  cur.execute("SELECT extname FROM pg_extension WHERE extname='vector'")
91
91
  pgvector = cur.fetchone() is not None
@@ -9,9 +9,11 @@ from __future__ import annotations
9
9
 
10
10
  import shutil
11
11
  import sqlite3
12
+ from contextlib import closing
12
13
  from pathlib import Path
13
14
  from typing import Any, Dict, Optional
14
15
 
16
+ from ..quiet import quiet
15
17
  from .base import StorageCapabilities, StorageEngine
16
18
 
17
19
 
@@ -23,7 +25,7 @@ def _load_sqlite_vec(conn: sqlite3.Connection) -> tuple[bool, Optional[str]]:
23
25
  try:
24
26
  conn.enable_load_extension(True)
25
27
  except Exception:
26
- pass
28
+ quiet()
27
29
  try:
28
30
  sqlite_vec.load(conn)
29
31
  except Exception as exc:
@@ -53,7 +55,7 @@ class SQLiteEngine(StorageEngine):
53
55
  return conn
54
56
 
55
57
  def initialize(self) -> Dict[str, Any]:
56
- with self.connect() as conn:
58
+ with self.session() as conn:
57
59
  conn.execute(
58
60
  """
59
61
  CREATE TABLE IF NOT EXISTS storage_meta (
@@ -86,7 +88,7 @@ class SQLiteEngine(StorageEngine):
86
88
  )
87
89
  # Probe on demand so status is accurate even before the graph opens.
88
90
  try:
89
- with self.connect():
91
+ with self.session():
90
92
  pass
91
93
  except Exception as exc:
92
94
  return StorageCapabilities(
@@ -122,7 +124,7 @@ class SQLiteEngine(StorageEngine):
122
124
  def backup(self, destination: Path) -> Dict[str, Any]:
123
125
  dest = Path(destination)
124
126
  dest.parent.mkdir(parents=True, exist_ok=True)
125
- with self.connect() as src, sqlite3.connect(str(dest)) as dst:
127
+ with self.session() as src, closing(sqlite3.connect(str(dest))) as dst:
126
128
  src.backup(dst)
127
129
  return {"engine": self.name, "path": str(dest), "bytes": dest.stat().st_size}
128
130
 
@@ -26,10 +26,12 @@ from __future__ import annotations
26
26
  from dataclasses import dataclass, field
27
27
  from typing import Any, Callable, Dict, List, Optional
28
28
 
29
- from lattice_brain.runtime.contracts import runtime_boundary_contract, workflow_run_contract
29
+ from lattice_brain.runtime.contracts import (
30
+ runtime_boundary_contract,
31
+ workflow_run_contract,
32
+ )
30
33
  from lattice_brain.utils import now_iso as _now
31
34
 
32
-
33
35
  WORKFLOW_ENGINE_VERSION = "2.2.0"
34
36
 
35
37
  # The node vocabulary a workflow can be built from. ``trigger`` and ``output``
@@ -1,3 +1,3 @@
1
1
  """Lattice AI - modular server package."""
2
2
 
3
- __version__ = "9.9.9"
3
+ __version__ = "10.2.0"
@@ -223,7 +223,7 @@ def create_admin_router(
223
223
  async def admin_roles(request: Request):
224
224
  _, users = require_admin(request)
225
225
  counts: Dict[str, int] = defaultdict(int)
226
- for email, user in users.items():
226
+ for email, user in users.items(): # noqa: B007 — the key is the payload; the loop var documents the shape
227
227
  role = (get_user_role(email, users) or "user").lower()
228
228
  counts[role] += 1
229
229
  matrix = policy_matrix() if policy_matrix else [
@@ -16,7 +16,6 @@ from pydantic import BaseModel
16
16
 
17
17
  from latticeai.api.ui_redirects import app_redirect
18
18
 
19
-
20
19
  _CORE_EXECUTION_ROLES = ["planner", "executor", "reviewer"]
21
20
  _MEMORY_GROUNDED_ROLES = ["researcher", *_CORE_EXECUTION_ROLES]
22
21
 
@@ -63,8 +62,11 @@ def create_agents_router(
63
62
  agent_runtime: Any = None,
64
63
  run_executor: Any = None,
65
64
  ) -> APIRouter:
65
+ from lattice_brain.runtime.agent_runtime import (
66
+ AgentRuntime,
67
+ AgentRuntimeUnavailable,
68
+ )
66
69
  from lattice_brain.runtime.multi_agent import AGENT_ROLES, ROLE_AGENT_IDS
67
- from lattice_brain.runtime.agent_runtime import AgentRuntime, AgentRuntimeUnavailable
68
70
 
69
71
  # Single AgentRuntime boundary: the router (and via it, the frontend) talks
70
72
  # to this façade instead of reaching into the orchestrator/store directly.
@@ -13,12 +13,16 @@ from fastapi import APIRouter, HTTPException, Request
13
13
  from fastapi.responses import JSONResponse, RedirectResponse
14
14
  from pydantic import BaseModel
15
15
 
16
- from latticeai.core.users import normalize_email
17
16
  from latticeai.core.oidc import (
18
17
  OIDCValidationError,
18
+ )
19
+ from latticeai.core.oidc import (
19
20
  fetch_jwks as _default_fetch_jwks,
21
+ )
22
+ from latticeai.core.oidc import (
20
23
  verify_id_token as _default_verify_id_token,
21
24
  )
25
+ from latticeai.core.users import normalize_email
22
26
 
23
27
 
24
28
  class UserRegister(BaseModel):
@@ -20,6 +20,7 @@ from typing import Any, Callable, Optional
20
20
  from fastapi import APIRouter, HTTPException, Request
21
21
  from pydantic import BaseModel
22
22
 
23
+ from latticeai.core.quiet import quiet
23
24
  from latticeai.services.automation_execution import (
24
25
  build_last_execution,
25
26
  dry_run_report,
@@ -162,7 +163,7 @@ def create_automation_intelligence_router(
162
163
  workspace_id=scope,
163
164
  )
164
165
  except Exception: # noqa: BLE001 — surfacing must never undo the run
165
- pass
166
+ quiet()
166
167
 
167
168
  @router.post("/api/automation/run-now")
168
169
  async def automation_run_now(req: AutomationRunNowRequest, request: Request):
@@ -26,8 +26,9 @@ from urllib.parse import SplitResult, urljoin, urlsplit, urlunsplit
26
26
  from fastapi import APIRouter, HTTPException, Request
27
27
  from pydantic import BaseModel
28
28
 
29
- from latticeai import __version__
30
29
  from lattice_brain.ingestion import IngestionItem, capture_quality_verdict
30
+ from latticeai import __version__
31
+ from latticeai.core.quiet import quiet
31
32
 
32
33
  MAX_TAB_BYTES = 4 * 1024 * 1024 # 4 MB per captured tab payload
33
34
  MAX_URL_FETCH_BYTES = 4 * 1024 * 1024 # 4 MB cap on a fetched page
@@ -95,7 +96,7 @@ def extract_readable_text(html: str) -> Tuple[str, str]:
95
96
  try:
96
97
  parser.feed(html or "")
97
98
  except Exception: # noqa: BLE001 — malformed HTML must still yield best-effort text
98
- pass
99
+ quiet()
99
100
  return parser.title.strip(), parser.text()
100
101
 
101
102
 
@@ -16,7 +16,6 @@ from fastapi import APIRouter, HTTPException, Request
16
16
  from fastapi.responses import JSONResponse, StreamingResponse
17
17
 
18
18
  from latticeai.api.chat_agent_http import AgentHTTPController
19
- from latticeai.core.project_sessions import ProjectSessionStore
20
19
  from latticeai.api.chat_contracts import (
21
20
  AgentEvalRequest,
22
21
  AgentRequest,
@@ -46,9 +45,14 @@ from latticeai.api.chat_helpers import (
46
45
  workspace_scope_from_request,
47
46
  )
48
47
  from latticeai.api.chat_history import HistoryRouteDependencies, register_history_routes
49
- from latticeai.core.run_store import AgentRunStore
48
+ from latticeai.api.chat_hybrid import (
49
+ maybe_hybrid_stream_response,
50
+ resolve_request_network_mode,
51
+ )
50
52
  from latticeai.api.chat_intents import ChatIntentController
51
53
  from latticeai.api.chat_stream import stream_chat
54
+ from latticeai.core.project_sessions import ProjectSessionStore
55
+ from latticeai.core.run_store import AgentRunStore
52
56
  from latticeai.services.app_context import AppContext
53
57
  from latticeai.services.chat_service import ChatService
54
58
  from latticeai.services.tool_dispatch import build_agent_runtime, enforce_tool_policy
@@ -61,7 +65,6 @@ from latticeai.tools import (
61
65
  network_status,
62
66
  )
63
67
 
64
-
65
68
  __all__ = [
66
69
  "AgentEvalRequest",
67
70
  "AgentRequest",
@@ -163,8 +166,6 @@ def create_chat_router(context: AppContext) -> APIRouter:
163
166
 
164
167
  agent_runtime = context.chat_agent_runtime
165
168
  if agent_runtime is None:
166
- # Keep construction here so the historical monkeypatch/import seam on
167
- # latticeai.api.chat.build_agent_runtime remains valid.
168
169
  agent_runtime = build_agent_runtime(
169
170
  model_router=model_router,
170
171
  execute_tool=execute_tool,
@@ -198,8 +199,6 @@ def create_chat_router(context: AppContext) -> APIRouter:
198
199
  if context.data_dir
199
200
  else None
200
201
  ),
201
- # Multi-turn project loop (v9.9.6): optional — without a data dir the
202
- # agent runs exactly as before, one run at a time with no project state.
203
202
  project_sessions=(
204
203
  ProjectSessionStore(Path(context.data_dir) / "project_sessions")
205
204
  if context.data_dir
@@ -293,8 +292,6 @@ def create_chat_router(context: AppContext) -> APIRouter:
293
292
  selected_model_id = request_model(req.model)
294
293
  file_intent = is_file_action_request(req.message)
295
294
  if file_intent and context.funnel_metrics is not None:
296
- # UX funnel (backlog #16): one file-intent request counted once,
297
- # regardless of which delivery path handles it below.
298
295
  context.funnel_metrics.increment("file_requests")
299
296
  if file_intent:
300
297
  direct_response = await intent_controller.direct_file_action(
@@ -386,9 +383,6 @@ def create_chat_router(context: AppContext) -> APIRouter:
386
383
  if context_trace is not None and isinstance(trace_seed, dict):
387
384
  trace_seed["context_assembly"] = context_trace
388
385
 
389
- # v9.8.0 honest RAG signal: how well the graph grounded this answer.
390
- # Rides the same channel as sources/evidence (the answer trace) and is
391
- # additionally exposed top-level on both response shapes below.
392
386
  context_quality = build_context_quality(
393
387
  req.message,
394
388
  knowledge_graph=context.knowledge_graph
@@ -401,8 +395,6 @@ def create_chat_router(context: AppContext) -> APIRouter:
401
395
  if context.funnel_metrics is not None and int(
402
396
  context_quality.get("nodes") or 0
403
397
  ) > 0:
404
- # UX funnel (backlog #16): a grounded answer is "first value" —
405
- # the TTFV clock ends on the first successful recall.
406
398
  context.funnel_metrics.record_recall_success()
407
399
 
408
400
  history_message = (
@@ -428,6 +420,28 @@ def create_chat_router(context: AppContext) -> APIRouter:
428
420
  if document_response is not None:
429
421
  return document_response
430
422
 
423
+ # Hybrid Phase 2: cloud_allowed → minimal KG context → cloud stream.
424
+ network_mode = resolve_request_network_mode(
425
+ request_mode=getattr(req, "network_mode", None),
426
+ user_email=effective_email,
427
+ workspace_id=workspace_id,
428
+ )
429
+ hybrid_response = maybe_hybrid_stream_response(
430
+ req=req,
431
+ mode=network_mode,
432
+ knowledge_graph=context.knowledge_graph,
433
+ enable_graph=context.enable_graph,
434
+ effective_email=effective_email,
435
+ workspace_id=workspace_id,
436
+ history_meta=history_meta,
437
+ history_user=history_user,
438
+ chat_service=chat_service,
439
+ notify=notify_chat_message,
440
+ model_id=selected_model_id,
441
+ )
442
+ if hybrid_response is not None:
443
+ return hybrid_response
444
+
431
445
  if req.stream:
432
446
  recent_context = recent_chat_context(
433
447
  user_email=effective_email,
@@ -493,9 +507,6 @@ def create_chat_router(context: AppContext) -> APIRouter:
493
507
  req.image_data,
494
508
  )
495
509
  response_text = str(result)
496
- # Answer-citation binding (backlog #11): annotate — never block — how
497
- # the answer relates to the retrieved sources. Recorded on the trace
498
- # so the Review/proof surfaces see the same verdict as the client.
499
510
  grounding = assess_answer_grounding(
500
511
  response_text,
501
512
  trace=trace_seed if isinstance(trace_seed, dict) else None,
@@ -17,10 +17,19 @@ from fastapi import APIRouter, HTTPException, Request
17
17
  from fastapi.responses import StreamingResponse
18
18
 
19
19
  from lattice_brain.runtime.hooks import dispatch_tool
20
- from latticeai.api.chat_contracts import AgentEvalRequest, AgentRequest, AgentResumeRequest
21
- from latticeai.api.chat_helpers import _LANG_HINT, detect_language, workspace_scope_from_request
20
+ from latticeai.api.chat_contracts import (
21
+ AgentEvalRequest,
22
+ AgentRequest,
23
+ AgentResumeRequest,
24
+ )
25
+ from latticeai.api.chat_helpers import (
26
+ _LANG_HINT,
27
+ detect_language,
28
+ workspace_scope_from_request,
29
+ )
22
30
  from latticeai.api.chat_stream import agent_live_stream
23
31
  from latticeai.core.agent import AgentRunContext, AgentState, normalize_plan
32
+ from latticeai.core.quiet import quiet
24
33
  from latticeai.core.run_explain import explain_run
25
34
  from latticeai.core.run_store import (
26
35
  AgentRunStore,
@@ -139,7 +148,7 @@ class AgentHTTPController:
139
148
  try:
140
149
  done.result()
141
150
  except asyncio.CancelledError:
142
- pass
151
+ quiet()
143
152
  except Exception as exc:
144
153
  logging.warning("background chat task failed: %s", exc)
145
154
 
@@ -188,7 +197,12 @@ class AgentHTTPController:
188
197
  self.hooks,
189
198
  action_name,
190
199
  case_input,
191
- lambda: self.execute_tool(action_name, case_input),
200
+ # Bound as defaults: the lambda is invoked inside this
201
+ # iteration today, so late binding is harmless — until
202
+ # someone defers it and every case runs the last input.
203
+ lambda action_name=action_name, case_input=case_input: self.execute_tool(
204
+ action_name, case_input
205
+ ),
192
206
  source="eval",
193
207
  )
194
208
  criteria = case.get("pass_criteria", "")
@@ -470,7 +484,7 @@ class AgentHTTPController:
470
484
  try:
471
485
  self.run_store.delete(run_id)
472
486
  except Exception: # noqa: BLE001 — hygiene only
473
- pass
487
+ quiet()
474
488
 
475
489
  def pending_approvals(self, request: Request) -> Dict[str, Any]:
476
490
  """Unexpired paused runs for the current user (memory ∪ disk).
@@ -525,7 +539,7 @@ class AgentHTTPController:
525
539
  try:
526
540
  self.run_store.delete(run_id)
527
541
  except Exception: # noqa: BLE001
528
- pass
542
+ quiet()
529
543
  raise HTTPException(
530
544
  status_code=410,
531
545
  detail={
@@ -724,7 +738,7 @@ class AgentHTTPController:
724
738
  try:
725
739
  self.run_store.delete(req.context_id)
726
740
  except Exception: # noqa: BLE001
727
- pass
741
+ quiet()
728
742
  raise HTTPException(
729
743
  status_code=404,
730
744
  detail="Agent context not found or expired. Start a new request.",
@@ -791,7 +805,7 @@ class AgentHTTPController:
791
805
  try:
792
806
  self.run_store.delete(req.run_id or "")
793
807
  except Exception: # noqa: BLE001
794
- pass
808
+ quiet()
795
809
  raise HTTPException(
796
810
  status_code=410,
797
811
  detail={
@@ -21,6 +21,10 @@ class ChatRequest(BaseModel):
21
21
  user_nickname: Optional[str] = None
22
22
  image_data: Optional[str] = None
23
23
  allow_file_context: bool = False
24
+ # Hybrid Phase 1: optional per-request override of NetworkBoundaryMode.
25
+ # When omitted, the persisted dial (user/workspace/default) is used.
26
+ # Values: "local_only" | "cloud_allowed"
27
+ network_mode: Optional[str] = None
24
28
 
25
29
 
26
30
  class AgentRequest(BaseModel):
@@ -20,7 +20,11 @@ from latticeai.core.context_builder import (
20
20
  format_sources_footnote,
21
21
  retrieve_context_for_generation,
22
22
  )
23
- from latticeai.core.document_generator import DocumentGenerationSession, detect_document_intent
23
+ from latticeai.core.document_generator import (
24
+ DocumentGenerationSession,
25
+ detect_document_intent,
26
+ )
27
+ from latticeai.core.quiet import quiet
24
28
 
25
29
 
26
30
  def extract_screenshot_context(image_data: Optional[str]) -> str:
@@ -77,7 +81,7 @@ def extract_screenshot_context(image_data: Optional[str]) -> str:
77
81
  try:
78
82
  Path(temp_path).unlink()
79
83
  except OSError:
80
- pass
84
+ quiet()
81
85
  return "\n".join(lines)
82
86
 
83
87
 
@@ -0,0 +1,82 @@
1
+ """Chat-path helpers for hybrid cloud branching (Phase 2).
2
+
3
+ Keeps the main chat router thin: resolve network mode, decide whether to
4
+ enter the hybrid stream, and build the StreamingResponse.
5
+ """
6
+
7
+ from __future__ import annotations
8
+
9
+ from typing import Any, Dict, Optional
10
+
11
+ from fastapi.responses import StreamingResponse
12
+
13
+ from latticeai.core.network_boundary import (
14
+ NetworkBoundaryMode,
15
+ normalize_network_mode,
16
+ )
17
+ from latticeai.runtime.network_boundary_wiring import resolve_active_network_mode
18
+ from latticeai.services.hybrid_chat import stream_hybrid_cloud_turn
19
+
20
+
21
+ def resolve_request_network_mode(
22
+ *,
23
+ request_mode: Optional[str],
24
+ user_email: Optional[str],
25
+ workspace_id: Optional[str],
26
+ ) -> NetworkBoundaryMode:
27
+ """Per-request override wins; otherwise use the persisted dial."""
28
+ if request_mode:
29
+ return normalize_network_mode(request_mode)
30
+ return normalize_network_mode(
31
+ resolve_active_network_mode(user_email=user_email, workspace_id=workspace_id)
32
+ )
33
+
34
+
35
+ def maybe_hybrid_stream_response(
36
+ *,
37
+ req: Any,
38
+ mode: NetworkBoundaryMode,
39
+ knowledge_graph: Any,
40
+ enable_graph: bool,
41
+ effective_email: Optional[str],
42
+ workspace_id: Optional[str],
43
+ history_meta: Dict[str, Any],
44
+ history_user: Dict[str, Any],
45
+ chat_service: Any,
46
+ notify: Any,
47
+ model_id: Optional[str],
48
+ ) -> Optional[StreamingResponse]:
49
+ """Return a StreamingResponse when cloud path should run; else None."""
50
+ if mode != NetworkBoundaryMode.CLOUD_ALLOWED:
51
+ return None
52
+ if not (enable_graph and knowledge_graph is not None):
53
+ # Without a graph there is nothing minimal to send; fall back to local.
54
+ return None
55
+
56
+ return StreamingResponse(
57
+ stream_hybrid_cloud_turn(
58
+ user_message=req.message,
59
+ knowledge_graph=knowledge_graph,
60
+ mode=mode,
61
+ workspace_id=workspace_id,
62
+ user_email=effective_email,
63
+ model=model_id,
64
+ chat_service=chat_service,
65
+ history_meta=history_meta,
66
+ history_user=history_user,
67
+ notify=notify,
68
+ source=req.source,
69
+ ),
70
+ media_type="text/event-stream",
71
+ headers={
72
+ "X-Model": model_id or "cloud",
73
+ "X-Network-Mode": mode.value,
74
+ "X-Hybrid": "1",
75
+ },
76
+ )
77
+
78
+
79
+ __all__ = [
80
+ "resolve_request_network_mode",
81
+ "maybe_hybrid_stream_response",
82
+ ]