ltcai 10.8.0 → 10.9.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 (84) hide show
  1. package/README.md +54 -52
  2. package/docs/CHANGELOG.md +37 -0
  3. package/docs/COMMUNITY_AND_PLUGINS.md +1 -1
  4. package/docs/DEVELOPMENT.md +1 -1
  5. package/docs/ONBOARDING.md +1 -1
  6. package/docs/OPERATIONS.md +1 -1
  7. package/docs/TRUST_MODEL.md +1 -1
  8. package/docs/WHY_LATTICE.md +1 -1
  9. package/docs/kg-schema.md +1 -1
  10. package/lattice_brain/__init__.py +1 -1
  11. package/lattice_brain/runtime/multi_agent.py +1 -1
  12. package/latticeai/__init__.py +1 -1
  13. package/latticeai/api/chat.py +9 -6
  14. package/latticeai/api/chat_agent_http.py +2 -1
  15. package/latticeai/api/chat_history.py +4 -2
  16. package/latticeai/api/chat_intents.py +14 -14
  17. package/latticeai/api/knowledge_graph.py +5 -4
  18. package/latticeai/api/local_files.py +25 -15
  19. package/latticeai/api/mcp.py +15 -11
  20. package/latticeai/api/memory.py +2 -1
  21. package/latticeai/api/models.py +39 -24
  22. package/latticeai/api/network_boundary.py +9 -4
  23. package/latticeai/api/portability.py +27 -19
  24. package/latticeai/api/project_sessions.py +7 -5
  25. package/latticeai/api/review_queue.py +12 -7
  26. package/latticeai/api/setup.py +10 -11
  27. package/latticeai/api/static_routes.py +62 -46
  28. package/latticeai/api/tools.py +7 -6
  29. package/latticeai/core/legacy_compatibility.py +1 -1
  30. package/latticeai/core/marketplace.py +1 -1
  31. package/latticeai/core/mcp_registry.py +20 -4
  32. package/latticeai/core/messages.py +222 -0
  33. package/latticeai/core/workspace_os_constants.py +1 -1
  34. package/latticeai/integrations/telegram_bot.py +20 -18
  35. package/latticeai/services/architecture_readiness.py +1 -1
  36. package/latticeai/services/model_loading.py +63 -27
  37. package/latticeai/services/product_readiness.py +1 -1
  38. package/package.json +3 -2
  39. package/scripts/check_current_release_docs.mjs +1 -1
  40. package/scripts/check_server_i18n.mjs +96 -0
  41. package/scripts/release_screen_claims.json +17 -8
  42. package/src-tauri/Cargo.lock +1 -1
  43. package/src-tauri/Cargo.toml +1 -1
  44. package/src-tauri/tauri.conf.json +1 -1
  45. package/static/app/asset-manifest.json +37 -37
  46. package/static/app/assets/{Act-CbQNsFgc.js → Act-CS9IeqUX.js} +1 -1
  47. package/static/app/assets/{AdminConsole-Bs8SR8fE.js → AdminConsole-3UkIEWGA.js} +1 -1
  48. package/static/app/assets/{Brain-BZIPsZlH.js → Brain-B22EmNqS.js} +1 -1
  49. package/static/app/assets/{BrainHome-BaH5MDce.js → BrainHome-CMDqgJF4.js} +2 -2
  50. package/static/app/assets/{BrainSignals-aDt-nSsW.js → BrainSignals-BeE8RJo3.js} +1 -1
  51. package/static/app/assets/{Capture-DDv940Bv.js → Capture-ZX9bQh68.js} +1 -1
  52. package/static/app/assets/{CommandPalette-Dq_MmPBl.js → CommandPalette-86m4FCcN.js} +1 -1
  53. package/static/app/assets/{Library-BkShbqmN.js → Library-Bhz5LUca.js} +1 -1
  54. package/static/app/assets/{LivingBrain-CXb8094H.js → LivingBrain-BSa0wpFG.js} +1 -1
  55. package/static/app/assets/{ProductFlow-BaiqFWLO.js → ProductFlow-IP4Q-5aQ.js} +1 -1
  56. package/static/app/assets/{ReviewCard-Be7E7X18.js → ReviewCard-BepjSDpN.js} +1 -1
  57. package/static/app/assets/{System-DCstbTxC.js → System-Bwx1h_jT.js} +1 -1
  58. package/static/app/assets/arrow-left-ig7AZU8B.js +1 -0
  59. package/static/app/assets/{bot-BR0eoeXY.js → bot-DQj0-LkM.js} +1 -1
  60. package/static/app/assets/{brain-BmWa1wea.js → brain-D8OEwmVj.js} +1 -1
  61. package/static/app/assets/{button-U1nU7xX4.js → button-CmTknyAP.js} +1 -1
  62. package/static/app/assets/{circle-pause-DG5KHjDL.js → circle-pause-yTCWRziJ.js} +1 -1
  63. package/static/app/assets/{circle-play-Chhcuq90.js → circle-play-Ccrva84R.js} +1 -1
  64. package/static/app/assets/{cpu-pkOSlT9h.js → cpu-CbJqWTlS.js} +1 -1
  65. package/static/app/assets/{download-CSRwz923.js → download-CkSzbzU-.js} +1 -1
  66. package/static/app/assets/{folder-open-DiMJ7W_7.js → folder-open-CKyjQ4PU.js} +1 -1
  67. package/static/app/assets/{hard-drive-iyLPAPdp.js → hard-drive-DAzk9um0.js} +1 -1
  68. package/static/app/assets/{index-BZRvGOM2.css → index-BfD-jhA9.css} +1 -1
  69. package/static/app/assets/{index-_ldE9TAX.js → index-CxOcwsHV.js} +3 -3
  70. package/static/app/assets/{input-e_r2hFAY.js → input-DcMETmZ7.js} +1 -1
  71. package/static/app/assets/{permissionCopy-9uOHktz3.js → permissionCopy-BVf13_25.js} +1 -1
  72. package/static/app/assets/{primitives-CnEAOmg2.js → primitives-CQV9Q2YM.js} +1 -1
  73. package/static/app/assets/search-CXGASMMH.js +1 -0
  74. package/static/app/assets/{share-2-DLDJJKmI.js → share-2-COWCHNZm.js} +1 -1
  75. package/static/app/assets/{shield-alert-BTtE-2EZ.js → shield-alert-BDrvilyK.js} +1 -1
  76. package/static/app/assets/{textarea-Bbzkcgqe.js → textarea-rUmsc8cP.js} +1 -1
  77. package/static/app/assets/{useFocusTrap-DJzQd-Za.js → useFocusTrap-Bi5UY_8v.js} +1 -1
  78. package/static/app/assets/{useQuery-6R_tqHRe.js → useQuery-C-AicB-3.js} +1 -1
  79. package/static/app/assets/{utils-XDsjcrhS.js → utils-BMwWg78e.js} +1 -1
  80. package/static/app/assets/{workspace-CwrG8LHq.js → workspace-Y93tls8P.js} +1 -1
  81. package/static/app/index.html +4 -4
  82. package/static/sw.js +1 -1
  83. package/static/app/assets/arrow-left-DC9Oquhs.js +0 -1
  84. package/static/app/assets/search-veep4n6f.js +0 -1
