ltcai 9.9.9 → 10.2.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (228) hide show
  1. package/README.md +51 -38
  2. package/docs/CHANGELOG.md +197 -0
  3. package/docs/COMMUNITY_AND_PLUGINS.md +1 -1
  4. package/docs/DEVELOPMENT.md +1 -1
  5. package/docs/HYBRID_CLOUD_KG_STREAMING.md +101 -0
  6. package/docs/ONBOARDING.md +1 -1
  7. package/docs/OPERATIONS.md +1 -1
  8. package/docs/TRUST_MODEL.md +1 -1
  9. package/docs/WHY_LATTICE.md +1 -1
  10. package/docs/kg-schema.md +1 -1
  11. package/lattice_brain/__init__.py +1 -1
  12. package/lattice_brain/archive.py +5 -4
  13. package/lattice_brain/conversations.py +14 -3
  14. package/lattice_brain/embeddings.py +12 -2
  15. package/lattice_brain/graph/_kg_common.py +5 -5
  16. package/lattice_brain/graph/_kg_fsutil.py +4 -3
  17. package/lattice_brain/graph/discovery.py +4 -3
  18. package/lattice_brain/graph/discovery_index.py +0 -1
  19. package/lattice_brain/graph/documents.py +3 -2
  20. package/lattice_brain/graph/fusion.py +4 -0
  21. package/lattice_brain/graph/ingest.py +12 -2
  22. package/lattice_brain/graph/projection.py +3 -2
  23. package/lattice_brain/graph/provenance.py +5 -3
  24. package/lattice_brain/graph/rerank.py +4 -1
  25. package/lattice_brain/graph/retrieval.py +0 -1
  26. package/lattice_brain/graph/retrieval_docgen.py +0 -1
  27. package/lattice_brain/graph/retrieval_reads.py +0 -1
  28. package/lattice_brain/graph/retrieval_vector.py +0 -1
  29. package/lattice_brain/graph/schema.py +4 -3
  30. package/lattice_brain/graph/store.py +18 -4
  31. package/lattice_brain/graph/write_master.py +46 -1
  32. package/lattice_brain/ingestion.py +4 -1
  33. package/lattice_brain/portability.py +5 -2
  34. package/lattice_brain/quality.py +12 -5
  35. package/lattice_brain/quiet.py +43 -0
  36. package/lattice_brain/runtime/agent_runtime.py +12 -8
  37. package/lattice_brain/runtime/hooks.py +2 -1
  38. package/lattice_brain/runtime/multi_agent.py +2 -3
  39. package/lattice_brain/sensitivity.py +94 -0
  40. package/lattice_brain/storage/base.py +30 -2
  41. package/lattice_brain/storage/migration.py +3 -2
  42. package/lattice_brain/storage/postgres.py +2 -2
  43. package/lattice_brain/storage/sqlite.py +6 -4
  44. package/lattice_brain/workflow.py +4 -2
  45. package/latticeai/__init__.py +1 -1
  46. package/latticeai/api/admin.py +1 -1
  47. package/latticeai/api/agents.py +4 -2
  48. package/latticeai/api/auth.py +5 -1
  49. package/latticeai/api/automation_intelligence.py +2 -1
  50. package/latticeai/api/browser.py +3 -2
  51. package/latticeai/api/chat.py +28 -17
  52. package/latticeai/api/chat_agent_http.py +22 -8
  53. package/latticeai/api/chat_contracts.py +4 -0
  54. package/latticeai/api/chat_documents.py +6 -2
  55. package/latticeai/api/chat_hybrid.py +82 -0
  56. package/latticeai/api/computer_use.py +8 -3
  57. package/latticeai/api/knowledge_graph.py +1 -1
  58. package/latticeai/api/mcp.py +4 -4
  59. package/latticeai/api/models.py +5 -2
  60. package/latticeai/api/network_boundary.py +220 -0
  61. package/latticeai/api/permissions.py +0 -1
  62. package/latticeai/api/realtime.py +1 -1
  63. package/latticeai/api/security_dashboard.py +1 -1
  64. package/latticeai/api/setup.py +16 -3
  65. package/latticeai/api/static_routes.py +2 -1
  66. package/latticeai/api/tools.py +12 -8
  67. package/latticeai/api/voice_capture.py +3 -1
  68. package/latticeai/api/workflow_designer.py +1 -1
  69. package/latticeai/api/workspace.py +1 -2
  70. package/latticeai/app_factory.py +131 -78
  71. package/latticeai/cli/entrypoint.py +6 -4
  72. package/latticeai/core/agent.py +55 -495
  73. package/latticeai/core/agent_eval.py +2 -2
  74. package/latticeai/core/agent_helpers.py +493 -0
  75. package/latticeai/core/agent_prompts.py +0 -1
  76. package/latticeai/core/agent_registry.py +3 -1
  77. package/latticeai/core/agent_state.py +41 -0
  78. package/latticeai/core/audit.py +1 -1
  79. package/latticeai/core/builtin_hooks.py +2 -1
  80. package/latticeai/core/config.py +0 -1
  81. package/latticeai/core/embedding_providers.py +12 -1
  82. package/latticeai/core/file_generation.py +3 -0
  83. package/latticeai/core/invitations.py +4 -1
  84. package/latticeai/core/io_utils.py +3 -1
  85. package/latticeai/core/legacy_compatibility.py +1 -2
  86. package/latticeai/core/local_embeddings.py +12 -1
  87. package/latticeai/core/marketplace.py +1 -2
  88. package/latticeai/core/mcp_registry.py +0 -1
  89. package/latticeai/core/model_compat.py +1 -1
  90. package/latticeai/core/model_resolution.py +1 -1
  91. package/latticeai/core/network_boundary.py +168 -0
  92. package/latticeai/core/oidc.py +3 -0
  93. package/latticeai/core/permission_mode.py +6 -6
  94. package/latticeai/core/plugins.py +3 -2
  95. package/latticeai/core/policy.py +0 -1
  96. package/latticeai/core/quiet.py +84 -0
  97. package/latticeai/core/realtime.py +3 -2
  98. package/latticeai/core/run_store.py +4 -2
  99. package/latticeai/core/security.py +4 -0
  100. package/latticeai/core/users.py +5 -3
  101. package/latticeai/core/workspace_graph_trace.py +3 -0
  102. package/latticeai/core/workspace_os.py +65 -273
  103. package/latticeai/core/workspace_os_constants.py +126 -0
  104. package/latticeai/core/workspace_os_state.py +180 -0
  105. package/latticeai/core/workspace_os_utils.py +3 -1
  106. package/latticeai/core/workspace_permissions.py +1 -0
  107. package/latticeai/core/workspace_snapshots.py +3 -1
  108. package/latticeai/core/workspace_timeline.py +3 -1
  109. package/latticeai/integrations/telegram_bot.py +25 -16
  110. package/latticeai/models/router.py +6 -3
  111. package/latticeai/runtime/access_runtime.py +3 -1
  112. package/latticeai/runtime/audit_runtime.py +3 -2
  113. package/latticeai/runtime/chat_wiring.py +4 -1
  114. package/latticeai/runtime/history_runtime.py +1 -1
  115. package/latticeai/runtime/lifespan_runtime.py +3 -1
  116. package/latticeai/runtime/network_boundary_wiring.py +124 -0
  117. package/latticeai/runtime/persistence_runtime.py +3 -1
  118. package/latticeai/runtime/router_registration.py +11 -1
  119. package/latticeai/services/architecture_readiness.py +1 -2
  120. package/latticeai/services/change_proposals.py +2 -1
  121. package/latticeai/services/cloud_egress_audit.py +85 -0
  122. package/latticeai/services/cloud_extraction.py +129 -0
  123. package/latticeai/services/cloud_streaming.py +266 -0
  124. package/latticeai/services/cloud_token_guard.py +84 -0
  125. package/latticeai/services/command_center.py +2 -1
  126. package/latticeai/services/folder_watch.py +3 -0
  127. package/latticeai/services/funnel_metrics.py +3 -1
  128. package/latticeai/services/hybrid_chat.py +265 -0
  129. package/latticeai/services/hybrid_context.py +228 -0
  130. package/latticeai/services/hybrid_policy.py +178 -0
  131. package/latticeai/services/memory_service.py +1 -1
  132. package/latticeai/services/model_catalog.py +10 -1
  133. package/latticeai/services/model_engines.py +35 -14
  134. package/latticeai/services/model_loading.py +3 -2
  135. package/latticeai/services/model_runtime.py +68 -17
  136. package/latticeai/services/multimodal_streaming.py +123 -0
  137. package/latticeai/services/network_boundary_service.py +154 -0
  138. package/latticeai/services/openai_compatible_adapter.py +100 -0
  139. package/latticeai/services/p_reinforce.py +4 -0
  140. package/latticeai/services/platform_runtime.py +9 -4
  141. package/latticeai/services/process_audit.py +0 -1
  142. package/latticeai/services/product_readiness.py +1 -1
  143. package/latticeai/services/run_executor.py +3 -2
  144. package/latticeai/services/search_service.py +1 -4
  145. package/latticeai/services/setup_detection.py +2 -1
  146. package/latticeai/services/tool_dispatch.py +7 -2
  147. package/latticeai/services/upload_service.py +2 -1
  148. package/latticeai/setup/auto_setup.py +10 -7
  149. package/latticeai/setup/wizard.py +15 -11
  150. package/latticeai/tools/__init__.py +3 -3
  151. package/latticeai/tools/commands.py +7 -7
  152. package/latticeai/tools/computer.py +3 -2
  153. package/latticeai/tools/documents.py +10 -9
  154. package/latticeai/tools/filesystem.py +7 -4
  155. package/latticeai/tools/knowledge.py +2 -0
  156. package/latticeai/tools/local_files.py +1 -1
  157. package/latticeai/tools/network.py +2 -1
  158. package/package.json +6 -4
  159. package/scripts/bench_agent_smoke.py +0 -1
  160. package/scripts/bench_models.py +0 -1
  161. package/scripts/brain_quality_eval.py +7 -2
  162. package/scripts/bump_version.py +2 -1
  163. package/scripts/check_current_release_docs.mjs +1 -1
  164. package/scripts/migrate_brain_storage.py +5 -1
  165. package/scripts/profile_kg.py +2 -7
  166. package/scripts/verify_hf_model_registry.py +2 -2
  167. package/src-tauri/Cargo.lock +1 -1
  168. package/src-tauri/Cargo.toml +1 -1
  169. package/src-tauri/tauri.conf.json +1 -1
  170. package/static/app/asset-manifest.json +37 -37
  171. package/static/app/assets/Act-CbdGD-2i.js +1 -0
  172. package/static/app/assets/AdminConsole-LgCkXpnf.js +1 -0
  173. package/static/app/assets/Brain-CoPGJI1L.js +321 -0
  174. package/static/app/assets/BrainHome-gVnaxSwp.js +2 -0
  175. package/static/app/assets/BrainSignals-ChxAYHtj.js +1 -0
  176. package/static/app/assets/Capture-DBtgkHZg.js +1 -0
  177. package/static/app/assets/CommandPalette-Ovtv5I0Y.js +1 -0
  178. package/static/app/assets/Library-CBia2Fvm.js +1 -0
  179. package/static/app/assets/LivingBrain-CNz-6NSd.js +1 -0
  180. package/static/app/assets/{ProductFlow-cB3EFinZ.js → ProductFlow-ePX-Y73J.js} +1 -1
  181. package/static/app/assets/ReviewCard-C4mpvkwH.js +3 -0
  182. package/static/app/assets/System-BvWNK1zc.js +1 -0
  183. package/static/app/assets/activity-CiauPV_3.js +1 -0
  184. package/static/app/assets/{bot-DtKkLfXj.js → bot-Dwct-Q1x.js} +1 -1
  185. package/static/app/assets/brain-BKDW1F0T.js +1 -0
  186. package/static/app/assets/{button-BTxZDMFJ.js → button-CCB9Kuw7.js} +1 -1
  187. package/static/app/assets/{circle-pause--qxF9-EO.js → circle-pause-DNa8WHC5.js} +1 -1
  188. package/static/app/assets/{circle-play-89x3S6jm.js → circle-play-8jmxn5mf.js} +1 -1
  189. package/static/app/assets/{cpu-DTUI63vo.js → cpu-mxvF3V1I.js} +1 -1
  190. package/static/app/assets/{download-EJS5_O0f.js → download--SmCcx0p.js} +1 -1
  191. package/static/app/assets/{folder-open-BL9zYTnc.js → folder-open-D4Wy5roo.js} +1 -1
  192. package/static/app/assets/{hard-drive-BFnwi1Lu.js → hard-drive-BwQcSPlS.js} +1 -1
  193. package/static/app/assets/index-CQWdDU3z.css +2 -0
  194. package/static/app/assets/index-DDV2YZwM.js +10 -0
  195. package/static/app/assets/input-DFhSjmLS.js +1 -0
  196. package/static/app/assets/{network-zKa3jAwl.js → network-Bts7VO94.js} +1 -1
  197. package/static/app/assets/primitives-BuSMEJY8.js +1 -0
  198. package/static/app/assets/search-DZzxhWaQ.js +1 -0
  199. package/static/app/assets/{shield-alert-CpHeCsaF.js → shield-alert-BfTO6X_G.js} +1 -1
  200. package/static/app/assets/textarea-BjctW1oh.js +1 -0
  201. package/static/app/assets/{useFocusTrap-58TNCONM.js → useFocusTrap-CE43-LrA.js} +1 -1
  202. package/static/app/assets/{useQuery-CcVlXlBd.js → useQuery-Bv3ejLL5.js} +1 -1
  203. package/static/app/assets/{users-C0cFMj6o.js → users-CsNqLZAj.js} +1 -1
  204. package/static/app/assets/utils-KFFdVG_t.js +7 -0
  205. package/static/app/assets/workspace-wdCvdyPF.js +1 -0
  206. package/static/app/index.html +4 -4
  207. package/static/sw.js +1 -1
  208. package/static/app/assets/Act-BFbpl2_Y.js +0 -1
  209. package/static/app/assets/AdminConsole-DaZPha9Y.js +0 -1
  210. package/static/app/assets/Brain-DGrV0heZ.js +0 -321
  211. package/static/app/assets/BrainHome-Re6Zxh_W.js +0 -2
  212. package/static/app/assets/BrainSignals-BQ11jDpJ.js +0 -1
  213. package/static/app/assets/Capture-DRaf1zLH.js +0 -1
  214. package/static/app/assets/CommandPalette-DAKPm4uJ.js +0 -1
  215. package/static/app/assets/LanguageSwitcher-BgTwHoWv.js +0 -1
  216. package/static/app/assets/Library-BVtrSt12.js +0 -1
  217. package/static/app/assets/LivingBrain-7glOmK0R.js +0 -1
  218. package/static/app/assets/ReviewCard-CShhHS-w.js +0 -3
  219. package/static/app/assets/System-B4nQDy8a.js +0 -1
  220. package/static/app/assets/brain-wVV63_1z.js +0 -1
  221. package/static/app/assets/index-BFtDYI6l.js +0 -10
  222. package/static/app/assets/index-BY9bpxyx.css +0 -2
  223. package/static/app/assets/input-DEz5UjBz.js +0 -1
  224. package/static/app/assets/primitives-jNrZm2O9.js +0 -1
  225. package/static/app/assets/search-ku6-_6wY.js +0 -1
  226. package/static/app/assets/textarea-DpcQkn75.js +0 -1
  227. package/static/app/assets/utils-DYf7pqrR.js +0 -7
  228. package/static/app/assets/workspace-CjthHsLN.js +0 -1
