ltcai 5.6.0 → 6.0.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 (40) hide show
  1. package/README.md +42 -25
  2. package/docs/CHANGELOG.md +38 -0
  3. package/frontend/openapi.json +39 -0
  4. package/frontend/src/api/client.ts +104 -23
  5. package/frontend/src/api/openapi.ts +48 -0
  6. package/frontend/src/components/FirstRunGuide.tsx +3 -3
  7. package/frontend/src/features/review/ReviewCard.tsx +91 -0
  8. package/frontend/src/features/review/ReviewInbox.tsx +112 -0
  9. package/frontend/src/features/review/reviewHelpers.ts +69 -0
  10. package/frontend/src/i18n.ts +8 -8
  11. package/frontend/src/pages/Act.tsx +5 -177
  12. package/frontend/src/routes.ts +1 -0
  13. package/lattice_brain/__init__.py +1 -1
  14. package/lattice_brain/runtime/multi_agent.py +1 -1
  15. package/latticeai/__init__.py +1 -1
  16. package/latticeai/api/review_queue.py +7 -3
  17. package/latticeai/app_factory.py +224 -473
  18. package/latticeai/core/marketplace.py +1 -1
  19. package/latticeai/core/workspace_os.py +1 -1
  20. package/latticeai/runtime/app_context_runtime.py +13 -0
  21. package/latticeai/runtime/automation_runtime.py +64 -0
  22. package/latticeai/runtime/bootstrap.py +48 -0
  23. package/latticeai/runtime/context_runtime.py +43 -0
  24. package/latticeai/runtime/hooks_runtime.py +77 -0
  25. package/latticeai/runtime/lifespan_runtime.py +138 -0
  26. package/latticeai/runtime/persistence_runtime.py +87 -0
  27. package/latticeai/runtime/platform_services_runtime.py +39 -0
  28. package/latticeai/runtime/router_registration.py +570 -0
  29. package/latticeai/runtime/web_runtime.py +65 -0
  30. package/latticeai/services/review_queue.py +20 -4
  31. package/package.json +1 -1
  32. package/src-tauri/Cargo.lock +1 -1
  33. package/src-tauri/Cargo.toml +1 -1
  34. package/src-tauri/tauri.conf.json +1 -1
  35. package/static/app/asset-manifest.json +3 -3
  36. package/static/app/assets/index-D2zafMYb.js +16 -0
  37. package/static/app/assets/index-D2zafMYb.js.map +1 -0
  38. package/static/app/index.html +1 -1
  39. package/static/app/assets/index-xMFu94cX.js +0 -16
  40. package/static/app/assets/index-xMFu94cX.js.map +0 -1
