ltcai 9.1.0 → 9.3.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (51) hide show
  1. package/README.md +55 -41
  2. package/docs/CHANGELOG.md +58 -0
  3. package/docs/COMMUNITY_AND_PLUGINS.md +4 -3
  4. package/docs/DEVELOPMENT.md +8 -8
  5. package/docs/LEGACY_COMPATIBILITY.md +1 -1
  6. package/docs/ONBOARDING.md +2 -2
  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/docs/mcp-tools.md +1 -1
  12. package/lattice_brain/__init__.py +1 -1
  13. package/lattice_brain/runtime/multi_agent.py +1 -1
  14. package/latticeai/__init__.py +1 -1
  15. package/latticeai/api/brain_intelligence.py +68 -0
  16. package/latticeai/api/chat_helpers.py +4 -0
  17. package/latticeai/api/chat_intents.py +30 -16
  18. package/latticeai/app_factory.py +4 -0
  19. package/latticeai/core/agent.py +31 -4
  20. package/latticeai/core/agent_prompts.py +5 -0
  21. package/latticeai/core/file_generation.py +451 -0
  22. package/latticeai/core/legacy_compatibility.py +1 -1
  23. package/latticeai/core/marketplace.py +1 -1
  24. package/latticeai/core/workspace_os.py +1 -1
  25. package/latticeai/runtime/chat_wiring.py +2 -0
  26. package/latticeai/runtime/persistence_runtime.py +7 -0
  27. package/latticeai/runtime/router_registration.py +16 -0
  28. package/latticeai/services/architecture_readiness.py +1 -1
  29. package/latticeai/services/brain_intelligence.py +439 -0
  30. package/latticeai/services/memory_service.py +65 -5
  31. package/latticeai/services/product_readiness.py +1 -1
  32. package/latticeai/services/router_context.py +1 -0
  33. package/package.json +1 -1
  34. package/scripts/check_current_release_docs.mjs +1 -1
  35. package/src-tauri/Cargo.lock +1 -1
  36. package/src-tauri/Cargo.toml +1 -1
  37. package/src-tauri/tauri.conf.json +1 -1
  38. package/static/app/asset-manifest.json +11 -11
  39. package/static/app/assets/{Act-Bzz0bUyW.js → Act-ls57ctKH.js} +1 -1
  40. package/static/app/assets/{Brain-Dj2J20YA.js → Brain-Jd3V4Obm.js} +1 -1
  41. package/static/app/assets/{Capture-CqlEl1Ga.js → Capture-DLj9Uzed.js} +1 -1
  42. package/static/app/assets/{Library-B03FP1Yx.js → Library-dPbez7Tc.js} +1 -1
  43. package/static/app/assets/{System-80lHW0Ux.js → System-CkROWBuq.js} +1 -1
  44. package/static/app/assets/index-BRUqy2zk.css +2 -0
  45. package/static/app/assets/index-CAlo2Lm-.js +17 -0
  46. package/static/app/assets/{primitives-Q1A96_7v.js → primitives-DrwaJ7dy.js} +1 -1
  47. package/static/app/assets/{textarea-D13RtnTo.js → textarea-DNvCwmmK.js} +1 -1
  48. package/static/app/index.html +2 -2
  49. package/static/sw.js +1 -1
  50. package/static/app/assets/index-BiMofBTM.js +0 -17
  51. package/static/app/assets/index-Bmx9rzTc.css +0 -2
@@ -69,9 +69,16 @@ class AgentRunContext:
69
69
  self.approved_by_human: bool = False
70
70
 
71
71
 
72
+ _THINK_BLOCK_RE = re.compile(
73
+ r"<(think|thinking|reasoning)>.*?</\1>", flags=re.DOTALL | re.IGNORECASE
74
+ )
75
+
76
+
72
77
  def extract_action(raw: str) -> Dict:
73
78
  """Parse one JSON action object out of an LLM response (tolerant of fences/prose)."""
74
- text = raw.strip()
79
+ # Small local models often prepend <think>...</think> reasoning that can
80
+ # itself contain braces — drop it before locating the action object.
81
+ text = _THINK_BLOCK_RE.sub("", raw).strip()
75
82
  fenced = re.search(r"```(?:json)?\s*(\{.*?\})\s*```", text, flags=re.DOTALL)