@@ -20,59 +20,81 @@ only owns the state machine.
20
20
 
21
21
  from __future__ import annotations
22
22
 
23
- import ast
24
23
  import json
25
24
  import logging
26
- import re
27
25
  from dataclasses import dataclass
28
- from enum import Enum
29
26
  from pathlib import Path
30
- from typing import Any, Awaitable, Callable, Dict, FrozenSet, List, Mapping, Optional, Tuple
27
+ from typing import Any, Awaitable, Callable, Dict, FrozenSet, List, Optional, Tuple
31
28
 
29
+ from lattice_brain.runtime.contracts import (
30
+ runtime_boundary_contract,
31
+ single_agent_contract,
32
+ )
32
33
  from lattice_brain.runtime.hooks import dispatch_tool
33
- from lattice_brain.runtime.contracts import runtime_boundary_contract, single_agent_contract
34
+ from latticeai.core.agent_helpers import (
35
+ PhaseBudgets,
36
+ TranscriptBudget,
37
+ _truncate_strings,
38
+ artifact_checklist,
39
+ compact_transcript,
40
+ extract_action,
41
+ extract_action_details,
42
+ files_written,
43
+ filter_learnings,
44
+ format_artifact_checklist,
45
+ format_requirement_coverage,
46
+ normalize_plan,
47
+ requirement_coverage,
48
+ )
34
49
  from latticeai.core.agent_permission import (
35
50
  block_reason_for_tool,
36
51
  non_auto_plan_steps,
37
52
  resolve_deps_mode,
38
53
  )