@@ -0,0 +1,69 @@
1
+ import type * as React from "react";
2
+ import type { ReviewItem, ReviewSourceFilter, ReviewStatusFilter } from "@/api/client";
3
+ import type { Badge } from "@/components/ui/badge";
4
+
5
+ export type ReviewAction = "approve" | "dismiss" | "snooze" | "unsnooze" | "run_now";
6
+
7
+ export const reviewStatusFilters: Array<{ id: ReviewStatusFilter; label: string }> = [
8
+ { id: "pending", label: "Pending" },
9
+ { id: "snoozed", label: "Snoozed" },
10
+ { id: "all", label: "All" },
11
+ { id: "approved", label: "Approved" },
12
+ { id: "dismissed", label: "Dismissed" },
13
+ ];
14
+
15
+ export const reviewSourceFilters: Array<{ id: ReviewSourceFilter; label: string }> = [
16
+ { id: "all", label: "All sources" },
17
+ { id: "workflow_run", label: "Workflow" },
18
+ { id: "trigger", label: "Trigger" },
19
+ { id: "kg_change_digest", label: "KG digest" },
20
+ ];
21
+
22
+ export function reviewStatusVariant(status: string): React.ComponentProps<typeof Badge>["variant"] {
23
+ if (status === "pending") return "warning";
24
+ if (status === "snoozed") return "muted";
25
+ if (status === "approved") return "success";
26
+ if (status === "dismissed") return "danger";
27
+ return "muted";
28
+ }
29
+
30
+ export function reviewSourceLabel(source?: string) {
31
+ if (source === "workflow_run") return "Workflow run";
32
+ if (source === "trigger") return "Trigger";
33
+ if (source === "kg_change_digest") return "KG digest";
34
+ return source || "Automation";
35
+ }
36
+
37
+ export function reviewSourceDetail(provenance: Record<string, unknown>, source?: string) {
38
+ const detail = provenance.source_detail;
39
+ if (detail != null && String(detail).trim()) return String(detail);
40
+ const triggerId = provenance.trigger_id;
41
+ if (triggerId != null && String(triggerId).trim()) return String(triggerId);
42
+ return reviewSourceLabel(source);
43
+ }
44
+
45
+ export function defaultSnoozeUntil() {
46
+ const until = new Date();
47
+ until.setDate(until.getDate() + 1);
48
+ return until.toISOString();
49
+ }
50
+
51
+ export function formatSnoozedUntil(value?: string | null) {
52
+ if (!value) return "Snoozed";
53
+ const date = new Date(value);
54
+ if (Number.isNaN(date.getTime())) return `Snoozed until ${value}`;
55
+ return `Snoozed until ${new Intl.DateTimeFormat(undefined, {
56
+ dateStyle: "medium",
57
+ timeStyle: "short",
58
+ }).format(date)}`;
59
+ }
60
+
61
+ export function isActionableReview(item: ReviewItem) {
62
+ return item.effective_status === "pending" || item.effective_status === "snoozed";
63
+ }
64
+
65
+ export function hasRunBefore(item: ReviewItem) {
66
+ const payload = item.payload || {};
67
+ const provenance = item.provenance || {};
68
+ return Boolean(payload.last_run_id || provenance.run_id);
69
+ }
@@ -77,7 +77,7 @@ export const COPY: Record<Language, TextMap> = {
77
77
  "admin.body": "사용자, 로그, 보안, Brain 상태는 일반 사용자 화면과 분리됩니다.",
78
78
  "flow.shell": "내 로컬 AI 브레인 만들기",
79
79
  "flow.login.title": "내 AI 브레인의 주인을 정합니다.",
80
- "flow.login.body": "Lattice AI는 모델이 바뀌어도 내 지식과 맥락을 보존하는 로컬 우선 AI 브레인입니다. 대화와 기억은 기본적으로 컴퓨터에 저장됩니다.",
80
+ "flow.login.body": "Lattice AI는 내 지식과 맥락을 컴퓨터에 보관하는 로컬 우선 AI 브레인입니다. 모델은 바꿀 있고, 외부 전송은 사용자가 선택할 때만 시작됩니다.",
81
81
  "flow.name": "이름",
82
82
  "flow.email": "이메일",
83
83
  "flow.password": "비밀번호",
@@ -94,9 +94,9 @@ export const COPY: Record<Language, TextMap> = {
94
94
  "flow.promise.model.k": "교체 가능한 모델",
95
95
  "flow.promise.model.v": "모델은 목소리이고, 자산은 Brain입니다.",
96
96
  "flow.promise.ownership.k": "사용자 소유",
97
- "flow.promise.ownership.v": "백업, 복원, 이동이 가능합니다.",
97
+ "flow.promise.ownership.v": "백업, 복원, 이동을 직접 결정합니다.",
98
98
  "flow.analysis.title": "이 컴퓨터에서 가능한 경험을 확인합니다.",
99
- "flow.analysis.body": "스펙 점수가 아니라, 이 Mac에서 어떤 로컬 AI 브레인 경험이 편한지 알려드립니다. 클라우드 모델은 선택 사항입니다.",
99
+ "flow.analysis.body": "스펙 점수가 아니라, 이 Mac에서 어떤 로컬 AI 브레인 경험이 편한지 알려드립니다. 확인은 로컬 상태를 읽는 것이고, 클라우드 모델은 선택 사항입니다.",
100
100
  "flow.analysis.finding": "가장 편한 설정을 찾는 중...",
101
101
  "flow.analysis.ready": "추천 모델을 바로 시작할 수 있게 준비했습니다.",
102
102
  "flow.analysis.wait": "잠시만 기다리면 자동으로 정리됩니다.",
@@ -109,7 +109,7 @@ export const COPY: Record<Language, TextMap> = {
109
109
  "flow.recommend.back": "뒤로",
110
110
  "flow.recommend.hint": "잘 모르겠다면 추천대로 시작하면 됩니다.",
111
111
  "flow.install.title": "모델을 설치하고 시작합니다.",
112
- "flow.install.body": "이 모델이 Brain의 로컬 목소리가 됩니다. 다운로드할 때만 인터넷이 필요하고, 실행은 컴퓨터에서 진행됩니다.",
112
+ "flow.install.body": "이 모델이 Brain의 로컬 목소리가 됩니다. 인터넷은 다운로드할 때만 필요하고, 실행과 기억은 컴퓨터에서 유지됩니다.",
113
113
  "flow.install.wait": "Brain이 사용할 모델을 기다리고 있습니다.",
114
114
  "flow.install.prepare": "Brain 준비 중입니다.",
115
115
  "flow.install.done": "Brain이 준비되었습니다.",
@@ -190,7 +190,7 @@ export const COPY: Record<Language, TextMap> = {
190
190
  "admin.body": "Users, logs, security, and Brain health stay out of the normal user experience.",
191
191
  "flow.shell": "Create your local AI Brain",
192
192
  "flow.login.title": "Choose the owner of your AI Brain.",
193
- "flow.login.body": "Lattice AI is a local-first Digital Brain that keeps your knowledge durable across any AI model. Conversations and memories are stored on this computer by default.",
193
+ "flow.login.body": "Lattice AI is a local-first Digital Brain that keeps your knowledge on this computer. Models can change; external transfer starts only when you choose it.",
194
194
  "flow.name": "Name",
195
195
  "flow.email": "Email",
196
196
  "flow.password": "Password",
@@ -207,9 +207,9 @@ export const COPY: Record<Language, TextMap> = {
207
207
  "flow.promise.model.k": "Replaceable models",
208
208
  "flow.promise.model.v": "The model is the voice; the Brain is the asset.",
209
209
  "flow.promise.ownership.k": "User owned",
210
- "flow.promise.ownership.v": "Back up, restore, and move it.",
210
+ "flow.promise.ownership.v": "You decide when to back up, restore, or move it.",
211
211
  "flow.analysis.title": "Checking what this computer can do.",
212
- "flow.analysis.body": "This is not a spec test. Lattice explains what local AI Brain experience this Mac can support. Cloud models remain optional.",
212
+ "flow.analysis.body": "This is not a spec test. Lattice reads local capability and explains what AI Brain experience this Mac can support. Cloud models remain optional.",
213
213
  "flow.analysis.finding": "Finding the easiest setup...",
214
214
  "flow.analysis.ready": "Your recommended model is ready to start.",
215
215
  "flow.analysis.wait": "This will be summarized automatically in a moment.",
@@ -222,7 +222,7 @@ export const COPY: Record<Language, TextMap> = {
222
222
  "flow.recommend.back": "Back",
223
223
  "flow.recommend.hint": "If unsure, start with the recommendation.",
224
224
  "flow.install.title": "Install the model and start.",
225
- "flow.install.body": "This model becomes your Brain's local voice. Internet is needed only for download; execution happens on this computer.",
225
+ "flow.install.body": "This model becomes your Brain's local voice. Internet is needed only for download; execution and memory stay on this computer.",
226
226
  "flow.install.wait": "Waiting for the model your Brain will use.",
227
227
  "flow.install.prepare": "Preparing your Brain.",
228
228
  "flow.install.done": "Your Brain is ready.",
@@ -2,30 +2,23 @@ import * as React from "react";
2
2
  import { useMutation, useQuery, useQueryClient } from "@tanstack/react-query";
3
3
  import ReactFlow, { Background, Controls, Edge, Node } from "reactflow";
4
4
  import { Bot, CalendarClock, GitBranch, PauseCircle, Play, ShieldCheck, Workflow } from "lucide-react";
5
- import { latticeApi, type ReviewItem } from "@/api/client";
6
- import { ActionButton, DataPanel, EmptyState, EntityList, KeyValueList, LoadingPanel, ModeGate, OperationResult, StructuredView, Tabs } from "@/components/primitives";
5
+ import { latticeApi } from "@/api/client";
6
+ import { ActionButton, DataPanel, EntityList, KeyValueList, ModeGate, OperationResult, StructuredView, Tabs } from "@/components/primitives";
7
7
  import { Badge } from "@/components/ui/badge";
8
8
  import { Button } from "@/components/ui/button";
9
9
  import { Card, CardContent, CardDescription, CardHeader, CardTitle } from "@/components/ui/card";
10
10
  import { Input } from "@/components/ui/input";
11
11
  import { Textarea } from "@/components/ui/textarea";
12
+ import { ReviewInbox } from "@/features/review/ReviewInbox";
12
13
  import { useAppStore } from "@/store/appStore";
13
14
  import { asArray, shortId } from "@/lib/utils";
14
15
 
15
16
  type ActTab = "agents" | "runs" | "workflows" | "hooks" | "tools";
16
17
  type RunsSubTab = "runs" | "review";
17
- type ReviewSourceFilter = "all" | "workflow_run" | "trigger" | "kg_change_digest";
18
18
 
19
19
  const runsSubTabs: Array<{ id: RunsSubTab; label: string }> = [
20
20
  { id: "runs", label: "Runs" },
21
- { id: "review", label: "Review" },
22
- ];
23
-
24
- const reviewSourceFilters: Array<{ id: ReviewSourceFilter; label: string }> = [
25
- { id: "all", label: "All" },
26
- { id: "workflow_run", label: "Workflow" },
27
- { id: "trigger", label: "Trigger" },
28
- { id: "kg_change_digest", label: "KG digest" },
21
+ { id: "review", label: "Review Center" },
29
22
  ];
30
23
 
31
24
  const tabs: Array<{ id: ActTab; label: string }> = [
@@ -150,7 +143,7 @@ function RunsPanel({ subTab, onSubTabChange }: { subTab: RunsSubTab; onSubTabCha
150
143
  return (
151
144
  <div className="space-y-4">
152
145
  <Tabs tabs={runsSubTabs} value={subTab} onChange={(id) => onSubTabChange(id as RunsSubTab)} />
153
- {subTab === "runs" ? <RunsListPanel /> : <ReviewInboxPanel />}
146
+ {subTab === "runs" ? <RunsListPanel /> : <ReviewInbox />}
154
147
  </div>
155
148
  );
156
149
  }
@@ -198,171 +191,6 @@ function RunsListPanel() {
198
191
  );
199
192
  }
200
193
 
201
- function reviewStatusVariant(status: string): React.ComponentProps<typeof Badge>["variant"] {
202
- if (status === "pending") return "warning";
203
- if (status === "snoozed") return "muted";
204
- if (status === "approved") return "success";
205
- if (status === "dismissed") return "danger";
206
- return "muted";
207
- }
208
-
209
- function reviewSourceLabel(source?: string) {
210
- if (source === "workflow_run") return "Workflow run";
211
- if (source === "trigger") return "Trigger";
212
- if (source === "kg_change_digest") return "KG digest";
213
- return source || "Automation";
214
- }
215
-
216
- function reviewSourceDetail(provenance: Record<string, unknown>, source?: string) {
217
- const detail = provenance.source_detail;
218
- if (detail != null && String(detail).trim()) return String(detail);
219
- const triggerId = provenance.trigger_id;
220
- if (triggerId != null && String(triggerId).trim()) return String(triggerId);
221
- return reviewSourceLabel(source);
222
- }
223
-
224
- function defaultSnoozeUntil() {
225
- const until = new Date();
226
- until.setDate(until.getDate() + 1);
227
- return until.toISOString();
228
- }
229
-
230
- function ReviewInboxPanel() {
231
- const mode = useAppStore((state) => state.mode);
232
- const qc = useQueryClient();
233
- const [sourceFilter, setSourceFilter] = React.useState<ReviewSourceFilter>("all");
234
- const [runFeedback, setRunFeedback] = React.useState<Record<string, string>>({});
235
- const reviews = useQuery({
236
- queryKey: ["automationReviews", sourceFilter],
237
- queryFn: () => latticeApi.automationReviews({
238
- status: "pending",
239
- ...(sourceFilter !== "all" ? { source: sourceFilter } : {}),
240
- }),
241
- });
242
- const items = asArray<ReviewItem>((reviews.data?.data as { items?: ReviewItem[] })?.items);
243
- const actionable = (item: ReviewItem) => item.effective_status === "pending" || item.effective_status === "snoozed";
244
-
245
- const actOnReview = async (
246
- item: ReviewItem,
247
- action: "approve" | "dismiss" | "snooze" | "run_now",
248
- hadRunBefore = false,
249
- ) => {
250
- const call =
251
- action === "approve" ? () => latticeApi.approveReviewItem(item.id) :
252
- action === "dismiss" ? () => latticeApi.dismissReviewItem(item.id) :
253
- action === "snooze" ? () => latticeApi.snoozeReviewItem(item.id, defaultSnoozeUntil()) :
254
- () => latticeApi.runNowReviewItem(item.id);
255
- const result = await call();
256
- if (result.ok) {
257
- if (action === "run_now") {
258
- setRunFeedback((prev) => ({
259
- ...prev,
260
- [item.id]: hadRunBefore ? "Regenerated" : "Executed",
261
- }));
262
- } else {
263
- setRunFeedback((prev) => {
264
- const next = { ...prev };
265
- delete next[item.id];
266
- return next;
267
- });
268
- }
269
- await qc.invalidateQueries({ queryKey: ["automationReviews"] });
270
- }
271
- return result;
272
- };
273
-
274
- if (reviews.isLoading) return <LoadingPanel title="Review inbox" />;
275
-
276
- return (
277
- <Card>
278
- <CardHeader className="gap-3">
279
- <div className="flex flex-wrap items-start justify-between gap-3">
280
- <div>
281
- <CardTitle>Review inbox</CardTitle>
282
- <CardDescription>Automation suggestions waiting for your decision. Run now previews without approving.</CardDescription>
283
- </div>
284
- {reviews.data ? (
285
- <Badge variant={reviews.data.ok ? "success" : "warning"}>{reviews.data.ok ? "connected" : "unavailable"}</Badge>
286
- ) : null}
287
- </div>
288
- <Tabs
289
- tabs={reviewSourceFilters}
290
- value={sourceFilter}
291
- onChange={(id) => setSourceFilter(id as ReviewSourceFilter)}
292
- />
293
- </CardHeader>
294
- <CardContent>
295
- {reviews.isError || (reviews.data && !reviews.data.ok) ? (
296
- <EmptyState
297
- title="Could not load review inbox"
298
- detail={reviews.data?.error || "The review queue is not available right now."}
299
- />
300
- ) : !items.length ? (
301
- <EmptyState
302
- title="Nothing to review"
303
- detail="When automations opt into the review queue, new suggestions will appear here."
304
- />
305
- ) : (
306
- <div className="grid gap-3">
307
- {items.map((item) => {
308
- const provenance = (item.provenance || {}) as Record<string, unknown>;
309
- const payload = (item.payload || {}) as Record<string, unknown>;
310
- const hadRunBefore = Boolean(payload.last_run_id || provenance.run_id);
311
- const feedback = runFeedback[item.id];
312
- return (
313
- <div key={item.id} className="rounded-lg border border-border bg-background/55 p-4">
314
- <div className="flex flex-wrap items-start justify-between gap-3">
315
- <div className="min-w-0 flex-1">
316
- <div className="font-medium">{item.title}</div>
317
- {item.summary ? <p className="mt-1 text-sm text-muted-foreground">{item.summary}</p> : null}
318
- </div>
319
- <div className="flex flex-wrap items-center gap-2">
320
- <Badge variant="muted">{reviewSourceLabel(item.source)}</Badge>
321
- <Badge variant={reviewStatusVariant(item.effective_status)}>{item.effective_status}</Badge>
322
- </div>
323
- </div>
324
- {mode !== "basic" ? (
325
- <div className="mt-3">
326
- <KeyValueList
327
- data={{
328
- workflow: provenance.workflow_id,
329
- trigger: provenance.trigger_id,
330
- run: payload.last_run_id || provenance.run_id,
331
- source_detail: reviewSourceDetail(provenance, item.source),
332
- snoozed_until: item.snoozed_until,
333
- created_at: item.created_at,
334
- updated_at: item.updated_at,
335
- }}
336
- limit={8}
337
- />
338
- </div>
339
- ) : null}
340
- {actionable(item) ? (
341
- <div className="mt-4 flex flex-wrap gap-2">
342
- <ActionButton
343
- label="Run now"
344
- successLabel={hadRunBefore ? "Regenerated" : "Executed"}
345
- action={() => actOnReview(item, "run_now", hadRunBefore)}
346
- invalidate={[]}
347
- />
348
- <ActionButton label="Approve" action={() => actOnReview(item, "approve")} invalidate={[]} />
349
- <ActionButton label="Snooze" action={() => actOnReview(item, "snooze")} invalidate={[]} />
350
- <ActionButton label="Dismiss" action={() => actOnReview(item, "dismiss")} invalidate={[]} variant="destructive" />
351
- </div>
352
- ) : null}
353
- {feedback ? (
354
- <p className="mt-2 text-xs text-emerald-300">{feedback} — item stays open until you approve or dismiss.</p>
355
- ) : null}
356
- </div>
357
- );
358
- })}
359
- </div>
360
- )}
361
- </CardContent>
362
- </Card>
363
- );
364
- }
365
-
366
194
  function RunList({ runs, kind }: { runs: Array<Record<string, unknown>>; kind: "agent" | "workflow" }) {
367
195
  if (!runs.length) return <EntityList items={[]} />;
368
196
  return (
@@ -32,6 +32,7 @@ export const routeAliases: Record<string, { primary: PrimaryRoute; tab?: string
32
32
  agents: { primary: "act", tab: "agents" },
33
33
  runs: { primary: "act", tab: "runs" },
34
34
  review: { primary: "act", tab: "review" },
35
+ "review-center": { primary: "act", tab: "review" },
35
36
  workflows: { primary: "act", tab: "workflows" },
36
37
  planning: { primary: "act", tab: "agents" },
37
38
  hooks: { primary: "act", tab: "hooks" },
@@ -26,7 +26,7 @@ from .storage import (
26
26
  storage_from_env,
27
27
  )
28
28
 
29
- __version__ = "5.6.0"
29
+ __version__ = "6.0.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 = "5.6.0"
22
+ MULTI_AGENT_VERSION = "6.0.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__ = "5.6.0"
3
+ __version__ = "6.0.0"
@@ -7,8 +7,8 @@ frontend without massaging.
7
7
 
8
8
  Action semantics live in :class:`~latticeai.services.review_queue.ReviewQueueService`:
9
9
 
10
- * ``approve`` / ``dismiss`` / ``snooze`` are status transitions; an illegal
11
- transition returns **409**.
10
+ * ``approve`` / ``dismiss`` / ``snooze`` / ``unsnooze`` are status transitions;
11
+ an illegal transition returns **409**.
12
12
  * ``run_now`` previews/regenerates without changing status (back-links the run).
13
13
  """
14
14
 
@@ -125,6 +125,10 @@ def create_review_queue_router(
125
125
  append_audit_event("review_item_snooze", user_email=user, item_id=item_id)
126
126
  return item
127
127
 
128
+ @router.post("/automation/reviews/{item_id}/unsnooze", response_model=ReviewItem)
129
+ async def unsnooze_item(item_id: str, request: Request):
130
+ return _act(request, item_id, "unsnooze")
131
+
128
132
  @router.post("/automation/reviews/{item_id}/run_now", response_model=ReviewItem)
129
133
  async def run_now_item(item_id: str, request: Request):
130
134
  user = require_user(request)
@@ -155,4 +159,4 @@ def create_review_queue_router(
155
159
  append_audit_event(f"review_item_{action}", user_email=user, item_id=item_id)
156
160
  return item
157
161
 
158
- return router
162
+ return router