ltcai 8.3.0 → 8.5.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.
package/README.md CHANGED
@@ -1,6 +1,6 @@
1
1
  # Lattice AI
2
2
 
3
- **Lattice AI 8.3.0 is the local-first Digital Brain platform with managed legacy compatibility, stronger AgentRuntime/workflow orchestration, and graph ingestion coverage that keeps workspace memory isolated.**
3
+ **Lattice AI 8.5.0 is the local-first Digital Brain platform. This release hardens the Tool Registry (now drift-free and ready) and improves dependency injection for configuration (e.g. timezone flows through Config to automation runtimes) after full codebase scan and targeted refactors.**
4
4
 
5
5
  **Lattice AI는 모델이 바뀌어도 내 지식과 맥락을 보존하는 로컬 우선 AI 브레인입니다.**
6
6
 
@@ -11,7 +11,7 @@ downloads, update checks, and other external communication happen only after
11
11
  explicit consent.
12
12
 
13
13
  It is not a ChatGPT clone, a model launcher, a graph database, or a note app.
14
- It is the finished private AI memory layer wrapped in a Living Brain experience — now with an 8.3.0 product-readiness contract behind onboarding, orchestration, graph ingestion, and the plugin/community path.
14
+ It is the finished private AI memory layer wrapped in a Living Brain experience — with ToolRegistry readiness, Config-driven DI for automation, and continued AgentRuntime/Tool wiring seams.
15
15
 