39
54
  from latticeai.core.agent_profiles import AgentProfile, profile_for_model
55
+
56
+ # The state vocabulary and the pure helpers live in sibling modules so this one
57
+ # holds only the loop. They are re-exported (see ``__all__``) because callers —
58
+ # the HTTP layer, run_store, the eval harness, and the tests — have always
59
+ # imported them from here, and that contract does not change.
60
+ from latticeai.core.agent_state import AGENT_TERMINAL_STATES, AgentState
40
61
  from latticeai.core.agent_trace import LoopTrace
62
+ from latticeai.core.file_generation import (
63
+ generate_file_content,
64
+ infer_file_target,
65
+ sanitize_write_content,
66
+ )
41
67
  from latticeai.core.permission_mode import (
42
68
  PermissionMode,
43
69
  is_circuit_breaker,
44
70
  plan_requires_approval,
45
71
  should_stage_proposal,
46
72
  )
47
- from latticeai.core.file_generation import (
48
- generate_file_content,
49
- infer_file_target,
50
- infer_project_manifest,
51
- sanitize_write_content,
52
- )
53
73
  from latticeai.core.tool_registry import SCOPED_KNOWLEDGE_TOOLS
54
74
  from latticeai.tools import ToolError
55
75
 
56
-
57
- class AgentState(str, Enum):
58
- IDLE = "IDLE"
59
- PLANNING = "PLANNING"
60
- WAITING_APPROVAL = "WAITING_APPROVAL"
61
- EXECUTING = "EXECUTING"
62
- VERIFYING = "VERIFYING"
63
- FAILED = "FAILED"
64
- ROLLBACK = "ROLLBACK"
65
- # Terminal, non-success: the run ended but completion could not be
66
- # verified (critic unavailable/unparseable, or a PASS with no execution
67
- # evidence). Never presented as success — the user must check the result.
68
- NEEDS_REVIEW = "NEEDS_REVIEW"
69
- DONE = "DONE"
70
-
71
-
72
- # Terminal states — the agent loop exits when reaching one of these
73
- AGENT_TERMINAL_STATES: FrozenSet[AgentState] = frozenset(
74
- {AgentState.DONE, AgentState.FAILED, AgentState.NEEDS_REVIEW}
75
- )
76
+ __all__ = [
77
+ # this module
78
+ "AgentDeps",
79
+ "AgentRunContext",
80
+ "SingleAgentRuntime",
81
+ # re-exported from agent_state
82
+ "AGENT_TERMINAL_STATES",
83
+ "AgentState",
84
+ # re-exported from agent_helpers
85
+ "PhaseBudgets",
86
+ "TranscriptBudget",
87
+ "artifact_checklist",
88
+ "compact_transcript",
89
+ "extract_action",
90
+ "extract_action_details",
91
+ "files_written",
92
+ "filter_learnings",
93
+ "format_artifact_checklist",
94
+ "format_requirement_coverage",
95
+ "normalize_plan",
96
+ "requirement_coverage",
97
+ ]
76
98
 
