ltcai 10.0.0 → 10.2.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (211) hide show
  1. package/README.md +48 -32
  2. package/docs/CHANGELOG.md +156 -0
  3. package/docs/COMMUNITY_AND_PLUGINS.md +1 -1
  4. package/docs/DEVELOPMENT.md +1 -1
  5. package/docs/HYBRID_CLOUD_KG_STREAMING.md +101 -0
  6. package/docs/ONBOARDING.md +1 -1
  7. package/docs/OPERATIONS.md +1 -1
  8. package/docs/TRUST_MODEL.md +1 -1
  9. package/docs/WHY_LATTICE.md +1 -1
  10. package/docs/kg-schema.md +1 -1
  11. package/lattice_brain/__init__.py +1 -1
  12. package/lattice_brain/archive.py +5 -4
  13. package/lattice_brain/conversations.py +14 -3
  14. package/lattice_brain/embeddings.py +12 -2
  15. package/lattice_brain/graph/_kg_common.py +5 -5
  16. package/lattice_brain/graph/_kg_fsutil.py +4 -3
  17. package/lattice_brain/graph/discovery.py +4 -3
  18. package/lattice_brain/graph/discovery_index.py +0 -1
  19. package/lattice_brain/graph/documents.py +3 -2
  20. package/lattice_brain/graph/fusion.py +4 -0
  21. package/lattice_brain/graph/ingest.py +12 -2
  22. package/lattice_brain/graph/projection.py +3 -2
  23. package/lattice_brain/graph/provenance.py +5 -3
  24. package/lattice_brain/graph/rerank.py +4 -1
  25. package/lattice_brain/graph/retrieval.py +0 -1
  26. package/lattice_brain/graph/retrieval_docgen.py +0 -1
  27. package/lattice_brain/graph/retrieval_reads.py +0 -1
  28. package/lattice_brain/graph/retrieval_vector.py +0 -1
  29. package/lattice_brain/graph/schema.py +4 -3
  30. package/lattice_brain/graph/store.py +18 -4
  31. package/lattice_brain/graph/write_master.py +46 -1
  32. package/lattice_brain/ingestion.py +4 -1
  33. package/lattice_brain/portability.py +5 -2
  34. package/lattice_brain/quality.py +12 -5
  35. package/lattice_brain/quiet.py +43 -0
  36. package/lattice_brain/runtime/agent_runtime.py +12 -8
  37. package/lattice_brain/runtime/hooks.py +2 -1
  38. package/lattice_brain/runtime/multi_agent.py +2 -3
  39. package/lattice_brain/sensitivity.py +94 -0
  40. package/lattice_brain/storage/base.py +30 -2
  41. package/lattice_brain/storage/migration.py +3 -2
  42. package/lattice_brain/storage/postgres.py +2 -2
  43. package/lattice_brain/storage/sqlite.py +6 -4
  44. package/lattice_brain/workflow.py +4 -2
  45. package/latticeai/__init__.py +1 -1
  46. package/latticeai/api/admin.py +1 -1
  47. package/latticeai/api/agents.py +4 -2
  48. package/latticeai/api/auth.py +5 -1
  49. package/latticeai/api/automation_intelligence.py +2 -1
  50. package/latticeai/api/browser.py +3 -2
  51. package/latticeai/api/chat.py +28 -17
  52. package/latticeai/api/chat_agent_http.py +22 -8
  53. package/latticeai/api/chat_contracts.py +4 -0
  54. package/latticeai/api/chat_documents.py +6 -2
  55. package/latticeai/api/chat_hybrid.py +82 -0
  56. package/latticeai/api/computer_use.py +8 -3
  57. package/latticeai/api/knowledge_graph.py +1 -1
  58. package/latticeai/api/mcp.py +4 -4
  59. package/latticeai/api/models.py +5 -2
  60. package/latticeai/api/network_boundary.py +220 -0
  61. package/latticeai/api/permissions.py +0 -1
  62. package/latticeai/api/realtime.py +1 -1
  63. package/latticeai/api/security_dashboard.py +1 -1
  64. package/latticeai/api/setup.py +16 -3
  65. package/latticeai/api/static_routes.py +2 -1
  66. package/latticeai/api/tools.py +12 -8
  67. package/latticeai/api/voice_capture.py +3 -1
  68. package/latticeai/api/workflow_designer.py +1 -1
  69. package/latticeai/api/workspace.py +1 -2
  70. package/latticeai/app_factory.py +131 -78
  71. package/latticeai/cli/entrypoint.py +6 -4
  72. package/latticeai/core/agent.py +55 -495
  73. package/latticeai/core/agent_eval.py +2 -2
  74. package/latticeai/core/agent_helpers.py +493 -0
  75. package/latticeai/core/agent_prompts.py +0 -1
  76. package/latticeai/core/agent_registry.py +3 -1
  77. package/latticeai/core/agent_state.py +41 -0
  78. package/latticeai/core/audit.py +1 -1
  79. package/latticeai/core/builtin_hooks.py +2 -1
  80. package/latticeai/core/config.py +0 -1
  81. package/latticeai/core/embedding_providers.py +12 -1
  82. package/latticeai/core/file_generation.py +3 -0
  83. package/latticeai/core/invitations.py +4 -1
  84. package/latticeai/core/io_utils.py +3 -1
  85. package/latticeai/core/legacy_compatibility.py +1 -2
  86. package/latticeai/core/local_embeddings.py +12 -1
  87. package/latticeai/core/marketplace.py +1 -2
  88. package/latticeai/core/mcp_registry.py +0 -1
  89. package/latticeai/core/model_compat.py +1 -1
  90. package/latticeai/core/model_resolution.py +1 -1
  91. package/latticeai/core/network_boundary.py +168 -0
  92. package/latticeai/core/oidc.py +3 -0
  93. package/latticeai/core/permission_mode.py +6 -6
  94. package/latticeai/core/plugins.py +3 -2
  95. package/latticeai/core/policy.py +0 -1
  96. package/latticeai/core/quiet.py +84 -0
  97. package/latticeai/core/realtime.py +3 -2
  98. package/latticeai/core/run_store.py +4 -2
  99. package/latticeai/core/security.py +4 -0
  100. package/latticeai/core/users.py +5 -3
  101. package/latticeai/core/workspace_graph_trace.py +3 -0
  102. package/latticeai/core/workspace_os.py +65 -273
  103. package/latticeai/core/workspace_os_constants.py +126 -0
  104. package/latticeai/core/workspace_os_state.py +180 -0
  105. package/latticeai/core/workspace_os_utils.py +3 -1
  106. package/latticeai/core/workspace_permissions.py +1 -0
  107. package/latticeai/core/workspace_snapshots.py +3 -1
  108. package/latticeai/core/workspace_timeline.py +3 -1
  109. package/latticeai/integrations/telegram_bot.py +25 -16
  110. package/latticeai/models/router.py +6 -3
  111. package/latticeai/runtime/access_runtime.py +3 -1
  112. package/latticeai/runtime/audit_runtime.py +3 -2
  113. package/latticeai/runtime/chat_wiring.py +4 -1
  114. package/latticeai/runtime/history_runtime.py +1 -1
  115. package/latticeai/runtime/lifespan_runtime.py +3 -1
  116. package/latticeai/runtime/network_boundary_wiring.py +124 -0
  117. package/latticeai/runtime/persistence_runtime.py +3 -1
  118. package/latticeai/runtime/router_registration.py +11 -1
  119. package/latticeai/services/architecture_readiness.py +1 -2
  120. package/latticeai/services/change_proposals.py +2 -1
  121. package/latticeai/services/cloud_egress_audit.py +85 -0
  122. package/latticeai/services/cloud_extraction.py +129 -0
  123. package/latticeai/services/cloud_streaming.py +266 -0
  124. package/latticeai/services/cloud_token_guard.py +84 -0
  125. package/latticeai/services/command_center.py +2 -1
  126. package/latticeai/services/folder_watch.py +3 -0
  127. package/latticeai/services/funnel_metrics.py +3 -1
  128. package/latticeai/services/hybrid_chat.py +265 -0
  129. package/latticeai/services/hybrid_context.py +228 -0
  130. package/latticeai/services/hybrid_policy.py +178 -0
  131. package/latticeai/services/memory_service.py +1 -1
  132. package/latticeai/services/model_catalog.py +10 -1
  133. package/latticeai/services/model_engines.py +35 -14
  134. package/latticeai/services/model_loading.py +3 -2
  135. package/latticeai/services/model_runtime.py +68 -17
  136. package/latticeai/services/multimodal_streaming.py +123 -0
  137. package/latticeai/services/network_boundary_service.py +154 -0
  138. package/latticeai/services/openai_compatible_adapter.py +100 -0
  139. package/latticeai/services/p_reinforce.py +4 -0
  140. package/latticeai/services/platform_runtime.py +9 -4
  141. package/latticeai/services/process_audit.py +0 -1
  142. package/latticeai/services/product_readiness.py +1 -1
  143. package/latticeai/services/run_executor.py +3 -2
  144. package/latticeai/services/search_service.py +1 -4
  145. package/latticeai/services/setup_detection.py +2 -1
  146. package/latticeai/services/tool_dispatch.py +7 -2
  147. package/latticeai/services/upload_service.py +2 -1
  148. package/latticeai/setup/auto_setup.py +10 -7
  149. package/latticeai/setup/wizard.py +15 -11
  150. package/latticeai/tools/__init__.py +3 -3
  151. package/latticeai/tools/commands.py +7 -7
  152. package/latticeai/tools/computer.py +3 -2
  153. package/latticeai/tools/documents.py +10 -9
  154. package/latticeai/tools/filesystem.py +7 -4
  155. package/latticeai/tools/knowledge.py +2 -0
  156. package/latticeai/tools/local_files.py +1 -1
  157. package/latticeai/tools/network.py +2 -1
  158. package/package.json +5 -3
  159. package/scripts/bench_agent_smoke.py +0 -1
  160. package/scripts/bench_models.py +0 -1
  161. package/scripts/brain_quality_eval.py +7 -2
  162. package/scripts/bump_version.py +2 -1
  163. package/scripts/check_current_release_docs.mjs +1 -1
  164. package/scripts/migrate_brain_storage.py +5 -1
  165. package/scripts/profile_kg.py +2 -7
  166. package/scripts/verify_hf_model_registry.py +2 -2
  167. package/src-tauri/Cargo.lock +1 -1
  168. package/src-tauri/Cargo.toml +1 -1
  169. package/src-tauri/tauri.conf.json +1 -1
  170. package/static/app/asset-manifest.json +37 -37
  171. package/static/app/assets/{Act-BtCREeN1.js → Act-CbdGD-2i.js} +1 -1
  172. package/static/app/assets/{AdminConsole-TPeeN18T.js → AdminConsole-LgCkXpnf.js} +1 -1
  173. package/static/app/assets/{Brain-BKs6JAp0.js → Brain-CoPGJI1L.js} +1 -1
  174. package/static/app/assets/{BrainHome-BPGOvSd6.js → BrainHome-gVnaxSwp.js} +1 -1
  175. package/static/app/assets/{BrainSignals-CtzQZ15J.js → BrainSignals-ChxAYHtj.js} +1 -1
  176. package/static/app/assets/{Capture-1_NaHWqB.js → Capture-DBtgkHZg.js} +1 -1
  177. package/static/app/assets/{CommandPalette-pqvQOXe4.js → CommandPalette-Ovtv5I0Y.js} +1 -1
  178. package/static/app/assets/{Library-DhvoPvC7.js → Library-CBia2Fvm.js} +1 -1
  179. package/static/app/assets/{LivingBrain-DlQ20Q75.js → LivingBrain-CNz-6NSd.js} +1 -1
  180. package/static/app/assets/{ProductFlow-BZvGDRi_.js → ProductFlow-ePX-Y73J.js} +1 -1
  181. package/static/app/assets/{ReviewCard-BWgI0D2s.js → ReviewCard-C4mpvkwH.js} +1 -1
  182. package/static/app/assets/System-BvWNK1zc.js +1 -0
  183. package/static/app/assets/{activity-Dlfk8YC7.js → activity-CiauPV_3.js} +1 -1
  184. package/static/app/assets/{bot-CDvUB76P.js → bot-Dwct-Q1x.js} +1 -1
  185. package/static/app/assets/{brain-xczrohrt.js → brain-BKDW1F0T.js} +1 -1
  186. package/static/app/assets/{button-SOdH3Oyf.js → button-CCB9Kuw7.js} +1 -1
  187. package/static/app/assets/{circle-pause-CG1ythH4.js → circle-pause-DNa8WHC5.js} +1 -1
  188. package/static/app/assets/{circle-play-HXwvjS6W.js → circle-play-8jmxn5mf.js} +1 -1
  189. package/static/app/assets/{cpu-B0d-rGyk.js → cpu-mxvF3V1I.js} +1 -1
  190. package/static/app/assets/{download-BGIkTQL6.js → download--SmCcx0p.js} +1 -1
  191. package/static/app/assets/{folder-open-Dst_Z0_K.js → folder-open-D4Wy5roo.js} +1 -1
  192. package/static/app/assets/{hard-drive-D53MsWkV.js → hard-drive-BwQcSPlS.js} +1 -1
  193. package/static/app/assets/index-CQWdDU3z.css +2 -0
  194. package/static/app/assets/{index-C_IrlQMV.js → index-DDV2YZwM.js} +3 -3
  195. package/static/app/assets/{input-C5m0riF6.js → input-DFhSjmLS.js} +1 -1
  196. package/static/app/assets/{network-C5a-E5iS.js → network-Bts7VO94.js} +1 -1
  197. package/static/app/assets/{primitives-vNXYf58F.js → primitives-BuSMEJY8.js} +1 -1
  198. package/static/app/assets/search-DZzxhWaQ.js +1 -0
  199. package/static/app/assets/{shield-alert-Cc-WVXqN.js → shield-alert-BfTO6X_G.js} +1 -1
  200. package/static/app/assets/{textarea-BkZ0EqVO.js → textarea-BjctW1oh.js} +1 -1
  201. package/static/app/assets/{useFocusTrap-CTtKbAOU.js → useFocusTrap-CE43-LrA.js} +1 -1
  202. package/static/app/assets/{useQuery-Dx1fi4Wu.js → useQuery-Bv3ejLL5.js} +1 -1
  203. package/static/app/assets/{users-BFpQXtEF.js → users-CsNqLZAj.js} +1 -1
  204. package/static/app/assets/{utils-BA_lmW3J.js → utils-KFFdVG_t.js} +2 -2
  205. package/static/app/assets/workspace-wdCvdyPF.js +1 -0
  206. package/static/app/index.html +4 -4
  207. package/static/sw.js +1 -1
  208. package/static/app/assets/System-CSMdYLMy.js +0 -1
  209. package/static/app/assets/index-FxDusbr0.css +0 -2
  210. package/static/app/assets/search-DhbSgW6m.js +0 -1
  211. package/static/app/assets/workspace-DBPB0jkX.js +0 -1