16
16
  [![PyPI Version](https://img.shields.io/pypi/v/ltcai?label=PyPI)](https://pypi.org/project/ltcai/)
17
17
  [![npm Version](https://img.shields.io/npm/v/ltcai?label=npm)](https://www.npmjs.com/package/ltcai)
@@ -204,27 +204,32 @@ See [docs/DEVELOPMENT.md](docs/DEVELOPMENT.md) for developer workflow details.
204
204
 
205
205
  ## Current Release
206
206
 
207
- The current release is **8.3.0 — Orchestrated Brain Readiness**:
207
+ The current release is **8.5.0 — Tool Registry Readiness & Config DI**:
208
208
 
209
- - Legacy compatibility is now tracked as a managed inventory in `latticeai.core.legacy_compatibility`, so root shims have owners, replacements, and removal phases instead of living as hidden debt.
210
- - AgentRuntime and workflow orchestration expose clearer lifecycle/config boundaries while preserving compatibility for legacy run records.
211
- - `/knowledge-graph/ingest` now routes through the unified ingestion pipeline when available, giving MCP/note/message writes the same provenance and hook lifecycle as files and browser sources.
212
- - Graph ingestion now keeps duplicate content isolated per workspace, avoiding cross-workspace metadata overwrites while preserving content-hash duplicate detection.
213
- - Upload failures surface honest client errors, and upload-to-KG behavior is covered by integration-style unit tests.
214
- - Onboarding, community, plugin, and release docs now define the five-minute first-run path and ecosystem growth path for 8.3.0.
209
+ - Brain Chat now routes explicit file create/write/save/edit requests through
210
+ the governed workspace file tool instead of treating them as ordinary prose
211
+ generation.
212
+ - Requests such as `hello.md 파일 만들어줘` are executed through
213
+ `write_file`/`edit_file` style tool paths, and created artifacts are returned
214
+ in the chat response.
215
+ - Normal Q&A stays on the low-friction `/chat` generation path; only explicit
216
+ side-effect requests move into the tool-running agent loop.
217
+ - The release keeps the 8.3.0 managed legacy inventory, AgentRuntime/workflow
218
+ boundaries, unified graph ingestion, and workspace-safe duplicate content
219
+ protections active.
215
220
 
216
- Expected artifacts for 8.3.0 release must use exact filenames:
221
+ Expected artifacts for 8.5.0 release must use exact filenames:
217
222
 
218
- - `dist/ltcai-8.3.0-py3-none-any.whl`
219
- - `dist/ltcai-8.3.0.tar.gz`
220
- - `ltcai-8.3.0.tgz`
221
- - `dist/ltcai-8.3.0.vsix`
222
- - `src-tauri/target/release/bundle/dmg/Lattice AI_8.3.0_aarch64.dmg`
223
+ - `dist/ltcai-8.5.0-py3-none-any.whl`
224
+ - `dist/ltcai-8.5.0.tar.gz`
225
+ - `ltcai-8.5.0.tgz`
226
+ - `dist/ltcai-8.5.0.vsix`
227
+ - `src-tauri/target/release/bundle/dmg/Lattice AI_8.5.0_aarch64.dmg`
223
228
 
224
229
  Do not use wildcard artifact uploads. Package registry publishing remains owner-run.
225
230
 
226
231
  See [docs/ROADMAP_RECOMMENDATIONS.md](docs/ROADMAP_RECOMMENDATIONS.md) for the
227
- strategic roadmap slices applied through 8.3.0 and the follow-up tracks.
232
+ strategic roadmap slices applied through 8.5.0 and the follow-up tracks.
228
233
 
229
234
  ## Known Limitations
230
235
 
@@ -241,6 +246,7 @@ strategic roadmap slices applied through 8.3.0 and the follow-up tracks.
241
246
 
242
247
  | Version | Theme |
243
248
  | --- | --- |
249
+ | 8.4.0 | Action-Aware Brain Chat: explicit file create/write/save/edit requests from Brain Chat route into the governed workspace file tool so files are actually created instead of returned as code-only answers |
244
250
  | 8.3.0 | Orchestrated Brain Readiness: managed legacy shim inventory, stronger AgentRuntime/workflow boundaries, unified graph ingestion, workspace-safe duplicate content, first-run onboarding, and explicit community/plugin growth path |
245
251
  | 8.2.0 | Brain Brief: evidence-backed home briefing, honest empty-state guidance, recall/graph/model-proof next actions, and continued model/workspace runtime extraction |
246
252
  | 8.1.0 | Intuitive Brain Home: living Brain, recent memory, connected topic, next action, and composer are visible in one product-first screen with refreshed 8.1.0 evidence and artifacts |
package/docs/CHANGELOG.md CHANGED
@@ -6,7 +6,46 @@ existed at that release.
6
6
 
7
7
  ## [Unreleased]
8
8
 
9
- No unreleased changes.
9
+ ## [8.5.0] - 2026-07-01
10
+
11
+ ### Added
12
+ - ToolRegistry now reports `ready: true` with full handler/governance/description alignment (added `vision_analyze` policy and description).
13
+ - `tz_name` now flows from central `Config` into `TriggerService` (via updated automation and platform wiring runtimes) for better DI and Config centralization.
14
+
15
+ ### Changed
16
+ - Full codebase scan completed; improvements prioritized per AGENTS.md (registry, config injection, wiring seams).
17
+ - Version bumped to 8.5.0 across pyproject.toml, package.json, vscode-extension; all current-release doc references synchronized.
18
+ - Documentation sync performed for README, RELEASE.md, docs/CHANGELOG.md and Current release markers.
19
+
20
+ ### Fixed
21
+ - Eliminated ToolRegistry drift for `vision_analyze` (handler existed in tools/ but missing from core registry governance surface used by diagnostics, MCP, and permission views).
22
+
23
+ ## [8.4.0] - 2026-07-01
24
+
25
+ ### Added
26
+ - Added a chat-to-agent file action gate so explicit file create/write/save/edit
27
+ requests from Brain Chat execute through the governed workspace file tool.
28
+ - Added regression coverage that verifies `/chat` routes file creation intent to
29
+ the workspace file tool and returns created artifact metadata.
30
+
31
+ ### Changed
32
+ - Kept ordinary Q&A on `/chat` while routing only explicit side-effect file
33
+ requests into the planner/executor/reviewer tool loop.
34
+ - Synchronized package/runtime/static/Tauri metadata and current-release docs to
35
+ 8.4.0.
36
+
37
+ ### Fixed
38
+ - Allowed literal `/chat` file writes with user-provided content to execute
39
+ before model loading, while still using a loaded model when content must be
40
+ synthesized.
41
+ - Narrowed file target and content parsing to avoid treating surrounding prose
42
+ as the workspace path or literal file body.
43
+ - Restored model loading dependency exports after the runtime/loading split so
44
+ `/models/load` can prepare local MLX models again.
45
+ - Added the common Gemma 4 26B shorthand alias to the canonical
46
+ `mlx-community/gemma-4-26b-a4b-it-4bit` model id.
47
+ - Updated the local server launcher to run `python -m uvicorn` from the active
48
+ virtual environment, avoiding stale console-script interpreter bindings.
10
49
 
11
50
  ## [8.3.0] - 2026-07-01
12
51
 
@@ -1,8 +1,8 @@
1
1
  # Community And Plugins
2
2
 
3
- Current release: **8.3.0 - Orchestrated Brain Readiness**.
3
+ Current release: **8.5.0 - Registry Polish & Config DI**.
4
4
 
5
- LatticeAI 8.3.0 defines the path from a strong local-first framework to a
5
+ LatticeAI defines the path from a strong local-first framework (8.4.0 action-aware baseline, 8.5.0 registry+DI hardening) to a
6
6
  product ecosystem. The immediate goal is small and practical: make it clear how
7
7
  contributors can extend the Brain without weakening local-first trust,
8
8
  workspace scoping, or release quality.
@@ -34,7 +34,7 @@ Plugins should add one bounded capability at a time:
34
34
  - Compatibility requirements belong in `lattice_version`, and examples should
35
35
  use current host-compatible values.
36
36
 
37
- ## 8.3.0 Ecosystem Tasks
37
+ ## 8.4.0 Ecosystem Tasks
38
38
 
39
39
  - Keep the hello-world plugin valid and boring.
40
40
  - Expand examples only when they exercise real extension seams.
@@ -1,10 +1,10 @@
1
1
  # Lattice AI Development
2
2
 
3
- Current release: **8.3.0 - Orchestrated Brain Readiness**.
3
+ Current release: **8.5.0 Tool Registry Readiness & Config DI**.
4
4
 
5
5
  This document is for contributors working on the local-first Digital Brain
6
6
  codebase. Product positioning and quick start stay in `README.md`; release
7
- history is intentionally limited to 7.0.0-8.3.0 in `docs/CHANGELOG.md` and
7
+ history is intentionally limited to 7.0.0-8.5.0 in `docs/CHANGELOG.md` and
8
8
  `RELEASE.md`.
9
9
 
10
10
  ## Product Contract
@@ -112,10 +112,10 @@ For user-facing, API, runtime, release, or packaging changes, check:
112
112
  Release/publish examples must use exact target-version filenames. Do not
113
113
  document wildcard artifact upload commands.
114
114
 
115
- For 8.3.0 release work, exact artifacts are:
115
+ For 8.5.0 release work, exact artifacts are:
116
116
 
117
- - `dist/ltcai-8.3.0-py3-none-any.whl`
118
- - `dist/ltcai-8.3.0.tar.gz`
119
- - `ltcai-8.3.0.tgz`
120
- - `dist/ltcai-8.3.0.vsix`
121
- - `src-tauri/target/release/bundle/dmg/Lattice AI_8.3.0_aarch64.dmg`
117
+ - `dist/ltcai-8.5.0-py3-none-any.whl`
118
+ - `dist/ltcai-8.5.0.tar.gz`
119
+ - `ltcai-8.5.0.tgz`
120
+ - `dist/ltcai-8.5.0.vsix`
121
+ - `src-tauri/target/release/bundle/dmg/Lattice AI_8.5.0_aarch64.dmg`
@@ -1,14 +1,14 @@
1
1
  # Legacy Compatibility Map
2
2
 
3
- Current release: **8.3.0 - Orchestrated Brain Readiness**.
3
+ Current release: **8.5.0 Tool Registry Readiness & Config DI**.
4
4
 
5
5
  Lattice AI is moving toward a smaller, modular architecture centered on
6
6
  `lattice_brain`, `latticeai.services`, `latticeai.api`, and `latticeai.runtime`.
7
7
  Some root-level modules remain packaged for compatibility with older imports,
8
8
  CLI entrypoints, or extension workflows. Their presence does not define the
9
- current 8.3.0 architecture.
9
+ current 8.5.0 architecture.
10
10
 
11
- 8.3.0 also tracks the inner compatibility layers that sit below the root
11
+ 8.5.0 also tracks the inner compatibility layers that sit below the root
12
12
  modules. The managed inventory lives in `latticeai.core.legacy_compatibility`
13
13
  and groups shims by layer:
14
14
 
@@ -1,6 +1,6 @@
1
1
  # Lattice AI Onboarding
2
2
 
3
- Current release: **8.3.0 - Orchestrated Brain Readiness**.
3
+ Current release: **8.5.0 Tool Registry Readiness & Config DI**.
4
4
 
5
5
  The first-run goal is a five-minute path from "I opened the app" to "my Brain
6
6
  has a source, a question, and proof." This page is the product contract behind
@@ -32,7 +32,7 @@ read the docs first.
32
32
 
33
33
  ## Release Gate
34
34
 
35
- 8.3.0 treats onboarding as a release gate, not marketing copy. The current
35
+ 8.5.0 treats onboarding as a release gate, not marketing copy. The current
36
36
  machine-checkable product readiness report requires this five-minute contract,
37
37
  the Brain Home surface, setup helpers, graph ingestion tests, and exact release
38
38
  artifact documentation before the release can be called complete.
@@ -1,6 +1,6 @@
1
1
  # Lattice AI Trust Model
2
2
 
3
- Current release: **8.3.0 - Orchestrated Brain Readiness**.
3
+ Current release: **8.5.0 Tool Registry Readiness & Config DI**.
4
4
 
5
5
  Lattice AI is local-first, explicit about external communication, and honest
6
6
  when a capability is unavailable.
@@ -1,6 +1,6 @@
1
1
  # Why Lattice AI Exists
2
2
 
3
- Current release: **8.3.0 - Orchestrated Brain Readiness**.
3
+ Current release: **8.5.0 Tool Registry Readiness & Config DI**.
4
4
 
5
5
  **Lattice AI is a local-first Digital Brain that keeps your knowledge durable
6
6
  across any AI model.**
@@ -32,7 +32,7 @@ The graph matters, but it is not the product identity. The product identity is a
32
32
  local-first Digital Brain: a place where the user can talk, add sources, watch
33
33
  memory grow, and inspect the Knowledge Graph when they need proof.
34
34
 
35
- In 8.3.0 the first screen is intentionally not a dashboard. The living Brain,
35
+ In 8.5.0 the first screen is intentionally not a dashboard. The living Brain,
36
36
  conversation composer, evidence-backed Brain Brief, and five-minute onboarding
37
37
  path appear together so the user immediately knows what matters, what to add,
38
38
  and why the answer is grounded in local memory.
package/docs/kg-schema.md CHANGED
@@ -1,6 +1,6 @@
1
1
  # Knowledge Graph Schema
2
2
 
3
- Current release: **8.3.0 - Orchestrated Brain Readiness**.
3
+ Current release: **8.5.0 Tool Registry Readiness & Config DI**.
4
4
 
5
5
  명세 출처: `lattice_ai_full_spec.pptx` 슬라이드 20·21·22
6
6
  구현: `kg_schema.py`
@@ -90,7 +90,7 @@ Edge {
90
90
 
91
91
  ## Current Knowledge Graph First 엔티티/관계
92
92
 
93
- 8.3.0 은 "모든 데이터 소스가 Knowledge Graph 로 수렴한다"는 원칙을 1급 스키마로
93
+ 8.4.0 은 "모든 데이터 소스가 Knowledge Graph 로 수렴한다"는 원칙을 1급 스키마로
94
94
  승격한다. 아래 타입은 **추가형(additive)**이다 — 기존 enum/legacy 매핑을 깨지 않고
95
95
  `from_legacy` 가 무손실로 정규화하며, 알 수 없는 타입은 여전히 `CONCEPT`/`MENTIONS` 로
96
96
  폴백한다. 스키마는 **확장 가능**하게 유지한다: 새 도메인 엔티티는 enum 멤버 1개 +
@@ -26,7 +26,7 @@ from .storage import (
26
26
  storage_from_env,
27
27
  )
28
28
 
29
- __version__ = "8.3.0"
29
+ __version__ = "8.5.0"
30
30
 
31
31
  __all__ = [
32
32
  "AgentRuntime",
@@ -21,7 +21,7 @@ from typing import Any, Callable, Dict, List, Optional
21
21
  from .contracts import multi_agent_contract
22
22
 
23
23
 
24
- MULTI_AGENT_VERSION = "8.3.0"
24
+ MULTI_AGENT_VERSION = "8.5.0"
25
25
 
26
26
  AGENT_ROLES = ("researcher", "planner", "executor", "reviewer", "release")
27
27
  CORE_PIPELINE = ("planner", "executor", "reviewer")
@@ -1,3 +1,3 @@
1
1
  """Lattice AI - modular server package."""
2
2
 
3
- __version__ = "8.3.0"
3
+ __version__ = "8.5.0"
@@ -123,6 +123,90 @@ def is_current_url_request(text: str) -> bool:
123
123
  def is_clear_command(text: str) -> bool:
124
124
  return (text or "").strip().lower() in {"/clear", "/clear_all"}
125
125
 
126
+ # Path segments intentionally exclude spaces: allowing spaces let the target
127
+ # match swallow preceding words (e.g. "create a text file report.txt" resolved
128
+ # to the whole phrase as the path). Chat file targets are single tokens.
129
+ _FILE_TARGET_RE = re.compile(
130
+ r"(?<![\w.-])(?:[~./\\]?[\w.@()+-]+[\\/])*[\w.@()+-]+"
131
+ r"\.(?:py|js|jsx|ts|tsx|md|markdown|txt|json|yaml|yml|toml|html|css|csv|xml|pdf|docx|xlsx|pptx|sh|sql)",
132
+ re.IGNORECASE,
133
+ )
134
+
135
+
136
+ def is_file_action_request(text: str) -> bool:
137
+ """Return True for chat requests that should execute file tools, not prose.
138
+
139
+ The Brain chat composer posts to ``/chat``. Without this gate, explicit
140
+ side-effect requests such as "create hello.md" are handled as plain model
141
+ generation, which commonly produces a code block instead of a real file.
142
+ Keep the gate narrow so normal Q&A and "how do I create a file?" stay in
143
+ ordinary chat.
144
+ """
145
+ raw = (text or "").strip()
146
+ if not raw:
147
+ return False
148
+ lower = raw.lower()
149
+
150
+ if any(phrase in lower for phrase in (
151
+ "how to ", "how do i ", "방법", "어떻게", "예시", "sample", "example",
152
+ )) and not any(phrase in lower for phrase in (
153
+ "actually create", "real file", "실제로", "파일로", "저장해", "만들어",
154
+ )):
155
+ return False
156
+
157
+ has_target = bool(_FILE_TARGET_RE.search(raw))
158
+ has_file_word = any(word in lower for word in (
159
+ "file", "파일", "문서", "artifact", "아티팩트", "save as", "저장",
160
+ ))
161
+ has_action = any(word in lower for word in (
162
+ "create", "make", "write", "save", "generate", "edit", "update",
163
+ "만들", "생성", "작성", "저장", "수정", "써줘", "만들어줘",
164
+ ))
165
+
166
+ if not has_action:
167
+ return False
168
+ if has_target and has_action:
169
+ return True
170
+ return has_file_word and has_action
171
+
172
+
173
+ def file_action_target(text: str) -> Optional[str]:
174
+ """Extract the first explicit workspace file target from a request."""
175
+ match = _FILE_TARGET_RE.search(text or "")
176
+ if not match:
177
+ return None
178
+ return match.group(0).strip().strip("`'\".,:;)]}")
179
+
180
+
181
+ def inline_file_action_content(text: str) -> Optional[str]:
182
+ """Extract short user-provided content for deterministic file writes."""
183
+ raw = (text or "").strip()
184
+ # Each pattern requires an explicit binder so ambiguous words like "text"
185
+ # ("create a text file report.txt") or a bare "with" ("report.md with a
186
+ # summary of X") do not get captured as literal file content.
187
+ patterns = [
188
+ r"(?:내용|본문)\s*(?:은|는|이에요|입니다)\s*(.+)$",
189
+ r"(?:내용|본문|content|body|text)\s*[:=]\s*(.+)$",
190
+ r"(?:content|body)\s+(?:is|as)\s+(.+)$",
191
+ r"(?:with the content|with content|containing)\s+(.+)$",
192
+ ]
193
+ for pattern in patterns:
194
+ match = re.search(pattern, raw, flags=re.IGNORECASE | re.DOTALL)
195
+ if match:
196
+ content = match.group(1).strip()
197
+ return content.strip("`'\"")
198
+ return None
199
+
200
+
201
+ def strip_generated_file_content(text: str) -> str:
202
+ """Remove common chat wrappers when a model is asked for file content only."""
203
+ content = (text or "").strip()
204
+ fenced = re.search(r"```(?:[\w.+-]+)?\s*(.*?)\s*```", content, flags=re.DOTALL)
205
+ if fenced:
206
+ content = fenced.group(1).strip()
207
+ return content
208
+
209
+
126
210
  def format_network_status(info: Dict) -> str:
127
211
  lines = [
128
212
  f"내부 IP: {info.get('local_ip') or '확인 안 됨'}",
@@ -243,11 +327,11 @@ def create_chat_router(context: AppContext) -> APIRouter:
243
327
  user_email=user_email,
244
328
  conversation_id=conversation_id,
245
329
  )
246
-
330
+
247
331
  def extract_screenshot_context(image_data: Optional[str]) -> str:
248
332
  if not image_data:
249
333
  return ""
250
-
334
+
251
335
  lines = ["[SCREENSHOT INGESTION]"]
252
336
  image_bytes = b""
253
337
  try:
@@ -258,18 +342,18 @@ def create_chat_router(context: AppContext) -> APIRouter:
258
342
  except Exception as e:
259
343
  lines.append(f"- image_decode_error: {e}")
260
344
  return "\n".join(lines)
261
-
345
+
262
346
  tesseract_path = shutil.which("tesseract")
263
347
  if not tesseract_path:
264
348
  lines.append("- ocr: unavailable; install `tesseract` to enable OCR text extraction.")
265
349
  return "\n".join(lines)
266
-
350
+
267
351
  temp_path = None
268
352
  try:
269
353
  with tempfile.NamedTemporaryFile(prefix="ltcai-screenshot-", suffix=".png", delete=False) as temp:
270
354
  temp.write(image_bytes)
271
355
  temp_path = temp.name
272
-
356
+
273
357
  ocr_text = ""
274
358
  for lang in ("kor+eng", "eng"):
275
359
  completed = subprocess.run(
@@ -283,7 +367,7 @@ def create_chat_router(context: AppContext) -> APIRouter:
283
367
  ocr_text = completed.stdout.strip()
284
368
  lines.append(f"- ocr_language: {lang}")
285
369
  break
286
-
370
+
287
371
  if ocr_text:
288
372
  lines.append("- ocr_text:")
289
373
  lines.append(ocr_text[:4000])
@@ -297,7 +381,7 @@ def create_chat_router(context: AppContext) -> APIRouter:
297
381
  Path(temp_path).unlink()
298
382
  except OSError:
299
383
  pass
300
-
384
+
301
385
  return "\n".join(lines)
302
386
 
303
387
  _AGENT_RUNTIME = context.chat_agent_runtime
@@ -329,7 +413,7 @@ def create_chat_router(context: AppContext) -> APIRouter:
329
413
  "conversation_id": req.conversation_id,
330
414
  "workspace_id": workspace_scope_from_request(request),
331
415
  }
332
-
416
+
333
417
  if is_network_status_request(req.message):
334
418
  history_message = f"{req.message}\n[Image attached]" if req.image_data else req.message
335
419
  save_to_history("user", history_message, **history_meta, **history_user)
@@ -347,7 +431,7 @@ def create_chat_router(context: AppContext) -> APIRouter:
347
431
  headers={"X-Model": "network_status"},
348
432
  )
349
433
  return JSONResponse(content={"response": answer})
350
-
434
+
351
435
  if is_clear_command(req.message):
352
436
  command = req.message.strip().lower()
353
437
  clear_scope = "all" if command == "/clear_all" else "conversation"
@@ -392,7 +476,7 @@ def create_chat_router(context: AppContext) -> APIRouter:
392
476
  headers={"X-Model": "history"},
393
477
  )
394
478
  return JSONResponse(content={"response": answer})
395
-
479
+
396
480
  if is_current_url_request(req.message) and req.client_url:
397
481
  answer = f"현재 페이지 URL: {req.client_url}"
398
482
  save_to_history("user", req.message, **history_meta, **history_user)
@@ -406,7 +490,74 @@ def create_chat_router(context: AppContext) -> APIRouter:
406
490
  headers={"X-Model": "client_url"},
407
491
  )
408
492
  return JSONResponse(content={"response": answer})
409
-
493
+
494
+ if is_file_action_request(req.message):
495
+ target_path = file_action_target(req.message)
496
+ if target_path:
497
+ content = inline_file_action_content(req.message)
498
+ if content is None and router.current_model_id:
499
+ if req.model and req.model != router.current_model_id:
500
+ if req.model not in router.loaded_model_ids:
501
+ raise HTTPException(status_code=404, detail=f"Model '{req.model}' not loaded.")
502
+ router.switch_model(req.model)
503
+ generation_context = (
504
+ "Create the exact content for the requested file. "
505
+ "Return only the file bytes as plain text. "
506
+ "Do not wrap the answer in Markdown fences, commentary, or explanations.\n\n"
507
+ f"Target path: {target_path}\n"
508
+ f"User request: {req.message}"
509
+ )
510
+ raw_content = await router.generate_as(
511
+ router.current_model_id,
512
+ message="Return only the requested file content.",
513
+ context=generation_context,
514
+ max_tokens=req.max_tokens,
515
+ temperature=req.temperature,
516
+ )
517
+ content = strip_generated_file_content(str(raw_content))
518
+ if content is None:
519
+ content = ""
520
+ try:
521
+ result = execute_tool("write_file", {"path": target_path, "content": content})
522
+ except ToolError as exc:
523
+ raise HTTPException(status_code=400, detail=str(exc)) from exc
524
+ answer = f"{result.get('path') or target_path} 파일을 만들었습니다."
525
+ created_files = [{
526
+ "path": result.get("path") or target_path,
527
+ "filename": Path(result.get("path") or target_path).name,
528
+ "bytes": result.get("bytes", 0),
529
+ "action": "write_file",
530
+ }]
531
+ notify_chat_message("user", req.message, req.source)
532
+ notify_chat_message("assistant", answer, req.source)
533
+ payload = {
534
+ "status": "ok",
535
+ "response": answer,
536
+ "workspace": str(AGENT_ROOT),
537
+ "steps": [{
538
+ "state": AgentState.EXECUTING.value,
539
+ "action": "write_file",
540
+ "args": {"path": target_path},
541
+ "result": result,
542
+ }],
543
+ "state_history": [AgentState.EXECUTING.value, AgentState.DONE.value],
544
+ "final_state": AgentState.DONE.value,
545
+ "created_files": created_files,
546
+ "routed_to_agent": True,
547
+ "action_route": "direct_write_file",
548
+ }
549
+ if req.stream:
550
+ async def _stream_file_result():
551
+ yield f"data: {json.dumps({'chunk': answer, 'model': router.current_model_id, 'agent': payload}, ensure_ascii=False)}\n\n"
552
+ yield f"data: {json.dumps({'chunk': '', 'model': router.current_model_id, 'agent': payload}, ensure_ascii=False)}\n\n"
553
+ yield "data: [DONE]\n\n"
554
+ return StreamingResponse(
555
+ _stream_file_result(),
556
+ media_type="text/event-stream",
557
+ headers={"X-Model": router.current_model_id or "tool", "X-Routed-To": "agent"},
558
+ )
559
+ return JSONResponse(content=payload)
560
+
410
561
  if not router.current_model_id:
411
562
  detail = "No model loaded. Call /models/load first."
412
563
  if CONFIG.is_public:
@@ -420,12 +571,40 @@ def create_chat_router(context: AppContext) -> APIRouter:
420
571
  "action": "load_model",
421
572
  },
422
573
  )
423
-
574
+
424
575
  if req.model and req.model != router.current_model_id:
425
576
  if req.model not in router.loaded_model_ids:
426
577
  raise HTTPException(status_code=404, detail=f"Model '{req.model}' not loaded.")
427
578
  router.switch_model(req.model)
428
-
579
+
580
+ if is_file_action_request(req.message):
581
+ agent_req = AgentRequest(
582
+ message=req.message,
583
+ conversation_id=req.conversation_id,
584
+ source=req.source or "web",
585
+ max_steps=25,
586
+ temperature=min(req.temperature, 0.2),
587
+ user_email=effective_email,
588
+ user_nickname=req.user_nickname,
589
+ workspace_id=workspace_scope_from_request(request),
590
+ )
591
+ result = await agent(agent_req, request)
592
+ answer = str(result.get("response") or "파일 작업을 처리했습니다.")
593
+ notify_chat_message("user", req.message, req.source)
594
+ notify_chat_message("assistant", answer, req.source)
595
+ result["routed_to_agent"] = True
596
+ if req.stream:
597
+ async def _stream_agent_result():
598
+ yield f"data: {json.dumps({'chunk': answer, 'model': router.current_model_id, 'agent': result}, ensure_ascii=False)}\n\n"
599
+ yield f"data: {json.dumps({'chunk': '', 'model': router.current_model_id, 'agent': result}, ensure_ascii=False)}\n\n"
600
+ yield "data: [DONE]\n\n"
601
+ return StreamingResponse(
602
+ _stream_agent_result(),
603
+ media_type="text/event-stream",
604
+ headers={"X-Model": router.current_model_id, "X-Routed-To": "agent"},
605
+ )
606
+ return JSONResponse(content=result)
607
+
429
608
  lang = detect_language(req.message)
430
609
  context = f"[LANGUAGE: {_LANG_HINT[lang]}]\n" + (req.context or "")
431
610
  # v4 Context System: one budgeted, provenance-carrying assembly
@@ -462,12 +641,12 @@ def create_chat_router(context: AppContext) -> APIRouter:
462
641
  print("📝 Document generation context retrieved from knowledge graph.")
463
642
  except Exception as e:
464
643
  logging.warning("Knowledge graph reinforcement skipped: %s", e)
465
-
644
+
466
645
  if req.image_data:
467
646
  screenshot_context = extract_screenshot_context(req.image_data)
468
647
  if screenshot_context:
469
648
  context += f"\n\n{screenshot_context}"
470
-
649
+
471
650
  if CONFIG.auto_read_chat_paths:
472
651
  _file_path_re = re.compile(r'(?:^|[\s\'\"(])((~|/[\w.])[^\s\'")\]]*)', re.MULTILINE)
473
652
  requested_paths = [_m.group(1).strip() for _m in _file_path_re.finditer(req.message or "")]
@@ -487,7 +666,7 @@ def create_chat_router(context: AppContext) -> APIRouter:
487
666
  "Attach the file, upload it, or use an approved local-file tool flow."
488
667
  ),
