ltcai 6.0.0 → 6.1.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 (34) hide show
  1. package/README.md +38 -35
  2. package/docs/CHANGELOG.md +36 -0
  3. package/docs/V4_1_FRONTEND_MIGRATION_REPORT.md +1 -1
  4. package/docs/V4_6_1_RELEASE_REFRESH_REPORT.md +1 -1
  5. package/docs/V4_7_0_ADMIN_SEPARATION_REPORT.md +1 -1
  6. package/docs/V4_7_1_ADMIN_OPERATIONS_REPORT.md +1 -1
  7. package/frontend/src/App.tsx +5 -0
  8. package/frontend/src/components/ProductFlow.tsx +7 -0
  9. package/frontend/src/features/review/ReviewCard.tsx +15 -10
  10. package/frontend/src/i18n.ts +10 -0
  11. package/frontend/src/styles.css +20 -0
  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/chat.py +52 -33
  16. package/latticeai/app_factory.py +29 -2
  17. package/latticeai/cli/__init__.py +1 -0
  18. package/latticeai/cli/runtime.py +37 -0
  19. package/latticeai/core/marketplace.py +1 -1
  20. package/latticeai/core/workspace_os.py +1 -1
  21. package/latticeai/services/app_context.py +1 -0
  22. package/latticeai/services/tool_dispatch.py +82 -25
  23. package/ltcai_cli.py +5 -31
  24. package/package.json +1 -1
  25. package/src-tauri/Cargo.lock +1 -1
  26. package/src-tauri/Cargo.toml +1 -1
  27. package/src-tauri/tauri.conf.json +1 -1
  28. package/static/app/asset-manifest.json +5 -5
  29. package/static/app/assets/{index-xRn29gI8.css → index-B744yblP.css} +1 -1
  30. package/static/app/assets/{index-D2zafMYb.js → index-DYaUKNfl.js} +2 -2
  31. package/static/app/assets/index-DYaUKNfl.js.map +1 -0
  32. package/static/app/index.html +2 -2
  33. package/telegram_bot.py +3 -9
  34. package/static/app/assets/index-D2zafMYb.js.map +0 -1
package/README.md CHANGED
@@ -69,42 +69,42 @@ You need Lattice AI when:
69
69
  Choose the owner of the Brain. The profile is not a SaaS account by default; it
70
70
  is the local identity for the knowledge you keep.
71
71
 
72
- ![Login](output/release/v6.0.0/screenshots/01-login.png)
72
+ ![Login](output/release/v6.1.0/screenshots/01-login.png)
73
73
 
74
74
  ### 2. Environment Analysis
75
75
 
76
76
  See what kind of local AI experience this computer can support before choosing a
77
77
  model.
78
78
 
79
- ![Environment Analysis](output/release/v6.0.0/screenshots/02-environment-analysis.png)
79
+ ![Environment Analysis](output/release/v6.1.0/screenshots/02-environment-analysis.png)
80
80
 
81
81
  ### 3. Recommended Models
82
82
 
83
83
  Start with a short list: safest recommendation, faster model, stronger model.
84
84
  Advanced details stay available without overwhelming first-time users.
85
85
 
86
- ![Recommended Models](output/release/v6.0.0/screenshots/03-recommended-models.png)
86
+ ![Recommended Models](output/release/v6.1.0/screenshots/03-recommended-models.png)
87
87
 
88
88
  ### 4. Install And Load
89
89
 
90
90
  Download and load only after consent. Lattice explains model size, local
91
91
  execution, and network use before work starts.
92
92
 
93
- ![Install and Load](output/release/v6.0.0/screenshots/04-install-load-progress.png)
93
+ ![Install and Load](output/release/v6.1.0/screenshots/04-install-load-progress.png)
94
94
 
95
95
  ### 5. Brain Chat
96
96
 
97
97
  Talk normally. Useful decisions and context become memory, then appear later as
98
98
  topics, relationships, and graph structure.
99
99
 
100
- ![Brain Chat Home](output/release/v6.0.0/screenshots/05-brain-chat-home.png)
100
+ ![Brain Chat Home](output/release/v6.1.0/screenshots/05-brain-chat-home.png)
101
101
 
102
102
  ### 6. Review Center
103
103
 
104
104
  Automation results are staged for review before they become durable decisions.
105
105
  Snooze, unsnooze, run now, approve, and dismiss actions stay explicit.
106
106
 
