ltcai 11.5.1 → 11.5.2

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 (176) hide show
  1. package/README.md +85 -53
  2. package/docs/CHANGELOG.md +54 -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 +8 -4
  6. package/docs/MULTI_AGENT_RUNTIME.md +1 -1
  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/WORKFLOW_DESIGNER.md +1 -1
  12. package/docs/architecture.md +4 -4
  13. package/docs/kg-schema.md +1 -1
  14. package/docs/v11.5.2_TIGHT_SHIP_PLAN.md +144 -0
  15. package/lattice_brain/__init__.py +1 -1
  16. package/lattice_brain/graph/proactive.py +1 -1
  17. package/lattice_brain/graph/projection/v2_schema.py +0 -28
  18. package/lattice_brain/graph/vector_index/base.py +0 -3
  19. package/lattice_brain/graph/vector_index/brute_force.py +0 -4
  20. package/lattice_brain/graph/vector_index/hnsw.py +0 -3
  21. package/lattice_brain/graph/vector_index/quantized.py +0 -3
  22. package/lattice_brain/ingestion_jobs.py +0 -3
  23. package/lattice_brain/memory.py +0 -27
  24. package/lattice_brain/portability/sharing.py +0 -4
  25. package/lattice_brain/quality.py +0 -49
  26. package/lattice_brain/runtime/agent_runtime.py +1 -1
  27. package/lattice_brain/runtime/hooks.py +0 -13
  28. package/lattice_brain/runtime/multi_agent.py +1 -1
  29. package/lattice_brain/sealed_box.py +0 -4
  30. package/latticeai/__init__.py +1 -1
  31. package/latticeai/api/admin.py +12 -12
  32. package/latticeai/api/auth.py +25 -2
  33. package/latticeai/api/chat.py +3 -3
  34. package/latticeai/api/chat_agent_http.py +3 -12
  35. package/latticeai/api/chat_helpers.py +10 -13
  36. package/latticeai/api/computer_use.py +31 -42
  37. package/latticeai/api/health.py +21 -1
  38. package/latticeai/api/local_files.py +14 -0
  39. package/latticeai/api/permissions.py +38 -12
  40. package/latticeai/api/search.py +24 -0
  41. package/latticeai/api/static_routes.py +4 -1
  42. package/latticeai/cli/runtime.py +7 -3
  43. package/latticeai/core/config.py +47 -3
  44. package/latticeai/core/csrf.py +28 -2
  45. package/latticeai/core/embedding_providers/__init__.py +3 -5
  46. package/latticeai/core/embedding_providers/base.py +1 -1
  47. package/latticeai/core/embedding_providers/text.py +1 -1
  48. package/latticeai/core/enterprise.py +1 -4
  49. package/latticeai/core/http_origin.py +146 -0
  50. package/latticeai/core/invitations.py +3 -2
  51. package/latticeai/core/io_utils.py +2 -11
  52. package/latticeai/core/legacy_compatibility.py +1 -1
  53. package/latticeai/core/marketplace.py +1 -1
  54. package/latticeai/core/messages.py +5 -0
  55. package/latticeai/core/model_compat.py +2 -8
  56. package/latticeai/core/module_probe.py +37 -0
  57. package/latticeai/core/run_explain.py +5 -2
  58. package/latticeai/core/run_store.py +2 -2
  59. package/latticeai/core/security.py +13 -0
  60. package/latticeai/core/sessions.py +3 -2
  61. package/latticeai/core/sse.py +25 -0
  62. package/latticeai/core/users.py +0 -9
  63. package/latticeai/core/workspace_graph_trace.py +2 -8
  64. package/latticeai/core/workspace_os.py +0 -10
  65. package/latticeai/core/workspace_os_constants.py +1 -1
  66. package/latticeai/core/workspace_os_utils.py +34 -0
  67. package/latticeai/core/workspace_review_items.py +2 -8
  68. package/latticeai/core/workspace_runs.py +2 -8
  69. package/latticeai/core/workspace_skills.py +2 -7
  70. package/latticeai/models/router/documents.py +0 -35
  71. package/latticeai/models/router/registry.py +0 -4
  72. package/latticeai/runtime/access_runtime.py +16 -4
  73. package/latticeai/runtime/build_phases/features.py +2 -0
  74. package/latticeai/runtime/build_phases/web.py +2 -0
  75. package/latticeai/runtime/feature_toggle_wiring.py +12 -18
  76. package/latticeai/runtime/network_boundary_wiring.py +8 -19
  77. package/latticeai/runtime/permission_mode_wiring.py +6 -13
  78. package/latticeai/runtime/router_registration.py +4 -0
  79. package/latticeai/runtime/runtime_context.py +1 -13
  80. package/latticeai/runtime/service_singletons.py +55 -0
  81. package/latticeai/services/architecture_readiness.py +1 -1
  82. package/latticeai/services/brain_intelligence/proposals.py +0 -5
  83. package/latticeai/services/change_proposals.py +2 -36
  84. package/latticeai/services/chronicle.py +4 -6
  85. package/latticeai/services/command_center.py +4 -4
  86. package/latticeai/services/evidence_actions.py +5 -2
  87. package/latticeai/services/hybrid_chat.py +2 -2
  88. package/latticeai/services/hybrid_policy.py +8 -57
  89. package/latticeai/services/mode_store.py +132 -0
  90. package/latticeai/services/model_capability_registry.py +0 -15
  91. package/latticeai/services/model_catalog.py +0 -7
  92. package/latticeai/services/model_loading.py +3 -2
  93. package/latticeai/services/model_runtime/__init__.py +0 -57
  94. package/latticeai/services/model_runtime/engines.py +27 -128
  95. package/latticeai/services/model_runtime/loading.py +2 -2
  96. package/latticeai/services/network_boundary_service.py +7 -44
  97. package/latticeai/services/permission_mode_service.py +8 -56
  98. package/latticeai/services/product_readiness.py +1 -1
  99. package/latticeai/services/setup_detection.py +67 -1
  100. package/latticeai/services/tool_dispatch.py +0 -4
  101. package/latticeai/services/upload_service.py +7 -20
  102. package/latticeai/services/workspace_service.py +0 -6
  103. package/latticeai/setup/auto_setup.py +15 -26
  104. package/latticeai/setup/wizard/catalog.py +5 -2
  105. package/latticeai/setup/wizard/detect.py +6 -27
  106. package/latticeai/setup/wizard/paths.py +2 -5
  107. package/latticeai/setup/wizard/plans.py +2 -2
  108. package/package.json +2 -4
  109. package/scripts/bump_version.py +14 -1
  110. package/scripts/check_current_release_docs.mjs +1 -1
  111. package/scripts/check_legacy_debt.mjs +1 -1
  112. package/scripts/generate_agent_loop_fixtures.py +87 -0
  113. package/scripts/generate_rust_parity_fixtures.py +3 -95
  114. package/scripts/parity_fixture_corpus_context.py +162 -0
  115. package/scripts/release_screen_claims.json +10 -0
  116. package/src-tauri/Cargo.lock +7 -7
  117. package/src-tauri/Cargo.toml +1 -1
  118. package/src-tauri/src/backend.rs +76 -2
  119. package/src-tauri/src/main.rs +13 -3
  120. package/src-tauri/tauri.conf.json +2 -2
  121. package/static/app/asset-manifest.json +40 -40
  122. package/static/app/assets/{Act-Drs_jd-O.js → Act-DcQizkl1.js} +1 -1
  123. package/static/app/assets/{AdminConsole-BqNx5oF6.js → AdminConsole-cf4npybT.js} +1 -1
  124. package/static/app/assets/{Brain-C7bdNmz-.js → Brain-3VCSHFcn.js} +1 -1
  125. package/static/app/assets/{BrainHome-DcTnINcI.js → BrainHome-Qm8eaztx.js} +1 -1
  126. package/static/app/assets/{BrainSignals-Df1MRIPD.js → BrainSignals-DS9BtKOW.js} +1 -1
  127. package/static/app/assets/{Capture-Dgsxvnbx.js → Capture-DiQ219jW.js} +1 -1
  128. package/static/app/assets/{Chronicle-BrkTxJK1.js → Chronicle-BGvuAchH.js} +1 -1
  129. package/static/app/assets/{CommandPalette-DDziw7Oj.js → CommandPalette-Bqhm0Urn.js} +1 -1
  130. package/static/app/assets/{Library-CYxeuraA.js → Library-BV6NnF0a.js} +1 -1
  131. package/static/app/assets/{LivingBrain-CNp6mvCm.js → LivingBrain-GzenJchP.js} +1 -1
  132. package/static/app/assets/{ProductFlow-BVsowu3Z.js → ProductFlow-DEP6-vML.js} +1 -1
  133. package/static/app/assets/{ReviewCard-D1LbOfJS.js → ReviewCard-CNZ7XjWG.js} +1 -1
  134. package/static/app/assets/{System-DcRAq7wj.js → System-CieofHQa.js} +1 -1
  135. package/static/app/assets/arrow-left-kfsrk0mv.js +1 -0
  136. package/static/app/assets/{bot-BK2xQ9mN.js → bot-B_K1Tdmw.js} +1 -1
  137. package/static/app/assets/{brain-BZNztFBb.js → brain-DWyaV1L1.js} +1 -1
  138. package/static/app/assets/{button-CjueubVZ.js → button-aTn4s84A.js} +1 -1
  139. package/static/app/assets/circle-check-qqLug9nU.js +1 -0
  140. package/static/app/assets/{circle-pause-HRjSmpyY.js → circle-pause-xKgeGXkT.js} +1 -1
  141. package/static/app/assets/{circle-play-CMcKDVwu.js → circle-play-DkT6tYPX.js} +1 -1
  142. package/static/app/assets/{cpu-B_MyTfYr.js → cpu-85xYObUC.js} +1 -1
  143. package/static/app/assets/{download-CTy0EByV.js → download-B5Fm7YXo.js} +1 -1
  144. package/static/app/assets/{folder-open-DF6bLhTg.js → folder-open-kk2Xa52u.js} +1 -1
  145. package/static/app/assets/{hard-drive-CpWHG77C.js → hard-drive-DkA3zBW_.js} +1 -1
  146. package/static/app/assets/{index-Dd6abJHX.js → index-BMPdTmlY.js} +3 -3
  147. package/static/app/assets/{input-bVgIRN3s.js → input-B0nRf2jO.js} +1 -1
  148. package/static/app/assets/{link-2-bg6CG5Vk.js → link-2-Dwb4gnTc.js} +1 -1
  149. package/static/app/assets/{permissionCopy-DFTf1HDZ.js → permissionCopy-CQDUBrOZ.js} +1 -1
  150. package/static/app/assets/{primitives-D7D-sQ3T.js → primitives-SNp0LRJz.js} +1 -1
  151. package/static/app/assets/search-BcHqkjoy.js +1 -0
  152. package/static/app/assets/{share-2-BLzo7U4L.js → share-2-BsrxFglO.js} +1 -1
  153. package/static/app/assets/{shield-alert-BmngqnsJ.js → shield-alert-5BStfp2_.js} +1 -1
  154. package/static/app/assets/{textarea-DOSlAQQ3.js → textarea-Cg8IUA-k.js} +1 -1
  155. package/static/app/assets/{useFocusTrap-pZhgeee4.js → useFocusTrap-CYKvE46M.js} +1 -1
  156. package/static/app/assets/{useMutation-BMDwNk4I.js → useMutation-CSn9t1op.js} +1 -1
  157. package/static/app/assets/{useQuery-CSjKttRo.js → useQuery-CY2OI2uy.js} +1 -1
  158. package/static/app/assets/{utils-D3u_yv7B.js → utils-Ddol2RWD.js} +1 -1
  159. package/static/app/assets/{workspace-H3bMjYXC.js → workspace-BqDwOz_p.js} +1 -1
  160. package/static/app/index.html +3 -3
  161. package/static/sw.js +1 -1
  162. package/desktop/electron/README.md +0 -9
  163. package/desktop/electron/main.cjs +0 -58
  164. package/desktop/electron/preload.cjs +0 -5
  165. package/latticeai/core/graph_curator.py +0 -11
  166. package/latticeai/core/hooks.py +0 -11
  167. package/latticeai/core/local_embeddings.py +0 -104
  168. package/latticeai/core/multi_agent.py +0 -11
  169. package/latticeai/core/workflow_engine.py +0 -11
  170. package/latticeai/services/ingestion.py +0 -11
  171. package/latticeai/services/kg_portability.py +0 -11
  172. package/latticeai/services/multimodal_streaming.py +0 -129
  173. package/scripts/measure_brain_home_fill.mjs +0 -141
  174. package/static/app/assets/arrow-left-BHYmOTWU.js +0 -1
  175. package/static/app/assets/circle-check-CLeWJr67.js +0 -1
  176. package/static/app/assets/search-BgSVX6NG.js +0 -1