77
99
 
78
100
  class AgentRunContext:
@@ -111,468 +133,6 @@ class AgentRunContext:
111
133
  self.permission_mode: Optional[str] = None
112
134
 
113
135
 
114
- _THINK_BLOCK_RE = re.compile(
115
- r"<(think|thinking|reasoning)>.*?</\1>", flags=re.DOTALL | re.IGNORECASE
116
- )
117
-
118
-
119
- def extract_action_details(raw: str) -> Tuple[Dict, List[str]]:
120
- """Parse one JSON action object out of an LLM response (tolerant of fences/prose).
121
-
122
- Returns ``(action, repairs)`` where ``repairs`` names every tolerance that
123
- was needed — the loop trace and the weak-model robustness harness consume
124
- it to measure how much help a given model needs.
125
- """
126
- repairs: List[str] = []
127
- # Small local models often prepend <think>...</think> reasoning that can
128
- # itself contain braces — drop it before locating the action object.
129
- text = _THINK_BLOCK_RE.sub("", raw).strip()
130
- if text != str(raw).strip():
131
- repairs.append("think_strip")
132
- fenced = re.search(r"```(?:json)?\s*(\{.*?\})\s*```", text, flags=re.DOTALL)
133
- if fenced:
134
- text = fenced.group(1).strip()
135
- repairs.append("fence")
136
- elif not text.startswith("{"):
137
- start = text.find("{")
138
- end = text.rfind("}")
139
- if start >= 0 and end > start:
140
- text = text[start : end + 1]
141
- repairs.append("slice")
142
-
143
- action: Any = None
144
- try:
145
- action = json.loads(text)
146
- except json.JSONDecodeError:
147
- # Second chance for the most common small-model JSON slips: trailing
148
- # commas before a closing brace/bracket.
149
- repaired = re.sub(r",\s*([}\]])", r"\1", text)
150
- try:
151
- action = json.loads(repaired)
152
- repairs.append("trailing_comma")
153
- except json.JSONDecodeError as exc:
154
- # Last chance: weak models sometimes emit a Python dict literal
155
- # (single quotes, True/False/None). ast.literal_eval parses that
156
- # deterministically without evaluating code.
157
- try:
158
- literal = ast.literal_eval(text)
159
- except (ValueError, SyntaxError):
160
- raise ValueError(f"Agent did not return valid JSON: {exc}") from exc
161
- if not isinstance(literal, dict):
162
- raise ValueError(f"Agent did not return valid JSON: {exc}") from exc
163
- action = literal
164
- repairs.append("python_literal")
165
-
166
- if not isinstance(action, dict) or "action" not in action:
167
- raise ValueError("Agent JSON must include an action field.")
168
- return action, repairs
169
-
170
-
171
- def extract_action(raw: str) -> Dict:
172
- """Back-compat wrapper over :func:`extract_action_details`."""
173
- action, _ = extract_action_details(raw)
174
- return action
175
-
176
-
177
- _FILE_CREATE_PLAN_ACTIONS = frozenset({"write_file", "generate_file"})
178
-
179
-
180
- def _plan_misses_manifest(steps: List[Dict[str, Any]], manifest: Dict[str, Any]) -> bool:
181
- """True when a pure file-writing plan fails to cover the manifest's file types.
182
-
183
- Only pure file-creation plans are candidates for rewriting — a plan with
184
- read/search steps reflects real planner intent and stays untouched.
185
- """
186
- if any(s.get("action") not in _FILE_CREATE_PLAN_ACTIONS for s in steps):
187
- return False
188
-
189
- def _ext(path: Any) -> str:
190
- text = str(path or "")
191
- dot = text.rfind(".")
192
- return text[dot:].lower() if dot >= 0 else ""
193
-
194
- planned_exts = {
195
- _ext((s.get("args") or {}).get("path")) for s in steps
196
- }
197
- manifest_exts = {_ext(spec.get("path")) for spec in manifest.get("files", [])}
198
- return not manifest_exts.issubset(planned_exts)
199
-
200
-
201
- def normalize_plan(plan: Any, user_message: str) -> Tuple[Dict[str, Any], List[str]]:
202
- """Enforce the minimal plan schema so execution never starts adrift.
203
-
204
- A weak planner that returns junk steps / a missing goal previously flowed
205
- straight into the executor, which then had to reconstruct intent from the
206
- raw request. Normalization keeps the loop honest: ``goal`` is always a
207
- non-empty string, ``steps`` only contains dicts with an ``action``, and an
208
- empty plan for an obvious file-creation request gets a deterministic
209
- single ``write_file`` step instead of leaving the executor to improvise.
210
-
211
- Returns ``(plan, fixes)`` where ``fixes`` names every applied repair —
212
- the loop trace records them so plan quality is observable per model.
213
- """
214
- fixes: List[str] = []
215
- if not isinstance(plan, dict):
216
- plan = {}
217
- fixes.append("plan_not_object")
218
- plan = dict(plan)
219
-
220
- goal = str(plan.get("goal") or "").strip()
221
- if not goal:
222
- plan["goal"] = user_message
223
- fixes.append("goal_defaulted")
224
-
225
- raw_steps = plan.get("steps")
226
- steps = [
227
- s for s in (raw_steps if isinstance(raw_steps, list) else [])
228
- if isinstance(s, dict) and s.get("action")
229
- ]
230
- if raw_steps and steps != raw_steps:
231
- fixes.append("steps_filtered")
232
-
233
- # Manifest-aware planning (review Wave 0.4): when the request is a
234
- # recognized multi-file project, the deterministic manifest — not the
235
- # planner's improvisation — decides the file set, exactly like the direct
236
- # chat path. Rewrites apply only when the plan is empty or is a pure
237
- # file-writing plan that misses part of the manifest, so a planner that
238
- # already covered every requested file type is left untouched.
239
- manifest = infer_project_manifest(user_message)
240
- if manifest:
241
- manifest_steps = [{
242
- "action": "write_file",
243
- "args": {"path": spec["path"]},
244
- "description": spec["brief"],
245
- } for spec in manifest["files"]]
246
- if not steps:
247
- steps = manifest_steps
248
- fixes.append("manifest_steps")
249
- elif _plan_misses_manifest(steps, manifest):
250
- steps = manifest_steps
251
- fixes.append("manifest_rewrite")
252
-
253
- if not steps:
254
- inferred = infer_file_target(user_message)
255
- if inferred:
256
- steps = [{
257
- "action": "write_file",
258
- "args": {"path": inferred},
259
- "description": f"Create {inferred} for: {user_message[:120]}",
260
- }]
261
- fixes.append("heuristic_file_step")
262
- plan["steps"] = steps
263
-
264
- try:
265
- estimated = int(plan.get("estimated_steps") or 0)
266
- except (TypeError, ValueError):
267
- estimated = 0
268
- fixes.append("estimated_steps_invalid")
269
- plan["estimated_steps"] = max(1, estimated, len(steps))
270
- plan["requires_approval"] = bool(plan.get("requires_approval", False))
271
- if not isinstance(plan.get("rollback_strategy"), str):
272
- plan["rollback_strategy"] = "none"
273
- return plan, fixes
274
-
275
-
276
- _TRIVIAL_LEARNING_RE = re.compile(
277
- r"^(파일(을|이)?\s*(만들|생성|작성|저장)|작업(을|이)?\s*(완료|성공)|성공적으로"
278
- r"|task\s+(was\s+)?complet|file\s+(was\s+)?(creat|written|saved)"
279
- r"|(successfully\s+)?(created|completed|finished|done)\b)",
280
- re.IGNORECASE,
281
- )
282
-
283
-
284
- def filter_learnings(learnings: List[Any]) -> List[str]:
285
- """Drop trivial/duplicate learnings before they enter the brain.
286
-
287
- "파일을 만들었다"-class statements restate what the transcript already
288
- records and pollute recall. A learning survives when it is long enough to
289
- carry information and is not a bare completion announcement.
290
- """
291
- kept: List[str] = []
292
- seen: set = set()
293
- for raw in learnings or []:
294
- text = str(raw or "").strip()
295
- if len(text) < 12:
296
- continue
297
- if _TRIVIAL_LEARNING_RE.match(text) and len(text) < 48:
298
- continue
299
- key = text.lower()
300
- if key in seen:
301
- continue
302
- seen.add(key)
303
- kept.append(text)
304
- return kept
305
-
306
-
307
- def _truncate_strings(value: Any, limit: int) -> Any:
308
- """Deep-copy ``value`` with every string capped at ``limit`` chars.
309
-
310
- Long tool outputs (file bodies, command output) dominate executor prompt
311
- size without adding decision-relevant signal. The cap keeps the head of
312
- each string and names how much was dropped, so the model still sees what
313
- the value was — never a silent hole.
314
- """
315
- if isinstance(value, str):
316
- if len(value) <= limit:
317
- return value
318
- return value[:limit] + f"…[+{len(value) - limit} chars]"
319
- if isinstance(value, dict):
320
- return {k: _truncate_strings(v, limit) for k, v in value.items()}
321
- if isinstance(value, list):
322
- return [_truncate_strings(v, limit) for v in value]
323
- return value
324
-
325
-
326
- def compact_transcript(
327
- transcript: List[Dict[str, Any]],
328
- *,
329
- window: int = 8,
330
- result_chars: int = 700,
331
- ) -> List[Dict[str, Any]]:
332
- """Bounded executor view of a transcript (review Wave 0.3).
333
-
334
- The executor prompt previously embedded the *entire* transcript JSON every
335
- step — O(steps²) token growth that starved :class:`PhaseBudgets` on long
336
- runs and buried weak models in stale detail. This view keeps the most
337
- recent ``window`` steps in full (with string values capped at
338
- ``result_chars``) and reduces every older step to a one-line summary, so
339
- the prompt stays bounded while no step disappears entirely.
340
- """
341
- steps = list(transcript or [])
342
- if len(steps) <= window:
343
- return [_truncate_strings(step, result_chars) for step in steps]
344
- older, recent = steps[:-window], steps[-window:]
345
- summarized: List[Dict[str, Any]] = [{
346
- "summarized_older_steps": len(older),
347
- "note": "older steps compacted — full detail retained in the run record",
348
- }]
349
- for step in older:
350
- entry: Dict[str, Any] = {"state": step.get("state")}
351
- for key in ("action", "verdict", "retry_attempt"):
352
- if step.get(key) is not None:
353
- entry[key] = step.get(key)
354
- if step.get("error"):
355
- entry["error"] = str(step["error"])[:160]
356
- elif isinstance(step.get("result"), dict):
357
- entry["ok"] = True
358
- path = step["result"].get("path") or (step.get("args") or {}).get("path")
359
- if path:
360
- entry["path"] = str(path)
361
- summarized.append(entry)
362
- summarized.extend(_truncate_strings(step, result_chars) for step in recent)
363
- return summarized
364
-
365
-
366
- def files_written(
367
- transcript: List[Dict[str, Any]],
368
- file_create_actions: FrozenSet[str],
369
- ) -> List[str]:
370
- """Ordered unique paths of files this run successfully wrote (review L5).
371
-
372
- Later executor steps get this as explicit context, so "만들고 이어서
373
- 설명해" multi-step work sees its own output instead of a stale
374
- workspace picture.
375
- """
376
- seen: List[str] = []
377
- for step in transcript:
378
- if step.get("state") != AgentState.EXECUTING.value:
379
- continue
380
- if step.get("action") not in file_create_actions:
381
- continue
382
- if not isinstance(step.get("result"), dict):
383
- continue
384
- path = step["result"].get("path") or (step.get("args") or {}).get("path")
385
- if path and str(path) not in seen:
386
- seen.append(str(path))
387
- return seen
388
-
389
-
390
- def artifact_checklist(
391
- transcript: List[Dict[str, Any]],
392
- file_create_actions: FrozenSet[str],
393
- ) -> List[Dict[str, Any]]:
394
- """Deterministic artifact facts for the critic (review L4).
395
-
396
- The critic previously judged file work from prose alone; this surfaces
397
- the sanitize/repair honesty flags per written file so a repaired
398
- placeholder can never pass as a fulfilled request unchecked.
399
- """
400
- checklist: List[Dict[str, Any]] = []
401
- for step in transcript:
402
- if step.get("state") != AgentState.EXECUTING.value:
403
- continue
404
- if step.get("action") not in file_create_actions:
405
- continue
406
- if not isinstance(step.get("result"), dict):
407
- continue
408
- path = step["result"].get("path") or (step.get("args") or {}).get("path")
409
- if not path:
410
- continue
411
- sanitize_meta = step.get("content_sanitize") or {}
412
- checklist.append({
413
- "path": str(path),
414
- "sanitized": bool(sanitize_meta.get("sanitized")),
415
- "repaired": bool(sanitize_meta.get("repaired")),
416
- })
417
- return checklist
418
-
419
-
420
- # Explicit requirement lines a user writes out: "- 다크모드", "1. 검색 기능",
421
- # "* dark mode". Free prose is deliberately NOT parsed — a wrong requirement
422
- # is worse than no requirement.
423
- _REQUIREMENT_LINE_RE = re.compile(r"^\s*(?:[-*•]|\d+[.)])\s+(.{3,120})$", re.MULTILINE)
424
-
425
-
426
- def requirement_coverage(
427
- user_message: str,
428
- transcript: List[Dict[str, Any]],
429
- file_create_actions: FrozenSet[str],
430
- ) -> Dict[str, Any]:
431
- """Did the run produce what the request actually asked for? (review 루프 §2)
432
-
433
- The critic judged prose against prose, so "make an HTML+CSS+JS todo app"
434
- could pass with one file written. This is the deterministic half of the
435
- answer, built from two sources that are honest about their own limits:
436
-
437
- * **manifest files** — when the request maps to a known multi-file project
438
- (:func:`infer_project_manifest`), every declared path must have been
439
- written. A missing manifest file is a *hard* miss: it is not a matter of
440
- taste whether ``style.css`` exists.
441
- * **explicit requirement lines** — bullet/numbered lines the user wrote
442
- out. These are reported to the critic as a checklist but never block on
443
- their own: matching a feature to a transcript is a judgement call, and
444
- guessing it wrong would either fake completion or block real work.
445
-
446
- Returns ``{"files": {...}, "requirements": [...], "missing_files": [...],
447
- "complete": bool}`` where ``complete`` is false only when a declared
448
- manifest file is missing.
449
- """
450
- written = files_written(transcript, file_create_actions)
451
- written_names = {Path(path).name.lower() for path in written}
452
- manifest = infer_project_manifest(user_message) or {}
453
- declared = [str(spec.get("path") or "") for spec in manifest.get("files", [])]
454
- missing = [
455
- path for path in declared
456
- if path and Path(path).name.lower() not in written_names
457
- ]
458
- requirements = [
459
- line.strip()
460
- for line in _REQUIREMENT_LINE_RE.findall(str(user_message or ""))
461
- ][:10]
462
- return {
463
- "files": {"declared": declared, "written": written},
464
- "missing_files": missing,
465
- "requirements": requirements,
466
- "complete": not missing,
467
- }
468
-
469
-
470
- def _format_requirement_coverage(coverage: Dict[str, Any]) -> str:
471
- """Requirement facts for the critic prompt, or "" when there is nothing."""
472
- lines: List[str] = []
473
- declared = coverage["files"]["declared"]
474
- if declared:
475
- written = set(coverage["files"]["written"])
476
- lines.append("Requested files (deterministic, from the request):")
477
- for path in declared:
478
- got = any(Path(item).name.lower() == Path(path).name.lower() for item in written)
479
- lines.append(f"- {path}: {'written' if got else 'MISSING'}")
480
- if coverage["requirements"]:
481
- lines.append(
482
- "Requirements the user listed explicitly — check each one against "
483
- "the artifacts, not against the plan:"
484
- )
485
- lines.extend(f"- {item}" for item in coverage["requirements"])
486
- return "\n\n" + "\n".join(lines) if lines else ""
487
-
488
-
489
- def _format_artifact_checklist(checklist: List[Dict[str, Any]]) -> str:
490
- lines = []
491
- for item in checklist:
492
- state = (
493
- "auto-REPAIRED scaffold" if item["repaired"]
494
- else ("sanitized model output" if item["sanitized"] else "written as produced")
495
- )
496
- lines.append(f"- {item['path']}: {state}")
497
- return (
498
- "Artifact checklist (deterministic, from the transcript):\n"
499
- + "\n".join(lines)
500
- + "\nVerify each artifact actually fulfills the user's request. An "
501
- "auto-repaired scaffold is NOT completion unless its content "
502
- "satisfies what was asked."
503
- )
504
-
505
-
506
- @dataclass(frozen=True)
507
- class TranscriptBudget:
508
- """Executor/critic prompt shaping caps (review Wave 0.3).
509
-
510
- ``window`` full recent steps for the executor; per-string caps keep tool
511
- output bodies from dominating either prompt. Overridable through the same
512
- ``Config.from_env`` pattern as :class:`PhaseBudgets`.
513
- """
514
-
515
- window: int = 8
516
- result_chars: int = 700
517
- verify_chars: int = 1200
518
-
519
- @classmethod
520
- def from_env(cls, env: Optional[Mapping[str, str]] = None) -> "TranscriptBudget":
521
- from latticeai.core.config import _int
522
-
523
- if env is None:
524
- import os
525
-
526
- env = os.environ
527
-
528
- def cap(key: str, default: int, floor: int) -> int:
529
- return max(floor, _int(env, key, default))
530
-
531
- return cls(
532
- window=cap("LATTICEAI_AGENT_TRANSCRIPT_WINDOW", cls.window, 2),
533
- result_chars=cap("LATTICEAI_AGENT_TRANSCRIPT_CHARS", cls.result_chars, 120),
534
- verify_chars=cap("LATTICEAI_AGENT_VERIFY_CHARS", cls.verify_chars, 200),
535
- )
536
-
537
-
538
- @dataclass(frozen=True)
539
- class PhaseBudgets:
540
- """Per-phase token budgets for the agent loop.
541
-
542
- One shared budget let a weak model burn everything on planning prose and
543
- reach EXECUTE with nothing left. Each role phase now has its own cap, so
544
- a verbose planner can never starve execution or verification. Defaults
545
- match the historical hardcoded values; every cap is overridable through
546
- the ``Config.from_env`` environment pattern.
547
- """
548
-
549
- plan_tokens: int = 1024
550
- execute_tokens: int = 4096
551
- verify_tokens: int = 512
552
- memory_tokens: int = 256
553
-
554
- @classmethod
555
- def from_env(cls, env: Optional[Mapping[str, str]] = None) -> "PhaseBudgets":
556
- from latticeai.core.config import _int
557
-
558
- if env is None:
559
- import os
560
-
561
- env = os.environ
562
-
563
- def cap(key: str, default: int) -> int:
564
- # A misconfigured/absurd value must not brick the loop: floor at a
565
- # budget that still fits one JSON action object.
566
- return max(128, _int(env, key, default))
567
-
568
- return cls(
569
- plan_tokens=cap("LATTICEAI_AGENT_PLAN_TOKENS", cls.plan_tokens),
570
- execute_tokens=cap("LATTICEAI_AGENT_EXECUTE_TOKENS", cls.execute_tokens),
571
- verify_tokens=cap("LATTICEAI_AGENT_VERIFY_TOKENS", cls.verify_tokens),
572
- memory_tokens=cap("LATTICEAI_AGENT_MEMORY_TOKENS", cls.memory_tokens),
573
- )
574
-
575
-
576
136
  @dataclass