107
- ![Review Center](output/release/v6.0.0/screenshots/13-review-center.png)
107
+ ![Review Center](output/release/v6.1.0/screenshots/13-review-center.png)
108
108
 
109
109
  ## Brain Depths
110
110
 
@@ -120,10 +120,10 @@ The user travels inward from everyday memory to deeper structure:
120
120
 
121
121
  Walkthrough:
122
122
 
123
- ![v6.0.0 Living Brain walkthrough](output/release/v6.0.0/gifs/v6.0.0-living-brain-walkthrough.gif)
123
+ ![v6.1.0 Living Brain walkthrough](output/release/v6.1.0/gifs/v6.1.0-living-brain-walkthrough.gif)
124
124
 
125
125
  Screenshot index and capture notes:
126
- [output/release/v6.0.0/SCREENSHOT_INDEX.md](output/release/v6.0.0/SCREENSHOT_INDEX.md)
126
+ [output/release/v6.1.0/SCREENSHOT_INDEX.md](output/release/v6.1.0/SCREENSHOT_INDEX.md)
127
127
 
128
128
  ## Install
129
129
 
@@ -200,33 +200,36 @@ See [docs/DEVELOPMENT.md](docs/DEVELOPMENT.md) for developer workflow details.
200
200
 
201
201
  ## Current Release Preparation
202
202
 
203
- The current development target is **6.0.0 Product Reset / Review Center Completion**:
204
-
205
- - Review Center now includes Pending, Snoozed, and All filters so users can find
206
- deferred automation suggestions without waiting for expiry.
207
- - `/automation/reviews` adds explicit `unsnooze` support while preserving
208
- read-time snooze expiry and `run_now != approve` semantics.
209
- - Review Center frontend code is split into `frontend/src/features/review/`
210
- modules instead of living inside the Act page.
211
- - Review item frontend types are derived from regenerated OpenAPI component
212
- - schemas, and Review Center API calls use generated operation paths for list
213
- and state-transition actions.
214
- - `app_factory.py` is decomposed behind runtime seams for session, hooks, web
215
- shell, persistence, lifespan, automation, context/search, platform services,
216
- app context, and router registration while preserving the frozen 364-entry
217
- route/mount snapshot.
218
- - First-run copy now states the local-first trust boundary more directly:
219
- knowledge stays on this computer by default, downloads/external transfer start
220
- only after user action, and models remain replaceable.
221
- - All package/runtime/static/OpenAPI versions are synchronized to 6.0.0.
222
-
223
- Expected artifacts for 6.0.0 release must use exact filenames:
224
-
225
- - `dist/ltcai-6.0.0-py3-none-any.whl`
226
- - `dist/ltcai-6.0.0.tar.gz`
227
- - `ltcai-6.0.0.tgz`
228
- - `dist/ltcai-6.0.0.vsix`
229
- - `src-tauri/target/release/bundle/dmg/Lattice AI_6.0.0_aarch64.dmg`
203
+ The current development target is **6.1.0 Product Hardening / Digital Brain Completion**:
204
+
205
+ - First-run now lets users create or open the local Brain even when they defer
206
+ model setup, keeping the Brain durable while models stay replaceable.
207
+ - Brain Home explains the first memory loop: save useful context, see it return
208
+ in the Brain state, then protect it with backup/export.
209
+ - Review Center keeps Pending, Snoozed, and All filters while clarifying that
210
+ Run now is a preview/regenerate action, not approval.
211
+ - `lattice_brain` has an AST import guard so Brain Core does not import
212
+ `latticeai` or `ltcai`.
213
+ - Model download consent has a unit guard: token presence alone does not start
214
+ an external download path.
215
+ - `ltcai_cli.py` keeps the root entrypoint while pure CLI runtime helpers live
216
+ under `latticeai.cli.runtime`.
217
+ - Tool dispatch authorization now has an injectable service boundary around
218
+ the shared ToolRegistry, reducing app-factory reliance on module globals.
219
+ - Chat agent runtime construction moved into app-factory assembly and is passed
220
+ through `AppContext`, so the chat router no longer owns production runtime
221
+ construction.
222
+ - v6.1 documentation tracks backend hardening, frontend UX hardening, and the
223
+ baseline scan for version metadata, root legacy modules, Brain Core boundary,
224
+ and local-first trust gates.
225
+
226
+ Expected artifacts for 6.1.0 release must use exact filenames:
227
+
228
+ - `dist/ltcai-6.1.0-py3-none-any.whl`
229
+ - `dist/ltcai-6.1.0.tar.gz`
230
+ - `ltcai-6.1.0.tgz`
231
+ - `dist/ltcai-6.1.0.vsix`
232
+ - `src-tauri/target/release/bundle/dmg/Lattice AI_6.1.0_aarch64.dmg`
230
233
 