489
668
  )
490
-
669
+
491
670
  trace_seed = CHAT_SERVICE.build_graph_trace(
492
671
  req.message,
493
672
  KNOWLEDGE_GRAPH if (ENABLE_GRAPH and KNOWLEDGE_GRAPH) else None,
@@ -497,11 +676,11 @@ def create_chat_router(context: AppContext) -> APIRouter:
497
676
  # Persisted with the answer trace: 'why is this in my context?'
498
677
  # is answerable from the stored record (UI surface lands in T9b).
499
678
  trace_seed["context_assembly"] = context_trace
500
-
679
+
501
680
  history_message = f"{req.message}\n[Image attached]" if req.image_data else req.message
502
681
  save_to_history("user", history_message, **history_meta, **history_user)
503
682
  notify_chat_message("user", req.message, req.source)
504
-
683
+
505
684
  if is_doc_gen and ENABLE_GRAPH and KNOWLEDGE_GRAPH:
506
685
  conv_key = req.conversation_id or "default"
507
686
  session = _doc_gen_sessions.get(conv_key)
@@ -512,7 +691,7 @@ def create_chat_router(context: AppContext) -> APIRouter:
512
691
  system_prompt = session.get_system_prompt(graph_md)
513
692
  sources = (doc_gen_context_result or {}).get("sources", [])
514
693
  footnote = format_sources_footnote(sources)
515
-
694
+
516
695
  if req.stream:
517
696
  async def _stream_doc_gen():
518
697
  collected = []
@@ -563,7 +742,7 @@ def create_chat_router(context: AppContext) -> APIRouter:
563
742
  )