76
83
  if fenced:
77
84
  text = fenced.group(1).strip()
@@ -83,8 +90,14 @@ def extract_action(raw: str) -> Dict:
83
90
 
84
91
  try:
85
92
  action = json.loads(text)
86
- except json.JSONDecodeError as exc:
87
- raise ValueError(f"Agent did not return valid JSON: {exc}") from exc
93
+ except json.JSONDecodeError:
94
+ # Second chance for the most common small-model JSON slips: trailing
95
+ # commas before a closing brace/bracket.
96
+ repaired = re.sub(r",\s*([}\]])", r"\1", text)
97
+ try:
98
+ action = json.loads(repaired)
99
+ except json.JSONDecodeError as exc:
100
+ raise ValueError(f"Agent did not return valid JSON: {exc}") from exc
88
101
 
89
102
  if not isinstance(action, dict) or "action" not in action:
90
103
  raise ValueError("Agent JSON must include an action field.")
@@ -248,6 +261,7 @@ class SingleAgentRuntime:
248
261
  d = self.deps
249
262
  exec_count = sum(1 for s in ctx.transcript if s.get("state") == AgentState.EXECUTING.value)
250
263
  budget = max(1, max_steps - exec_count)
264
+ parse_failures = 0
251
265
 
252
266
  for _ in range(budget):
