ltcai 10.0.0 → 10.3.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 (214) hide show
  1. package/README.md +43 -32
  2. package/docs/CHANGELOG.md +204 -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/MYPY_BACKLOG.md +98 -0
  7. package/docs/ONBOARDING.md +1 -1
  8. package/docs/OPERATIONS.md +1 -1
  9. package/docs/TRUST_MODEL.md +1 -1
  10. package/docs/WHY_LATTICE.md +1 -1
  11. package/docs/kg-schema.md +1 -1
  12. package/lattice_brain/__init__.py +1 -1
  13. package/lattice_brain/archive.py +5 -4
  14. package/lattice_brain/conversations.py +14 -3
  15. package/lattice_brain/embeddings.py +12 -2
  16. package/lattice_brain/graph/_kg_common.py +5 -5
  17. package/lattice_brain/graph/_kg_fsutil.py +5 -4
  18. package/lattice_brain/graph/discovery.py +4 -3
  19. package/lattice_brain/graph/discovery_index.py +0 -1
  20. package/lattice_brain/graph/documents.py +3 -2
  21. package/lattice_brain/graph/fusion.py +4 -0
  22. package/lattice_brain/graph/ingest.py +12 -2
  23. package/lattice_brain/graph/projection.py +3 -2
  24. package/lattice_brain/graph/provenance.py +5 -3
  25. package/lattice_brain/graph/rerank.py +4 -1
  26. package/lattice_brain/graph/retrieval.py +0 -1
  27. package/lattice_brain/graph/retrieval_docgen.py +0 -1
  28. package/lattice_brain/graph/retrieval_reads.py +0 -1
  29. package/lattice_brain/graph/retrieval_vector.py +0 -1
  30. package/lattice_brain/graph/schema.py +4 -3
  31. package/lattice_brain/graph/store.py +18 -4
  32. package/lattice_brain/graph/write_master.py +46 -1
  33. package/lattice_brain/ingestion.py +4 -1
  34. package/lattice_brain/portability.py +5 -2
  35. package/lattice_brain/quality.py +12 -5
  36. package/lattice_brain/quiet.py +43 -0
  37. package/lattice_brain/runtime/agent_runtime.py +19 -10
  38. package/lattice_brain/runtime/hooks.py +3 -2
  39. package/lattice_brain/runtime/multi_agent.py +2 -3
  40. package/lattice_brain/sensitivity.py +94 -0
  41. package/lattice_brain/storage/base.py +30 -2
  42. package/lattice_brain/storage/migration.py +3 -2
  43. package/lattice_brain/storage/postgres.py +2 -2
  44. package/lattice_brain/storage/sqlite.py +6 -4
  45. package/lattice_brain/workflow.py +4 -2
  46. package/latticeai/__init__.py +1 -1
  47. package/latticeai/api/admin.py +1 -1
  48. package/latticeai/api/agents.py +4 -2
  49. package/latticeai/api/auth.py +5 -1
  50. package/latticeai/api/automation_intelligence.py +2 -1
  51. package/latticeai/api/browser.py +3 -2
  52. package/latticeai/api/chat.py +28 -17
  53. package/latticeai/api/chat_agent_http.py +22 -8
  54. package/latticeai/api/chat_contracts.py +4 -0
  55. package/latticeai/api/chat_documents.py +6 -2
  56. package/latticeai/api/chat_hybrid.py +82 -0
  57. package/latticeai/api/computer_use.py +8 -3
  58. package/latticeai/api/knowledge_graph.py +1 -1
  59. package/latticeai/api/mcp.py +4 -4
  60. package/latticeai/api/models.py +5 -2
  61. package/latticeai/api/network_boundary.py +220 -0
  62. package/latticeai/api/permissions.py +0 -1
  63. package/latticeai/api/realtime.py +1 -1
  64. package/latticeai/api/security_dashboard.py +1 -1
  65. package/latticeai/api/setup.py +16 -3
  66. package/latticeai/api/static_routes.py +2 -1
  67. package/latticeai/api/tools.py +12 -8
  68. package/latticeai/api/voice_capture.py +3 -1
  69. package/latticeai/api/workflow_designer.py +1 -1
  70. package/latticeai/api/workspace.py +1 -2
  71. package/latticeai/app_factory.py +174 -145
  72. package/latticeai/cli/entrypoint.py +6 -4
  73. package/latticeai/core/agent.py +55 -495
  74. package/latticeai/core/agent_eval.py +2 -2
  75. package/latticeai/core/agent_helpers.py +493 -0
  76. package/latticeai/core/agent_prompts.py +0 -1
  77. package/latticeai/core/agent_registry.py +3 -1
  78. package/latticeai/core/agent_state.py +41 -0
  79. package/latticeai/core/audit.py +1 -1
  80. package/latticeai/core/builtin_hooks.py +2 -1
  81. package/latticeai/core/config.py +0 -1
  82. package/latticeai/core/embedding_providers.py +12 -1
  83. package/latticeai/core/file_generation.py +3 -0
  84. package/latticeai/core/invitations.py +4 -1
  85. package/latticeai/core/io_utils.py +3 -1
  86. package/latticeai/core/legacy_compatibility.py +1 -2
  87. package/latticeai/core/local_embeddings.py +12 -1
  88. package/latticeai/core/marketplace.py +1 -2
  89. package/latticeai/core/mcp_registry.py +0 -1
  90. package/latticeai/core/model_compat.py +1 -1
  91. package/latticeai/core/model_resolution.py +1 -1
  92. package/latticeai/core/network_boundary.py +168 -0
  93. package/latticeai/core/oidc.py +3 -0
  94. package/latticeai/core/permission_mode.py +6 -6
  95. package/latticeai/core/plugins.py +3 -2
  96. package/latticeai/core/policy.py +0 -1
  97. package/latticeai/core/quiet.py +84 -0
  98. package/latticeai/core/realtime.py +3 -2
  99. package/latticeai/core/run_store.py +4 -2
  100. package/latticeai/core/security.py +4 -0
  101. package/latticeai/core/users.py +5 -3
  102. package/latticeai/core/workspace_graph_trace.py +3 -0
  103. package/latticeai/core/workspace_os.py +65 -273
  104. package/latticeai/core/workspace_os_constants.py +126 -0
  105. package/latticeai/core/workspace_os_state.py +180 -0
  106. package/latticeai/core/workspace_os_utils.py +3 -1
  107. package/latticeai/core/workspace_permissions.py +1 -0
  108. package/latticeai/core/workspace_snapshots.py +3 -1
  109. package/latticeai/core/workspace_timeline.py +3 -1
  110. package/latticeai/integrations/telegram_bot.py +25 -16
  111. package/latticeai/models/router.py +6 -3
  112. package/latticeai/runtime/access_runtime.py +3 -1
  113. package/latticeai/runtime/audit_runtime.py +3 -2
  114. package/latticeai/runtime/chat_wiring.py +4 -1
  115. package/latticeai/runtime/history_runtime.py +1 -1
  116. package/latticeai/runtime/history_writer.py +138 -0
  117. package/latticeai/runtime/lifespan_runtime.py +3 -1
  118. package/latticeai/runtime/network_boundary_wiring.py +124 -0
  119. package/latticeai/runtime/persistence_runtime.py +3 -1
  120. package/latticeai/runtime/router_registration.py +11 -1
  121. package/latticeai/services/architecture_readiness.py +1 -2
  122. package/latticeai/services/change_proposals.py +2 -1
  123. package/latticeai/services/cloud_egress_audit.py +85 -0
  124. package/latticeai/services/cloud_extraction.py +129 -0
  125. package/latticeai/services/cloud_streaming.py +266 -0
  126. package/latticeai/services/cloud_token_guard.py +84 -0
  127. package/latticeai/services/command_center.py +2 -1
  128. package/latticeai/services/folder_watch.py +3 -0
  129. package/latticeai/services/funnel_metrics.py +3 -1
  130. package/latticeai/services/hybrid_chat.py +265 -0
  131. package/latticeai/services/hybrid_context.py +228 -0
  132. package/latticeai/services/hybrid_policy.py +178 -0
  133. package/latticeai/services/memory_service.py +1 -1
  134. package/latticeai/services/model_catalog.py +10 -1
  135. package/latticeai/services/model_engines.py +35 -14
  136. package/latticeai/services/model_loading.py +3 -2
  137. package/latticeai/services/model_runtime.py +68 -17
  138. package/latticeai/services/multimodal_streaming.py +123 -0
  139. package/latticeai/services/network_boundary_service.py +154 -0
  140. package/latticeai/services/openai_compatible_adapter.py +100 -0
  141. package/latticeai/services/p_reinforce.py +4 -0
  142. package/latticeai/services/platform_runtime.py +9 -4
  143. package/latticeai/services/process_audit.py +0 -1
  144. package/latticeai/services/product_readiness.py +1 -1
  145. package/latticeai/services/run_executor.py +3 -2
  146. package/latticeai/services/search_service.py +1 -4
  147. package/latticeai/services/setup_detection.py +2 -1
  148. package/latticeai/services/tool_dispatch.py +7 -2
  149. package/latticeai/services/upload_service.py +2 -1
  150. package/latticeai/setup/auto_setup.py +10 -7
  151. package/latticeai/setup/wizard.py +15 -11
  152. package/latticeai/tools/__init__.py +3 -3
  153. package/latticeai/tools/commands.py +7 -7
  154. package/latticeai/tools/computer.py +3 -2
  155. package/latticeai/tools/documents.py +10 -9
  156. package/latticeai/tools/filesystem.py +7 -4
  157. package/latticeai/tools/knowledge.py +2 -0
  158. package/latticeai/tools/local_files.py +1 -1
  159. package/latticeai/tools/network.py +2 -1
  160. package/package.json +7 -3
  161. package/scripts/bench_agent_smoke.py +0 -1
  162. package/scripts/bench_models.py +0 -1
  163. package/scripts/brain_quality_eval.py +7 -2
  164. package/scripts/bump_version.py +2 -1
  165. package/scripts/check_current_release_docs.mjs +1 -1
  166. package/scripts/migrate_brain_storage.py +5 -1
  167. package/scripts/profile_kg.py +2 -7
  168. package/scripts/verify_hf_model_registry.py +2 -2
  169. package/src-tauri/Cargo.lock +1 -1
  170. package/src-tauri/Cargo.toml +1 -1
  171. package/src-tauri/tauri.conf.json +1 -1
  172. package/static/app/asset-manifest.json +37 -37
  173. package/static/app/assets/{Act-BtCREeN1.js → Act-DrKIGZeJ.js} +1 -1
  174. package/static/app/assets/{AdminConsole-TPeeN18T.js → AdminConsole-CEV1VefA.js} +1 -1
  175. package/static/app/assets/{Brain-BKs6JAp0.js → Brain-BZSXnwWU.js} +1 -1
  176. package/static/app/assets/{BrainHome-BPGOvSd6.js → BrainHome-CEBd86k6.js} +1 -1
  177. package/static/app/assets/{BrainSignals-CtzQZ15J.js → BrainSignals-O4s6R7av.js} +1 -1
  178. package/static/app/assets/{Capture-1_NaHWqB.js → Capture-AGbOxsg-.js} +1 -1
  179. package/static/app/assets/{CommandPalette-pqvQOXe4.js → CommandPalette-CeqBjCn2.js} +1 -1
  180. package/static/app/assets/Library-CXnbNiQW.js +1 -0
  181. package/static/app/assets/{LivingBrain-DlQ20Q75.js → LivingBrain-D9Iei2ZJ.js} +1 -1
  182. package/static/app/assets/{ProductFlow-BZvGDRi_.js → ProductFlow-JlXiDfMM.js} +1 -1
  183. package/static/app/assets/{ReviewCard-BWgI0D2s.js → ReviewCard-QQly2bTV.js} +1 -1
  184. package/static/app/assets/System-Bp9ZY4ER.js +1 -0
  185. package/static/app/assets/{activity-Dlfk8YC7.js → activity-CxRPOQ2F.js} +1 -1
  186. package/static/app/assets/{bot-CDvUB76P.js → bot-1bcsWB2s.js} +1 -1
  187. package/static/app/assets/{brain-xczrohrt.js → brain-CxaeKCRM.js} +1 -1
  188. package/static/app/assets/{button-SOdH3Oyf.js → button-ZYFqGan7.js} +1 -1
  189. package/static/app/assets/{circle-pause-CG1ythH4.js → circle-pause-B5NOIqvk.js} +1 -1
  190. package/static/app/assets/{circle-play-HXwvjS6W.js → circle-play-BiJVFXf7.js} +1 -1
  191. package/static/app/assets/{cpu-B0d-rGyk.js → cpu-Vvq0OKTr.js} +1 -1
  192. package/static/app/assets/{download-BGIkTQL6.js → download-SEcXzkx1.js} +1 -1
  193. package/static/app/assets/{folder-open-Dst_Z0_K.js → folder-open-Dw1ff_Qa.js} +1 -1
  194. package/static/app/assets/{hard-drive-D53MsWkV.js → hard-drive-B00fR7_r.js} +1 -1
  195. package/static/app/assets/index-CEkL4_M-.css +2 -0
  196. package/static/app/assets/{index-C_IrlQMV.js → index-Dm9VXl1o.js} +3 -3
  197. package/static/app/assets/{input-C5m0riF6.js → input-DtdWYQEa.js} +1 -1
  198. package/static/app/assets/{network-C5a-E5iS.js → network-CpjBu0kI.js} +1 -1
  199. package/static/app/assets/{primitives-vNXYf58F.js → primitives-CwsudBwc.js} +1 -1
  200. package/static/app/assets/search-CwEJWBEA.js +1 -0
  201. package/static/app/assets/{shield-alert-Cc-WVXqN.js → shield-alert-CpjBc-gQ.js} +1 -1
  202. package/static/app/assets/{textarea-BkZ0EqVO.js → textarea-DUPyjHQs.js} +1 -1
  203. package/static/app/assets/{useFocusTrap-CTtKbAOU.js → useFocusTrap-BWcMtCWH.js} +1 -1
  204. package/static/app/assets/{useQuery-Dx1fi4Wu.js → useQuery-Dg5fPqTS.js} +1 -1
  205. package/static/app/assets/{users-BFpQXtEF.js → users-BgcdTQGM.js} +1 -1
  206. package/static/app/assets/{utils-BA_lmW3J.js → utils-a3YXFtEO.js} +2 -2
  207. package/static/app/assets/workspace-BMUEiXUm.js +1 -0
  208. package/static/app/index.html +4 -4
  209. package/static/sw.js +1 -1
  210. package/static/app/assets/Library-DhvoPvC7.js +0 -1
  211. package/static/app/assets/System-CSMdYLMy.js +0 -1
  212. package/static/app/assets/index-FxDusbr0.css +0 -2
  213. package/static/app/assets/search-DhbSgW6m.js +0 -1
  214. package/static/app/assets/workspace-DBPB0jkX.js +0 -1