@@ -0,0 +1,493 @@
1
+ """Pure helpers for the single-agent runtime.
2
+
3
+ Extracted from :mod:`latticeai.core.agent` so the state machine module
4
+ owns only the loop. Every function here is deterministic and free of
5
+ I/O / side effects (aside from reading environment in the budget
6
+ ``from_env`` constructors).
7
+
8
+ Public names are re-exported from :mod:`latticeai.core.agent` so callers
9
+ keep importing from the original location.
10
+ """
11
+
12
+ from __future__ import annotations
13
+
14
+ import ast
15
+ import json
16
+ import re
17
+ from dataclasses import dataclass
18
+ from pathlib import Path
19
+ from typing import Any, Dict, FrozenSet, List, Mapping, Optional, Tuple
20
+
21
+ from latticeai.core.agent_state import AgentState
22
+ from latticeai.core.file_generation import infer_file_target, infer_project_manifest
23
+
24
+ # ── action parsing ─────────────────────────────────────────────────────
25
+
26
+ _THINK_BLOCK_RE = re.compile(
27
+ r"<(think|thinking|reasoning)>.*?</\1>", flags=re.DOTALL | re.IGNORECASE
28
+ )
29
+
30
+
31
+ def extract_action_details(raw: str) -> Tuple[Dict, List[str]]:
32
+ """Parse one JSON action object out of an LLM response (tolerant of fences/prose).
33
+
34
+ Returns ``(action, repairs)`` where ``repairs`` names every tolerance that
35
+ was needed — the loop trace and the weak-model robustness harness consume
36
+ it to measure how much help a given model needs.
37
+ """
38
+ repairs: List[str] = []
39
+ # Small local models often prepend <think>...</think> reasoning that can
40
+ # itself contain braces — drop it before locating the action object.
41
+ text = _THINK_BLOCK_RE.sub("", raw).strip()
42
+ if text != str(raw).strip():
43
+ repairs.append("think_strip")
44
+ fenced = re.search(r"```(?:json)?\s*(\{.*?\})\s*```", text, flags=re.DOTALL)
45
+ if fenced:
46
+ text = fenced.group(1).strip()
47
+ repairs.append("fence")
48
+ elif not text.startswith("{"):
49
+ start = text.find("{")
50
+ end = text.rfind("}")
51
+ if start >= 0 and end > start:
52
+ text = text[start : end + 1]
53
+ repairs.append("slice")
54
+
55
+ action: Any = None
56
+ try:
57
+ action = json.loads(text)
58
+ except json.JSONDecodeError:
59
+ # Second chance for the most common small-model JSON slips: trailing
60
+ # commas before a closing brace/bracket.
61
+ repaired = re.sub(r",\s*([}\]])", r"\1", text)
62
+ try:
63
+ action = json.loads(repaired)
64
+ repairs.append("trailing_comma")
65
+ except json.JSONDecodeError as exc:
66
+ # Last chance: weak models sometimes emit a Python dict literal
67
+ # (single quotes, True/False/None). ast.literal_eval parses that
68
+ # deterministically without evaluating code.
69
+ try:
70
+ literal = ast.literal_eval(text)
71
+ except (ValueError, SyntaxError):
72
+ raise ValueError(f"Agent did not return valid JSON: {exc}") from exc
73
+ if not isinstance(literal, dict):
74
+ raise ValueError(f"Agent did not return valid JSON: {exc}") from exc
75
+ action = literal
76
+ repairs.append("python_literal")
77
+
78
+ if not isinstance(action, dict) or "action" not in action:
79
+ raise ValueError("Agent JSON must include an action field.")
80
+ return action, repairs
81
+
82
+
83
+ def extract_action(raw: str) -> Dict:
84
+ """Back-compat wrapper over :func:`extract_action_details`."""
85
+ action, _ = extract_action_details(raw)
86
+ return action
87
+
88
+
89
+ # ── plan normalization ─────────────────────────────────────────────────
90
+
91
+ _FILE_CREATE_PLAN_ACTIONS = frozenset({"write_file", "generate_file"})
92
+
93
+
94
+ def _plan_misses_manifest(steps: List[Dict[str, Any]], manifest: Dict[str, Any]) -> bool:
95
+ """True when a pure file-writing plan fails to cover the manifest's file types.
96
+
97
+ Only pure file-creation plans are candidates for rewriting — a plan with
98
+ read/search steps reflects real planner intent and stays untouched.
99
+ """
100
+ if any(s.get("action") not in _FILE_CREATE_PLAN_ACTIONS for s in steps):
101
+ return False
102
+
103
+ def _ext(path: Any) -> str:
104
+ text = str(path or "")
105
+ dot = text.rfind(".")
106
+ return text[dot:].lower() if dot >= 0 else ""
107
+
108
+ planned_exts = {
109
+ _ext((s.get("args") or {}).get("path")) for s in steps
110
+ }
111
+ manifest_exts = {_ext(spec.get("path")) for spec in manifest.get("files", [])}
112
+ return not manifest_exts.issubset(planned_exts)
113
+
114
+
115
+ def normalize_plan(plan: Any, user_message: str) -> Tuple[Dict[str, Any], List[str]]:
116
+ """Enforce the minimal plan schema so execution never starts adrift.
117
+
118
+ A weak planner that returns junk steps / a missing goal previously flowed
119
+ straight into the executor, which then had to reconstruct intent from the
120
+ raw request. Normalization keeps the loop honest: ``goal`` is always a
121
+ non-empty string, ``steps`` only contains dicts with an ``action``, and an
122
+ empty plan for an obvious file-creation request gets a deterministic
123
+ single ``write_file`` step instead of leaving the executor to improvise.
124
+
125
+ Returns ``(plan, fixes)`` where ``fixes`` names every applied repair —
126
+ the loop trace records them so plan quality is observable per model.
127
+ """
128
+ fixes: List[str] = []
129
+ if not isinstance(plan, dict):
130
+ plan = {}
131
+ fixes.append("plan_not_object")
132
+ plan = dict(plan)
133
+
134
+ goal = str(plan.get("goal") or "").strip()
135
+ if not goal:
136
+ plan["goal"] = user_message
137
+ fixes.append("goal_defaulted")
138
+
139
+ raw_steps = plan.get("steps")
140
+ steps = [
141
+ s for s in (raw_steps if isinstance(raw_steps, list) else [])
142
+ if isinstance(s, dict) and s.get("action")
143
+ ]
144
+ if raw_steps and steps != raw_steps:
145
+ fixes.append("steps_filtered")
146
+
147
+ # Manifest-aware planning (review Wave 0.4): when the request is a
148
+ # recognized multi-file project, the deterministic manifest — not the
149
+ # planner's improvisation — decides the file set, exactly like the direct
150
+ # chat path. Rewrites apply only when the plan is empty or is a pure
151
+ # file-writing plan that misses part of the manifest, so a planner that
152
+ # already covered every requested file type is left untouched.
153
+ manifest = infer_project_manifest(user_message)
154
+ if manifest:
155
+ manifest_steps = [{
156
+ "action": "write_file",
157
+ "args": {"path": spec["path"]},
158
+ "description": spec["brief"],
159
+ } for spec in manifest["files"]]
160
+ if not steps:
161
+ steps = manifest_steps
162
+ fixes.append("manifest_steps")
163
+ elif _plan_misses_manifest(steps, manifest):
164
+ steps = manifest_steps
165
+ fixes.append("manifest_rewrite")
166
+
167
+ if not steps:
168
+ inferred = infer_file_target(user_message)
169
+ if inferred:
170
+ steps = [{
171
+ "action": "write_file",
172
+ "args": {"path": inferred},
173
+ "description": f"Create {inferred} for: {user_message[:120]}",
174
+ }]
175
+ fixes.append("heuristic_file_step")
176
+ plan["steps"] = steps
177
+
178
+ try:
179
+ estimated = int(plan.get("estimated_steps") or 0)
180
+ except (TypeError, ValueError):
181
+ estimated = 0
182
+ fixes.append("estimated_steps_invalid")
183
+ plan["estimated_steps"] = max(1, estimated, len(steps))
184
+ plan["requires_approval"] = bool(plan.get("requires_approval", False))
185
+ if not isinstance(plan.get("rollback_strategy"), str):
186
+ plan["rollback_strategy"] = "none"
187
+ return plan, fixes
188
+
189
+
190
+ # ── learnings filter ───────────────────────────────────────────────────
191
+
192
+ _TRIVIAL_LEARNING_RE = re.compile(
193
+ r"^(파일(을|이)?\s*(만들|생성|작성|저장)|작업(을|이)?\s*(완료|성공)|성공적으로"
194
+ r"|task\s+(was\s+)?complet|file\s+(was\s+)?(creat|written|saved)"
195
+ r"|(successfully\s+)?(created|completed|finished|done)\b)",
196
+ re.IGNORECASE,
197
+ )
198
+
199
+
200
+ def filter_learnings(learnings: List[Any]) -> List[str]:
201
+ """Drop trivial/duplicate learnings before they enter the brain.
202
+
203
+ "파일을 만들었다"-class statements restate what the transcript already
204
+ records and pollute recall. A learning survives when it is long enough to
205
+ carry information and is not a bare completion announcement.
206
+ """
207
+ kept: List[str] = []
208
+ seen: set = set()
209
+ for raw in learnings or []:
210
+ text = str(raw or "").strip()
211
+ if len(text) < 12:
212
+ continue
213
+ if _TRIVIAL_LEARNING_RE.match(text) and len(text) < 48:
214
+ continue
215
+ key = text.lower()
216
+ if key in seen:
217
+ continue
218
+ seen.add(key)
219
+ kept.append(text)
220
+ return kept
221
+
222
+
223
+ # ── transcript shaping ─────────────────────────────────────────────────
224
+
225
+ def _truncate_strings(value: Any, limit: int) -> Any:
226
+ """Deep-copy ``value`` with every string capped at ``limit`` chars.
227
+
228
+ Long tool outputs (file bodies, command output) dominate executor prompt
229
+ size without adding decision-relevant signal. The cap keeps the head of
230
+ each string and names how much was dropped, so the model still sees what
231
+ the value was — never a silent hole.
232
+ """
233
+ if isinstance(value, str):
234
+ if len(value) <= limit:
235
+ return value
236
+ return value[:limit] + f"…[+{len(value) - limit} chars]"
237
+ if isinstance(value, dict):
238
+ return {k: _truncate_strings(v, limit) for k, v in value.items()}
239
+ if isinstance(value, list):
240
+ return [_truncate_strings(v, limit) for v in value]
241
+ return value
242
+
243
+
244
+ def compact_transcript(
245
+ transcript: List[Dict[str, Any]],
246
+ *,
247
+ window: int = 8,
248
+ result_chars: int = 700,
249
+ ) -> List[Dict[str, Any]]:
250
+ """Bounded executor view of a transcript (review Wave 0.3).
251
+
252
+ The executor prompt previously embedded the *entire* transcript JSON every
253
+ step — O(steps²) token growth that starved :class:`PhaseBudgets` on long
254
+ runs and buried weak models in stale detail. This view keeps the most
255
+ recent ``window`` steps in full (with string values capped at
256
+ ``result_chars``) and reduces every older step to a one-line summary, so
257
+ the prompt stays bounded while no step disappears entirely.
258
+ """
259
+ steps = list(transcript or [])
260
+ if len(steps) <= window:
261
+ return [_truncate_strings(step, result_chars) for step in steps]
262
+ older, recent = steps[:-window], steps[-window:]
263
+ summarized: List[Dict[str, Any]] = [{
264
+ "summarized_older_steps": len(older),
265
+ "note": "older steps compacted — full detail retained in the run record",
266
+ }]
267
+ for step in older:
268
+ entry: Dict[str, Any] = {"state": step.get("state")}
269
+ for key in ("action", "verdict", "retry_attempt"):
270
+ if step.get(key) is not None:
271
+ entry[key] = step.get(key)
272
+ if step.get("error"):
273
+ entry["error"] = str(step["error"])[:160]
274
+ elif isinstance(step.get("result"), dict):
275
+ entry["ok"] = True
276
+ path = step["result"].get("path") or (step.get("args") or {}).get("path")
277
+ if path:
278
+ entry["path"] = str(path)
279
+ summarized.append(entry)
280
+ summarized.extend(_truncate_strings(step, result_chars) for step in recent)
281
+ return summarized
282
+
283
+
284
+ def files_written(
285
+ transcript: List[Dict[str, Any]],
286
+ file_create_actions: FrozenSet[str],
287
+ ) -> List[str]:
288
+ """Ordered unique paths of files this run successfully wrote (review L5).
289
+
290
+ Later executor steps get this as explicit context, so "만들고 이어서
291
+ 설명해" multi-step work sees its own output instead of a stale
292
+ workspace picture.
293
+ """
294
+ seen: List[str] = []
295
+ for step in transcript:
296
+ if step.get("state") != AgentState.EXECUTING.value:
297
+ continue
298
+ if step.get("action") not in file_create_actions:
299
+ continue
300
+ if not isinstance(step.get("result"), dict):
301
+ continue
302
+ path = step["result"].get("path") or (step.get("args") or {}).get("path")
303
+ if path and str(path) not in seen:
304
+ seen.append(str(path))
305
+ return seen
306
+
307
+
308
+ def artifact_checklist(
309
+ transcript: List[Dict[str, Any]],
310
+ file_create_actions: FrozenSet[str],
311
+ ) -> List[Dict[str, Any]]:
312
+ """Deterministic artifact facts for the critic (review L4).
313
+
314
+ The critic previously judged file work from prose alone; this surfaces
315
+ the sanitize/repair honesty flags per written file so a repaired
316
+ placeholder can never pass as a fulfilled request unchecked.
317
+ """
318
+ checklist: List[Dict[str, Any]] = []
319
+ for step in transcript:
320
+ if step.get("state") != AgentState.EXECUTING.value:
321
+ continue
322
+ if step.get("action") not in file_create_actions:
323
+ continue
324
+ if not isinstance(step.get("result"), dict):
325
+ continue
326
+ path = step["result"].get("path") or (step.get("args") or {}).get("path")
327
+ if not path:
328
+ continue
329
+ sanitize_meta = step.get("content_sanitize") or {}
330
+ checklist.append({
331
+ "path": str(path),
332
+ "sanitized": bool(sanitize_meta.get("sanitized")),
333
+ "repaired": bool(sanitize_meta.get("repaired")),
334
+ })
335
+ return checklist
336
+
337
+
338
+ # Explicit requirement lines a user writes out: "- 다크모드", "1. 검색 기능",
339
+ # "* dark mode". Free prose is deliberately NOT parsed — a wrong requirement
340
+ # is worse than no requirement.
341
+ _REQUIREMENT_LINE_RE = re.compile(r"^\s*(?:[-*•]|\d+[.)])\s+(.{3,120})$", re.MULTILINE)
342
+
343
+
344
+ def requirement_coverage(
345
+ user_message: str,
346
+ transcript: List[Dict[str, Any]],
347
+ file_create_actions: FrozenSet[str],
348
+ ) -> Dict[str, Any]:
349
+ """Did the run produce what the request actually asked for? (review 루프 §2)
350
+
351
+ The critic judged prose against prose, so "make an HTML+CSS+JS todo app"
352
+ could pass with one file written. This is the deterministic half of the
353
+ answer, built from two sources that are honest about their own limits:
354
+
355
+ * **manifest files** — when the request maps to a known multi-file project
356
+ (:func:`infer_project_manifest`), every declared path must have been
357
+ written. A missing manifest file is a *hard* miss: it is not a matter of
358
+ taste whether ``style.css`` exists.
359
+ * **explicit requirement lines** — bullet/numbered lines the user wrote
360
+ out. These are reported to the critic as a checklist but never block on
361
+ their own: matching a feature to a transcript is a judgement call, and
362
+ guessing it wrong would either fake completion or block real work.
363
+
364
+ Returns ``{"files": {...}, "requirements": [...], "missing_files": [...],
365
+ "complete": bool}`` where ``complete`` is false only when a declared
366
+ manifest file is missing.
367
+ """
368
+ written = files_written(transcript, file_create_actions)
369
+ written_names = {Path(path).name.lower() for path in written}
370
+ manifest = infer_project_manifest(user_message) or {}
371
+ declared = [str(spec.get("path") or "") for spec in manifest.get("files", [])]
372
+ missing = [
373
+ path for path in declared
374
+ if path and Path(path).name.lower() not in written_names
375
+ ]
376
+ requirements = [
377
+ line.strip()
378
+ for line in _REQUIREMENT_LINE_RE.findall(str(user_message or ""))
379
+ ][:10]
380
+ return {
381
+ "files": {"declared": declared, "written": written},
382
+ "missing_files": missing,
383
+ "requirements": requirements,
384
+ "complete": not missing,
385
+ }
386
+
387
+
388
+ def format_requirement_coverage(coverage: Dict[str, Any]) -> str:
389
+ """Requirement facts for the critic prompt, or "" when there is nothing."""
390
+ lines: List[str] = []
391
+ declared = coverage["files"]["declared"]
392
+ if declared:
393
+ written = set(coverage["files"]["written"])
394
+ lines.append("Requested files (deterministic, from the request):")
395
+ for path in declared:
396
+ got = any(Path(item).name.lower() == Path(path).name.lower() for item in written)
397
+ lines.append(f"- {path}: {'written' if got else 'MISSING'}")
398
+ if coverage["requirements"]:
399
+ lines.append(
400
+ "Requirements the user listed explicitly — check each one against "
401
+ "the artifacts, not against the plan:"
402
+ )
403
+ lines.extend(f"- {item}" for item in coverage["requirements"])
404
+ return "\n\n" + "\n".join(lines) if lines else ""
405
+
406
+
407
+ def format_artifact_checklist(checklist: List[Dict[str, Any]]) -> str:
408
+ lines = []
409
+ for item in checklist:
410
+ state = (
411
+ "auto-REPAIRED scaffold" if item["repaired"]
412
+ else ("sanitized model output" if item["sanitized"] else "written as produced")
413
+ )
414
+ lines.append(f"- {item['path']}: {state}")
415
+ return (
416
+ "Artifact checklist (deterministic, from the transcript):\n"
417
+ + "\n".join(lines)
418
+ + "\nVerify each artifact actually fulfills the user's request. An "
419
+ "auto-repaired scaffold is NOT completion unless its content "
420
+ "satisfies what was asked."
421
+ )
422
+
423
+
424
+ # ── budgets ────────────────────────────────────────────────────────────
425
+
426
+ @dataclass(frozen=True)
427
+ class TranscriptBudget:
428
+ """Executor/critic prompt shaping caps (review Wave 0.3).
429
+
430
+ ``window`` full recent steps for the executor; per-string caps keep tool
431
+ output bodies from dominating either prompt. Overridable through the same
432
+ ``Config.from_env`` pattern as :class:`PhaseBudgets`.
433
+ """
434
+
435
+ window: int = 8
436
+ result_chars: int = 700
437
+ verify_chars: int = 1200
438
+
439
+ @classmethod
440
+ def from_env(cls, env: Optional[Mapping[str, str]] = None) -> "TranscriptBudget":
441
+ from latticeai.core.config import _int
442
+
443
+ if env is None:
444
+ import os
445
+
446
+ env = os.environ
447
+
448
+ def cap(key: str, default: int, floor: int) -> int:
449
+ return max(floor, _int(env, key, default))
450
+
451
+ return cls(
452
+ window=cap("LATTICEAI_AGENT_TRANSCRIPT_WINDOW", cls.window, 2),
453
+ result_chars=cap("LATTICEAI_AGENT_TRANSCRIPT_CHARS", cls.result_chars, 120),
454
+ verify_chars=cap("LATTICEAI_AGENT_VERIFY_CHARS", cls.verify_chars, 200),
455
+ )
456
+
457
+
458
+ @dataclass(frozen=True)
459
+ class PhaseBudgets:
460
+ """Per-phase token budgets for the agent loop.
461
+
462
+ One shared budget let a weak model burn everything on planning prose and
463
+ reach EXECUTE with nothing left. Each role phase now has its own cap, so
464
+ a verbose planner can never starve execution or verification. Defaults
465
+ match the historical hardcoded values; every cap is overridable through
466
+ the ``Config.from_env`` environment pattern.
467
+ """
468
+
469
+ plan_tokens: int = 1024
470
+ execute_tokens: int = 4096
471
+ verify_tokens: int = 512
472
+ memory_tokens: int = 256
473
+
474
+ @classmethod
475
+ def from_env(cls, env: Optional[Mapping[str, str]] = None) -> "PhaseBudgets":
476
+ from latticeai.core.config import _int
477
+
478
+ if env is None:
479
+ import os
480
+
481
+ env = os.environ
482
+
483
+ def cap(key: str, default: int) -> int:
484
+ # A misconfigured/absurd value must not brick the loop: floor at a
485
+ # budget that still fits one JSON action object.
486
+ return max(128, _int(env, key, default))
487
+
488
+ return cls(
489
+ plan_tokens=cap("LATTICEAI_AGENT_PLAN_TOKENS", cls.plan_tokens),
490
+ execute_tokens=cap("LATTICEAI_AGENT_EXECUTE_TOKENS", cls.execute_tokens),
491
+ verify_tokens=cap("LATTICEAI_AGENT_VERIFY_TOKENS", cls.verify_tokens),
492
+ memory_tokens=cap("LATTICEAI_AGENT_MEMORY_TOKENS", cls.memory_tokens),
493
+ )
@@ -4,7 +4,6 @@ from __future__ import annotations
4
4
 