564
743
  notify_chat_message("assistant", str(result), req.source)
565
744
  return JSONResponse(content={"response": str(result), "trace_id": trace_record["id"], "trace": trace_record})
566
-
745
+
567
746
  if req.stream:
568
747
  recent_context = recent_chat_context(user_email=effective_email, conversation_id=req.conversation_id)
569
748
  stream_context = context
@@ -593,9 +772,9 @@ def create_chat_router(context: AppContext) -> APIRouter:
593
772
  else:
594
773
  history_context = recent_chat_context(user_email=effective_email, conversation_id=req.conversation_id)
595
774
  full_context = f"{history_context}\n{context}" if context else history_context
596
-
775
+
597
776
  result = await router.generate(req.message, full_context, req.max_tokens, req.temperature, req.image_data)
598
-
777
+
599
778
  save_to_history("assistant", str(result), **history_meta, **history_user)
600
779
  trace_record = CHAT_SERVICE.record_trace(
601
780
  question=req.message,
@@ -605,22 +784,22 @@ def create_chat_router(context: AppContext) -> APIRouter:
605
784
  trace=trace_seed,
606
785
  )
607
786
  notify_chat_message("assistant", str(result), req.source)
608
-
787
+
609
788
  return JSONResponse(content={"response": str(result), "trace_id": trace_record["id"], "trace": trace_record})