package/README.md CHANGED
@@ -11,7 +11,7 @@
11
11
  [![CI Status](https://github.com/TaeSooPark-PTS/LatticeAI/actions/workflows/ci.yml/badge.svg?branch=main)](https://github.com/TaeSooPark-PTS/LatticeAI/actions/workflows/ci.yml)
12
12
  [![License](https://img.shields.io/badge/license-MIT-green)](LICENSE)
13
13
 
14
- ![v10.8.0 Living Brain walkthrough](output/release/v10.8.0/gifs/v10.8.0-living-brain-walkthrough.gif)
14
+ ![v10.9.0 Living Brain walkthrough](output/release/v10.9.0/gifs/v10.9.0-living-brain-walkthrough.gif)
15
15
 
16
16
  Chat, files, folders, notes, and web pages all flow into one durable knowledge
17
17
  graph on your computer. Any model — local MLX or cloud — can speak with that
@@ -24,10 +24,10 @@ memory. Nothing leaves your machine without explicit consent.
24
24
 
25
25
  | | |
26
26
  | --- | --- |
27
- | **Chat with a Brain that remembers** — every conversation grows durable, source-linked memory ![Brain Chat](output/release/v10.8.0/screenshots/04-brain-chat-home.png) | **See how knowledge connects** — a real relationship graph, not a file list ![Memory Graph](output/release/v10.8.0/screenshots/05-memory-graph.png) |
28
- | **Capture anything** — files, whole folders, notes, screenshots, web pages ![Capture](output/release/v10.8.0/screenshots/06-capture.png) | **Automate with review** — agent changes become proposals you approve first ![Review Center](output/release/v10.8.0/screenshots/12-review-center.png) |
29
- | **Pick a model in one click** — recommended local models for your hardware ![Recommended Models](output/release/v10.8.0/screenshots/02-recommended-models.png) | **Stay in control** — audit, roles, retention in a separate admin surface ![Admin Console](output/release/v10.8.0/screenshots/10-admin-console.png) |
30
- | **Watch a file become memory** — three named steps, not a pipeline diagram ![Material to memory](output/release/v10.8.0/screenshots/11-knowledge-journey.png) | **Say how much it may do alone** — one dial in plain words; dangerous actions stay blocked either way ![Settings](output/release/v10.8.0/screenshots/08-system.png) |
27
+ | **Chat with a Brain that remembers** — every conversation grows durable, source-linked memory ![Brain Chat](output/release/v10.9.0/screenshots/04-brain-chat-home.png) | **See how knowledge connects** — a real relationship graph, not a file list ![Memory Graph](output/release/v10.9.0/screenshots/05-memory-graph.png) |
28
+ | **Capture anything** — files, whole folders, notes, screenshots, web pages ![Capture](output/release/v10.9.0/screenshots/06-capture.png) | **Automate with review** — agent changes become proposals you approve first ![Review Center](output/release/v10.9.0/screenshots/12-review-center.png) |
29
+ | **Pick a model in one click** — recommended local models for your hardware ![Recommended Models](output/release/v10.9.0/screenshots/02-recommended-models.png) | **Stay in control** — audit, roles, retention in a separate admin surface ![Admin Console](output/release/v10.9.0/screenshots/10-admin-console.png) |
30
+ | **Watch a file become memory** — three named steps, not a pipeline diagram ![Material to memory](output/release/v10.9.0/screenshots/11-knowledge-journey.png) | **Say how much it may do alone** — one dial in plain words; dangerous actions stay blocked either way ![Settings](output/release/v10.9.0/screenshots/08-system.png) |
31
31
 
32
32
  ## Why Lattice AI
33
33
 
@@ -58,63 +58,64 @@ First-run flow — wake the Brain, pick the owner, load a recommended model:
58
58
 
59
59
  | | | |
60
60
  | --- | --- | --- |
61
- | ![Login](output/release/v10.8.0/screenshots/01-login.png) | ![Model install](output/release/v10.8.0/screenshots/03-install-load-progress.png) | ![Model library](output/release/v10.8.0/screenshots/07-model-library.png) |
61
+ | ![Login](output/release/v10.9.0/screenshots/01-login.png) | ![Model install](output/release/v10.9.0/screenshots/03-install-load-progress.png) | ![Model library](output/release/v10.9.0/screenshots/07-model-library.png) |
62
62
 
63
63
  Screenshot index and capture notes:
64
- [output/release/v10.8.0/SCREENSHOT_INDEX.md](output/release/v10.8.0/SCREENSHOT_INDEX.md)
64
+ [output/release/v10.9.0/SCREENSHOT_INDEX.md](output/release/v10.9.0/SCREENSHOT_INDEX.md)
65
65
 
66
66
  ## Current Release
67
67
 
68
- The current release is **10.8.0 — Within Reach**:
69
-
70
- 10.7.0 rearranged all twelve screens. 10.8.0 is about the things that were
71
- already there but out of reach: a button below the fold, a message in a
72
- language you did not choose, a file a small model nearly produced.
73
-
74
- - **The first three screens fit on the screen.** Onboarding rendered a 390px
75
- decorative Brain above *every* step, so on a 1440×900 display the login
76
- form, the recommended model and the install progress all began below the
77
- fold — the first thing the product asked a new person to do was scroll. The
78
- organism is a hero on the welcome step and a 104px mark after it; the
79
- welcome screen itself went from 1175px of content to 770px, so its primary
80
- button is visible without scrolling.
81
- - **One language per screen, including the ones the server writes.** API
82
- messages were literals at the raise site, written in whichever language the
83
- endpoint's author was thinking in — Korean from `auth.py`, English from
84
- `browser.py`. `latticeai/core/messages.py` is a catalog with both, resolved
85
- from `X-Lattice-Language` (falling back to `Accept-Language`); the browser
86
- extension, which showed English and Korean in the same popup, has one too.
87
- `tests/unit/test_server_messages.py` fails on a message that exists in only
88
- one language, and on a router that reverts to a hardcoded literal.
89
- - **A small model's near-miss is no longer thrown away.** File generation
90
- picked the *longest* rejected reply to repair, so a 900-character apology
91
- beat a truncated-but-real HTML document; it now scores candidates by how
92
- close they are to being a file. A byte-identical repeated reply buys one
93
- extra attempt with a prompt that names the repetition. Prose file types
94
- (`.md`, `.txt`) had no validation at all and would save "Sure! Here is the
95
- document:" as the document.
96
- - **Re-indexing costs what changed.** The incremental vector rebuild
97
- materialised every node and chunk in memory and asked one `SELECT` per item
98
- whether it had changed. It streams now, with a single prefetched hash map —
99
- and a settled index performs zero embeddings, asserted rather than assumed.
100
- - **More ground under the frontend.** Statement coverage 47.35% → 52.26%,
101
- 424 → 504 tests, including the app shell (`App.tsx`, previously 0%), the
102
- admin console (0%), the folder picker (2.7%) and the API client.
103
- - **Evidence bound to this build.** `output/release/v10.8.0/` holds the twelve
68
+ The current release is **10.9.0 — Never Blocks**:
69
+
70
+ Lattice AI runs one event loop on your computer. Several things you could ask
71
+ it to do were being done *on* that loop, so while they ran the server could not
72
+ answer anything else. Downloading a model has a fifteen-minute timeout; that is
73
+ how long the whole product could be frozen by one click.
74
+
75
+ - **Nothing long runs on the event loop.** Model pulls, engine installs, MCP
76
+ package installs and the CPU/RAM probe behind the System screen all moved to
77
+ a worker thread. `/local/sysinfo` is the one most people met without knowing:
78
+ it is read by the System screen *and* by first-run analysis, so its ~2s
79
+ freeze landed exactly while an answer was streaming. Two things keep it
80
+ fixed — ruff's `ASYNC` blocking-call rules, and
81
+ `tests/unit/test_event_loop_not_blocked.py`, which runs a ticker coroutine
82
+ during the handler and asserts the loop kept getting control.
83
+ - **A focus ring you can see.** 10.8.0 added `border-color` to the transition
84
+ on the capture pills, so keyboard focus faded in over 150ms and at the
85
+ instant it landed the pill still looked idle. Focus rings may not ease.
86
+ - **The Brain stops thinking when it has answered.** A retrieval pulse parked a
87
+ 900ms timer that put the organism back into "thinking"; if the answer
88
+ finished inside that window the timer still fired, and the Brain sat visibly
89
+ working on a question it had already answered.
90
+ - **One language, further in.** Fourteen more routers moved to the message
91
+ catalog — chat, memory, graph, files, portability, review, models, tools,
92
+ MCP, setup. `models.py`, `mcp.py` and `tools.py` had been answering some
93
+ errors in Korean and others in English on the same screen. Two gates keep
94
+ them migrated, and a third fails if the two gates disagree.
95
+ - **The welcome screen fits.** It measured 770px against a 747px viewport, so
96
+ the line explaining what you are agreeing to sat under the fold. Nothing was
97
+ removed; the whitespace gave back the 25px.
98
+ - **The streaming answer path has tests.** `useBrainChat` was 12% covered
99
+ because asserting anything about a stream needs frames arriving over time.
100
+ `frontend/src/test/fakeChatStream.ts` is that harness; eleven cases now cover
101
+ the answer building up, the stop button, refusals, grounding badges and live
102
+ step frames.
103
+ - **Evidence bound to this build.** `output/release/v10.9.0/` holds the twelve
104
104
  capture screens, walkthrough gif/webm, and the asset-manifest / mock-server
105
- fingerprints that lint re-checks before merge.
106
- - **Exact artifact names only.** Publish paths list `dist/ltcai-10.8.0-*` and
107
- `ltcai-10.8.0.tgz` — never `dist/*`.
105
+ fingerprints that lint re-checks before merge. 10.9.0 claims no screen
106
+ redesign, and `scripts/release_screen_claims.json` says so explicitly.
107
+ - **Exact artifact names only.** Publish paths list `dist/ltcai-10.9.0-*` and
108
+ `ltcai-10.9.0.tgz` — never `dist/*`.
108
109
 
109
110
  Release notes: [RELEASE.md](RELEASE.md) · Full history: [docs/CHANGELOG.md](docs/CHANGELOG.md)
110
111
 
111
- Expected artifacts for 10.8.0 release must use exact filenames:
112
+ Expected artifacts for 10.9.0 release must use exact filenames:
112
113
 
113
- - `dist/ltcai-10.8.0-py3-none-any.whl`
114
- - `dist/ltcai-10.8.0.tar.gz`
115
- - `ltcai-10.8.0.tgz`
116
- - `dist/ltcai-10.8.0.vsix`
117
- - `src-tauri/target/release/bundle/dmg/Lattice AI_10.8.0_aarch64.dmg`
114
+ - `dist/ltcai-10.9.0-py3-none-any.whl`
115
+ - `dist/ltcai-10.9.0.tar.gz`
116
+ - `ltcai-10.9.0.tgz`
117
+ - `dist/ltcai-10.9.0.vsix`
118
+ - `src-tauri/target/release/bundle/dmg/Lattice AI_10.9.0_aarch64.dmg`
118
119
 
119
120
  Do not use wildcard artifact uploads. Package registry publishing remains owner-run.
120
121
 
@@ -149,6 +150,7 @@ See [ARCHITECTURE.md](ARCHITECTURE.md) for details and
149
150
 
150
151
  | Version | Theme |
151
152
  | --- | --- |
153
+ | 10.9.0 | Never Blocks |
152
154
  | 10.8.0 | Within Reach |
153
155
  | 10.7.0 | Plain Surface |
154
156
  | 10.6.4 | Loud Limits |
package/docs/CHANGELOG.md CHANGED
@@ -4,6 +4,43 @@ The top entry is either the current unreleased main-branch work or the current
4
4
  release line. Older entries are historical and may describe behavior as it
5
5
  existed at that release.
6
6
 
7
+ ## [10.9.0] - 2026-08-05
8
+
9
+ ### Fixed
10
+ - 모델 내려받기(`ollama pull`, Hugging Face), 엔진 설치, MCP `pip`/`npm` 설치,
11
+ `/local/sysinfo` 호스트 측정이 `async def` 안에서 그대로 실행되어 실행 중
12
+ 서버 전체가 다른 요청을 처리하지 못하던 문제 — 전부 `asyncio.to_thread`로
13
+ 이관. 최악의 경우 15분(다운로드 타임아웃)간 채팅·`/health`·UI가 모두 멈췄습니다.
14
+ - Telegram 브리지의 사진/문서 업로드가 파일을 이벤트 루프에서 읽던 문제.
15
+ - 캡처 필의 키보드 포커스 테두리가 150ms에 걸쳐 서서히 나타나, 포커스가 닿는
16
+ 순간에는 여전히 idle 색이던 문제(`home-simple.css` transition에서
17
+ `border-color` 제거).
18
+ - 답변이 끝난 뒤에도 Brain 유기체가 "생각 중"에 머물던 문제 — 회상 펄스가
19
+ 건 900ms 타이머가 스트림 종료 시 취소되지 않았습니다.
20
+ - 같은 틱에 두 번 전송될 때 `stopStreaming`이 중단 핸들을 잃던 문제.
21
+ - 환영 화면이 747px 뷰포트에서 770px이라 마지막 안내 문장이 접힌 부분 아래에
22
+ 있던 문제(`@media (max-height: 820px)` 단계 추가).
23
+
24
+ ### Added
25
+ - `tests/unit/test_event_loop_not_blocked.py` — 핸들러 실행 중 티커 코루틴을
26
+ 돌려 이벤트 루프가 실제로 제어권을 받았는지 측정하는 회귀 테스트.
27
+ - `frontend/src/test/fakeChatStream.ts` — 프레임이 시간에 걸쳐 도착하는 가짜
28
+ SSE 하네스. `useBrainChat.test.tsx` 11개 시나리오.
29
+ - `scripts/check_server_i18n.mjs` — 이관 완료 선언된 라우터에서 문자열 리터럴
30
+ detail을 금지하는 lint 게이트(`npm run lint`에 편입).
31
+ - `tests/visual/v3.spec.js` — 환영 화면 fold 테스트, 캡처 필 포커스 테두리.
32
+ - ruff `ASYNC210/220/221/222/230/251` 규칙.
33
+
34
+ ### Changed
35
+ - 서버 메시지 카탈로그를 14개 라우터로 확대(chat, chat_history, chat_intents,
36
+ memory, knowledge_graph, local_files, portability, review_queue,
37
+ project_sessions, network_boundary, models, tools, mcp, setup) — 총 17개.
38
+ `MIGRATED_ROUTERS`와 lint 게이트 목록이 어긋나면 테스트가 실패합니다.
39
+ - `latticeai/services/model_loading.py`: 엔진 준비/다운로드 블록을
40
+ `_prepare_model_sources()`로 추출(워커 스레드에서 실행 가능한 동기 함수).
41
+ - `latticeai/api/static_routes.py`: 호스트 측정을 `_probe_host_capacity()`로
42
+ 추출.
43
+
7
44
  ## [10.8.0] - 2026-08-04
8
45
 
9
46
  ### Fixed
@@ -1,6 +1,6 @@
1
1
  # Community And Plugins
2
2
 
3
- Current release: **10.8.0 — Within Reach**.
3
+ Current release: **10.9.0 — Never Blocks**.
4
4
 
5
5
  LatticeAI defines the path from a strong local-first framework (8.4.0
6
6
  action-aware baseline, 8.5.0 registry+DI hardening, 8.6.0 capture/navigation
@@ -3,7 +3,7 @@
3
3
  > **Status: canonical** — current contributor guidance, kept in sync with the
4
4
  > current release.
5
5
 
6
- Current release: **10.8.0 — Within Reach**.
6
+ Current release: **10.9.0 — Never Blocks**.
7
7
 
8
8
  This document is for contributors working on the local-first Digital Brain
9
9
  codebase. Product positioning and quick start stay in `README.md`; release
@@ -1,6 +1,6 @@
1
1
  # Lattice AI Onboarding
2
2
 
3
- Current release: **10.8.0 — Within Reach**.
3
+ Current release: **10.9.0 — Never Blocks**.
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
@@ -1,4 +1,4 @@
1
- # Lattice AI — Operations Guide (v10.8.0)
1
+ # Lattice AI — Operations Guide (v10.9.0)
2
2
 
3
3
  > **Status: canonical** — kept in sync with the current release. Storage layout
4
4
  > below reflects the SQLite live Brain store and workspace scoping, not the
@@ -1,6 +1,6 @@
1
1
  # Lattice AI Trust Model
2
2
 
3
- Current release: **10.8.0 — Within Reach**.
3
+ Current release: **10.9.0 — Never Blocks**.
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: **10.8.0 — Within Reach**.
3
+ Current release: **10.9.0 — Never Blocks**.
4
4
 
5
5
  **Lattice AI is a local-first Digital Brain that keeps your knowledge durable
6
6
  across any AI model.**
package/docs/kg-schema.md CHANGED
@@ -1,6 +1,6 @@
1
1
  # Knowledge Graph Schema
2
2
 
3
- Current release: **10.8.0 — Within Reach**.
3
+ Current release: **10.9.0 — Never Blocks**.
4
4
 
5
5
  명세 출처: `lattice_ai_full_spec.pptx` 슬라이드 20·21·22
6
6
  구현: `lattice_brain/graph/schema.py`
@@ -26,7 +26,7 @@ from .storage import (
26
26
  storage_from_env,
27
27
  )
28
28
 
29
- __version__ = "10.8.0"
29
+ __version__ = "10.9.0"
30
30
 
31
31
  __all__ = [
32
32
  "AgentRuntime",
@@ -49,7 +49,7 @@ from typing import Any, Callable, Dict, List, Optional
49
49
  from ..utils import now_iso as _now
50
50
  from .contracts import multi_agent_contract
51
51
 
52
- MULTI_AGENT_VERSION = "10.8.0"
52
+ MULTI_AGENT_VERSION = "10.9.0"
53
53
 
54
54
  AGENT_ROLES = ("researcher", "planner", "executor", "reviewer", "release")
55
55
  CORE_PIPELINE = ("planner", "executor", "reviewer")
@@ -1,3 +1,3 @@
1
1
  """Lattice AI - modular server package."""
2
2
 
3
- __version__ = "10.8.0"
3
+ __version__ = "10.9.0"
@@ -51,6 +51,7 @@ from latticeai.api.chat_hybrid import (
51
51
  )
52
52
  from latticeai.api.chat_intents import ChatIntentController
53
53
  from latticeai.api.chat_stream import stream_chat
54
+ from latticeai.core.messages import DEFAULT_LANGUAGE, resolve_language, translate
54
55
  from latticeai.core.project_sessions import ProjectSessionStore
55
56
  from latticeai.core.run_store import AgentRunStore
56
57
  from latticeai.services.app_context import AppContext
@@ -122,12 +123,13 @@ def create_chat_router(context: AppContext) -> APIRouter:
122
123
  def authenticated_identity(
123
124
  current_user: str,
124
125
  claimed_email: Optional[str],
126
+ language: str = DEFAULT_LANGUAGE,
125
127
  ) -> Optional[str]:
126
128
  if current_user and claimed_email:
127
129
  if current_user.strip().lower() != claimed_email.strip().lower():
128
130
  raise HTTPException(
129
131
  status_code=403,
130
- detail="user_email must match the authenticated user.",
132
+ detail=translate("common.user_mismatch", language),
131
133
  )
132
134
  return current_user or claimed_email or None
133
135
 
@@ -239,12 +241,12 @@ def create_chat_router(context: AppContext) -> APIRouter:
239
241
  notify=notify_chat_message,
240
242
  )
241
243
 
242
- def request_model(model_id: Optional[str]) -> Optional[str]:
244
+ def request_model(model_id: Optional[str], language: str = DEFAULT_LANGUAGE) -> Optional[str]:
243
245
  selected = model_id or model_router.current_model_id
244
246
  if model_id and model_id not in model_router.loaded_model_ids:
245
247
  raise HTTPException(
246
248
  status_code=404,
247
- detail=f"Model '{model_id}' not loaded.",
249
+ detail=translate("chat.model_not_loaded", language, model=model_id),
248
250
  )
249
251
  return selected
250
252
 
@@ -258,7 +260,7 @@ def create_chat_router(context: AppContext) -> APIRouter:
258
260
  len(req.image_data) if req.image_data else 0,
259
261
  len(req.message or ""),
260
262
  )
261
- effective_email = authenticated_identity(current_user, req.user_email)
263
+ effective_email = authenticated_identity(current_user, req.user_email, resolve_language(request))
262
264
  workspace_id = write_workspace(
263
265
  workspace_scope_from_request(request),
264
266
  current_user,
@@ -293,7 +295,7 @@ def create_chat_router(context: AppContext) -> APIRouter:
293
295
  history_user=history_user,
294
296
  )
295
297
 
296
- selected_model_id = request_model(req.model)
298
+ selected_model_id = request_model(req.model, resolve_language(request))
297
299
  file_intent = is_file_action_request(req.message)
298
300
  if file_intent and context.funnel_metrics is not None:
299
301
  context.funnel_metrics.increment("file_requests")
@@ -303,11 +305,12 @@ def create_chat_router(context: AppContext) -> APIRouter:
303
305
  model_id=selected_model_id,
304
306
  effective_email=effective_email,
305
307
  workspace_id=workspace_id,
308
+ language=resolve_language(request),
306
309
  )
307
310
  if direct_response is not None:
308
311
  return direct_response
309
312
  if not selected_model_id:
310
- return intent_controller.no_model_response()
313
+ return intent_controller.no_model_response(resolve_language(request))
311
314
  if file_intent:
312
315
  return await intent_controller.route_file_to_agent(
313
316
  req,
@@ -29,6 +29,7 @@ from latticeai.api.chat_helpers import (
29
29
  )
30
30
  from latticeai.api.chat_stream import agent_live_stream
31
31
  from latticeai.core.agent import AgentRunContext, AgentState, normalize_plan
32
+ from latticeai.core.messages import resolve_language
32
33
  from latticeai.core.quiet import quiet
33
34
  from latticeai.core.run_explain import explain_run
34
35
 
@@ -266,7 +267,7 @@ class AgentHTTPController:
266
267
  """
267
268
  current_user = self.require_user(request)
268
269
  self.enforce_rate_limit(current_user, "agent")
269
- effective_email = self.authenticated_identity(current_user, req.user_email)
270
+ effective_email = self.authenticated_identity(current_user, req.user_email, resolve_language(request))
270
271
  header_workspace = workspace_scope_from_request(request)
271
272
  if req.workspace_id and header_workspace and req.workspace_id != header_workspace:
272
273
  raise HTTPException(
@@ -9,7 +9,9 @@ from __future__ import annotations
9
9
  from dataclasses import dataclass
10
10
  from typing import Any, Callable, Dict
11
11
 
12
- from fastapi import APIRouter, HTTPException, Request
12
+ from fastapi import APIRouter, Request
13
+
14
+ from latticeai.core.messages import http_error, resolve_language
13
15
 
14
16
 
15
17
  @dataclass(frozen=True)
@@ -48,7 +50,7 @@ def register_history_routes(router: APIRouter, deps: HistoryRouteDependencies) -
48
50
  **deps.scope_for_user(current_user),
49
51
  )
50
52
  if not messages:
51
- raise HTTPException(status_code=404, detail="대화를 찾을 수 없습니다.")
53
+ raise http_error(404, "chat.conversation_not_found", resolve_language(request))
52
54
  return {"id": conversation_id, "messages": messages}
53
55
 
54
56
  @router.delete("/history/conversations/{conversation_id:path}")
@@ -31,9 +31,10 @@ from latticeai.core.file_generation import (
31
31
  repair_bundle_references,
32
32
  validate_project_bundle,
33
33
  )
34
+ from latticeai.core.messages import DEFAULT_LANGUAGE, translate
34
35
 
35
36
 
36
- def next_available_path(root: Path, target: str) -> str:
37
+ def next_available_path(root: Path, target: str, language: str = DEFAULT_LANGUAGE) -> str:
37
38
  """Return ``target`` unchanged, or a ``name_2.ext``-style variant when the
38
39
  file already exists in the workspace.
39
40
 
@@ -54,7 +55,7 @@ def next_available_path(root: Path, target: str) -> str:
54
55
  return str(Path(target).with_name(variant.name))
55
56
  raise HTTPException(
56
57
  status_code=409,
57
- detail=f"'{target}' 이름의 파일이 너무 많습니다. 다른 이름을 지정해 주세요.",
58
+ detail=translate("chat.file_name_collision", language, name=target),
58
59
  )
59
60
 
60
61
 
@@ -118,14 +119,10 @@ class ChatIntentController:
118
119
  except Exception as exc: # noqa: BLE001 — metrics must never break chat
119
120
  logging.warning("funnel metrics increment failed: %s", exc)
120
121
 
121
- def no_model_response(self) -> JSONResponse:
122
- detail = "No model loaded. Call /models/load first."
122
+ def no_model_response(self, language: str = DEFAULT_LANGUAGE) -> JSONResponse:
123
+ detail = translate("chat.no_model_loaded", language)
123
124
  if self.config.is_public:
124
- detail = (
125
- "No public model loaded. Set OPENAI_API_KEY and "
126
- f"LATTICEAI_PUBLIC_MODEL={self.public_model}, or call /models/load "
127
- "with an OpenAI-compatible model."
128
- )
125
+ detail = translate("models.public_model_missing", language, model=self.public_model)
129
126
  return JSONResponse(
130
127
  status_code=400,
131
128
  content={
@@ -249,6 +246,7 @@ class ChatIntentController:
249
246
  model_id: Optional[str],
250
247
  effective_email: Optional[str] = None,
251
248
  workspace_id: Optional[str] = None,
249
+ language: str = DEFAULT_LANGUAGE,
252
250
  ):
253
251
  # An explicit path ("report.txt") wins; otherwise infer one from an
254
252
  # explicit type keyword ("html 파일 만들어줘") so weak models never
@@ -266,19 +264,20 @@ class ChatIntentController:
266
264
  model_id=model_id,
267
265
  effective_email=effective_email,
268
266
  workspace_id=workspace_id,
267
+ language=language,
269
268
  )
270
269
  target_path = explicit_target or infer_file_target(req.message)
271
270
  if not target_path:
272
271
  return None
273
272
  # Never silently overwrite: an existing target gets a _2/_3 suffix so
274
273
  # regeneration is safe and repeatable without the proposal flow.
275
- deduped_path = next_available_path(self.agent_root, target_path)
274
+ deduped_path = next_available_path(self.agent_root, target_path, language)
276
275
  renamed = deduped_path != target_path
277
276
  target_path = deduped_path
278
277
  content = inline_file_action_content(req.message)
279
278
  generation_meta: Optional[Dict[str, Any]] = None
280
279
  if content is None and not model_id:
281
- return self.no_model_response()
280
+ return self.no_model_response(language)
282
281
  if content is None and model_id:
283
282
  # Model-agnostic pipeline: strict extension-aware prompt →
284
283
  # extraction → validation → one corrective retry → deterministic
@@ -302,7 +301,7 @@ class ChatIntentController:
302
301
  if content is None:
303
302
  raise HTTPException(
304
303
  status_code=400,
305
- detail="File content could not be generated.",
304
+ detail=translate("chat.file_generation_failed", language),
306
305
  )
307
306
  try:
308
307
  result = self.execute_tool(
@@ -395,6 +394,7 @@ class ChatIntentController:
395
394
  model_id: Optional[str],
396
395
  effective_email: Optional[str] = None,
397
396
  workspace_id: Optional[str] = None,
397
+ language: str = DEFAULT_LANGUAGE,
398
398
  ):
399
399
  """Artifact Loop: manifest → per-file generate/validate → bundle write.
400
400
 
@@ -405,8 +405,8 @@ class ChatIntentController:
405
405
  standard ``artifacts[]`` contract, and downloadable as a zip.
406
406
  """
407
407
  if not model_id:
408
- return self.no_model_response()
409
- project_dir = next_available_path(self.agent_root, manifest["name"])
408
+ return self.no_model_response(language)
409
+ project_dir = next_available_path(self.agent_root, manifest["name"], language)
410
410
  bundle_files = [str(f["path"]) for f in manifest["files"]]
411
411
 
412
412
  async def _generate(context: str) -> str:
@@ -19,6 +19,7 @@ from latticeai.api.workspace_scope import (
19
19
  resolve_workspace_scope,
20
20
  workspace_scope_from_request,
21
21
  )
22
+ from latticeai.core.messages import http_error, resolve_language
22
23
  from latticeai.core.quiet import quiet
23
24
 
24
25
 
@@ -456,9 +457,9 @@ def create_knowledge_graph_router(
456
457
  async def knowledge_graph_neighbors(node_id: str, request: Request):
457
458
  kg, allowed = _scoped(request)
458
459
  if not node_id:
459
- raise HTTPException(status_code=400, detail="node_id required")
460
+ raise http_error(400, "graph.node_id_required", resolve_language(request))
460
461
  if allowed is not None and not _filter_scoped(kg, [{"id": node_id}], allowed):
461
- raise HTTPException(status_code=404, detail="node not found")
462
+ raise http_error(404, "graph.node_not_found", resolve_language(request))
462
463
  payload = kg.neighbors(node_id)
463
464
  if allowed is not None:
464
465
  neighbors = _filter_scoped(kg, payload.get("neighbors", []), allowed)
@@ -476,13 +477,13 @@ def create_knowledge_graph_router(
476
477
  current_user = require_user(request)
477
478
  if current_user and req.user_email:
478
479
  if current_user.strip().lower() != req.user_email.strip().lower():
479
- raise HTTPException(status_code=403, detail="user_email must match the authenticated user.")
480
+ raise http_error(403, "common.user_mismatch", resolve_language(request))
480
481
  effective_user = current_user or req.user_email or None
481
482
  kg = graph()
482
483
  workspace_id = _write_workspace(request, current_user)
483
484
  event_type = (req.type or "").strip().lower()
484
485
  if event_type not in {"message", "ai_response", "note"}:
485
- raise HTTPException(status_code=400, detail="지원하는 type: message, ai_response, note")
486
+ raise http_error(400, "graph.unsupported_type", resolve_language(request))
486
487
  role = req.role or ("assistant" if event_type == "ai_response" else "user")
487
488
  if ingestion_pipeline is not None:
488
489
  source_type = "chat_message" if event_type in {"message", "ai_response"} else "note"