5
5
  from latticeai.core.tool_registry import TOOL_CATALOG_BRIEF
6
6
 
7
-
8
7
  PLANNER_PROMPT = """You are the PLANNER role in Lattice AI's multi-role agent harness.
9
8
  Your ONLY job: analyze the request and produce a structured execution plan.
10
9
  You do NOT call tools or write code.
@@ -27,6 +27,8 @@ from lattice_brain.runtime.multi_agent import (
27
27
  MULTI_AGENT_VERSION,
28
28
  ROLE_AGENT_IDS,
29
29
  )
30
+ from latticeai.core.quiet import quiet
31
+
30
32
  from .timeutil import now_iso as _now
31
33
 
32
34
  AGENT_TYPES = ("planner", "researcher", "executor", "reviewer", "release", "custom")
@@ -76,7 +78,7 @@ class AgentRegistry:
76
78
  data.setdefault("config_overrides", {})
77
79
  return data
78
80
  except Exception:
79
- pass
81
+ quiet()
80
82
  return {"custom": [], "config_overrides": {}}
81
83
 
82
84
  def _save(self) -> None:
@@ -0,0 +1,41 @@
1
+ """The agent loop's state vocabulary.
2
+
3
+ Its own module because both sides of the runtime need it and neither can own
4
+ it: :mod:`latticeai.core.agent` holds the state machine, and
5
+ :mod:`latticeai.core.agent_helpers` holds the pure functions the machine calls.
6
+ If the enum lived in ``agent``, the helpers would have to import the module
7
+ that imports them — so they would fall back to writing ``"EXECUTING"`` as a
8
+ bare string, and a rename of the enum value would silently stop matching.
9
+
10
+ Both names stay importable from :mod:`latticeai.core.agent`, which is where
11
+ every existing caller expects them.
12
+ """
13
+
14
+ from __future__ import annotations
15
+
16
+ from enum import Enum
17
+ from typing import FrozenSet
18
+
19
+
20
+ class AgentState(str, Enum):
21
+ IDLE = "IDLE"
22
+ PLANNING = "PLANNING"
23
+ WAITING_APPROVAL = "WAITING_APPROVAL"
24
+ EXECUTING = "EXECUTING"
25
+ VERIFYING = "VERIFYING"
26
+ FAILED = "FAILED"
27
+ ROLLBACK = "ROLLBACK"
28
+ # Terminal, non-success: the run ended but completion could not be
29
+ # verified (critic unavailable/unparseable, or a PASS with no execution
30
+ # evidence). Never presented as success — the user must check the result.
31
+ NEEDS_REVIEW = "NEEDS_REVIEW"
32
+ DONE = "DONE"
33
+
34
+
35
+ # Terminal states — the agent loop exits when reaching one of these
36
+ AGENT_TERMINAL_STATES: FrozenSet[AgentState] = frozenset(
37
+ {AgentState.DONE, AgentState.FAILED, AgentState.NEEDS_REVIEW}
38
+ )
39
+
40
+
41
+ __all__ = ["AgentState", "AGENT_TERMINAL_STATES"]
@@ -1,11 +1,11 @@
1
1
  """Audit logging, sensitivity analysis, and admin reporting."""
2
2
 
3
+ import hashlib
3
4
  import json
4
5
  import logging
5
6
  import os
6
7
  import re
7
8
  import threading
8
- import hashlib
9
9
  from pathlib import Path
10
10
  from typing import Any, Callable, Dict, List, Optional
11
11
 
@@ -14,7 +14,8 @@ from __future__ import annotations
14
14
 
15
15
  from typing import Any, Callable
16
16
 
17
- from latticeai.core.security import SECRET_KEY_HINTS, redact_secrets as redact_secret_values
17
+ from latticeai.core.security import SECRET_KEY_HINTS
18
+ from latticeai.core.security import redact_secrets as redact_secret_values
18
19
 
19
20
 
20
21
  def register_builtin_hook_runners(
@@ -25,7 +25,6 @@ from typing import List, Mapping, Optional
25
25
 
26
26
  from latticeai.core.security import host_is_loopback
27
27
 
28
-
29
28
  __all__ = ["Config", "_value", "_str", "_bool", "_int"]
30
29
 
31
30
  def _value(env: Mapping[str, str], key: str, default: str = "") -> str:
@@ -238,7 +238,18 @@ class EmbeddingProvider:
238
238
  return list(struct.unpack(f"<{count}f", payload[: count * 4]))
239
239
 
240
240
  def similarity(self, left: Iterable[float], right: Iterable[float]) -> float:
241
- return float(sum(a * b for a, b in zip(left, right)))
241
+ # strict=True: a dimension mismatch means the two vectors came from
242
+ # different embedding models. Truncating to the shorter one produces a
243
+ # plausible-looking similarity that is meaningless — exactly the silent
244
+ # wrongness this codebase keeps finding. Callers that can hit a model
245
+ # swap already handle failure and fall back to lexical search.
246
+ left_v, right_v = list(left), list(right)
247
+ if len(left_v) != len(right_v):
248
+ raise ValueError(
249
+ f"embedding dimension mismatch: {len(left_v)} vs {len(right_v)}; "
250
+ "the vector index was built with a different model"
251
+ )
252
+ return float(sum(a * b for a, b in zip(left_v, right_v, strict=True)))
242
253
 
243
254
  # ── observability ─────────────────────────────────────────────────────
244
255
  def health(self) -> Dict[str, Any]:
@@ -31,6 +31,8 @@ import json
31
31
  import re
32
32
  from typing import Any, Awaitable, Callable, Dict, List, Optional, Tuple
33
33
 
34
+ from latticeai.core.quiet import quiet
35
+
34
36
  # ── extraction ──────────────────────────────────────────────────────────
35
37
 
36
38
  _THINK_BLOCK_RE = re.compile(
@@ -171,6 +173,7 @@ def _slice_json_document(content: str) -> Optional[str]:
171
173
  try:
172
174
  json.loads(candidate)
173
175
  except (ValueError, TypeError):
176
+ quiet()
174
177
  continue
175
178
  if best is None or len(candidate) > len(best):
176
179
  best = candidate
@@ -9,8 +9,11 @@ from datetime import datetime, timedelta
9
9
  from pathlib import Path
10
10
  from typing import Any, Dict, List, Optional
11
11
 
12
+ from latticeai.core.quiet import quiet
13
+
12
14
  from .timeutil import local_now as _now
13
15
 
16
+
14
17
  def _iso(dt: datetime) -> str:
15
18
  return dt.isoformat(timespec="seconds")
16
19
 
@@ -39,7 +42,7 @@ class InvitationStore:
39
42
  data.setdefault("invitations", [])
40
43
  return data
41
44
  except Exception:
42
- pass
45
+ quiet()
43
46
  return {"version": 1, "invitations": []}
44
47
 
45
48
  def _save(self, data: Dict[str, Any]) -> None:
@@ -9,6 +9,8 @@ from datetime import datetime
9
9
  from pathlib import Path
10
10
  from typing import Any, Dict, Optional
11
11
 
12
+ from latticeai.core.quiet import quiet
13
+
12
14
 
13
15
  def atomic_write_json(path: Path, payload: Dict[str, Any]) -> None:
14
16
  path.parent.mkdir(parents=True, exist_ok=True)
@@ -22,7 +24,7 @@ def atomic_write_json(path: Path, payload: Dict[str, Any]) -> None:
22
24
  except OSError:
23
25
  # Windows and unusual filesystems may not expose POSIX mode bits; the
24
26
  # atomic write is still the safest supported fallback there.
25
- pass
27
+ quiet()
26
28
 
27
29
 
28
30
  def parse_iso(value: Optional[str]) -> Optional[datetime]:
@@ -13,8 +13,7 @@ from dataclasses import dataclass
13
13
  from pathlib import Path
14
14
  from typing import Any, Dict, List
15
15
 
16
-
17
- LEGACY_COMPATIBILITY_VERSION = "10.0.0"
16
+ LEGACY_COMPATIBILITY_VERSION = "10.2.0"
18
17
 
19
18
 
20
19
  @dataclass(frozen=True)