253
267
  corrections_hint = (
@@ -280,11 +294,24 @@ class SingleAgentRuntime:
280
294
  try:
281
295
  action = extract_action(str(raw))
282
296
  except ValueError as exc:
297
+ parse_failures += 1
283
298
  ctx.transcript.append({
284
299
  "state": AgentState.EXECUTING.value, "action": "parse_error",
285
300
  "raw": str(raw)[:400], "error": str(exc),
286
301
  })
287
- break
302
+ if parse_failures >= 3:
303
+ break
304
+ # Weak models often need one concrete reminder of the wire
305
+ # format; feed it through the corrections channel and retry
306
+ # instead of aborting the whole run on the first slip.
307
+ hint = (
308
+ 'Your last reply was not a single JSON action object. Reply with '
309
+ 'EXACTLY one JSON object like {"thoughts": "...", "action": '
310
+ '"tool_name", "args": {...}} and nothing else.'
311
+ )
312
+ if hint not in ctx.corrections:
313
+ ctx.corrections.append(hint)
314
+ continue
288
315
 
289
316
  name = action.get("action")
290
317
  thoughts = str(action.get("thoughts") or "")[:600]
@@ -43,6 +43,11 @@ You think and act like a senior software engineer:
43
43
  Respond with exactly ONE JSON object per step:
44
44
  {"thoughts": "what you learned / why this next action", "action": "tool_name", "args": {...}}
45
45
 
46
+ When writing a file (write_file), args.content must be the COMPLETE raw file
47
+ content: no Markdown fences, no commentary, valid for the file's extension
48
+ (an .html file starts with <!DOCTYPE html> and ends with </html>; a .json
49
+ file must parse as strict JSON).
50
+
46
51
  When the task is fully done AND a tool result in this run confirms it:
47
52
  {"thoughts": "verified", "action": "final", "message": "한국어로 무엇을 했고 어디서 검증했는지 요약"}
48
53
 
@@ -0,0 +1,451 @@
1
+ """Model-agnostic file content generation pipeline.
2
+
3
+ Small local models (gemma/qwen/llama 7B class) asked to "generate an HTML
4
+ file" commonly wrap the payload in chat noise: leading commentary ("Sure!
5
+ Here is your page:"), Markdown fences, ``<think>`` reasoning blocks,
6
+ trailing explanations, or an incomplete document. The previous direct-write
7
+ path saved that reply nearly verbatim, so weak models produced broken files.
8
+
9
+ This module makes the file-creation flow robust regardless of which LLM is
10
+ loaded, by treating the model as an untrusted content source:
11
+
12
+ 1. Prompt — extension-aware instructions anchored with the exact first
13
+ line the reply must start with (small models follow examples, not rules).
14
+ 2. Extract — strip reasoning blocks and conversational framing, pick the
15
+ best fenced block, slice known document boundaries.
16
+ 3. Validate — per-extension structural checks (HTML document shape, JSON
17
+ parses, CSS has rule blocks, refusal/chat detection).
18
+ 4. Retry — one corrective attempt that tells the model what was wrong.
19
+ 5. Repair — deterministic scaffolds guarantee the user still gets a valid
20
+ file even when the model never produces usable output.
21
+
22
+ The pipeline is pure (no I/O, no FastAPI); the chat layer injects an async
23
+ ``generate(context) -> str`` callable.
24
+ """
25
+
26
+ from __future__ import annotations
27
+
28
+ import html as html_lib
29
+ import json
30
+ import re
31
+ from typing import Any, Awaitable, Callable, Dict, List, Optional, Tuple
32
+
33
+ # ── extraction ──────────────────────────────────────────────────────────
34
+
35
+ _THINK_BLOCK_RE = re.compile(
36
+ r"<(think|thinking|reasoning|reflection)>.*?</\1>",
37
+ re.DOTALL | re.IGNORECASE,
38
+ )
39
+ # Unclosed think block (model hit the token limit mid-reasoning).
40
+ _THINK_OPEN_RE = re.compile(r"<(think|thinking|reasoning)>.*\Z", re.DOTALL | re.IGNORECASE)
41
+
42
+ _FENCE_RE = re.compile(r"```([\w.+-]*)[ \t]*\n(.*?)```", re.DOTALL)
43
+
44
+ # Conversational lines that small models prepend/append around the payload.
45
+ _CHAT_LINE_RE = re.compile(
46
+ r"^\s*("
47
+ r"(sure|of course|certainly|okay|ok|alright|great|absolutely)\b[^\n]*"
48
+ r"|here('s| is| are)\b[^\n]*"
49
+ r"|i('ve| have) (created|written|generated|made)\b[^\n]*"
50
+ r"|(below|following) is\b[^\n]*"
51
+ r"|let me know\b[^\n]*"
52
+ r"|hope (this|that) helps[^\n]*"
53
+ r"|feel free\b[^\n]*"
54
+ r"|물론(입니다|이죠|이에요)?[!., ]*[^\n]*"
55
+ r"|네[,!. ][^\n]*"
56
+ r"|알겠습니다[^\n]*"
57
+ r"|다음은[^\n]*(입니다|합니다)[:.]?[^\n]*"
58
+ r"|아래는?[^\n]*(입니다|내용)[^\n]*"
59
+ r"|(요청하신|원하시는)[^\n]*(입니다|만들었습니다|작성했습니다)[^\n]*"
60
+ r"|(파일|내용|코드)[을를]?\s*(생성|작성|만들)[^\n]*"
61
+ r"|도움이 (필요하|되)[^\n]*"
62
+ r"|추가로[^\n]*(말씀|요청)[^\n]*"
63
+ r")\s*$",
64
+ re.IGNORECASE,
65
+ )
66
+
67
+ _REFUSAL_RE = re.compile(
68
+ r"(i can('|no)?t|i'?m (sorry|unable)|as an ai|cannot assist"
69
+ r"|죄송(하지만|합니다)|할 수 없|불가능합니다|도와드릴 수 없)",
70
+ re.IGNORECASE,
71
+ )
72
+
73
+ # Language tags that identify a fenced block as the payload for an extension.
74
+ _EXT_FENCE_LANGS: Dict[str, Tuple[str, ...]] = {
75
+ ".html": ("html", "htm", "xhtml"),
76
+ ".htm": ("html", "htm", "xhtml"),
77
+ ".css": ("css",),
78
+ ".js": ("js", "javascript"),
79
+ ".jsx": ("jsx", "javascript"),
80
+ ".ts": ("ts", "typescript"),
81
+ ".tsx": ("tsx", "typescript"),
82
+ ".py": ("py", "python"),
83
+ ".json": ("json",),
84
+ ".yaml": ("yaml", "yml"),
85
+ ".yml": ("yaml", "yml"),
86
+ ".toml": ("toml",),
87
+ ".md": ("md", "markdown"),
88
+ ".markdown": ("md", "markdown"),
89
+ ".sql": ("sql",),
90
+ ".sh": ("sh", "bash", "shell", "zsh"),
91
+ ".xml": ("xml", "svg"),
92
+ ".csv": ("csv",),
93
+ ".txt": ("txt", "text", "plaintext"),
94
+ }
95
+
96
+
97
+ def _ext(path: str) -> str:
98
+ dot = path.rfind(".")
99
+ return path[dot:].lower() if dot >= 0 else ""
100
+
101
+
102
+ def _strip_chat_lines(text: str) -> str:
103
+ """Drop leading/trailing conversational lines around the payload."""
104
+ lines = text.split("\n")
105
+ start, end = 0, len(lines)
106
+ while start < end and (not lines[start].strip() or _CHAT_LINE_RE.match(lines[start])):
107
+ start += 1
108
+ while end > start and (not lines[end - 1].strip() or _CHAT_LINE_RE.match(lines[end - 1])):
109
+ end -= 1
110
+ stripped = "\n".join(lines[start:end]).strip()
111
+ return stripped if stripped else text.strip()
112
+
113
+
114
+ def extract_file_content(raw: str, target_path: str) -> str:
115
+ """Recover the intended file payload from an arbitrary model reply."""
116
+ text = (raw or "").strip()
117
+ if not text:
118
+ return ""
119
+ text = _THINK_BLOCK_RE.sub("", text)
120
+ text = _THINK_OPEN_RE.sub("", text).strip()
121
+
122
+ ext = _ext(target_path)
123
+ fences = _FENCE_RE.findall(text + ("\n```" if text.count("```") % 2 else ""))
124
+ if fences:
125
+ wanted = _EXT_FENCE_LANGS.get(ext, ())
126
+ matching = [body for lang, body in fences if lang.lower() in wanted]
127
+ candidates = matching if matching else [body for _, body in fences]
128
+ # The payload is the largest block; short blocks are usually usage
129
+ # snippets ("run it with: python app.py").
130
+ content = max(candidates, key=len).strip()
131
+ else:
132
+ content = _strip_chat_lines(text)
133
+
134
+ if ext in (".html", ".htm"):
135
+ content = _slice_html_document(content)
136
+ elif ext == ".json":
137
+ sliced = _slice_json_document(content)
138
+ if sliced is not None:
139
+ content = sliced
140
+ return content.strip()
141
+
142
+
143
+ def _slice_html_document(content: str) -> str:
144
+ """Cut a complete HTML document out of surrounding prose when present."""
145
+ lower = content.lower()
146
+ start = lower.find("<!doctype")
147
+ if start < 0:
148
+ start = lower.find("<html")
149
+ if start > 0:
150
+ content = content[start:]
151
+ lower = lower[start:]
152
+ end = lower.rfind("</html>")
153
+ if end >= 0:
154
+ content = content[: end + len("</html>")]
155
+ return content
156
+
157
+
158
+ def _slice_json_document(content: str) -> Optional[str]:
159
+ """Return the largest parseable JSON value inside ``content``, if any."""
160
+ candidates: List[str] = [content]
161
+ for opener, closer in (("{", "}"), ("[", "]")):
162
+ start = content.find(opener)
163
+ end = content.rfind(closer)
164
+ if start >= 0 and end > start:
165
+ candidates.append(content[start : end + 1])
166
+ best: Optional[str] = None
167
+ for candidate in candidates:
168
+ try:
169
+ json.loads(candidate)
170
+ except (ValueError, TypeError):
171
+ continue
172
+ if best is None or len(candidate) > len(best):
173
+ best = candidate
174
+ return best
175
+
176
+
177
+ # ── validation ──────────────────────────────────────────────────────────
178
+
179
+ def looks_like_refusal(content: str) -> bool:
180
+ head = content[:300]
181
+ return bool(_REFUSAL_RE.search(head)) and len(content) < 600
182
+
183
+
184
+ def validate_file_content(content: str, target_path: str) -> Tuple[bool, str]:
185
+ """Structural sanity check per file type. Returns (ok, reason)."""
186
+ if not content.strip():
187
+ return False, "empty output"
188
+ if looks_like_refusal(content):
189
+ return False, "the reply was a refusal/chat message, not file content"
190
+
191
+ ext = _ext(target_path)
192
+ if ext in (".html", ".htm"):
193
+ lower = content.lower()
194
+ if "<html" not in lower and "<!doctype" not in lower:
195
+ return False, "not a complete HTML document (missing <!DOCTYPE html>/<html>)"
196
+ if "</html>" not in lower:
197
+ return False, "HTML document is truncated (missing </html>)"
198
+ return True, "ok"
199
+ if ext == ".json":
200
+ try:
201
+ json.loads(content)
202
+ except (ValueError, TypeError) as exc:
203
+ return False, f"invalid JSON: {exc}"
204
+ return True, "ok"
205
+ if ext == ".css":
206
+ if "{" not in content or "}" not in content:
207
+ return False, "no CSS rule blocks found"
208
+ return True, "ok"
209
+ if ext in (".py", ".js", ".jsx", ".ts", ".tsx", ".sh", ".sql"):
210
+ if "```" in content:
211
+ return False, "output still contains Markdown fences"
212
+ return True, "ok"
213
+ return True, "ok"
214
+
215
+
216
+ # ── prompting ───────────────────────────────────────────────────────────
217
+
218
+ _FIRST_LINE_HINTS: Dict[str, str] = {
219
+ ".html": "<!DOCTYPE html>",
220
+ ".htm": "<!DOCTYPE html>",
221
+ ".py": "# (python code — imports or code on the first line)",
222
+ ".sh": "#!/bin/sh",
223
+ ".json": "{",
224
+ ".xml": "<?xml version=\"1.0\" encoding=\"UTF-8\"?>",
225
+ }
226
+
227
+ _TYPE_RULES: Dict[str, str] = {
228
+ ".html": (
229
+ "Produce ONE complete standalone HTML5 document: <!DOCTYPE html>, <html>, "
230
+ "<head> with <meta charset=\"utf-8\"> and a <title>, inline <style> for CSS, "
231
+ "and a closed </html> tag. Do not reference external files."
232
+ ),
233
+ ".htm": (
234
+ "Produce ONE complete standalone HTML5 document ending with </html>."
235
+ ),
236
+ ".json": "Produce strictly valid JSON (double quotes, no comments, no trailing commas).",
237
+ ".css": "Produce valid CSS rules only.",
238
+ ".md": "Produce well-structured Markdown with headings.",
239
+ ".markdown": "Produce well-structured Markdown with headings.",
240
+ ".csv": "Produce CSV with a header row; comma-separated, one record per line.",
241
+ ".py": "Produce complete runnable Python source code.",
242
+ ".js": "Produce complete valid JavaScript source code.",
243
+ }
244
+
245
+
246
+ def build_file_generation_context(
247
+ target_path: str,
248
+ user_request: str,
249
+ feedback: Optional[str] = None,
250
+ ) -> str:
251
+ """Strict, extension-aware generation instructions.
252
+
253
+ Small models ignore abstract rules but reliably imitate concrete anchors,
254
+ so the prompt pins the exact first line of the expected output.
255
+ """
256
+ ext = _ext(target_path)
257
+ parts = [
258
+ "You are a file content generator. Your entire reply is saved verbatim "
259
+ f"as the file `{target_path}` — it is NOT shown in a chat.",
260
+ "Rules:",
261
+ "- Output ONLY the raw file content.",
262
+ "- No Markdown code fences (```), no explanations, no greetings, "
263
+ "no text before or after the content.",
264
+ ]
265
+ type_rule = _TYPE_RULES.get(ext)
266
+ if type_rule:
267
+ parts.append(f"- {type_rule}")
268
+ first_line = _FIRST_LINE_HINTS.get(ext)
269
+ if first_line:
270
+ parts.append(f"- The very first line of your reply must be: {first_line}")
271
+ if feedback:
272
+ parts.append(
273
+ "Your previous attempt was rejected: "
274
+ f"{feedback}. Fix that and output only the corrected file content."
275
+ )
276
+ parts.append(f"\nUser request: {user_request}")
277
+ return "\n".join(parts)
278
+
279
+
280
+ # ── repair (deterministic fallback) ─────────────────────────────────────
281
+
282
+ def repair_file_content(content: str, target_path: str, user_request: str) -> str:
283
+ """Turn whatever the model produced into a valid file of the target type.
284
+
285
+ This is the last resort after retries: the user asked for a file, so the
286
+ request must still end in a well-formed file, never an error.
287
+ """
288
+ ext = _ext(target_path)
289
+ salvage = content.strip()
290
+ if looks_like_refusal(salvage):
291
+ salvage = ""
292
+
293
+ if ext in (".html", ".htm"):
294
+ return _repair_html(salvage, user_request)
295
+ if ext == ".json":
296
+ sliced = _slice_json_document(salvage)
297
+ if sliced is not None:
298
+ return sliced
299
+ return json.dumps(
300
+ {"request": user_request, "content": salvage},
301
+ ensure_ascii=False,
302
+ indent=2,
303
+ )
304
+ if salvage:
305
+ return salvage
306
+ # Nothing usable at all — leave an honest placeholder in the right format.
307
+ comment = {
308
+ ".py": "# TODO: model produced no usable content for: ",
309
+ ".js": "// TODO: model produced no usable content for: ",
310
+ ".css": "/* TODO: model produced no usable content for: ",
311
+ ".sh": "# TODO: model produced no usable content for: ",
312
+ ".sql": "-- TODO: model produced no usable content for: ",
313
+ }.get(ext, "")
314
+ if ext == ".css":
315
+ return f"{comment}{user_request} */\n"
316
+ if comment:
317
+ return f"{comment}{user_request}\n"
318
+ return f"{user_request}\n"
319
+
320
+
321
+ def _repair_html(salvage: str, user_request: str) -> str:
322
+ lower = salvage.lower()
323
+ if "<html" in lower or "<!doctype" in lower:
324
+ # A real document that is merely truncated — close it.
325
+ doc = _slice_html_document(salvage)
326
+ low = doc.lower()
327
+ if "</body>" not in low and "<body" in low:
328
+ doc += "\n</body>"
329
+ if "</html>" not in low:
330
+ doc += "\n</html>"
331
+ return doc
332
+ if re.search(r"<\w+[^>]*>", salvage):
333
+ body = salvage # an HTML fragment — embed as-is
334
+ elif salvage:
335
+ body = "\n".join(
336
+ f" <p>{html_lib.escape(line)}</p>"
337
+ for line in salvage.splitlines()
338
+ if line.strip()
339
+ )
340
+ else:
341
+ body = f" <p>{html_lib.escape(user_request)}</p>"
342
+ title = html_lib.escape(user_request[:60] or "Generated page")
343
+ return (
344
+ "<!DOCTYPE html>\n"
345
+ "<html lang=\"ko\">\n"
346
+ "<head>\n"
347
+ " <meta charset=\"utf-8\">\n"
348
+ " <meta name=\"viewport\" content=\"width=device-width, initial-scale=1\">\n"
349
+ f" <title>{title}</title>\n"
350
+ " <style>\n"
351
+ " body { font-family: system-ui, sans-serif; margin: 2rem auto; "
352
+ "max-width: 720px; line-height: 1.6; padding: 0 1rem; }\n"
353
+ " </style>\n"
354
+ "</head>\n"
355
+ "<body>\n"
356
+ f"{body}\n"
357
+ "</body>\n"
358
+ "</html>"
359
+ )
360
+
361
+
362
+ # ── filename inference ──────────────────────────────────────────────────
363
+
364
+ _CREATE_VERB_RE = re.compile(
365
+ r"(만들|생성|작성|써\s*줘|저장|create|make|write|generate|build|save)",
366
+ re.IGNORECASE,
367
+ )
368
+
369
+ # Explicit type keyword → default filename. Ordered: first match wins.
370
+ _TYPE_KEYWORDS: Tuple[Tuple[str, str], ...] = (
371
+ (r"\bhtml\b|웹\s*페이지|웹페이지|홈페이지|landing\s*page|web\s*page", "generated_page.html"),
372
+ (r"\bcss\b|스타일\s*시트", "styles.css"),
373
+ (r"\bjavascript\b|\bjs\b\s*(파일|file)|자바스크립트", "script.js"),
374
+ (r"\bpython\b|파이썬", "script.py"),
375
+ (r"\bjson\b", "data.json"),
376
+ (r"\bcsv\b", "data.csv"),
377
+ (r"\byaml\b|\byml\b", "config.yaml"),
378
+ (r"\bxml\b", "data.xml"),
379
+ (r"\bsql\b", "query.sql"),
380
+ (r"마크다운|\bmarkdown\b|\bmd\b\s*(파일|file)", "notes.md"),
381
+ (r"텍스트\s*파일|\btext\s*file\b|\btxt\b", "notes.txt"),
382
+ )
383
+
384
+
385
+ def infer_file_target(message: str) -> Optional[str]:
386
+ """Infer a filename for creation requests that name a type but no path.
387
+
388
+ "html 파일 만들어줘" previously fell through to the agent JSON loop, which
389
+ small models fail at. Inference keeps such requests on the deterministic
390
+ direct-write path. Deliberately narrow: requires a creation verb and an
391
+ explicit file-type keyword — report/document prose requests keep flowing
392
+ to the document generator.
393
+ """
394
+ text = (message or "").strip()
395
+ if not text or not _CREATE_VERB_RE.search(text):
396
+ return None
397
+ lower = text.lower()
398
+ for pattern, filename in _TYPE_KEYWORDS:
399
+ if re.search(pattern, lower):
400
+ return filename
401
+ return None
402
+
403
+
404
+ # ── orchestration ───────────────────────────────────────────────────────
405
+
406
+ async def generate_file_content(
407
+ generate: Callable[[str], Awaitable[Any]],
408
+ *,
409
+ target_path: str,
410
+ user_request: str,
411
+ max_attempts: int = 2,
412
+ ) -> Tuple[str, Dict[str, Any]]:
413
+ """Generate validated file content with any LLM.
414
+
415
+ ``generate`` is an async callable ``context -> raw model text``. Runs up
416
+ to ``max_attempts`` model calls (the second with corrective feedback),
417
+ then falls back to deterministic repair, so the returned content is
418
+ always non-empty and structurally valid for the target type.
419
+ """
420
+ attempts: List[Dict[str, Any]] = []
421
+ feedback: Optional[str] = None
422
+ last_candidate = ""
423
+ for attempt in range(1, max_attempts + 1):
424
+ context = build_file_generation_context(target_path, user_request, feedback=feedback)
425
+ try:
426
+ raw = await generate(context)
427
+ except Exception as exc: # model backend hiccup — repair still delivers
428
+ attempts.append({"attempt": attempt, "valid": False, "reason": f"generation error: {exc}"})
429
+ feedback = "the model call failed"
430
+ continue
431
+ candidate = extract_file_content(str(raw or ""), target_path)
432
+ ok, reason = validate_file_content(candidate, target_path)
433
+ attempts.append({"attempt": attempt, "valid": ok, "reason": reason})
434
+ if ok:
435
+ return candidate, {"attempts": attempts, "repaired": False}
436
+ if len(candidate) > len(last_candidate):
437
+ last_candidate = candidate
438
+ feedback = reason
439
+ repaired = repair_file_content(last_candidate, target_path, user_request)
440
+ return repaired, {"attempts": attempts, "repaired": True}
441
+
442
+
443
+ __all__ = [
444
+ "build_file_generation_context",
445
+ "extract_file_content",
446
+ "generate_file_content",
447
+ "infer_file_target",
448
+ "looks_like_refusal",
449
+ "repair_file_content",
450
+ "validate_file_content",
451
+ ]
@@ -14,7 +14,7 @@ from pathlib import Path
14
14
  from typing import Any, Dict, List
15
15
 
16
16
 
17
- LEGACY_COMPATIBILITY_VERSION = "9.1.0"
17
+ LEGACY_COMPATIBILITY_VERSION = "9.3.0"
18
18
 
19
19
 
20
20
  @dataclass(frozen=True)
@@ -11,7 +11,7 @@ from copy import deepcopy
11
11
  from typing import Any, Dict, List, Optional
12
12
 
13
13
 
14
- MARKETPLACE_VERSION = "9.1.0"
14
+ MARKETPLACE_VERSION = "9.3.0"
15
15
  TEMPLATE_KINDS = ("plugin", "workflow", "agent", "ingestion_bridge")
16
16
 
17
17
 
@@ -49,7 +49,7 @@ __all__ = [
49
49
  "remove_skill_directory",
50
50
  ]
51
51
 
52
- WORKSPACE_OS_VERSION = "9.1.0"
52
+ WORKSPACE_OS_VERSION = "9.3.0"
53
53
 
54
54
  # Workspace types separate single-user Personal workspaces from shared
55
55
  # Organization workspaces. Both keep the same local-first JSON store; the type
@@ -65,6 +65,7 @@ def build_interaction_contexts(
65
65
  memory_service: Any,
66
66
  platform: Any,
67
67
  active_model_getter: Any = None,
68
+ brain_intelligence: Any = None,
68
69
  ) -> tuple[ToolRouterContext, InteractionRouterContext]:
69
70
  tool_router_context = ToolRouterContext(
70
71
  config=config,
@@ -105,6 +106,7 @@ def build_interaction_contexts(
105
106
  memory_service=memory_service,
106
107
  platform=platform,
107
108
  active_model_getter=active_model_getter,
109
+ brain_intelligence=brain_intelligence,
108
110
  )
109
111
  return tool_router_context, interaction_router_context
110
112
 
@@ -36,6 +36,7 @@ def build_persistence_runtime(
36
36
  from latticeai.core.plugins import PluginRegistry
37
37
  from latticeai.core.realtime import RealtimeBus
38
38
  from latticeai.core.workspace_os import WorkspaceOSStore
39
+ from latticeai.services.brain_intelligence import BrainIntelligenceService
39
40
  from latticeai.services.memory_service import MemoryService
40
41
  from latticeai.services.workspace_service import WorkspaceService
41
42
 
@@ -57,6 +58,11 @@ def build_persistence_runtime(
57
58
  history_file=history_file,
58
59
  conversation_store=conversations,
59
60
  )
61
+ brain_intelligence = BrainIntelligenceService(
62
+ knowledge_graph=knowledge_graph,
63
+ memory_service=memory_service,
64
+ enable_graph=enable_graph,
65
+ )
60
66
  ingestion_pipeline = IngestionPipeline(
61
67
  knowledge_graph,
62
68
  hooks=hooks_registry,
@@ -81,6 +87,7 @@ def build_persistence_runtime(
81
87
  "TEMPLATE_CATALOG": template_catalog,
82
88
  "AGENT_REGISTRY": agent_registry,
83
89
  "MEMORY_SERVICE": memory_service,
90
+ "BRAIN_INTELLIGENCE": brain_intelligence,
84
91
  "INGESTION_PIPELINE": ingestion_pipeline,
85
92
  "DEVICE_IDENTITY": device_identity,
86
93
  "KG_PORTABILITY": kg_portability,
@@ -485,6 +485,8 @@ def register_interaction_routers(
485
485
  memory_service: Any = None,
486
486
  platform: Any = None,
487
487
  active_model_getter: Any = None,
488
+ create_brain_intelligence_router: Any = None,
489
+ brain_intelligence: Any = None,
488
490
  ) -> tuple[Any, ...]:
489
491
  """Register chat/search/tools/hooks/registry/memory routes in order."""
490
492
 
@@ -504,6 +506,7 @@ def register_interaction_routers(
504
506
  memory_service = interaction_context.memory_service
505
507
  platform = interaction_context.platform
506
508
  active_model_getter = interaction_context.active_model_getter
509
+ brain_intelligence = interaction_context.brain_intelligence
507
510
 
508
511
  return register_routers(
509
512
  app,
@@ -561,6 +564,19 @@ def register_interaction_routers(
561
564
  append_audit_event=append_audit_event,
562
565
  active_model_getter=active_model_getter,
563
566
  ),
567
+ *(
568
+ (
569
+ create_brain_intelligence_router(
570
+ service=brain_intelligence,
571
+ require_user=require_user,
572
+ gate_read=platform.gate_read,
573
+ gate_write=platform.gate_write,
574
+ append_audit_event=append_audit_event,
575
+ ),
576
+ )
577
+ if create_brain_intelligence_router is not None and brain_intelligence is not None
578
+ else ()
579
+ ),
564
580
  )
565
581
 
566
582
 
@@ -17,7 +17,7 @@ from typing import Any, Dict, List
17
17
  from latticeai.core.legacy_compatibility import legacy_shim_report
18
18
 
19
19
 
20
- ARCHITECTURE_VERSION_TARGET = "9.1.0"
20
+ ARCHITECTURE_VERSION_TARGET = "9.3.0"
21
21
 
22
22
  PREFERRED_REFACTORING_ORDER = [
23
23
  "agent-runtime",