610
-
789
+
611
790
 
612
791
  @api_router.get("/history")
613
792
  async def fetch_history(request: Request):
614
793
  """웹 화면에서 이전 대화를 불러올 수 있도록 히스토리를 반환합니다."""
615
794
  require_user(request)
616
795
  return get_history()
617
-
796
+
618
797
  @api_router.get("/history/conversations")
619
798
  async def fetch_history_conversations(request: Request):
620
799
  """저장된 히스토리를 대화 단위로 묶어 반환합니다."""
621
800
  require_user(request)
622
801
  return group_history_conversations()
623
-
802
+
624
803
  @api_router.get("/history/conversations/{conversation_id:path}")
625
804
  async def fetch_history_conversation(conversation_id: str, request: Request):
626
805
  """선택한 대화의 메시지를 반환합니다."""
@@ -629,7 +808,7 @@ def create_chat_router(context: AppContext) -> APIRouter:
629
808
  if not messages:
630
809
  raise HTTPException(status_code=404, detail="대화를 찾을 수 없습니다.")
631
810
  return {"id": conversation_id, "messages": messages}
632
-
811
+
633
812
 
634
813
  @api_router.delete("/history/conversations/{conversation_id:path}")
635
814
  async def delete_history_conversation(conversation_id: str, request: Request):
@@ -645,7 +824,7 @@ def create_chat_router(context: AppContext) -> APIRouter:
645
824
  kept=result.get("kept", 0),
646
825
  )
647
826
  return result
648
-
827
+
649
828
 
650
829
  @api_router.delete("/history")
651
830
  async def delete_history(request: Request, keep_last: int = 0):
@@ -659,7 +838,7 @@ def create_chat_router(context: AppContext) -> APIRouter:
659
838
  kept=result.get("kept", 0),
660
839
  )
661
840
  return result
662
-
841
+
663
842
  @api_router.get("/history/search")
664
843
  async def search_history(q: str, request: Request):
665
844
  """키워드로 채팅 히스토리를 검색합니다."""
@@ -676,7 +855,7 @@ def create_chat_router(context: AppContext) -> APIRouter:
676
855
  grouped[cid] = {"conversation_id": cid, "title": conversation_title(item), "messages": []}
677
856
  grouped[cid]["messages"].append(item)
678
857
  return {"results": list(grouped.values())[-30:], "query": q}