@@ -47,6 +47,7 @@ from dataclasses import asdict, dataclass, field
47
47
  from pathlib import Path
48
48
  from typing import Any, Dict, List, Optional, Tuple
49
49
 
50
+ from latticeai.core.module_probe import module_available
50
51
  from latticeai.core.quiet import quiet
51
52
  from latticeai.services.process_audit import (
52
53
  CommandConfirmationError,
@@ -59,6 +60,8 @@ from latticeai.services.setup_detection import (
59
60
  detect_cuda,
60
61
  detect_tools,
61
62
  detect_wsl_from_text,
63
+ parse_windows_cpu_info,
64
+ windows_processor_features,
62
65
  )
63
66
  from latticeai.services.setup_detection import (
64
67
  parse_windows_video_controllers as _parse_windows_video_controllers,
@@ -282,37 +285,23 @@ def _detect_cpu_details(prof_os: str) -> Tuple[str, int, int, List[str]]:
282
285
  flags = line.split(":", 1)[-1].strip().lower().split()
283
286
  elif prof_os == "windows":
284
287
  raw = _run(["wmic", "cpu", "get", "Name,NumberOfCores,NumberOfLogicalProcessors", "/format:list"])
285
- for line in raw.splitlines():
286
- key, _, value = line.partition("=")
287
- if key == "Name" and value.strip():
288
- model = value.strip()
289
- elif key == "NumberOfCores" and value.strip():
290
- try:
291
- physical = int(value.strip())
292
- except ValueError:
293
- quiet()
294
- elif key == "NumberOfLogicalProcessors" and value.strip():
295
- try:
296
- logical = int(value.strip())
297
- except ValueError:
298
- quiet()
299
- try:
300
- import ctypes
301
- kernel32 = ctypes.windll.kernel32 # type: ignore[attr-defined] # Windows-only
302
- feature_map = {6: "sse", 10: "sse2", 13: "sse3", 19: "neon", 28: "rdrand"}
303
- flags.extend(name for code, name in feature_map.items() if kernel32.IsProcessorFeaturePresent(code))
304
- except Exception:
305
- quiet()
288
+ model, physical, logical = parse_windows_cpu_info(
289
+ raw, model=model, physical_cores=physical, logical_cores=logical,
290
+ )
291
+ flags.extend(windows_processor_features())
306
292
  interesting = {"avx", "avx2", "avx512f", "fma", "neon", "sse4_2", "sse", "sse2", "sse3", "rdrand"}
307
293
  return model, physical, logical, sorted({flag for flag in flags if flag in interesting})
308
294
 
309
295
 
310
296
  def _has_module(name: str) -> bool:
311
- try:
312
- __import__(name)
313
- return True
314
- except Exception:
315
- return False
297
+ """Importable *and loadable*.
298
+
299
+ ``strict=True`` on purpose: this probe drives what the setup wizard offers
300
+ to install, and an ``mlx`` wheel built for another architecture has a
301
+ findable spec right up until it raises on load. Answering from the spec
302
+ alone would plan a runtime that cannot start.
303
+ """
304
+ return module_available(name, strict=True)
316
305
 
317
306
 
318
307
  def probe() -> SystemProfile:
@@ -10,6 +10,8 @@ from __future__ import annotations
10
10
  import re
11
11
  from typing import Dict, List, Tuple
12
12
 
13
+ from latticeai.services.model_catalog import _version_tuple as _catalog_version_tuple
14
+
13
15
  # ── Model Catalog ─────────────────────────────────────────────────────────────
14
16
  # (model_id, display_name, size_gb, tag, description, min_ram_gb)
15
17
  # 11.2.0: 모든 repo id 를 2026-08-10 Hugging Face API 로 확인했다 — 존재 여부,
@@ -127,8 +129,9 @@ _BEST_MODEL_TIERS: Dict[str, List[Tuple[int, str]]] = {
127
129
  }
128
130
 
129
131
 
130
- def _version_tuple(raw: str) -> Tuple[int, ...]:
131
- return tuple(int(part) for part in raw.split(".") if part.isdigit())
132
+
133
+ #: The engine catalog owns this parse; the wizard compares the same strings.
134
+ _version_tuple = _catalog_version_tuple
132
135
 
133
136
 
134
137
  def _catalog_row_family_version(row: Tuple[str, str, float, str, str, int]) -> Tuple[str, Tuple[int, ...]] | None:
@@ -21,6 +21,8 @@ from latticeai.services.setup_detection import (
21
21
  detect_cuda,
22
22
  detect_tools,
23
23
  detect_wsl_from_text,
24
+ parse_windows_cpu_info,
25
+ windows_processor_features,
24
26
  )
25
27
  from latticeai.services.setup_detection import (
26
28
  parse_windows_video_controllers as _parse_windows_video_controllers,
@@ -98,33 +100,10 @@ def _detect_cpu() -> Dict[str, Any]:
98
100
  quiet()
99
101
  elif platform.system() == "Windows":
100
102
  raw = _cmd(["wmic", "cpu", "get", "Name,NumberOfCores,NumberOfLogicalProcessors", "/format:list"], timeout=5)
101
- for line in raw.splitlines():
102
- key, _, value = line.partition("=")
103
- if key == "Name" and value.strip():
104
- model = value.strip()
105
- elif key == "NumberOfCores" and value.strip():
106
- try:
107
- physical_cores = int(value.strip())
108
- except ValueError:
109
- quiet()
110
- elif key == "NumberOfLogicalProcessors" and value.strip():
111
- try:
112
- logical_cores = int(value.strip())
113
- except ValueError:
114
- quiet()
115
- try:
116
- import ctypes
117
- kernel32 = ctypes.windll.kernel32 # type: ignore[attr-defined] # Windows-only
118
- feature_map = {
119
- 6: "sse",
120
- 10: "sse2",
121
- 13: "sse3",
122
- 19: "neon",
123
- 28: "rdrand",
124
- }
125
- flags.extend(name for code, name in feature_map.items() if kernel32.IsProcessorFeaturePresent(code))
126
- except Exception:
127
- quiet()
103
+ model, physical_cores, logical_cores = parse_windows_cpu_info(
104
+ raw, model=model, physical_cores=physical_cores, logical_cores=logical_cores,
105
+ )
106
+ flags.extend(windows_processor_features())
128
107
  interesting = {"avx", "avx2", "avx512f", "fma", "neon", "sse4_2"}
129
108
  if platform.system() == "Windows":
130
109
  interesting.update({"sse", "sse2", "sse3", "rdrand"})
@@ -20,6 +20,8 @@ import shutil
20
20
  from pathlib import Path
21
21
  from typing import Any, Dict, List
22
22
 
23
+ from latticeai.core.module_probe import module_available as _module_available
24
+
23
25
  OFFICIAL_DOWNLOADS: Dict[str, str] = {
24
26
  "homebrew": "https://brew.sh",
25
27
  "python": "https://www.python.org/downloads/",
@@ -149,11 +151,6 @@ def _which_detail(binary: str) -> Dict[str, Any]:
149
151
  return {"installed": path is not None, "path": path}
150
152
 
151
153
 
152
- def _module_available(module_name: str) -> bool:
153
- import importlib.util
154
- return importlib.util.find_spec(module_name) is not None
155
-
156
-
157
154
  def _package_module(package: str) -> str:
158
155
  return PACKAGE_MODULES.get(package, package.replace("-", "_").split("[", 1)[0])
159
156
 
@@ -8,15 +8,15 @@ plan and therefore nothing to confirm.
8
8
 
9
9
  from __future__ import annotations
10
10
 
11
- import json as _json
12
11
  import sys
13
12
  from typing import Any, Dict, List
14
13
 
14
+ from latticeai.core.sse import sse_frame
15
15
  from latticeai.services.process_audit import command_plan_for_commands
16
16
 
17
17
 
18
18
  def _sse(data: Dict) -> str:
19
- return f"data: {_json.dumps(data, ensure_ascii=False)}\n\n"
19
+ return sse_frame(None, data)
20
20
 
21
21
 
22
22
  def _action_commands(action: Dict[str, Any]) -> List[List[str]]:
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "ltcai",
3
- "version": "11.5.1",
3
+ "version": "11.5.2",
4
4
  "description": "Lattice AI — local-first Digital Brain that keeps your knowledge durable across any AI model.",
5
5
  "homepage": "https://github.com/TaeSooPark-PTS/LatticeAI#readme",
6
6
  "repository": {
@@ -52,7 +52,6 @@
52
52
  "desktop:tauri:check": "cd src-tauri && cargo check",
53
53
  "rust:test": "cd rust && cargo test --workspace",
54
54
  "rust:lint": "cd rust && cargo fmt --all -- --check && cargo clippy --workspace --all-targets -- -D warnings",
55
- "desktop:electron": "electron desktop/electron/main.cjs",
56
55
  "package:vsix": "node scripts/build_vsix.mjs",
57
56
  "check:screenshot-delta": "node scripts/run_python.mjs scripts/check_screenshot_pixel_delta.py",
58
57
  "release:evidence": "node scripts/capture_release_evidence.mjs && node scripts/prune_release_evidence.mjs && npm run check:screenshot-delta",
@@ -124,7 +123,6 @@
124
123
  "!src-tauri/gen/**",
125
124
  "!src-tauri/target/",
126
125
  "!src-tauri/target/**",
127
- "desktop/electron/",
128
126
  "docs/*.md",
129
127
  "!docs/assets/",
130
128
  "!docs/images/"
@@ -142,6 +140,7 @@
142
140
  "@testing-library/react": "^16.3.2",
143
141
  "@testing-library/user-event": "^14.6.1",
144
142
  "@types/cytoscape": "^3.21.9",
143
+ "@types/node": "24.13.2",
145
144
  "@types/react": "^19.2.17",
146
145
  "@types/react-dom": "^19.2.3",
147
146
  "@vitejs/plugin-react": "^6.0.2",
@@ -150,7 +149,6 @@
150
149
  "class-variance-authority": "^0.7.1",
151
150
  "clsx": "^2.1.1",
152
151
  "cytoscape": "^3.34.0",
153
- "electron": "^42.4.0",
154
152
  "jsdom": "^29.1.1",
155
153
  "lucide-react": "^1.17.0",
156
154
  "openapi-fetch": "^0.17.0",
@@ -25,7 +25,20 @@ REPO = Path(__file__).resolve().parents[1]
25
25
  # it per crate — src-tauri's too, since 11.4.0, because the desktop shell now
26
26
  # depends on lattice-host by path — and a lockfile left behind is a file the
27
27
  # next `cargo build` silently rewrites underneath a tagged release.
28
- RUST_CRATES = ("lattice-core", "lattice-host", "lattice-retrieval")
28
+ #
29
+ # 11.5.2: this tuple listed only the three Phase-1 crates, so every bump since
30
+ # 11.5.0 left `lattice-agent`, `lattice-ingest` and `lattice-jobs` at the
31
+ # previous version in both lockfiles — and the first `cargo` invocation of the
32
+ # test suite rewrote them, dirtying the tree mid-release. Every workspace
33
+ # member belongs here.
34
+ RUST_CRATES = (
35
+ "lattice-agent",
36
+ "lattice-core",
37
+ "lattice-host",
38
+ "lattice-ingest",
39
+ "lattice-jobs",
40
+ "lattice-retrieval",
41
+ )
29
42
  RUST_LOCKFILES = ("rust/Cargo.lock", "src-tauri/Cargo.lock")
30
43
 
31
44
  # (path, kind, pattern) — pattern groups: (prefix, version)
@@ -6,7 +6,7 @@ const root = process.cwd();
6
6
  const pkg = JSON.parse(readFileSync(path.join(root, "package.json"), "utf8"));
7
7
  const version = pkg.version;
8
8
  const releaseDir = `output/release/v${version}`;
9
- const releaseTheme = "Rust Full Loop";
9
+ const releaseTheme = "Tight Ship";
10
10
  const title = `${version} — ${releaseTheme}`;
11
11
  const escapedVersion = version.replaceAll(".", "\\.");
12
12
 
@@ -31,7 +31,7 @@ const REMOVED_MODULES = [
31
31
  ];
32
32
  // Trees that must not import removed shims. static/app and node_modules are
33
33
  // build output; src-tauri/target is compiler output.
34
- const SOURCE_TREES = ["latticeai", "lattice_brain", "tests", "scripts", "bin", "desktop/electron"];
34
+ const SOURCE_TREES = ["latticeai", "lattice_brain", "tests", "scripts", "bin"];
35
35
 
36
36
  const failures = [];
37
37
 
@@ -90,6 +90,11 @@ from latticeai.core.agent_helpers import ( # noqa: E402
90
90
  normalize_plan,
91
91
  requirement_coverage,
92
92
  )
93
+ from latticeai.core.agent_profiles import ( # noqa: E402
94
+ COMPACT_MAX_PARAMS_B,
95
+ model_size_b,
96
+ profile_for_model,
97
+ )
93
98
  from latticeai.core.agent_trace import LoopTrace # noqa: E402
94
99
  from latticeai.core.file_generation import ( # noqa: E402
95
100
  infer_file_target,
@@ -107,6 +112,7 @@ from latticeai.core.tool_registry import ( # noqa: E402
107
112
  TOOL_GOVERNANCE,
108
113
  TOOL_GOVERNANCE_DEFAULT,
109
114
  )
115
+ from latticeai.tools.documents import document_output_target # noqa: E402
110
116
 
111
117
  FIXTURE_DIR = REPO_ROOT / "rust" / "fixtures" / "agent_loop"
112
118
  GOLDEN_DIR = FIXTURE_DIR / "golden"
@@ -260,6 +266,59 @@ INFERENCE_MESSAGES: List[str] = [
260
266
  "웹사이트 css js 만들어줘", "마크다운 파일 작성해줘", "yaml 만들어줘",
261
267
  ]
262
268
 
269
+ #: ``(tool_name, filename)`` pairs for the document-target resolver. Chosen for
270
+ #: the branches they reach, not for realism: a tool that is not a document
271
+ #: creator, each of the four that are, a name that already carries its suffix,
272
+ #: one that carries the wrong one, a path that must be reduced to its basename,
273
+ #: characters the sanitizer replaces, and the empty names that fall back to
274
+ #: ``artifact<suffix>``. The Rust port takes the basename with its own
275
+ #: ``path_name`` and has its own empty-name fallback, so those two are exactly
276
+ #: where the two implementations could disagree unnoticed.
277
+ DOCUMENT_TARGET_CASES: List[Dict[str, str]] = [
278
+ {"tool": "write_file", "filename": "notes.md"},
279
+ {"tool": "create_docx", "filename": "report.docx"},
280
+ {"tool": "create_docx", "filename": "report"},
281
+ {"tool": "create_docx", "filename": "report.pdf"},
282
+ {"tool": "create_xlsx", "filename": "budget.xlsx"},
283
+ {"tool": "create_pptx", "filename": "deck"},
284
+ {"tool": "create_pdf", "filename": "invoice"},
285
+ {"tool": "create_pdf", "filename": "sub/dir/invoice.pdf"},
286
+ {"tool": "create_pdf", "filename": "../../escape.pdf"},
287
+ {"tool": "create_docx", "filename": "회의 기록.docx"},
288
+ {"tool": "create_docx", "filename": "a/b*c?d.docx"},
289
+ {"tool": "create_docx", "filename": ""},
290
+ {"tool": "create_pptx", "filename": " "},
291
+ {"tool": "create_xlsx", "filename": "REPORT.XLSX"},
292
+ {"tool": "create_pdf", "filename": ".pdf"},
293
+ {"tool": "unknown_tool", "filename": "x.docx"},
294
+ ]
295
+
296
+ #: Model ids for the profile dial. The interesting ones are the quantization
297
+ #: suffixes (``4bit`` is not a parameter count), the multi-size ids where the
298
+ #: *smallest* wins, and the boundary at ``COMPACT_MAX_PARAMS_B`` itself.
299
+ PROFILE_MODEL_IDS: List[str] = [
300
+ "mlx-community/gemma-4-12B-it-4bit",
301
+ "qwen2.5-1.5b",
302
+ "llama-3.2-3B",
303
+ "mlx-community/Llama-3.2-3B-Instruct-4bit",
304
+ "phi-4-mini-3.8b-8bit",
305
+ "some-model-4b",
306
+ "some-model-4.0b",
307
+ "some-model-4.1b",
308
+ "gpt-4o",
309
+ "claude-sonnet",
310
+ "",
311
+ " ",
312
+ "model-8bit",
313
+ "abc123b",
314
+ "7b-and-1.5b-mixed",
315
+ "Model-70B-Instruct",
316
+ ]
317
+
318
+ #: ``LATTICEAI_AGENT_PROFILE`` values, including the two that must fall through
319
+ #: to the size heuristic rather than failing the run.
320
+ PROFILE_OVERRIDES: List[str] = ["", "standard", "compact", "COMPACT", "nonsense"]
321
+
263
322
  #: Transcripts the artifact/coverage helpers are asked about.
264
323
  TRANSCRIPT_CASES: Dict[str, Dict[str, Any]] = {
265
324
  "empty": {"message": "todo 앱 html css js 만들어줘", "transcript": []},
@@ -382,6 +441,31 @@ def helper_rows() -> Dict[str, Any]:
382
441
  {"input": case, "kept": filter_learnings(case)} for case in LEARNING_CASES
383
442
  ]
384
443
 
444
+ documents = [
445
+ {**case, "target": document_output_target(case["tool"], case["filename"])}
446
+ for case in DOCUMENT_TARGET_CASES
447
+ ]
448
+
449
+ # ``env`` is passed explicitly: the Rust twin reads the ambient process
450
+ # environment, and a golden that inherited this machine's would be a
451
+ # machine-specific value in a committed fixture.
452
+ profiles = []
453
+ for override in PROFILE_OVERRIDES:
454
+ env = {"LATTICEAI_AGENT_PROFILE": override} if override else {}
455
+ for model_id in PROFILE_MODEL_IDS:
456
+ profiles.append({
457
+ "override": override,
458
+ "model_id": model_id,
459
+ "size_b": model_size_b(model_id),
460
+ "profile": profile_for_model(model_id, env=env).__dict__,
461
+ })
462
+ profiles.append({
463
+ "override": "",
464
+ "model_id": None,
465
+ "size_b": model_size_b(""),
466
+ "profile": profile_for_model(None, env={}).__dict__,
467
+ })
468
+
385
469
  return normalize({
386
470
  "schema": SCHEMA,
387
471
  "extract_action_details": actions,
@@ -390,6 +474,8 @@ def helper_rows() -> Dict[str, Any]:
390
474
  "transcript_helpers": transcripts,
391
475
  "truncate_strings": truncated,
392
476
  "filter_learnings": learnings,
477
+ "document_targets": documents,
478
+ "agent_profiles": profiles,
393
479
  "budgets": {
394
480
  "phase": PhaseBudgets().__dict__,
395
481
  "transcript": TranscriptBudget().__dict__,
@@ -858,6 +944,7 @@ def manifest_payload() -> Dict[str, Any]:
858
944
  "transcript_budget": TranscriptBudget().__dict__,
859
945
  "max_state_history": 200,
860
946
  "max_retry": 3,
947
+ "compact_max_params_b": COMPACT_MAX_PARAMS_B,
861
948
  },
862
949
  }
863
950
 
@@ -50,6 +50,7 @@ for _import_root in (REPO_ROOT, Path(__file__).resolve().parent):
50
50
  if str(_import_root) not in sys.path:
51
51
  sys.path.insert(0, str(_import_root))
52
52
 
53
+ import parity_fixture_corpus_context as context_corpus # noqa: E402
53
54
  import parity_fixture_corpus_docgen as docgen # noqa: E402
54
55
 
55
56
  FIXTURE_DIR = REPO_ROOT / "rust" / "fixtures"
@@ -324,23 +325,6 @@ QUERIES: List[Dict[str, Any]] = [
324
325
 
325
326
  #: Every branch of the memories section: a workspace hit, one with no kind and an
326
327
  #: empty snippet, a non-workspace row to drop, and one past the limit.
327
- CONTEXT_MEMORIES: Dict[str, Any] = {
328
- "results": [
329
- {"id": "mem-1", "kind": "preference", "snippet": "답변은 한국어로", "score": 0.91, "source": "workspace"},
330
- {"id": "mem-2", "kind": None, "snippet": "", "score": 0.0, "source": "workspace"},
331
- {"id": "mem-3", "kind": "decision", "snippet": "ranking keeps alpha fusion", "score": 0.4, "source": "personal"},
332
- {"id": "mem-4", "kind": "fact", "snippet": "온보딩은 다섯 걸음", "score": 0.3, "source": "workspace"},
333
- ]
334
- }
335
-
336
- #: A ledger with a pathless row, a non-dict row, and more than the ten-row cut.
337
- CONTEXT_ARTIFACTS: List[Any] = [
338
- {"path": "notes/ranking.md", "at": "2026-07-20T09:00:00", "run_id": "run-1"},
339
- {"path": "notes/onboarding.md", "run_id": "run-2"},
340
- {"path": "", "at": "2026-07-20T09:00:01"},
341
- "not-a-dict",
342
- ] + [{"path": f"out/file-{index}.md", "at": None, "run_id": f"r{index}"} for index in range(10)]
343
-
344
328
  #: The Phase-2/3 suites: one spec list per ported entry point.
345
329
  SUITES: Dict[str, List[Dict[str, Any]]] = {
346
330
  "relationship": [
@@ -462,21 +446,7 @@ SUITES: Dict[str, List[Dict[str, Any]]] = {
462
446
  {"key": "limit_one", "query": "ranking", "limit": 1},
463
447
  {"key": "scoped_strict", "query": "ranking", "allowed": [WS_BETA], "legacy": False},
464
448
  ],
465
- "context_assemble": [
466
- {"key": "all_seams", "query": "회의 결정 사항", "memories": CONTEXT_MEMORIES, "artifacts": CONTEXT_ARTIFACTS, "notes": "정원 노트: 랭킹은 alpha 융합을 유지한다.", "recent": {"limit": 4}},
467
- {"key": "knowledge_only", "query": "hybrid retrieval ranking"},
468
- {"key": "no_seams", "query": "hybrid retrieval ranking", "knowledge": False},
469
- {"key": "memories_only", "query": "온보딩", "knowledge": False, "memories": CONTEXT_MEMORIES, "memory_limit": 2},
470
- {"key": "artifacts_only", "query": "온보딩", "knowledge": False, "artifacts": CONTEXT_ARTIFACTS},
471
- {"key": "notes_blank", "query": "온보딩", "knowledge": False, "notes": " "},
472
- {"key": "recent_conversation", "query": "온보딩", "knowledge": False, "recent": {"conversation_id": "conv-a", "limit": 10}},
473
- {"key": "recent_personal_workspace", "query": "온보딩", "knowledge": False, "recent": {"workspace_id": "personal", "limit": 6}},
474
- {"key": "recent_user_scoped", "query": "온보딩", "knowledge": False, "recent": {"user_email": "jiwon@lattice.ai", "limit": 5}},
475
- {"key": "budget_tiny", "query": "회의 결정 사항", "memories": CONTEXT_MEMORIES, "artifacts": CONTEXT_ARTIFACTS, "notes": "정원 노트: 랭킹은 alpha 융합을 유지한다.", "recent": {"limit": 4}, "budget": 20},
476
- {"key": "budget_one", "query": "회의 결정 사항", "memories": CONTEXT_MEMORIES, "artifacts": CONTEXT_ARTIFACTS, "notes": "노트", "recent": {"limit": 4}, "budget": 1},
477
- {"key": "budget_zero", "query": "회의 결정 사항", "memories": CONTEXT_MEMORIES, "notes": "노트", "budget": 0},
478
- {"key": "knowledge_limit_one", "query": "hybrid retrieval ranking", "knowledge_limit": 1},
479
- ],
449
+ **context_corpus.CONTEXT_SUITES,
480
450
  # v11.5.1: the document-generation ports, specs and runners alike.
481
451
  **docgen.DOCGEN_SUITES,
482
452
  }
@@ -799,68 +769,6 @@ def _run_history_search(h: Harness, spec: Dict[str, Any]) -> List[Dict[str, Any]
799
769
  )
800
770
 
801
771
 
802
- def _context_seams(h: Harness, spec: Dict[str, Any]) -> Dict[str, Any]:
803
- """The seam set for one context spec — data seams plus the real engines.
804
-
805
- ``memories`` / ``artifacts`` / ``notes`` are *data* seams: the payload is the
806
- spec, so both runtimes feed the assembler the same bytes and what is under
807
- test is the assembler. ``knowledge`` and ``recent`` are real engines — the
808
- service-layer hybrid search and the durable history reader.
809
- """
810
- from latticeai.api.chat_helpers import build_recent_chat_context
811
-
812
- # Signatures matter: the assembler inspects them to decide which context
813
- # fields a seam may be handed, so each one declares exactly what it accepts.
814
- seams: Dict[str, Any] = {}
815
- if spec.get("memories") is not None:
816
- memories = spec["memories"]
817
- seams["memory_recall"] = (
818
- lambda query, *, user_email=None, workspace_id=None, limit=5: memories
819
- )
820
- if spec.get("artifacts") is not None:
821
- artifacts = spec["artifacts"]
822
- seams["recent_artifacts"] = (
823
- lambda *, user_email=None, conversation_id=None, workspace_id=None: artifacts
824
- )
825
- if spec.get("knowledge", True):
826
- # Loopback trust: no workspace scoping, exactly as on the native route.
827
- seams["hybrid_search"] = (
828
- lambda query, *, limit=5, user_email=None, workspace_id=None:
829
- h.service.hybrid_search(query, limit=limit)
830
- )
831
- if spec.get("notes") is not None:
832
- notes = spec["notes"]
833
- seams["notes_context"] = lambda query, *, user_email=None, workspace_id=None: notes
834
- if spec.get("recent") is not None:
835
- recent = spec["recent"]
836
- seams["recent_chat"] = (
837
- lambda *, user_email=None, conversation_id=None, workspace_id=None:
838
- build_recent_chat_context(
839
- get_history=h.history_runtime["get_history"],
840
- limit=recent.get("limit", 10),
841
- include_image_missing_replies=recent.get("images", True),
842
- user_email=recent.get("user_email"),
843
- conversation_id=recent.get("conversation_id"),
844
- workspace_id=recent.get("workspace_id"),
845
- )
846
- )
847
- return seams
848
-
849
-
850
- def _run_context_assemble(h: Harness, spec: Dict[str, Any]) -> Dict[str, Any]:
851
- from lattice_brain.context import ContextAssembler
852
-
853
- assembled = ContextAssembler(**_context_seams(h, spec)).assemble(
854
- spec["query"], user_email=spec.get("user_email"),
855
- workspace_id=spec.get("workspace_id"),
856
- conversation_id=spec.get("conversation_id"), budget=spec.get("budget", 2000),
857
- memory_limit=spec.get("memory_limit", 5),
858
- knowledge_limit=spec.get("knowledge_limit", 5),
859
- )
860
- return {"text": assembled.text, "approx_tokens": assembled.approx_tokens,
861
- "trace": assembled.trace()}
862
-
863
-
864
772
  SUITE_RUNNERS: Dict[str, Callable[[Harness, Dict[str, Any]], Any]] = {
865
773
  "relationship": _run_relationship,
866
774
  "traverse": _run_traverse,
@@ -870,7 +778,7 @@ SUITE_RUNNERS: Dict[str, Callable[[Harness, Dict[str, Any]], Any]] = {
870
778
  "conversations": _run_conversations,
871
779
  "conversation_messages": _run_conversation_messages,
872
780
  "history_search": _run_history_search,
873
- "context_assemble": _run_context_assemble,
781
+ **context_corpus.CONTEXT_RUNNERS,
874
782
  **docgen.DOCGEN_RUNNERS,
875
783
  }
876
784