577
137
  class AgentDeps:
578
138
  """The ports a :class:`SingleAgentRuntime` needs from the outside world.
@@ -1445,7 +1005,7 @@ class SingleAgentRuntime:
1445
1005
  # sanitize/repair honesty flags per written file, not just prose.
1446
1006
  checklist = artifact_checklist(ctx.transcript, d.file_create_actions)
1447
1007
  checklist_hint = (
1448
- f"\n\n{_format_artifact_checklist(checklist)}" if checklist else ""
1008
+ f"\n\n{format_artifact_checklist(checklist)}" if checklist else ""
1449
1009
  )
1450
1010
  # Requirement coverage (review 루프 §2): the critic previously judged
1451
1011
  # "did this fulfill the request?" from prose alone. It now also sees
@@ -1459,7 +1019,7 @@ class SingleAgentRuntime:
1459
1019
  f"[LANGUAGE HINT: {lang_hint}]\n\n"
1460
1020
  f"Original request: {req.message}\n"
1461
1021
  f"Plan goal: {ctx.plan.get('goal', req.message)}{checklist_hint}"
1462
- f"{_format_requirement_coverage(coverage)}\n\n"
1022
+ f"{format_requirement_coverage(coverage)}\n\n"
1463
1023
  f"Full transcript:\n{json.dumps(verify_transcript, ensure_ascii=False, indent=2)}"
1464
1024
  )
1465
1025
  raw = await d.generate_as(
@@ -249,7 +249,7 @@ def _build_deps(
249
249
  knowledge_save=lambda *a, **kw: None,
250
250
  audit=lambda *a, **kw: None,
251
251
  planner_prompt="plan", executor_prompt="exec", critic_prompt="critic",
252
- memory_updater_prompt="mem", agent_root=Path("/tmp/agent-eval"),
252
+ memory_updater_prompt="mem", agent_root=Path("/tmp/agent-eval"), # noqa: S108 — eval sandbox root, created with 0700 by the harness
253
253
  change_governor=governor,
254
254
  )
255
255
 
@@ -257,7 +257,7 @@ def _build_deps(
257
257
  _PLAN = '{"action": "plan", "goal": "task", "steps": [{"action": "write_file"}]}'
258
258
  _WRITE = '{"action": "write_file", "args": {"path": "note.txt", "content": "hi"}}'
259
259
  _FINAL = '{"action": "final", "message": "done"}'
260
- _PASS = '{"action": "verdict", "verdict": "PASS", "next_state": "DONE", "reason": "ok"}'
260
+ _PASS = '{"action": "verdict", "verdict": "PASS", "next_state": "DONE", "reason": "ok"}' # noqa: S105 — a transcript marker string, not a credential
261
261
 
262
262
  # ── dirty write_file payloads (ArtifactWritePipeline scenarios) ──────────
263
263
  # What weak local models actually put in args.content: chat framing + a