679
-
858
+
680
859
  async def _stream_chat(
681
860
  req: ChatRequest,
682
861
  context: str = "",
@@ -696,7 +875,7 @@ def create_chat_router(context: AppContext) -> APIRouter:
696
875
  clean_chunk = chunk.split("text='")[1].split("', token=")[0].replace('\\n', '\n').replace('\\\\n', '\n')
697
876
  except Exception:
698
877
  pass
699
-
878
+
700
879
  full_response += str(clean_chunk)
701
880
  yield f"data: {json.dumps({'chunk': clean_chunk, 'model': router.current_model_id}, ensure_ascii=False)}\n\n"
702
881
  history_user = get_history_user(effective_email or req.user_email, req.user_nickname)
@@ -724,7 +903,7 @@ def create_chat_router(context: AppContext) -> APIRouter:
724
903
  schema_path = skill_dir / "schema.json"
725
904
  if not schema_path.exists():
726
905
  raise HTTPException(404, detail=f"Skill '{req.skill}' not found or missing schema.json")
727
-
906
+
728
907
  schema = json.loads(schema_path.read_text(encoding="utf-8"))
729
908
  eval_cases = schema.get("evals", [])
730
909
  if req.case_id:
@@ -732,7 +911,7 @@ def create_chat_router(context: AppContext) -> APIRouter:
732
911
  if not eval_cases:
733
912
  return {"skill": req.skill, "total": 0, "passed": 0, "failed": 0, "results": [],
734
913
  "message": "No eval cases defined in schema.json"}
735
-
914
+
736
915
  action_name = schema.get("action", req.skill)
737
916
  results = []
738
917
  for case in eval_cases:
@@ -754,19 +933,19 @@ def create_chat_router(context: AppContext) -> APIRouter:
754
933
  results.append({"id": case_id, "description": case.get("description", ""),
755
934
  "passed": False, "error": str(exc),
756
935
  "pass_criteria": case.get("pass_criteria", "")})
757
-
936
+
758
937
  n_passed = sum(1 for r in results if r.get("passed") is True)
759
938
  return {
760
939
  "skill": req.skill, "action": action_name,
761
940
  "total": len(results), "passed": n_passed, "failed": len(results) - n_passed,
762
941
  "results": results,
763
942
  }
764
-
943
+
765
944
 
766
945
  @api_router.post("/agent")
767
946
  async def agent(req: AgentRequest, request: Request):
768
947
  """Natural-language local agent.
769
-
948
+
770
949
  State machine:
771
950
  IDLE → PLANNING → WAITING_APPROVAL → EXECUTING → VERIFYING
772
951
  ↓ ↓
@@ -779,22 +958,22 @@ def create_chat_router(context: AppContext) -> APIRouter:
779
958
  req.workspace_id = req.workspace_id or workspace_scope_from_request(request)
780
959
  if not router.current_model_id:
781
960
  raise HTTPException(status_code=400, detail="No model loaded. Call /models/load first.")
782
-
961
+
783
962
  ensure_agent_root()
784
963
  lang = detect_language(req.message)
785
964
  lang_hint = _LANG_HINT[lang]
786
965
  max_steps = max(1, min(req.max_steps, 50))
787
966
  max_retry = 3
788
-
967
+
789
968
  ctx = AgentRunContext()
790
969
  ctx.executing_model = req.executing_model
791
970
  ctx.reviewing_model = req.reviewing_model
792
-
971
+
793
972
  # PLANNING phase
794
973
  ctx.state = AgentState.PLANNING
795
974
  ctx.state_history.append(ctx.state.value)
796
975
  await _AGENT_RUNTIME.plan(ctx, req, lang_hint, current_user, model_id=req.planning_model)
797
-
976
+
798
977
  # Human-in-the-loop: pause after planning, return plan to UI
799
978
  if req.human_in_loop:
800
979
  context_id = secrets.token_urlsafe(16)
@@ -810,11 +989,11 @@ def create_chat_router(context: AppContext) -> APIRouter:
810
989
  "executing_model": req.executing_model or router.current_model_id,
811
990
  "reviewing_model": req.reviewing_model or router.current_model_id,
812
991
  }
813
-
992
+
814
993
  # Auto-approve and run to completion (default behaviour)
815
994
  _AGENT_RUNTIME.approve(ctx, current_user)
816
995
  return await _agent_finish(ctx, req, lang_hint, current_user, max_steps, max_retry)
817
-
996
+
818
997
 
819
998
  async def _agent_finish(
820
999
  ctx: AgentRunContext, req: AgentRequest, lang_hint: str,
@@ -823,7 +1002,7 @@ def create_chat_router(context: AppContext) -> APIRouter:
823
1002
  """HTTP glue: drive the runtime to a terminal state, persist, shape the response."""
824
1003
  await _AGENT_RUNTIME.run_to_completion(ctx, req, lang_hint, current_user, max_steps, max_retry)
825
1004
  asyncio.create_task(_AGENT_RUNTIME.memory_update(ctx, req, current_user))
826
-
1005
+
827
1006
  message = ctx.final_message or "작업을 완료했습니다."
828
1007
  save_to_history("user", req.message, source=req.source or "web", conversation_id=req.conversation_id, workspace_id=req.workspace_id)
829
1008
  save_to_history("assistant", message, source=req.source or "web", conversation_id=req.conversation_id, workspace_id=req.workspace_id)
@@ -850,33 +1029,33 @@ def create_chat_router(context: AppContext) -> APIRouter:
850
1029
  "final_state": ctx.state.value,
851
1030
  "created_files": created_files,
852
1031
  }
853
-
1032
+
854
1033
 
855
1034
  @api_router.post("/agent/resume")
856
1035
  async def agent_resume(req: AgentResumeRequest, request: Request):
857
1036
  """Resume a paused agent after human approval of the plan."""
858
1037
  current_user = require_user(request)
859
-
1038
+
860
1039
  with _pending_agents_lock:
861
1040
  entry = _pending_agents.pop(req.context_id, None)
862
1041
  if not entry:
863
1042
  raise HTTPException(status_code=404, detail="Agent context not found or expired. Start a new request.")
864
-
1043
+
865
1044
  ctx, orig_req, lang_hint, _orig_user = entry
866
-
1045
+
867
1046
  if not req.approved:
868
1047
  return {"status": "cancelled", "response": "사용자가 계획을 취소했습니다."}
869
-
1048
+
870
1049
  if req.modified_plan:
871
1050
  ctx.plan = req.modified_plan
872
1051
  ctx.transcript[-1].update(ctx.plan) # keep transcript in sync
873
-
1052
+
874
1053
  # Apply model overrides from resume request (takes priority over original request)
875
1054
  ctx.executing_model = req.executing_model or ctx.executing_model
876
1055
  ctx.reviewing_model = req.reviewing_model or ctx.reviewing_model
877
-
1056
+
878
1057
  _AGENT_RUNTIME.approve(ctx, current_user)
879
-
1058
+
880
1059
  max_steps = max(1, min(orig_req.max_steps, 50))
881
1060
  max_retry = 3
882
1061
  return await _agent_finish(ctx, orig_req, lang_hint, current_user, max_steps, max_retry)
@@ -1192,6 +1192,7 @@ def _build(config: "Optional[Config]" = None) -> Dict[str, Any]:
1192
1192
  agent_registry=AGENT_REGISTRY,
1193
1193
  data_dir=DATA_DIR,
1194
1194
  append_audit_event=append_audit_event,
1195
+ tz_name=getattr(CONFIG, "timezone", None),
1195
1196
  )
1196
1197
  _llm_generate_sync = _platform_automation_runtime["_llm_generate_sync"]
1197
1198
  PLATFORM = _platform_automation_runtime["PLATFORM"]
@@ -1,7 +1,7 @@
1
1
  """Managed legacy compatibility surface for legacy import shims.
2
2
 
3
3
  Compatibility modules are intentionally still present for old scripts, VS Code
4
- extension paths, historical integrations, and pre-graph-package imports. 8.3.0
4
+ extension paths, historical integrations, and pre-graph-package imports. 8.4.0
5
5
  stops treating them as vague technical debt: every tracked shim has an owner,
6
6
  migration target, removal phase, and replacement import that can be surfaced in
7
7
  docs and tests.
@@ -14,7 +14,7 @@ from pathlib import Path
14
14
  from typing import Any, Dict, List
15
15
 
16
16
 
17
- LEGACY_COMPATIBILITY_VERSION = "8.3.0"
17
+ LEGACY_COMPATIBILITY_VERSION = "8.5.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 = "8.3.0"
14
+ MARKETPLACE_VERSION = "8.5.0"
15
15
  TEMPLATE_KINDS = ("plugin", "workflow", "agent")
16
16
 
17
17
 
@@ -180,6 +180,7 @@ TOOL_GOVERNANCE: Dict[str, ToolPolicy] = {
180
180
  risk="exec", destructive=False, shell=False, network=True,
181
181
  auto_approve=False, sandbox="system", rollback="none",
182
182
  ),
183
+ "vision_analyze": _r(sandbox="system"),
183
184
  }