@@ -30,12 +30,8 @@ from __future__ import annotations
30
30
 
31
31
  from typing import Any, Callable, Dict, List, Optional
32
32
 
33
- from .multi_agent import (
34
- AGENT_ROLES,
35
- CORE_PIPELINE,
36
- MULTI_AGENT_VERSION,
37
- ROLE_AGENT_IDS,
38
- )
33
+ from ..quiet import quiet
34
+ from ..utils import now_iso as _now
39
35
  from .contracts import (
40
36
  contract_view,
41
37
  contract_views,
@@ -44,11 +40,18 @@ from .contracts import (
44
40
  run_record_contract,
45
41
  runtime_boundary_contract,
46
42
  )
43
+ from .multi_agent import (
44
+ AGENT_ROLES,
45
+ CORE_PIPELINE,
46
+ MULTI_AGENT_VERSION,
47
+ ROLE_AGENT_IDS,
48
+ )
47
49
  from .statuses import (
48
50
  RUN_ACTIVE_STATUSES as _ACTIVE_STATUSES,
51
+ )
52
+ from .statuses import (
49
53
  RUN_TERMINAL_STATUSES as _TERMINAL_STATUSES,
50
54
  )
51
- from ..utils import now_iso as _now
52
55
 
53
56
  ROLE_DESCRIPTIONS = {
54
57
  "researcher": "Gathers workspace context and memory for the goal.",
@@ -494,7 +497,7 @@ class AgentRuntime:
494
497
  )
495
498
  except Exception:
496
499
  # Synthesis must never break the run record.
497
- pass
500
+ quiet()
498
501
 
499
502
  @staticmethod
500
503
  def _format_synthesis_section(title: str, items: List[str]) -> str:
@@ -515,8 +518,13 @@ class AgentRuntime:
515
518
  if sentence.strip(" -•\t")
516
519
  ]
