ltcai 10.3.0 → 10.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 (143) hide show
  1. package/README.md +37 -36
  2. package/docs/CHANGELOG.md +77 -0
  3. package/docs/COMMUNITY_AND_PLUGINS.md +1 -1
  4. package/docs/DEVELOPMENT.md +1 -1
  5. package/docs/MYPY_BACKLOG.md +59 -92
  6. package/docs/ONBOARDING.md +1 -1
  7. package/docs/OPERATIONS.md +1 -1
  8. package/docs/SURFACE_PARITY.md +33 -6
  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/context.py +23 -3
  14. package/lattice_brain/graph/_kg_common.py +164 -9
  15. package/lattice_brain/graph/_kg_contract.py +223 -0
  16. package/lattice_brain/graph/_kg_fsutil.py +1 -1
  17. package/lattice_brain/graph/curator.py +4 -4
  18. package/lattice_brain/graph/discovery.py +12 -1
  19. package/lattice_brain/graph/discovery_index.py +12 -1
  20. package/lattice_brain/graph/documents.py +17 -2
  21. package/lattice_brain/graph/ingest.py +12 -1
  22. package/lattice_brain/graph/proactive.py +9 -5
  23. package/lattice_brain/graph/projection.py +12 -1
  24. package/lattice_brain/graph/provenance.py +12 -1
  25. package/lattice_brain/graph/retrieval.py +12 -1
  26. package/lattice_brain/graph/retrieval_docgen.py +12 -1
  27. package/lattice_brain/graph/retrieval_reads.py +12 -1
  28. package/lattice_brain/graph/retrieval_vector.py +12 -1
  29. package/lattice_brain/graph/schema.py +6 -2
  30. package/lattice_brain/graph/write_master.py +12 -1
  31. package/lattice_brain/ingestion.py +1 -1
  32. package/lattice_brain/quality.py +5 -5
  33. package/lattice_brain/runtime/contracts.py +2 -1
  34. package/lattice_brain/runtime/multi_agent.py +1 -1
  35. package/lattice_brain/workflow.py +10 -7
  36. package/latticeai/__init__.py +1 -1
  37. package/latticeai/api/admin.py +2 -2
  38. package/latticeai/api/chat.py +19 -15
  39. package/latticeai/api/chat_agent_http.py +19 -7
  40. package/latticeai/api/chat_helpers.py +9 -8
  41. package/latticeai/api/chat_stream.py +2 -2
  42. package/latticeai/api/computer_use.py +2 -2
  43. package/latticeai/api/hooks.py +4 -2
  44. package/latticeai/api/local_files.py +8 -4
  45. package/latticeai/api/mcp.py +2 -2
  46. package/latticeai/api/models.py +5 -5
  47. package/latticeai/api/permissions.py +2 -2
  48. package/latticeai/api/search.py +5 -3
  49. package/latticeai/api/security_dashboard.py +3 -5
  50. package/latticeai/api/setup.py +3 -3
  51. package/latticeai/api/static_routes.py +7 -2
  52. package/latticeai/api/tools.py +9 -4
  53. package/latticeai/api/workspace.py +25 -22
  54. package/latticeai/app_factory.py +132 -1322
  55. package/latticeai/cli/entrypoint.py +1 -1
  56. package/latticeai/core/agent.py +28 -14
  57. package/latticeai/core/audit.py +5 -3
  58. package/latticeai/core/embedding_providers.py +19 -6
  59. package/latticeai/core/legacy_compatibility.py +1 -1
  60. package/latticeai/core/marketplace.py +2 -2
  61. package/latticeai/core/mcp_catalog.py +3 -1
  62. package/latticeai/core/model_compat.py +1 -1
  63. package/latticeai/core/plugins.py +1 -1
  64. package/latticeai/core/project_sessions.py +1 -1
  65. package/latticeai/core/run_explain.py +6 -4
  66. package/latticeai/core/workspace_os.py +0 -4
  67. package/latticeai/core/workspace_os_constants.py +1 -1
  68. package/latticeai/core/workspace_os_state.py +1 -1
  69. package/latticeai/core/workspace_snapshots.py +3 -3
  70. package/latticeai/integrations/telegram_bot.py +52 -3
  71. package/latticeai/models/router.py +61 -34
  72. package/latticeai/runtime/access_runtime.py +3 -1
  73. package/latticeai/runtime/build_phases.py +1417 -0
  74. package/latticeai/runtime/review_wiring.py +6 -1
  75. package/latticeai/runtime/runtime_context.py +276 -0
  76. package/latticeai/runtime/sso_config_runtime.py +1 -1
  77. package/latticeai/runtime/stages.py +8 -2
  78. package/latticeai/services/app_context.py +23 -0
  79. package/latticeai/services/architecture_readiness.py +1 -1
  80. package/latticeai/services/brain_intelligence.py +4 -2
  81. package/latticeai/services/cloud_streaming.py +5 -3
  82. package/latticeai/services/command_center.py +1 -1
  83. package/latticeai/services/evidence_actions.py +2 -1
  84. package/latticeai/services/hybrid_chat.py +1 -1
  85. package/latticeai/services/local_knowledge.py +25 -9
  86. package/latticeai/services/memory_service.py +8 -4
  87. package/latticeai/services/model_catalog.py +12 -12
  88. package/latticeai/services/model_engines.py +15 -17
  89. package/latticeai/services/model_loading.py +1 -1
  90. package/latticeai/services/model_runtime.py +31 -33
  91. package/latticeai/services/multimodal_streaming.py +10 -4
  92. package/latticeai/services/openai_compatible_adapter.py +4 -3
  93. package/latticeai/services/product_readiness.py +1 -1
  94. package/latticeai/services/run_executor.py +12 -3
  95. package/latticeai/services/search_service.py +1 -1
  96. package/latticeai/services/triggers.py +1 -1
  97. package/latticeai/setup/auto_setup.py +1 -1
  98. package/latticeai/setup/wizard.py +12 -3
  99. package/latticeai/tools/computer.py +3 -1
  100. package/latticeai/tools/documents.py +2 -2
  101. package/package.json +1 -1
  102. package/scripts/check_current_release_docs.mjs +1 -1
  103. package/src-tauri/Cargo.lock +1 -1
  104. package/src-tauri/Cargo.toml +1 -1
  105. package/src-tauri/tauri.conf.json +1 -1
  106. package/static/app/asset-manifest.json +36 -36
  107. package/static/app/assets/{Act-DrKIGZeJ.js → Act-BK__Y5VE.js} +1 -1
  108. package/static/app/assets/{AdminConsole-CEV1VefA.js → AdminConsole-ZrTyN8_F.js} +1 -1
  109. package/static/app/assets/{Brain-BZSXnwWU.js → Brain-CYHDXpPB.js} +1 -1
  110. package/static/app/assets/{BrainHome-CEBd86k6.js → BrainHome-bbJulb5g.js} +1 -1
  111. package/static/app/assets/{BrainSignals-O4s6R7av.js → BrainSignals-CesSPtUP.js} +1 -1
  112. package/static/app/assets/{Capture-AGbOxsg-.js → Capture-p5upXmz2.js} +1 -1
  113. package/static/app/assets/{CommandPalette-CeqBjCn2.js → CommandPalette-0zkyC0RY.js} +1 -1
  114. package/static/app/assets/{Library-CXnbNiQW.js → Library-Dt-vABI2.js} +1 -1
  115. package/static/app/assets/{LivingBrain-D9Iei2ZJ.js → LivingBrain-Bz-h6LBH.js} +1 -1
  116. package/static/app/assets/{ProductFlow-JlXiDfMM.js → ProductFlow-DAZ7DJCE.js} +1 -1
  117. package/static/app/assets/{ReviewCard-QQly2bTV.js → ReviewCard-Bv0WGbY6.js} +1 -1
  118. package/static/app/assets/{System-Bp9ZY4ER.js → System-BhrJQSD_.js} +1 -1
  119. package/static/app/assets/{activity-CxRPOQ2F.js → activity-AolpZJCH.js} +1 -1
  120. package/static/app/assets/{bot-1bcsWB2s.js → bot-CsVsT2nd.js} +1 -1
  121. package/static/app/assets/{brain-CxaeKCRM.js → brain-vuppco5y.js} +1 -1
  122. package/static/app/assets/{button-ZYFqGan7.js → button-1BP1PJZ3.js} +1 -1
  123. package/static/app/assets/{circle-pause-B5NOIqvk.js → circle-pause-5paeCOtq.js} +1 -1
  124. package/static/app/assets/{circle-play-BiJVFXf7.js → circle-play-By8mFYRV.js} +1 -1
  125. package/static/app/assets/{cpu-Vvq0OKTr.js → cpu-DvUQxdKe.js} +1 -1
  126. package/static/app/assets/{download-SEcXzkx1.js → download-CFqYG90a.js} +1 -1
  127. package/static/app/assets/{folder-open-Dw1ff_Qa.js → folder-open-CVSnp-Dp.js} +1 -1
  128. package/static/app/assets/{hard-drive-B00fR7_r.js → hard-drive-Ckr4Vd6-.js} +1 -1
  129. package/static/app/assets/{index-Dm9VXl1o.js → index--0zKF5Ow.js} +3 -3
  130. package/static/app/assets/{input-DtdWYQEa.js → input-D-JKquXU.js} +1 -1
  131. package/static/app/assets/{network-CpjBu0kI.js → network-DB88FZ2T.js} +1 -1
  132. package/static/app/assets/{primitives-CwsudBwc.js → primitives-D8U9OaIo.js} +1 -1
  133. package/static/app/assets/search-B8jsVTw_.js +1 -0
  134. package/static/app/assets/{shield-alert-CpjBc-gQ.js → shield-alert-DnsvO79P.js} +1 -1
  135. package/static/app/assets/{textarea-DUPyjHQs.js → textarea-4RchwbTa.js} +1 -1
  136. package/static/app/assets/{useFocusTrap-BWcMtCWH.js → useFocusTrap-CFJjy7cS.js} +1 -1
  137. package/static/app/assets/{useQuery-Dg5fPqTS.js → useQuery-3Reu-A4a.js} +1 -1
  138. package/static/app/assets/{users-BgcdTQGM.js → users-BTxYksxa.js} +1 -1
  139. package/static/app/assets/{utils-a3YXFtEO.js → utils-D02Vf-fw.js} +1 -1
  140. package/static/app/assets/{workspace-BMUEiXUm.js → workspace-CSJNgX-C.js} +1 -1
  141. package/static/app/index.html +3 -3
  142. package/static/sw.js +1 -1
  143. package/static/app/assets/search-CwEJWBEA.js +0 -1