231
234
  Do not upload `dist/*`. Package registry publishing remains owner-run.
232
235
 
package/docs/CHANGELOG.md CHANGED
@@ -3,6 +3,42 @@
3
3
  The top entry is the current release-preparation target. Older entries are
4
4
  historical and may describe behavior as it existed at that release.
5
5
 
6
+ ## [6.1.0] - 2026-06-16
7
+
8
+ > Product Hardening / Digital Brain Completion. Tightens the local-first Brain
9
+ > flow, Brain Core boundary, backend trust gates, and release documentation
10
+ > without adding broad new product surfaces.
11
+
12
+ ### Added
13
+ - First-run path to open the Brain without installing or loading a model first.
14
+ - Brain Home empty-state loop that shows first memory save, Brain state return,
15
+ and backup ownership.
16
+ - AST import guard proving `lattice_brain` does not import `latticeai` or
17
+ `ltcai`.
18
+ - Unit coverage for model download consent blocking in `model_runtime`.
19
+ - `latticeai.cli.runtime` for pure CLI runtime helpers while preserving the root
20
+ `ltcai_cli.py` entrypoint.
21
+ - v6.1 baseline, frontend UX, backend hardening, and instruction documents under
22
+ `docs/v6.1/`.
23
+
24
+ ### Changed
25
+ - Review Center cards clarify that Run now previews/executes without approval.
26
+ - Static app assets are refreshed for the v6.1 frontend hardening changes.
27
+ - Tool dispatch authorization now uses an injectable service boundary around
28
+ the shared ToolRegistry while preserving the legacy module-level API.
29
+ - Chat agent runtime construction moved out of `create_chat_router` for
30
+ production app assembly and is now injected through `AppContext`.
31
+ - README release evidence screenshots and walkthrough GIF are refreshed under
32
+ `output/release/v6.1.0/`, including the Review Center surface.
33
+ - README and release notes now describe v6.1.0 as the current hardening target
34
+ with exact expected artifact filenames.
35
+
36
+ ### Preserved
37
+ - Package publish, deployment, tag creation, and main merge remain out of scope
38
+ unless explicitly requested.
39
+ - Package/runtime version metadata is synchronized to 6.1.0; package publish,
40
+ deployment, tag creation, and main merge remain owner-run only.
41
+
6
42
  ## [6.0.0] - 2026-06-15
7
43
 
8
44
  > Product Reset / Review Center Completion. Raises the Review Center from a
@@ -61,7 +61,7 @@ contracts, and local-first/offline operation.
61
61
  - Legacy frontend build pipeline.
62
62
  - Legacy v3 frontend lint script.
63
63
  - Static v3 asset tree in release packages.
64
- - Direct current-release references to v4.0.1 in latest docs.
64
+ - Direct then-current release references to v4.0.1 in then-latest docs.
65
65
 
66
66
  ## Data Preservation
67
67
 
@@ -8,7 +8,7 @@ architecture or add unrelated product features.
8
8
 
9
9
  ## Completed Refresh Areas
10
10
 
11
- - Synchronized current-release version metadata to `4.6.1`.
11
+ - Synchronized then-current release version metadata to `4.6.1`.
12
12
  - Reframed README around the current Living Brain flow:
13
13
  Login -> Environment Analysis -> Recommended Models -> Install & Load ->
14
14
  Brain Chat.
@@ -19,7 +19,7 @@ maintenance. This release does not redesign Brain Core or the storage layer.
19
19
  - Added frontend API helpers for `/admin/stats` and `/admin/security/events`.
20
20
  - Updated visual validation to confirm the admin console is separate from the
21
21
  user Brain surface.
22
- - Synchronized current-release version metadata to `4.7.0`.
22
+ - Synchronized then-current release version metadata to `4.7.0`.
23
23
  - Updated README, RELEASE.md, release notes, changelog, architecture, security,
24
24
  feature status, VS Code extension docs, and release evidence references.
25
25
 
@@ -26,7 +26,7 @@ maintenance. This release does not redesign Brain Core or the storage layer.
26
26
  admin observability state remain separate in the frontend runtime.