517
520
  facts = [_compact_text(sentence, limit=180) for sentence in sentences[:4]]
518
- review = getattr(result, "review", None) if isinstance(getattr(result, "review", None), dict) else {}
519
- plan_review = getattr(result, "plan_review", None) if isinstance(getattr(result, "plan_review", None), dict) else {}
521
+ # `or {}` rather than the isinstance ternary: the ternary could still
522
+ # yield None when the attribute was a dict-typed None, and the next
523
+ # line calls .get on both.
524
+ review = getattr(result, "review", None)
525
+ review = review if isinstance(review, dict) else {}
526
+ plan_review = getattr(result, "plan_review", None)
527
+ plan_review = plan_review if isinstance(plan_review, dict) else {}
520
528
  decision_seed = review.get("decision") or review.get("status") or plan_review.get("decision") or getattr(result, "status", "")
521
529
  decisions = [
522
530
  _compact_text(f"Run finished with {decision_seed}", limit=160),
@@ -569,6 +577,7 @@ class AgentRuntime:
569
577
  workspace_id=scope,
570
578
  )
571
579
  except Exception:
580
+ quiet()
572
581
  continue
573
582
 
574
583
  def _post_run_hooks(
@@ -36,9 +36,9 @@ from collections import deque
36
36
  from pathlib import Path
37
37
  from typing import Any, Callable, Dict, List, Optional
38
38
 
39
+ from ..quiet import quiet
39
40
  from ..utils import now_iso as _now
40
41
 
41
-
42
42
  LOGGER = logging.getLogger(__name__)
43
43
 
44
44
 
@@ -344,7 +344,7 @@ class HooksRegistry:
344
344
  data.setdefault("overrides", {})
345
345
  return data
346
346
  except (OSError, json.JSONDecodeError, TypeError, ValueError) as exc:
347
- LOGGER.warning("hook registry state is unreadable at %s: %s", self._path, exc)
347
+ LOGGER.warning("hook registry state is unreadable at %s: %s", self.path, exc)
348
348
  return {"custom": [], "overrides": {}}
349
349
 
350
350
  def _save(self) -> None:
@@ -454,6 +454,7 @@ class HooksRegistry:
454
454
  try:
455
455
  self.set_order(hook_id, (idx + 1) * 10)
456
456
  except KeyError:
457
+ quiet()
457
458
  continue
458
459
  return self.list(kind=kind)
459
460
 
@@ -44,11 +44,10 @@ from __future__ import annotations
44
44
  from dataclasses import dataclass, field
45
45
  from typing import Any, Callable, Dict, List, Optional
46
46
 
47
- from .contracts import multi_agent_contract
48
47
  from ..utils import now_iso as _now
48
+ from .contracts import multi_agent_contract
49
49
 
50
-
51
- MULTI_AGENT_VERSION = "10.0.0"
50
+ MULTI_AGENT_VERSION = "10.3.0"
52
51
 
53
52
  AGENT_ROLES = ("researcher", "planner", "executor", "reviewer", "release")
54
53
  CORE_PIPELINE = ("planner", "executor", "reviewer")
@@ -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__ = "10.0.0"
3
+ __version__ = "10.3.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):