@@ -5,7 +5,12 @@ from __future__ import annotations
5
5
  from typing import Any, Callable, Dict, Optional
6
6
 
7
7
 
8
- def build_review_run_now_runner(platform: Any, http_exception: type[Exception]) -> Callable[..., Any]:
8
+ def build_review_run_now_runner(
9
+ platform: Any,
10
+ # Injected as `fastapi.HTTPException`, which takes (status_code, detail) —
11
+ # not the bare `Exception` signature the narrower annotation implied.
12
+ http_exception: Callable[..., Exception],
13
+ ) -> Callable[..., Any]:
9
14
  """Build the Review Center run-now runner used by the API router.
10
15
 
11
16
  The runner preserves the public contract: "Run now" previews/regenerates the
@@ -0,0 +1,276 @@
1
+ """The assembly state the build phases share.
2
+
3
+ ``app_factory._build`` was one 1,300-line function. That was not an accident:
4
+ its sections genuinely depend on each other, and several of them close over
5
+ names bound *further down* — ``save_to_history`` calls ``append_audit_event``,
6
+ which is built ~150 lines later. Inside a single function that works, because a
7
+ closure resolves a free variable when it is called, not when it is defined.
8
+ Any naive extraction breaks exactly there (10.3.0 hit this and documented it).
9
+
10
+ ``RuntimeContext`` is what makes the extraction safe. It is a mutable object
11
+ that every phase writes into and reads from, so a closure created in phase 2
12
+ that calls ``ctx.append_audit_event`` still resolves it at call time — the same
13
+ late binding the single function had, now with the shared state named instead
14
+ of implicit.
15
+
16
+ Two properties keep it honest:
17
+
18
+ * **Attributes are declared, not defaulted.** Reading one before its phase has
19
+ run raises ``AttributeError`` naming the attribute, rather than silently
20
+ yielding ``None`` and failing somewhere unrelated.
21
+ * **The phase order is a contract.** ``tests/unit/test_runtime_context.py``
22
+ fixes which phase produces which attribute, so reordering the phases in a way
23
+ that breaks a dependency fails a test instead of production.
24
+ """
25
+
26
+ from __future__ import annotations
27
+
28
+ from typing import Any, Dict, List, Set
29
+
30
+
31
+ class RuntimeContext:
32
+ """Mutable state carried across the ordered application build phases."""
33
+
34
+ # ── phase 1: platform ────────────────────────────────────────────────
35
+ mx: Any
36
+
37
+ # ── phase 2: configuration and paths ─────────────────────────────────
38
+ config_arg: Any
39
+ config_runtime: Any
40
+ security_runtime: Any
41
+ CONFIG: Any
42
+ APP_VERSION: str
43
+ APP_MODE: str
44
+ IS_PUBLIC_MODE: bool
45
+ DEFAULT_HOST: str
46
+ DEFAULT_PORT: int
47
+ ENABLE_TELEGRAM: bool
48
+ ENABLE_GRAPH: bool
49
+ AUTOLOAD_MODELS: bool
50
+ MODEL_IDLE_UNLOAD_SECONDS: Any
51
+ ALLOW_MODEL_DOWNLOADS: bool
52
+ MODEL_DOWNLOAD_TIMEOUT: Any
53
+ ALLOW_LOCAL_MODELS: bool
54
+ REQUIRE_AUTH: bool
55
+ ALLOW_PLAINTEXT_API_KEYS: bool
56
+ CORS_ALLOW_NETWORK: bool
57
+ CORS_EXTRA_ORIGINS: Any
58
+ PUBLIC_MODEL: str
59
+ LOCAL_MODEL: Any
60
+ LOCAL_DRAFT_MODEL: Any
61
+ OPEN_REGISTRATION: bool
62
+ SSO_DISCOVERY_URL: Any
63
+ SSO_CLIENT_ID: Any
64
+ SSO_CLIENT_SECRET: Any
65
+ SSO_REDIRECT_URI: Any
66
+ SSO_PROVIDER_NAME: Any
67
+ INVITE_CODE: Any
68
+ INVITE_COOKIE_SECRET: Any
69
+ INVITE_GATE_ENABLED: bool
70
+ SECURE_COOKIES: bool
71
+ BASE_DIR: Any
72
+ DATA_DIR: Any
73
+ STATIC_DIR: Any
74
+ USERS_FILE: Any
75
+ HISTORY_FILE: Any
76
+ VPC_FILE: Any
77
+ AUDIT_FILE: Any
78
+ SSO_FILE: Any
79
+ keyring: Any
80
+
81
+ # ── phase 3: identity, audit, history ────────────────────────────────
82
+ load_mcp_installs: Any
83
+ mcp_public_item: Any
84
+ recommend_mcps: Any
85
+ install_mcp: Any
86
+ _SESSION_TTL: Any
87
+ _session_store: Any
88
+ create_session: Any
89
+ get_session_email: Any
90
+ invalidate_session: Any
91
+ load_users: Any
92
+ save_users: Any
93
+ user_id_for_email: Any
94
+ verify_and_migrate_password: Any
95
+ redact_secret_text: Any
96
+ get_audit_log: Any
97
+ append_audit_event: Any
98
+ classify_sensitive_message: Any
99
+ build_sensitivity_report: Any
100
+ build_admin_audit_report: Any
101
+ get_user_role: Any
102
+ _extract_bearer_token: Any
103
+ get_current_user: Any
104
+ require_user: Any
105
+ require_admin: Any
106
+ public_user: Any
107
+ _RATE_LIMIT_ENABLED: bool
108
+ enforce_rate_limit: Any
109
+ _check_rate_limit: Any
110
+ _client_ip: Any
111
+ _bytes_match_extension: Any
112
+ _host_is_loopback: Any
113
+ get_history_user: Any
114
+ get_user_api_key: Any
115
+ set_user_api_key: Any
116
+ get_sso_settings: Any
117
+ public_sso_config: Any
118
+ save_sso_config: Any
119
+ _get_sso_discovery: Any
120
+ _sso_states: Any
121
+ _sso_env_defaults: Any
122
+ load_vpc_config: Any
123
+ save_vpc_config: Any
124
+
125
+ # ── phase 4: brain, persistence ──────────────────────────────────────
126
+ EMBEDDING_PROFILE: Any
127
+ EMBEDDER: Any
128
+ STORAGE_ENGINE: Any
129
+ brain_runtime: Any
130
+ KNOWLEDGE_GRAPH: Any
131
+ CONVERSATIONS: Any
132
+ save_to_history: Any
133
+ HOOKS_REGISTRY: Any
134
+ LOCAL_KG_WATCHER: Any
135
+ REALTIME_BUS: Any
136
+ WORKSPACE_OS: Any
137
+ WORKSPACE_SERVICE: Any
138
+ INVITATION_STORE: Any
139
+ PLUGIN_REGISTRY: Any
140
+ TEMPLATE_CATALOG: Any
141
+ AGENT_REGISTRY: Any
142
+ MEMORY_SERVICE: Any
143
+ BRAIN_INTELLIGENCE: Any
144
+ AUTOMATION_INTELLIGENCE: Any
145
+ INGESTION_PIPELINE: Any
146
+ DEVICE_IDENTITY: Any
147
+ KG_PORTABILITY: Any
148
+ FUNNEL_METRICS: Any
149
+ get_history: Any
150
+ conversation_title: Any
151
+ group_history_conversations: Any
152
+ get_conversation_messages: Any
153
+ clear_history: Any
154
+ clear_conversation: Any
155
+ _history_allowed_workspaces_for: Any
156
+ _history_include_legacy_global: Any
157
+ _require_graph: Any
158
+ _workspace_graph: Any
159
+
160
+ # ── phase 5: domain services ─────────────────────────────────────────
161
+ model_router: Any
162
+ gardener: Any
163
+ CHAT_SERVICE: Any
164
+ SEARCH_SERVICE: Any
165
+ BRAIN_MEMORY: Any
166
+ CONTEXT_ASSEMBLER: Any
167
+ ARTIFACT_LEDGER: Any
168
+ _scoped_hybrid_search: Any
169
+ CHAT_AGENT_RUNTIME: Any
170
+ on_chat_message: Any
171
+ _recent_chat_context: Any
172
+ _workspace_settings_payload: Any
173
+ _workspace_models_payload: Any
174
+ _embedding_info: Any
175
+ _allowed_workspaces_for: Any
176
+ app_context: Any
177
+
178
+ # ── phase 6: web app and foundation routers ──────────────────────────
179
+ _spawn: Any
180
+ lifespan: Any
181
+ app: Any
182
+ model_runtime_service: Any
183
+ STATIC_ROUTES: Any
184
+ ui_file_response: Any
185
+ local_sysinfo: Any
186
+ invite_authorized: Any
187
+ auth_router: Any
188
+ admin_router: Any
189
+ invitations_router: Any
190
+ security_router: Any
191
+ _graph_stats_safe: Any
192
+ _product_hardening_status: Any
193
+ _security_audit_events_safe: Any
194
+ _security_list_uploaded_files: Any
195
+
196
+ # ── phase 7: platform features ───────────────────────────────────────
197
+ _llm_generate_sync: Any
198
+ PLATFORM: Any
199
+ _automation_runtime: Any
200
+ REVIEW_QUEUE: Any
201
+ TRIGGER_SERVICE: Any
202
+ AGENT_RUNTIME: Any
203
+ RUN_EXECUTOR: Any
204
+ COMMAND_CENTER: Any
205
+ CHANGE_PROPOSALS: Any
206
+ EVIDENCE_ACTIONS_SERVICE: Any
207
+ VOICE_CAPTURE: Any
208
+ PROJECT_SESSIONS: Any
209
+
210
+ # ── phase 8: interaction routers ─────────────────────────────────────
211
+ model_runtime: Any
212
+
213
+ # ── legacy compatibility surface (phase 9 reads these) ───────────────
214
+ legacy: Dict[str, Any]
215
+
216
+ def __init__(self, config: Any = None) -> None:
217
+ self.config_arg = config
218
+ self.legacy = {}
219
+ self._produced: Dict[str, str] = {}
220
+ self._phase: str = "<none>"
221
+
222
+ # ── phase bookkeeping ────────────────────────────────────────────────
223
+ def enter(self, phase: str) -> None:
224
+ """Mark which phase subsequent ``set`` calls belong to."""
225
+ self._phase = phase
226
+
227
+ def set(self, **values: Any) -> None:
228
+ """Publish values produced by the current phase.
229
+
230
+ Recording the producing phase is what lets the ordering test assert a
231
+ contract instead of a comment.
232
+ """
233
+ for name, value in values.items():
234
+ setattr(self, name, value)
235
+ self._produced.setdefault(name, self._phase)
236
+
237
+ def adopt(self, stage: Any, *names: str) -> None:
238
+ """Publish selected keys of a runtime-stage mapping onto the context."""
239
+ self.set(**{name: stage[name] for name in names})
240
+
241
+ def require(self, name: str) -> Any:
242
+ """Read an attribute, failing with the phase contract if it is unset.
243
+
244
+ The bare ``AttributeError`` is already specific; this adds which phase
245
+ was supposed to have produced the value, which is the actual question
246
+ when a phase gets reordered.
247
+ """
248
+ try:
249
+ return getattr(self, name)
250
+ except AttributeError:
251
+ producer = self._produced.get(name)
252
+ hint = f" (produced by phase {producer!r})" if producer else ""
253
+ raise RuntimeError(
254
+ f"RuntimeContext.{name} was read before it was built{hint}"
255
+ ) from None
256
+
257
+ @property
258
+ def produced_by(self) -> Dict[str, str]:
259
+ """Attribute name → the phase that published it, for the order test."""
260
+ return dict(self._produced)
261
+
262
+ def names(self) -> Set[str]:
263
+ return set(self._produced)
264
+
265
+ def phases_run(self) -> List[str]:
266
+ seen: List[str] = []
267
+ for phase in self._produced.values():
268
+ if phase not in seen:
269
+ seen.append(phase)
270
+ return seen
271
+
272
+ def __repr__(self) -> str: # pragma: no cover - debugging aid
273
+ return f"<RuntimeContext phase={self._phase!r} names={len(self._produced)}>"
274
+
275
+
276
+ __all__ = ["RuntimeContext"]
@@ -97,7 +97,7 @@ def build_sso_config_runtime(
97
97
 
98
98
  async def _get_sso_discovery() -> Optional[Dict]:
99
99
  settings = get_sso_settings()
100
- url = settings.get("discovery_url", "")
100
+ url = str(settings.get("discovery_url", "") or "")
101
101
  if _discovery_cache["data"] and _discovery_cache["url"] == url:
102
102
  return _discovery_cache["data"]
103
103
  if not url:
@@ -2,13 +2,19 @@
2
2
 
3
3
  from __future__ import annotations
4
4
 
5
- from dataclasses import fields
6
- from typing import Any, Iterator, Mapping
5
+ from dataclasses import Field, fields
6
+ from typing import TYPE_CHECKING, Any, ClassVar, Dict, Iterator, Mapping
7
7
 
8
8
 
9
9
  class RuntimeStage(Mapping[str, Any]):
10
10
  """Dataclass mixin that preserves the legacy mapping access during DI migration."""
11
11
 
12
+ if TYPE_CHECKING:
13
+ # The mixin is only ever combined with @dataclass classes; declaring
14
+ # the marker attribute is how a type checker learns that `fields(self)`
15
+ # is legal here (see the dataclasses stubs' DataclassInstance protocol).
16
+ __dataclass_fields__: ClassVar[Dict[str, Field[Any]]]
17
+
12
18
  def __getitem__(self, key: str) -> Any:
13
19
  if key not in self:
14
20
  raise KeyError(key)
@@ -97,3 +97,26 @@ class AppContext:
97
97
  # persisted; ``None`` means no external chat mirror is registered. The
98
98
  # telegram bridge subscribes here only when ENABLE_TELEGRAM is truthy.
99
99
  on_chat_message: Optional[Callable[..., None]] = None
100
+
101
+ def require(self, field: str) -> Any:
102
+ """Return a dependency this router cannot operate without.
103
+
104
+ Every field defaults to ``None`` so a test can build a context holding
105
+ only what it exercises. A router that binds an absent dependency used
106
+ to discover it as ``TypeError: 'NoneType' object is not callable``
107
+ inside a request handler, far from the wiring mistake. Binding through
108
+ ``require`` moves that to router construction with the field named.
109
+
110
+ It is also what makes the binding statically honest: the declared type
111
+ is ``Optional[...]``, so a type checker reports every call site as
112
+ ``"None" not callable`` — 105 of them in the workspace router alone.
113
+ ``require`` states the precondition once instead.
114
+ """
115
+ if field not in self.__dataclass_fields__:
116
+ raise AttributeError(f"AppContext has no field {field!r}")
117
+ value = getattr(self, field)
118
+ if value is None:
119
+ raise RuntimeError(
120
+ f"AppContext.{field} is required by this router but was not provided"
121
+ )
122
+ return value
@@ -16,7 +16,7 @@ from typing import Any, Dict, List
16
16
 
17
17
  from latticeai.core.legacy_compatibility import legacy_shim_report
18
18
 
19
- ARCHITECTURE_VERSION_TARGET = "10.3.0"
19
+ ARCHITECTURE_VERSION_TARGET = "10.4.0"
20
20
 
21
21
  PREFERRED_REFACTORING_ORDER = [
22
22
  "agent-runtime",
@@ -143,7 +143,7 @@ class BrainIntelligenceService:
143
143
  stale_cutoff = now - timedelta(days=_STALE_DAYS)
144
144
  dated = [(_parse_ts(n.get("updated_at")), n) for n in nodes]
145
145
  known = [pair for pair in dated if pair[0] is not None]
146
- stale = [n for ts, n in known if ts < stale_cutoff]
146
+ stale = [n for ts, n in known if ts is not None and ts < stale_cutoff]
147
147
  fresh_ratio = 1.0 - (len(stale) / len(known)) if known else 0.0
148
148
  dimensions["freshness"] = {
149
149
  "status": "ok",
@@ -576,7 +576,9 @@ class BrainIntelligenceService:
576
576
  for c in conflicts
577
577
  ):
578
578
  continue
579
- other = next((r for r in memory_rows if r["id"] == other_id), None)
579
+ other: Dict[str, Any] | None = next(
580
+ (r for r in memory_rows if r["id"] == other_id), None
581
+ )
580
582
  conflicts.append({
581
583
  "kind": "memory_pair",
582
584
  "left_id": candidate.id,
@@ -46,7 +46,9 @@ class CloudTurnResult:
46
46
 
47
47
 
48
48
  class CloudLLMAdapter(Protocol):
49
- async def stream(
49
+ # `def`, not `async def`: implementations are async generators, so the
50
+ # call itself returns the iterator rather than a coroutine wrapping it.
51
+ def stream(
50
52
  self,
51
53
  *,
52
54
  system: str,
@@ -107,7 +109,7 @@ class CloudStreamingBridge:
107
109
  answer_text=answer,
108
110
  sent_node_ids=list(minimal.node_ids),
109
111
  provider=getattr(self._adapter, "provider_name", "cloud"),
110
- model=model or getattr(self._adapter, "default_model", ""),
112
+ model=str(model or getattr(self._adapter, "default_model", "")),
111
113
  )
112
114
 
113
115
 
@@ -157,7 +159,7 @@ def plan_kg_expansion(result: CloudTurnResult) -> KGExpansionPlan:
157
159
  )
158
160
 
159
161
  return KGExpansionPlan(
160
- conversation_title=conv_node["title"],
162
+ conversation_title=str(conv_node["title"]),
161
163
  new_nodes=[conv_node],
162
164
  new_edges=edges,
163
165
  provenance={
@@ -185,7 +185,7 @@ class CommandCenterService:
185
185
  "drafts": drafts,
186
186
  }
187
187
  if last_execution is not None:
188
- section["last_execution"] = last_execution
188
+ section["last_execution"] = last_execution # type: ignore[assignment]
189
189
  return section
190
190
 
191
191
  def _review_section(
@@ -150,7 +150,8 @@ class EvidenceActionService:
150
150
  missing.append(node_id)
151
151
  continue
152
152
  # /api/graph/node wraps the record; get_node returns it directly.
153
- record = node.get("node") if isinstance(node.get("node"), Mapping) else node
153
+ wrapped = node.get("node")
154
+ record: Mapping[str, Any] = wrapped if isinstance(wrapped, Mapping) else node
154
155
  title = str(record.get("title") or record.get("id") or node_id)
155
156
  body = str(record.get("summary") or record.get("content") or "").strip()
156
157
  metadata = record.get("metadata")
@@ -202,7 +202,7 @@ async def stream_hybrid_cloud_turn(
202
202
  answer_text=answer,
203
203
  sent_node_ids=list(minimal.node_ids),
204
204
  provider=getattr(chosen_adapter, "provider_name", "cloud"),
205
- model=model or getattr(chosen_adapter, "default_model", ""),
205
+ model=str(model or getattr(chosen_adapter, "default_model", "")),
206
206
  )
207
207
  else:
208
208
  result = await bridge.run_turn(
@@ -57,8 +57,9 @@ class LocalKnowledgeWatcher:
57
57
  self._hooks = hooks
58
58
  self._lock = threading.Lock()
59
59
  self._watched: Dict[str, Dict[str, Any]] = {}
60
- self._observer_cls = None
61
- self._event_handler_base = None
60
+ # watchdog is optional; these stay None when it is not installed.
61
+ self._observer_cls: Optional[Any] = None
62
+ self._event_handler_base: Optional[Any] = None
62
63
  self._import_error = ""
63
64
  try:
64
65
  from watchdog.events import FileSystemEventHandler
@@ -127,12 +128,21 @@ class LocalKnowledgeWatcher:
127
128
 
128
129
  self.stop_source(source_id)
129
130
 
130
- class Handler(_LocalWatchHandler, self._event_handler_base): # type: ignore[misc, valid-type]
131
+ handler_base = self._event_handler_base
132
+ observer_cls = self._observer_cls
133
+ if handler_base is None or observer_cls is None:
134
+ return {
135
+ "watching": False,
136
+ "source_id": source_id,
137
+ "error": self._import_error or "watchdog is not installed",
138
+ }
139
+
140
+ class Handler(_LocalWatchHandler, handler_base): # type: ignore[misc, valid-type]
131
141
  def __init__(handler_self):
132
- self._event_handler_base.__init__(handler_self)
142
+ handler_base.__init__(handler_self) # type: ignore[misc]
133
143
  _LocalWatchHandler.__init__(handler_self, lambda: self._schedule(source_id))
134
144
 
135
- observer = self._observer_cls()
145
+ observer = observer_cls()
136
146
  try:
137
147
  observer.schedule(Handler(), str(root), recursive=True)
138
148
  observer.start()
@@ -161,6 +171,8 @@ class LocalKnowledgeWatcher:
161
171
  timer.cancel()
162
172
  observer = item.get("observer")
163
173
  try:
174
+ if observer is None:
175
+ raise RuntimeError("watch entry has no observer")
164
176
  observer.stop()
165
177
  observer.join(timeout=3)
166
178
  except Exception as exc:
@@ -285,8 +297,10 @@ def create_local_knowledge_router(
285
297
  async def knowledge_graph_local_sources(request: Request):
286
298
  require_user(request)
287
299
  payload = graph().local_sources()
288
- watch_status = watcher.status() if watcher else {"available": False, "active": {}}
289
- active = watch_status.get("active", {})
300
+ watch_status: Dict[str, Any] = (
301
+ watcher.status() if watcher else {"available": False, "active": {}}
302
+ )
303
+ active: Dict[str, Any] = dict(watch_status.get("active") or {})
290
304
  for source in payload.get("sources", []):
291
305
  source["watch_active"] = source.get("id") in active
292
306
  source["watch_status"] = active.get(source.get("id"))
@@ -305,8 +319,10 @@ def create_local_knowledge_router(
305
319
  require_user(request)
306
320
  kg = graph()
307
321
  payload = kg.local_source_health(error_samples=error_samples)
308
- watch_status = watcher.status() if watcher else {"available": False, "active": {}}
309
- active = watch_status.get("active", {})
322
+ watch_status: Dict[str, Any] = (
323
+ watcher.status() if watcher else {"available": False, "active": {}}
324
+ )
325
+ active = dict(watch_status.get("active") or {})
310
326
  for folder in payload.get("folders", []):
311
327
  folder["watch_active"] = folder.get("id") in active
312
328
  payload["watch"] = watch_status
@@ -236,7 +236,7 @@ class MemoryService:
236
236
  elif kg_index:
237
237
  vector_total = kg_index.get("indexed") or kg_index.get("ready")
238
238
 
239
- sources = [
239
+ sources: List[Dict[str, Any]] = [
240
240
  {
241
241
  "id": "workspace", "type": "workspace", "label": "Workspace Memory",
242
242
  "count": len(ws_mem), "size_bytes": ws_bytes if ws_mem else 0,
@@ -272,7 +272,7 @@ class MemoryService:
272
272
  "detail": "Local embedding vector index." if kg_index else "Vector index unavailable.",
273
273
  },
274
274
  ]
275
- total_items = sum((s["count"] or 0) for s in sources)
275
+ total_items = sum(int(s["count"] or 0) for s in sources)
276
276
  total_bytes = ws_bytes + kg_bytes + conv_bytes
277
277
  healthy = sum(1 for s in sources if s["health"] == "ok")
278
278
  overall = "ok" if healthy >= 4 else "degraded" if healthy >= 1 else "unavailable"
@@ -747,7 +747,11 @@ class MemoryService:
747
747
  graph_count = int(sources.get("graph", {}).get("count") or 0)
748
748
  vector_count = int(sources.get("vector", {}).get("count") or 0)
749
749
  query = recall_query.strip() or self._latest_recall_query(user_email=user_email, workspace_id=workspace_id)
750
- recall = self.recall(query, user_email=user_email, workspace_id=workspace_id, limit=limit) if query else {"query": "", "results": [], "count": 0, "source": "live"}
750
+ recall: Dict[str, Any] = (
751
+ self.recall(query, user_email=user_email, workspace_id=workspace_id, limit=limit)
752
+ if query
753
+ else {"query": "", "results": [], "count": 0, "source": "live"}
754
+ )
751
755
  recall_items = [
752
756
  {
753
757
  "id": item.get("id"),
@@ -760,7 +764,7 @@ class MemoryService:
760
764
  "matched_terms": item.get("matched_terms") or [],
761
765
  "confidence": item.get("confidence") or "low",
762
766
  }
763
- for item in recall.get("results", [])[: max(1, min(limit, 8))]
767
+ for item in list(recall.get("results", []))[: max(1, min(limit, 8))]
764
768
  ]
765
769
  durable_items = workspace_count + conversation_count + graph_count
766
770
  # Capability and proof are deliberately separated. The brain is
@@ -13,7 +13,7 @@ from __future__ import annotations
13
13
 
14
14
  import re
15
15
  import sys
16
- from typing import Dict, List, Optional
16
+ from typing import Any, Dict, List, Optional
17
17
 
18
18
  from latticeai.services.model_capability_registry import (
19
19
  LOCAL_MLX_MODELS as _LOCAL_MLX_MODELS,
@@ -34,7 +34,7 @@ from latticeai.services.model_capability_registry import (
34
34
  get_verified_models as _get_verified_models,
35
35
  )
36
36
 
37
- ENGINE_INSTALLERS = {
37
+ ENGINE_INSTALLERS: Dict[str, Dict[str, Any]] = {
38
38
  "local_mlx": {
39
39
  "command": [sys.executable, "-m", "pip", "install", "--upgrade", "mlx-vlm>=0.6.3", "mlx-lm", "huggingface_hub[cli]"],
40
40
  "label": "Install MLX runtime",
@@ -89,10 +89,10 @@ ENGINE_INSTALLERS = {
89
89
  # user-facing ENGINE_MODEL_CATALOG below is then narrowed to the aggressive 5.2.0
90
90
  # policy (latest family generations only, no text-only/legacy weights) and the
91
91
  # engine-specific ids/sizes are normalised. See `_finalize_engine_catalog`.
92
- _RAW_ENGINE_MODEL_CATALOG: Dict[str, List[Dict[str, object]]] = _build_engine_model_catalog()
92
+ _RAW_ENGINE_MODEL_CATALOG: Dict[str, List[Dict[str, Any]]] = _build_engine_model_catalog()
93
93
  # Filled in at module end once the blocklist, alias map and family-version filter
94
94
  # are all defined; declared here so the public name exists for static readers.
95
- ENGINE_MODEL_CATALOG: Dict[str, List[Dict[str, object]]] = {}
95
+ ENGINE_MODEL_CATALOG: Dict[str, List[Dict[str, Any]]] = {}
96
96
 
97
97
  # Historical aliases preserved (used by _recommended_with_engine_options and resolution).
98
98
  # These can be enriched later from registry if needed; kept verbatim for safety.
@@ -173,7 +173,7 @@ def _version_tuple(raw: str) -> tuple[int, ...]:
173
173
  return tuple(int(part) for part in raw.split(".") if part.isdigit())
174
174
 
175
175
 
176
- def _model_family_version(model: Dict[str, object]) -> Optional[tuple[str, tuple[int, ...]]]:
176
+ def _model_family_version(model: Dict[str, Any]) -> Optional[tuple[str, tuple[int, ...]]]:
177
177
  text = " ".join(str(model.get(key) or "") for key in ("family", "name", "id"))
178
178
  for family, pattern in _VERSIONED_MODEL_PATTERNS:
179
179
  match = pattern.search(text)
@@ -184,9 +184,9 @@ def _model_family_version(model: Dict[str, object]) -> Optional[tuple[str, tuple
184
184
  return None
185
185
 
186
186
 
187
- def filter_lower_family_versions(models: List[Dict[str, object]]) -> List[Dict[str, object]]:
187
+ def filter_lower_family_versions(models: List[Dict[str, Any]]) -> List[Dict[str, Any]]:
188
188
  max_versions: Dict[str, tuple[int, ...]] = {}
189
- detected: List[tuple[Dict[str, object], Optional[tuple[str, tuple[int, ...]]]]] = []
189
+ detected: List[tuple[Dict[str, Any], Optional[tuple[str, tuple[int, ...]]]]] = []
190
190
  for model in models:
191
191
  version_info = _model_family_version(model)
192
192
  detected.append((model, version_info))
@@ -214,12 +214,12 @@ _BLOCKED_CATALOG_FRAGMENTS = (
214
214
  )
215
215
 
216
216
 
217
- def _is_blocked_catalog_id(model: Dict[str, object]) -> bool:
217
+ def _is_blocked_catalog_id(model: Dict[str, Any]) -> bool:
218
218
  ident = str(model.get("id") or "").lower()
219
219
  return any(fragment in ident for fragment in _BLOCKED_CATALOG_FRAGMENTS)
220
220
 
221
221
 
222
- def _normalize_engine_entry(engine: str, model: Dict[str, object]) -> Dict[str, object]:
222
+ def _normalize_engine_entry(engine: str, model: Dict[str, Any]) -> Dict[str, Any]:
223
223
  """Apply historical engine-specific id + size conventions to a raw entry.
224
224
 
225
225
  * Non-MLX engines resolve to their canonical packaged id via
@@ -243,9 +243,9 @@ def _normalize_engine_entry(engine: str, model: Dict[str, object]) -> Dict[str,
243
243
 
244
244
 
245
245
  def _finalize_engine_catalog(
246
- raw: Dict[str, List[Dict[str, object]]],
247
- ) -> Dict[str, List[Dict[str, object]]]:
248
- final: Dict[str, List[Dict[str, object]]] = {}
246
+ raw: Dict[str, List[Dict[str, Any]]],
247
+ ) -> Dict[str, List[Dict[str, Any]]]:
248
+ final: Dict[str, List[Dict[str, Any]]] = {}
249
249
  for engine, models in raw.items():
250
250
  kept = [
251
251
  _normalize_engine_entry(engine, m)