27
27
  - Updated visual validation to confirm the admin console is separate from the
28
28
  user Brain surface.
29
- - Synchronized current-release version metadata to `4.7.1`.
29
+ - Synchronized then-current release version metadata to `4.7.1`.
30
30
  - Updated README, RELEASE.md, release notes, changelog, architecture, security,
31
31
  feature status, VS Code extension docs, and release evidence references.
32
32
 
@@ -382,6 +382,11 @@ function BrainHome({
382
382
  </button>
383
383
  ))}
384
384
  </div>
385
+ <div className="mind-empty-trail" aria-label={t(language, "brain.empty.trail.label")}>
386
+ <span>{t(language, "brain.empty.trail.save")}</span>
387
+ <span>{t(language, "brain.empty.trail.recall")}</span>
388
+ <span>{t(language, "brain.empty.trail.backup")}</span>
389
+ </div>
385
390
  </div>
386
391
  ) : (
387
392
  messages.map((message, index) => (
@@ -123,6 +123,10 @@ export function ProductFlow({ onComplete }: { onComplete: () => void }) {
123
123
  <RecommendationScreen
124
124
  recommendations={recommendations}
125
125
  onBack={() => setStep("analysis")}
126
+ onSkipModel={() => {
127
+ try { localStorage.setItem(FLOW_COMPLETE_KEY, "true"); } catch {}
128
+ onComplete();
129
+ }}
126
130
  onSelect={(model) => {
127
131
  setSelected(model);
128
132
  setStep("install");
@@ -333,10 +337,12 @@ function AnalysisScreen({
333
337
  function RecommendationScreen({
334
338
  recommendations,
335
339
  onBack,
340
+ onSkipModel,
336
341
  onSelect,
337
342
  }: {
338
343
  recommendations: RecommendedModel[];
339
344
  onBack: () => void;
345
+ onSkipModel: () => void;
340
346
  onSelect: (model: RecommendedModel) => void;
341
347
  }) {
342
348
  const language = useAppStore((state) => state.language);
@@ -370,6 +376,7 @@ function RecommendationScreen({
370
376
 
371
377
  <div style={{ marginTop: "1.1rem", display: "flex", justifyContent: "center", gap: "1rem", alignItems: "center" }}>
372
378
  <Button variant="ghost" onClick={onBack}>{t(language, "flow.recommend.back")}</Button>
379
+ <Button variant="outline" onClick={onSkipModel}>{t(language, "flow.recommend.skip")}</Button>
373
380
  <div style={{ fontSize: "0.82rem", color: "hsl(var(--fg-muted))" }}>{t(language, "flow.recommend.hint")}</div>
374
381
  </div>
375
382
  </div>
@@ -71,16 +71,21 @@ export function ReviewCard({ item, feedback, onAction }: ReviewCardProps) {
71
71
  ) : null}
72
72
 
73
73
  {actionable ? (
74
- <div className="mt-4 flex flex-wrap gap-2">
75
- <ActionButton
76
- label="Run now"
77
- successLabel={hadRun ? "Regenerated" : "Executed"}
78
- action={() => onAction(item, "run_now", hadRun)}
79
- invalidate={[]}
80
- />
81
- <ActionButton label="Approve" action={() => onAction(item, "approve")} invalidate={[]} />
82
- {!snoozed ? <ActionButton label="Snooze 1 day" action={() => onAction(item, "snooze")} invalidate={[]} /> : null}
83
- <ActionButton label="Dismiss" action={() => onAction(item, "dismiss")} invalidate={[]} variant="destructive" />
74
+ <div className="mt-4 grid gap-2">
75
+ <p className="text-xs leading-5 text-muted-foreground">
76
+ Run now previews the action without approving it. Approve or dismiss when the result looks right.
77
+ </p>
78
+ <div className="flex flex-wrap gap-2" aria-label="Review actions">
79
+ <ActionButton
80
+ label="Run now"
81
+ successLabel={hadRun ? "Regenerated" : "Executed"}
82
+ action={() => onAction(item, "run_now", hadRun)}
83
+ invalidate={[]}
84
+ />
85
+ <ActionButton label="Approve" action={() => onAction(item, "approve")} invalidate={[]} />
86
+ {!snoozed ? <ActionButton label="Snooze 1 day" action={() => onAction(item, "snooze")} invalidate={[]} /> : null}
87
+ <ActionButton label="Dismiss" action={() => onAction(item, "dismiss")} invalidate={[]} variant="destructive" />
88
+ </div>
84
89
  </div>
85
90
  ) : null}
86
91
  {feedback ? (
@@ -31,6 +31,10 @@ export const COPY: Record<Language, TextMap> = {
31
31
  "brain.empty.kicker": "내 오래가는 기억",
32
32
  "brain.empty.title": "잊으면 안 되는 맥락부터 말해 주세요.",
33
33
  "brain.empty.body": "문서, 대화, 프로젝트, 결정이 이 컴퓨터의 Brain에 쌓이고 나중에 주제와 관계로 다시 보입니다.",
34
+ "brain.empty.trail.label": "첫 Brain 흐름",
35
+ "brain.empty.trail.save": "1. 첫 기억 저장",
36
+ "brain.empty.trail.recall": "2. Brain Home에서 확인",
37
+ "brain.empty.trail.backup": "3. 백업으로 소유",
34
38
  "brain.prompt.remember": "이 프로젝트 목표를 기억해줘: ",
35
39
  "brain.prompt.know": "이 문서를 내 Brain에 넣고 요약해줘: ",
36
40
  "brain.prompt.plan": "지난 결정들을 나중에 찾을 수 있게 정리해줘: ",
@@ -107,6 +111,7 @@ export const COPY: Record<Language, TextMap> = {
107
111
  "flow.recommend.primary": "추천대로 시작하기",
108
112
  "flow.recommend.unsupported": "이 컴퓨터에서 추가 확인이 필요합니다",
109
113
  "flow.recommend.back": "뒤로",
114
+ "flow.recommend.skip": "모델 없이 Brain 열기",
110
115
  "flow.recommend.hint": "잘 모르겠다면 추천대로 시작하면 됩니다.",
111
116
  "flow.install.title": "모델을 설치하고 시작합니다.",
112
117
  "flow.install.body": "이 모델이 Brain의 로컬 목소리가 됩니다. 인터넷은 다운로드할 때만 필요하고, 실행과 기억은 이 컴퓨터에서 유지됩니다.",
@@ -144,6 +149,10 @@ export const COPY: Record<Language, TextMap> = {
144
149
  "brain.empty.kicker": "Durable memory",
145
150
  "brain.empty.title": "Start with context that should not be forgotten.",
146
151
  "brain.empty.body": "Documents, conversations, projects, and decisions accumulate in the Brain on this computer, then reappear as topics and relationships.",
152
+ "brain.empty.trail.label": "First Brain flow",
153
+ "brain.empty.trail.save": "1. Save first memory",
154
+ "brain.empty.trail.recall": "2. See it in Brain Home",
155
+ "brain.empty.trail.backup": "3. Own it with backup",
147
156
  "brain.prompt.remember": "Remember this project goal: ",
148
157
  "brain.prompt.know": "Put this document into my Brain and summarize it: ",
149
158
  "brain.prompt.plan": "Organize past decisions so I can find them later: ",
@@ -220,6 +229,7 @@ export const COPY: Record<Language, TextMap> = {
220
229
  "flow.recommend.primary": "Start with recommendation",
221
230
  "flow.recommend.unsupported": "This computer needs one more check",
222
231
  "flow.recommend.back": "Back",
232
+ "flow.recommend.skip": "Open Brain without a model",
223
233
  "flow.recommend.hint": "If unsure, start with the recommendation.",
224
234
  "flow.install.title": "Install the model and start.",
225
235
  "flow.install.body": "This model becomes your Brain's local voice. Internet is needed only for download; execution and memory stay on this computer.",
@@ -2774,6 +2774,26 @@ body {
2774
2774
  transform: translateY(-1px);
2775
2775
  }
2776
2776
 
2777
+ .mind-empty-trail {
2778
+ display: flex;
2779
+ flex-wrap: wrap;
2780
+ justify-content: center;
2781
+ gap: 0.45rem;
2782
+ margin: 1rem auto 0;
2783
+ max-width: 40rem;
2784
+ }
2785
+
2786
+ .mind-empty-trail span {
2787
+ border: 1px solid hsl(var(--border) / 0.55);
2788
+ border-radius: 999px;
2789
+ background: hsl(var(--surface-glass));
2790
+ color: hsl(var(--fg-muted));
2791
+ padding: 0.34rem 0.62rem;
2792
+ font-size: 0.72rem;
2793
+ font-weight: 720;
2794
+ line-height: 1.15;
2795
+ }
2796
+
2777
2797
  .mind-empty .icon {
2778
2798
  opacity: 0.5;
2779
2799
  margin-bottom: 0.8rem;
@@ -26,7 +26,7 @@ from .storage import (
26
26
  storage_from_env,
27
27
  )
28
28
 
29
- __version__ = "6.0.0"
29
+ __version__ = "6.1.0"
30
30
 
31
31
  __all__ = [
32
32
  "AgentRuntime",
@@ -19,7 +19,7 @@ from datetime import datetime
19
19
  from typing import Any, Callable, Dict, List, Optional
20
20
 
21
21
 
22
- MULTI_AGENT_VERSION = "6.0.0"
22
+ MULTI_AGENT_VERSION = "6.1.0"
23
23
 
24
24
  AGENT_ROLES = ("researcher", "planner", "executor", "reviewer", "release")
25
25
  CORE_PIPELINE = ("planner", "executor", "reviewer")
@@ -1,3 +1,3 @@
1
1
  """Lattice AI - modular server package."""
2
2
 
3
- __version__ = "6.0.0"
3
+ __version__ = "6.1.0"
@@ -150,6 +150,35 @@ async def single_text_stream(text: str, model: str = "system") -> AsyncIterator[
150
150
  yield "data: [DONE]\n\n"
151
151
 
152
152
 
153
+ def build_recent_chat_context(
154
+ *,
155
+ get_history,
156
+ limit: int = 10,
157
+ include_image_missing_replies: bool = True,
158
+ user_email: Optional[str] = None,
159
+ conversation_id: Optional[str] = None,
160
+ ) -> str:
161
+ history = get_history()
162
+ if conversation_id:
163
+ history = [item for item in history if item.get("conversation_id") == conversation_id]
164
+ if user_email:
165
+ history = pair_user_history(history, user_email)
166
+ history = history[-limit:]
167
+ lines = []
168
+ for item in history:
169
+ role = item.get("role", "user")
170
+ content = item.get("content", "")
171
+ if not include_image_missing_replies and role == "assistant":
172
+ if "이미지" in content and any(word in content for word in ["업로드", "제공", "올려"]):
173
+ continue
174
+ source = item.get("source")
175
+ label = role
176
+ if source:
177
+ label = f"{role} ({source})"
178
+ lines.append(f"{label}: {content}")
179
+ return "\n".join(lines)
180
+
181
+
153
182
  def create_chat_router(context: AppContext) -> APIRouter:
154
183
  """Build the chat/history/agent router from the typed application context.
155
184
 
@@ -201,31 +230,19 @@ def create_chat_router(context: AppContext) -> APIRouter:
201
230
  except Exception as exc:
202
231
  logging.warning("chat message bridge failed: %s", exc)
203
232
 
204
- def build_recent_chat_context(
233
+ def recent_chat_context(
205
234
  limit: int = 10,
206
235
  include_image_missing_replies: bool = True,
207
236
  user_email: Optional[str] = None,
208
237
  conversation_id: Optional[str] = None,
209
238
  ) -> str:
210
- history = get_history()
211
- if conversation_id:
212
- history = [item for item in history if item.get("conversation_id") == conversation_id]
213
- if user_email:
214
- history = pair_user_history(history, user_email)
215
- history = history[-limit:]
216
- lines = []
217
- for item in history:
218
- role = item.get("role", "user")
219
- content = item.get("content", "")
220
- if not include_image_missing_replies and role == "assistant":
221
- if "이미지" in content and any(word in content for word in ["업로드", "제공", "올려"]):
222
- continue
223
- source = item.get("source")
224
- label = role
225
- if source:
226
- label = f"{role} ({source})"
227
- lines.append(f"{label}: {content}")
228
- return "\n".join(lines)
239
+ return build_recent_chat_context(
240
+ get_history=get_history,
241
+ limit=limit,
242
+ include_image_missing_replies=include_image_missing_replies,
243
+ user_email=user_email,
244
+ conversation_id=conversation_id,
245
+ )
229
246
 
230
247
  def extract_screenshot_context(image_data: Optional[str]) -> str:
231
248
  if not image_data:
@@ -283,16 +300,18 @@ def create_chat_router(context: AppContext) -> APIRouter:
283
300
 
284
301
  return "\n".join(lines)
285
302
 
286
- _AGENT_RUNTIME = build_agent_runtime(
287
- model_router=router,
288
- execute_tool=execute_tool,
289
- recent_chat_context=build_recent_chat_context,
290
- clear_history=clear_history,
291
- knowledge_save=knowledge_save,
292
- audit=append_audit_event,
293
- hooks=hooks,
294
- brain_memory=context.brain_memory,
295
- )
303
+ _AGENT_RUNTIME = context.chat_agent_runtime
304
+ if _AGENT_RUNTIME is None:
305
+ _AGENT_RUNTIME = build_agent_runtime(
306
+ model_router=router,
307
+ execute_tool=execute_tool,
308
+ recent_chat_context=recent_chat_context,
309
+ clear_history=clear_history,
310
+ knowledge_save=knowledge_save,
311
+ audit=append_audit_event,
312
+ hooks=hooks,
313
+ brain_memory=context.brain_memory,
314
+ )
296
315
 
297
316
  @api_router.post("/chat")
298
317
  async def chat(req: ChatRequest, request: Request):
@@ -546,7 +565,7 @@ def create_chat_router(context: AppContext) -> APIRouter:
546
565
  return JSONResponse(content={"response": str(result), "trace_id": trace_record["id"], "trace": trace_record})
547
566
 
548
567
  if req.stream:
549
- recent_context = build_recent_chat_context(user_email=effective_email, conversation_id=req.conversation_id)
568
+ recent_context = recent_chat_context(user_email=effective_email, conversation_id=req.conversation_id)
550
569
  stream_context = context
551
570
  if recent_context:
552
571
  stream_context = f"[RECENT CONVERSATION]\n{recent_context}\n\n{context}".strip()
@@ -564,7 +583,7 @@ def create_chat_router(context: AppContext) -> APIRouter:
564
583
  )
565
584
  else:
566
585
  if req.image_data:
567
- recent_context = build_recent_chat_context(
586
+ recent_context = recent_chat_context(
568
587
  limit=6,
569
588
  include_image_missing_replies=False,
570
589
  user_email=effective_email,
@@ -572,7 +591,7 @@ def create_chat_router(context: AppContext) -> APIRouter:
572
591
  )
573
592
  full_context = f"[RECENT CONVERSATION]\n{recent_context}\n\n{context}".strip() if recent_context else context
574
593
  else:
575
- history_context = build_recent_chat_context(user_email=effective_email, conversation_id=req.conversation_id)
594
+ history_context = recent_chat_context(user_email=effective_email, conversation_id=req.conversation_id)
576
595
  full_context = f"{history_context}\n{context}" if context else history_context
577
596
 
578
597
  result = await router.generate(req.message, full_context, req.max_tokens, req.temperature, req.image_data)
@@ -153,7 +153,7 @@ def _build(config: "Optional[Config]" = None) -> Dict[str, Any]:
153
153
  from latticeai.api.invitations import create_invitations_router
154
154
  from latticeai.api.marketplace import create_marketplace_router
155
155
  from latticeai.api.models import create_models_router
156
- from latticeai.api.chat import create_chat_router
156
+ from latticeai.api.chat import build_recent_chat_context, create_chat_router
157
157
  from latticeai.api.search import create_search_router
158
158
  from latticeai.api.tools import create_tools_router
159
159
  from latticeai.api.static_routes import create_static_routes_router
@@ -181,6 +181,7 @@ def _build(config: "Optional[Config]" = None) -> Dict[str, Any]:
181
181
  configure_tool_dispatch,
182
182
  get_tool_permission,
183
183
  list_tool_permissions,
184
+ build_agent_runtime,
184
185
  tool_response as _tool_response,
185
186
  )
186
187
  from latticeai.core.tool_registry import TOOL_CATALOG_BRIEF as _TOOL_CATALOG_BRIEF # noqa: F401
@@ -190,7 +191,7 @@ def _build(config: "Optional[Config]" = None) -> Dict[str, Any]:
190
191
  )
191
192
  from p_reinforce import PReinforceGardener
192
193
  from setup_wizard import get_recommendations, scan_environment
193
- from tools import ensure_agent_root
194
+ from tools import ensure_agent_root, execute_tool, knowledge_save
194
195
 
195
196
  try:
196
197
  import keyring
@@ -1242,6 +1243,31 @@ def _build(config: "Optional[Config]" = None) -> Dict[str, Any]:
1242
1243
  _spawn(broadcast_web_chat(role, text), name="telegram_broadcast")
1243
1244
  on_chat_message = _telegram_chat_mirror
1244
1245
 
1246
+ def _recent_chat_context(
1247
+ limit: int = 10,
1248
+ include_image_missing_replies: bool = True,
1249
+ user_email: Optional[str] = None,
1250
+ conversation_id: Optional[str] = None,
1251
+ ) -> str:
1252
+ return build_recent_chat_context(
1253
+ get_history=get_history,
1254
+ limit=limit,
1255
+ include_image_missing_replies=include_image_missing_replies,
1256
+ user_email=user_email,
1257
+ conversation_id=conversation_id,
1258
+ )
1259
+
1260
+ CHAT_AGENT_RUNTIME = build_agent_runtime(
1261
+ model_router=router,
1262
+ execute_tool=execute_tool,
1263
+ recent_chat_context=_recent_chat_context,
1264
+ clear_history=clear_history,
1265
+ knowledge_save=knowledge_save,
1266
+ audit=append_audit_event,
1267
+ hooks=HOOKS_REGISTRY,
1268
+ brain_memory=BRAIN_MEMORY,
1269
+ )
1270
+
1245
1271
  # ── Typed dependency context (latticeai.services.app_context) ────────────────
1246
1272
  # One context object replaces the historical 25-30-kwarg router wiring.
1247
1273
  context = build_app_context(
@@ -1258,6 +1284,7 @@ def _build(config: "Optional[Config]" = None) -> Dict[str, Any]:
1258
1284
  chat_service=CHAT_SERVICE,
1259
1285
  context_assembler=CONTEXT_ASSEMBLER,
1260
1286
  brain_memory=BRAIN_MEMORY,
1287
+ chat_agent_runtime=CHAT_AGENT_RUNTIME,
1261
1288
  gardener=gardener,
1262
1289
  hooks=HOOKS_REGISTRY,
1263
1290
  realtime_bus=REALTIME_BUS,
@@ -0,0 +1 @@
1
+ """LatticeAI CLI package."""
@@ -0,0 +1,37 @@
1
+ """Pure runtime helpers extracted from ltcai_cli entrypoint."""
2
+
3
+ from __future__ import annotations
4
+
5
+ import importlib.util
6
+ import os
7
+ from pathlib import Path
8
+
9
+
10
+ def _load_env_file(path: Path) -> None:
11
+ if not path.exists():
12
+ return
13
+ for raw_line in path.read_text(encoding="utf-8").splitlines():
14
+ line = raw_line.strip()
15
+ if not line or line.startswith("#") or "=" not in line:
16
+ continue
17
+ key, value = line.split("=", 1)
18
+ key = key.strip()
19
+ value = value.strip().strip('"').strip("'")
20
+ if key and key not in os.environ:
21
+ os.environ[key] = value
22
+
23
+
24
+ def _apply_extra_path() -> None:
25
+ extra = os.getenv("LATTICEAI_EXTRA_PATH", "")
26
+ if not extra:
27
+ return
28
+ current = [p for p in os.environ.get("PATH", "").split(os.pathsep) if p]
29
+ for item in reversed([p for p in extra.split(os.pathsep) if p]):
30
+ expanded = str(Path(item).expanduser())
31
+ if Path(expanded).exists() and expanded not in current:
32
+ current.insert(0, expanded)
33
+ os.environ["PATH"] = os.pathsep.join(current)
34
+
35
+
36
+ def _has_module(name: str) -> bool:
37
+ return importlib.util.find_spec(name) is not None
@@ -11,7 +11,7 @@ from copy import deepcopy
11
11
  from typing import Any, Dict, List, Optional
12
12
 
13
13
 
14
- MARKETPLACE_VERSION = "6.0.0"
14
+ MARKETPLACE_VERSION = "6.1.0"
15
15
  TEMPLATE_KINDS = ("plugin", "workflow", "agent")
16
16
 
17
17
 
@@ -19,7 +19,7 @@ from pathlib import Path
19
19
  from typing import Any, Callable, Dict, Iterable, List, Optional
20
20
 
21
21
 
22
- WORKSPACE_OS_VERSION = "6.0.0"
22
+ WORKSPACE_OS_VERSION = "6.1.0"
23
23
 
24
24
  # Workspace types separate single-user Personal workspaces from shared
25
25
  # Organization workspaces. Both keep the same local-first JSON store; the type