184
185
 
185
186
  TOOL_GOVERNANCE_DEFAULT = ToolPolicy(
@@ -221,6 +222,7 @@ MCP_TOOL_DESCRIPTIONS: Dict[str, str] = {
221
222
  "computer_status": "Check if Mac desktop control (pyautogui) is available.",
222
223
  "chrome_status": "Report Chrome desktop bridge availability.",
223
224
  "computer_use_status": "Report Mac desktop-control bridge availability.",
225
+ "vision_analyze": "Analyze a base64-encoded image (e.g. screenshot) using the active multimodal VLM. Returns structured description for agent consumption.",
224
226
  "knowledge_save": "Save a note into the local knowledge garden.",
225
227
  "knowledge_search": "Search the local knowledge garden.",
226
228
  "knowledge_tree": "List local knowledge garden markdown files.",
@@ -49,7 +49,7 @@ __all__ = [
49
49
  "remove_skill_directory",
50
50
  ]
51
51
 
52
- WORKSPACE_OS_VERSION = "8.3.0"
52
+ WORKSPACE_OS_VERSION = "8.5.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
@@ -6,7 +6,7 @@ Router registration remains in ``app_factory`` so route order stays unchanged.
6
6
 
7
7
  from __future__ import annotations
8
8
 
9
- from typing import Any, Callable, Dict
9
+ from typing import Any, Callable, Dict, Optional
10
10
 
11
11
 
12
12
  def build_automation_runtime(
@@ -17,6 +17,7 @@ def build_automation_runtime(
17
17
  workspace_graph: Callable[..., Any],
18
18
  append_audit_event: Callable[..., Any],
19
19
  hooks: Any,
20
+ tz_name: Optional[str] = None,
20
21
  ) -> Dict[str, Any]:
21
22
  """Construct automation services and attach the run executor boundary."""
22
23
 
@@ -37,6 +38,7 @@ def build_automation_runtime(
37
38
  ),
38
39
  data_dir=data_dir,
39
40
  review_sink=review_queue,
41
+ tz_name=tz_name,
40
42
  )
41
43
  agent_runtime = AgentRuntime(
42
44
  store=store,
@@ -7,7 +7,7 @@ surface without changing router order or the legacy exported runtime names.
7
7
 
8
8
  from __future__ import annotations
9
9
 
10
- from typing import Any, Callable, Dict
10
+ from typing import Any, Callable, Dict, Optional
11
11
 
12
12
 
13
13
  def build_platform_automation_runtime(
@@ -24,6 +24,7 @@ def build_platform_automation_runtime(
24
24
  agent_registry: Any,
25
25
  data_dir: Any,
26
26
  append_audit_event: Callable[..., Any],
27
+ tz_name: Optional[str] = None,
27
28
  ) -> Dict[str, Any]:
28
29
  """Build platform services, automation services, and hook bindings.
29
30
 
@@ -73,6 +74,7 @@ def build_platform_automation_runtime(
73
74
  workspace_graph=workspace_graph,
74
75
  append_audit_event=append_audit_event,
75
76
  hooks=hooks,
77
+ tz_name=tz_name,
76
78
  )
77
79
 
78
80
  return {
@@ -1,6 +1,6 @@
1
1
  """Machine-checkable architecture readiness gates for release work.
2
2
 
3
- 8.3.0 keeps the major architecture priorities under an explicit release
3
+ 8.5.0 keeps the major architecture priorities under an explicit release
4
4
  contract while product maturity work reduces visible beta seams. AgentRuntime, ToolRegistry,
5
5
  central Config, decomposed server runtime, and Knowledge Graph stabilization
6
6
  must remain discoverable, ordered, and backed by tests before the release can be
@@ -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 = "8.3.0"
20
+ ARCHITECTURE_VERSION_TARGET = "8.5.0"
21
21
 
22
22
  PREFERRED_REFACTORING_ORDER = [
23
23
  "agent-runtime",
@@ -102,6 +102,12 @@ MODEL_ENGINE_ALIASES = {
102
102
  "lmstudio": "ggml-org/gemma-4-12B-it-GGUF",
103
103
  "llamacpp": "ggml-org/gemma-4-12B-it-GGUF",
104
104
  },
105
+ "gemma-4-26b-it-4bit": {
106
+ "local_mlx": "mlx-community/gemma-4-26b-a4b-it-4bit",
107
+ },
108
+ "mlx-community/gemma-4-26b-a4b-it-4bit": {
109
+ "local_mlx": "mlx-community/gemma-4-26b-a4b-it-4bit",
110
+ },
105
111
  "gemma-4-31b-it-4bit": {
106
112
  "local_mlx": "mlx-community/gemma-4-31b-it-4bit",
107
113
  "ollama": "hf.co/ggml-org/gemma-4-31B-it-GGUF:Q4_K_M",
@@ -31,6 +31,11 @@ from latticeai.models.router import (
31
31
  parse_model_ref,
32
32
  )
33
33
  from latticeai.core.model_resolution import ModelResolution as _ModelResolution
34
+ from latticeai.core.model_compat import (
35
+ friendly_model_runtime_error as _friendly_model_runtime_error,
36
+ model_runtime_compatibility as _model_runtime_compatibility,
37
+ SMOKE_PROMPT as _SMOKE_PROMPT,
38
+ )
34
39
  from .model_engines import (
35
40
  ensure_lmstudio_server as _ensure_lmstudio_server,
36
41
  ensure_ollama_server as _ensure_ollama_server,
@@ -50,6 +55,14 @@ from .model_engines import (
50
55
  LOCAL_SERVER_PROCESSES as _LOCAL_SERVER_PROCESSES,
51
56
  )
52
57
 
58
+ # ``model_loading._get_model_runtime_deps`` imports these private names from
59
+ # this module to preserve the historical model_runtime wiring surface.
60
+ _MODEL_LOADING_COMPAT_EXPORTS = (
61
+ _friendly_model_runtime_error,
62
+ _model_runtime_compatibility,
63
+ _SMOKE_PROMPT,
64
+ )
65
+
53
66
 
54
67
  def _missing_current_user(_request: Request) -> Optional[str]:
55
68
  return None
@@ -1,7 +1,7 @@
1
- """Machine-checkable *product* readiness gates for the 8.3 line.
1
+ """Machine-checkable *product* readiness gates for the 8.4 line.
2
2
 
3
3
  Where ``architecture_readiness`` proves the internal structure is sound, this
4
- module answers the product question the 8.3 release exists to settle: *does the
4
+ module answers the product question the 8.4 release exists to settle: *does the
5
5
  app now feel like a finished product rather than only a strong framework?*
6
6
  It does so honestly: every gate is backed by
7
7
  evidence that is probed on disk, so a gate only reports ``complete`` when its
@@ -18,7 +18,7 @@ from typing import Any, Dict, List
18
18
 
19
19
  from latticeai.services.architecture_readiness import architecture_readiness
20
20
 
21
- PRODUCT_VERSION_TARGET = "8.3.0"
21
+ PRODUCT_VERSION_TARGET = "8.5.0"
22
22
 
23
23
 
24
24
  @dataclass(frozen=True)
@@ -52,6 +52,15 @@ PRODUCT_GATES: List[ProductGate] = [
52
52
  "scripts/brain_quality_eval.py",
53
53
  ],
54
54
  ),
55
+ ProductGate(
56
+ id="action-aware-chat",
57
+ title="File action requests create artifacts instead of code-only answers",
58
+ evidence=[
59
+ "latticeai/api/chat.py::is_file_action_request",
60
+ "latticeai/api/chat.py::direct_write_file",
61
+ "tests/unit/test_chat_telegram_decoupling.py::test_chat_file_creation_intent_writes_real_file",
62
+ ],
63
+ ),
55
64
  ProductGate(
56
65
  id="local-first-trust",
57
66
  title="Local-first privacy is stated and bounded",
@@ -67,10 +76,10 @@ PRODUCT_GATES: List[ProductGate] = [
67
76
  evidence=[
68
77
  "package.json::release:artifacts",
69
78
  "package.json::release:validate",
70
- "README.md::dist/ltcai-8.3.0-py3-none-any.whl",
71
- "README.md::dist/ltcai-8.3.0.tar.gz",
72
- "README.md::dist/ltcai-8.3.0.vsix",
73
- "README.md::ltcai-8.3.0.tgz",
79
+ "README.md::dist/ltcai-8.5.0-py3-none-any.whl",
80
+ "README.md::dist/ltcai-8.5.0.tar.gz",
81
+ "README.md::dist/ltcai-8.5.0.vsix",
82
+ "README.md::ltcai-8.5.0.tgz",
74
83
  "scripts/validate_release_artifacts.py",
75
84
  "scripts/release_smoke.py",
76
85
  "Dockerfile",
@@ -86,12 +95,12 @@ PRODUCT_GATES: List[ProductGate] = [
86
95
  title="Release story is documented and honest",
87
96
  evidence=[
88
97
  "README.md",
89
- "README.md::The current release is **8.3.0",
90
- "SECURITY.md::8.3.x (latest)",
91
- "vscode-extension/README.md::**8.3.0",
92
- "docs/CHANGELOG.md::## [8.3.0]",
98
+ "README.md::The current release is **8.5.0",
99
+ "SECURITY.md::8.5.x (latest)",
100
+ "vscode-extension/README.md::**8.5.0",
101
+ "docs/CHANGELOG.md::## [8.5.0]",
93
102
  "FEATURE_STATUS.md",
94
- "RELEASE_NOTES_v8.3.0.md",
103
+ "RELEASE_NOTES_v8.4.0.md",
95
104
  "latticeai/core/agent.py::SingleAgentRuntime",
96
105
  "latticeai/core/agent.py::AgentRuntime = SingleAgentRuntime",
97
106
  "lattice_brain/runtime/contracts.py::runtime-boundary/v1",
@@ -106,7 +115,7 @@ PRODUCT_GATES: List[ProductGate] = [
106
115
  id="ecosystem-path",
107
116
  title="Community and plugin growth path is explicit",
108
117
  evidence=[
109
- "docs/COMMUNITY_AND_PLUGINS.md::LatticeAI 8.3.0",
118
+ "docs/COMMUNITY_AND_PLUGINS.md::8.5.0",
110
119
  "docs/PLUGIN_SDK.md",
111
120
  "plugins/README.md",
112
121
  "plugins/hello-world/plugin.json",
@@ -49,6 +49,7 @@ class TriggerService:
49
49
  clock: Callable[[], float] = time.time,
50
50
  tick_seconds: float = DEFAULT_TICK_SECONDS,
51
51
  review_sink: Optional[Any] = None,
52
+ tz_name: Optional[str] = None,
52
53
  ) -> None:
53
54
  self._store = store
54
55
  self._run_workflow = run_workflow
@@ -63,7 +64,8 @@ class TriggerService:
63
64
  self._lock = threading.Lock()
64
65
  # LATTICE_TZ: wall-clock / display 용. interval 계산은 여전히 unix seconds (duration 기반, drift 방지).
65
66
  # describe()와 이벤트에 tz 정보 노출. calendar "daily at HH:MM" semantics 는 추후 cron 확장 시 사용.
66
- self._tz_name = os.environ.get("LATTICE_TZ") or "UTC"
67
+ # Prefer explicit tz_name (from Config) for DI; fall back to env for legacy/compat.
68
+ self._tz_name = (tz_name or os.environ.get("LATTICE_TZ") or "UTC").strip() or "UTC"
67
69
  self._tz = None
68
70
  if ZoneInfo is not None:
69
71
  try:
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "ltcai",
3
- "version": "8.3.0",
3
+ "version": "8.5.0",
4
4
  "description": "Lattice AI — local-first Digital Brain that keeps your knowledge durable across any AI model.",
5
5
  "homepage": "https://github.com/TaeSooPark-PTS/LatticeAI#readme",
6
6
  "repository": {
@@ -1584,7 +1584,7 @@ dependencies = [
1584
1584
 
1585
1585
  [[package]]
1586
1586
  name = "lattice-ai-desktop"
1587
- version = "8.3.0"
1587
+ version = "8.4.0"
1588
1588
  dependencies = [
1589
1589
  "plist",
1590
1590
  "serde",
@@ -1,6 +1,6 @@
1
1
  [package]
2
2
  name = "lattice-ai-desktop"
3
- version = "8.3.0"
3
+ version = "8.5.0"
4
4
  description = "Lattice AI Digital Brain desktop shell"
5
5
  authors = ["TaeSoo Park"]
6
6
  edition = "2021"
@@ -1,7 +1,7 @@
1
1
  {
2
2
  "$schema": "https://schema.tauri.app/config/2",
3
3
  "productName": "Lattice AI",
4
- "version": "8.3.0",
4
+ "version": "8.5.0",
5
5
  "identifier": "ai.lattice.desktop",
6
6
  "build": {
7
7
  "beforeDevCommand": "npm run frontend:dev",
@@ -1,5 +1,5 @@
1
1
  {
2
- "version": "8.3.0",
2
+ "version": "8.5.0",
3
3
  "generated_at": "vite",
4
4
  "entrypoints": {
5
5
  "app": "/static/app/assets/index-CoiuIFFP.js"
package/static/sw.js CHANGED
@@ -1,7 +1,7 @@
1
1
  // Lattice Service Worker — PWA install + offline shell for the /app SPA.
2
2
  // Strategy: precache the Vite app bundle from its asset manifest,
3
3
  // cache-first for static assets, network-only for everything dynamic.
4
- const CACHE = "lattice-v830";
4
+ const CACHE = "lattice-v850";
5
5
  const MANIFEST_URL = "/static/app/asset-manifest.json";
6
6
 
7
7
  // Non-manifest assets the shell needs offline.