ltcai 4.3.0 → 4.3.3

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 (71) hide show
  1. package/README.md +186 -276
  2. package/bin/ltcai.js +6 -2
  3. package/docs/CHANGELOG.md +124 -3
  4. package/docs/V4_3_2_DEADCODE_AUDIT_REPORT.md +174 -0
  5. package/docs/V4_3_2_DOCUMENTATION_CLEANUP_REPORT.md +81 -0
  6. package/docs/V4_3_2_GITHUB_VERCEL_CHECK_REPORT.md +75 -0
  7. package/docs/V4_3_2_GRAPH_UX_REPORT.md +48 -0
  8. package/docs/V4_3_2_INDEPENDENT_AUDIT_PACKAGE.md +209 -0
  9. package/docs/V4_3_2_PRODUCT_POLISH_REPORT.md +57 -0
  10. package/docs/V4_3_2_SELF_AUDIT_REPORT.md +63 -0
  11. package/docs/V4_3_2_VALIDATION_REPORT.md +97 -0
  12. package/docs/V4_3_3_VALIDATION_REPORT.md +46 -0
  13. package/docs/V4_DIGITAL_BRAIN_RECOVERY.md +18 -25
  14. package/frontend/openapi.json +11 -1
  15. package/frontend/src/App.tsx +15 -1
  16. package/frontend/src/api/client.ts +19 -1
  17. package/frontend/src/api/openapi.ts +10 -0
  18. package/frontend/src/components/primitives.tsx +92 -10
  19. package/frontend/src/pages/Act.tsx +72 -9
  20. package/frontend/src/pages/Ask.tsx +2 -2
  21. package/frontend/src/pages/Brain.tsx +607 -65
  22. package/frontend/src/pages/Capture.tsx +11 -7
  23. package/frontend/src/pages/Library.tsx +12 -6
  24. package/frontend/src/pages/System.tsx +186 -23
  25. package/lattice_brain/__init__.py +1 -1
  26. package/lattice_brain/archive.py +3 -3
  27. package/lattice_brain/storage/sqlite.py +15 -2
  28. package/latticeai/__init__.py +1 -1
  29. package/latticeai/api/agents.py +3 -1
  30. package/latticeai/api/models.py +66 -18
  31. package/latticeai/brain/projection.py +12 -2
  32. package/latticeai/brain/retrieval.py +10 -0
  33. package/latticeai/brain/store.py +6 -1
  34. package/latticeai/core/config.py +3 -1
  35. package/latticeai/core/marketplace.py +1 -1
  36. package/latticeai/core/multi_agent.py +1 -1
  37. package/latticeai/core/product_hardening.py +2 -1
  38. package/latticeai/core/workspace_os.py +1 -1
  39. package/latticeai/services/agent_runtime.py +52 -12
  40. package/latticeai/services/model_runtime.py +83 -2
  41. package/ltcai_cli.py +14 -3
  42. package/package.json +5 -7
  43. package/requirements.txt +17 -0
  44. package/scripts/build_vercel_static.mjs +77 -0
  45. package/scripts/check_markdown_links.mjs +75 -0
  46. package/src-tauri/Cargo.lock +1 -1
  47. package/src-tauri/Cargo.toml +1 -1
  48. package/src-tauri/src/main.rs +269 -27
  49. package/src-tauri/tauri.conf.json +20 -1
  50. package/static/app/asset-manifest.json +5 -5
  51. package/static/app/assets/index-CHHal8Zl.css +2 -0
  52. package/static/app/assets/index-pdzil9ac.js +333 -0
  53. package/static/app/assets/index-pdzil9ac.js.map +1 -0
  54. package/static/app/index.html +2 -2
  55. package/latticeai/api/deps.py +0 -15
  56. package/scripts/capture/README.md +0 -28
  57. package/scripts/capture/capture_enterprise.js +0 -8
  58. package/scripts/capture/capture_graph.js +0 -8
  59. package/scripts/capture/capture_onboarding.js +0 -8
  60. package/scripts/capture/capture_page.js +0 -43
  61. package/scripts/capture/capture_release_media.js +0 -125
  62. package/scripts/capture/capture_skills.js +0 -8
  63. package/scripts/capture/capture_v340.js +0 -88
  64. package/scripts/capture/capture_workspace.js +0 -8
  65. package/scripts/generate_diagrams.py +0 -512
  66. package/scripts/release-0.3.1.sh +0 -105
  67. package/scripts/take_screenshots.js +0 -69
  68. package/static/app/assets/index-RiJTJliG.js +0 -333
  69. package/static/app/assets/index-RiJTJliG.js.map +0 -1
  70. package/static/app/assets/index-yZswHE3d.css +0 -2
  71. package/static/css/tokens.3ba22e37.css +0 -260
@@ -6183,6 +6183,11 @@ export interface components {
6183
6183
  LoadModelRequest: {
6184
6184
  /** Adapter Path */
6185
6185
  adapter_path?: string | null;
6186
+ /**
6187
+ * Allow Download
6188
+ * @default false
6189
+ */
6190
+ allow_download: boolean;
6186
6191
  /** Draft Model Id */
6187
6192
  draft_model_id?: string | null;
6188
6193
  /** Engine */
@@ -6416,6 +6421,11 @@ export interface components {
6416
6421
  };
6417
6422
  /** PrepareModelRequest */
6418
6423
  PrepareModelRequest: {
6424
+ /**
6425
+ * Allow Download
6426
+ * @default false
6427
+ */
6428
+ allow_download: boolean;
6419
6429
  /** Engine */
6420
6430
  engine?: string | null;
6421
6431
  /** Model */
@@ -5,7 +5,7 @@ import { ApiResult } from "@/api/client";
5
5
  import { Badge } from "@/components/ui/badge";
6
6
  import { Button } from "@/components/ui/button";
7
7
  import { Card, CardContent, CardDescription, CardHeader, CardTitle } from "@/components/ui/card";
8
- import { cn, asArray, fmtNumber, titleize } from "@/lib/utils";
8
+ import { cn, asArray, fmtNumber, shortId, titleize } from "@/lib/utils";
9
9
 
10
10
  export function SourceBadge({ result }: { result?: Pick<ApiResult, "source" | "ok" | "status"> }) {
11
11
  if (!result) return <Badge variant="muted">not loaded</Badge>;
@@ -81,12 +81,41 @@ export function StatGrid({ stats }: { stats: Array<{ label: string; value: unkno
81
81
  );
82
82
  }
83
83
 
84
- export function JsonView({ value }: { value: unknown }) {
85
- return (
86
- <pre className="max-h-80 overflow-auto rounded-md border border-border bg-muted/40 p-3 text-xs leading-relaxed text-muted-foreground">
87
- {JSON.stringify(value, null, 2)}
88
- </pre>
89
- );
84
+ function isRecord(value: unknown): value is Record<string, unknown> {
85
+ return Boolean(value && typeof value === "object" && !Array.isArray(value));
86
+ }
87
+
88
+ function scalarText(value: unknown) {
89
+ if (value === null || value === undefined || value === "") return "-";
90
+ if (typeof value === "number") return Number.isFinite(value) ? fmtNumber(value) : "-";
91
+ if (typeof value === "boolean") return value ? "Enabled" : "Disabled";
92
+ return String(value);
93
+ }
94
+
95
+ export function ValuePreview({ value }: { value: unknown }) {
96
+ if (typeof value === "boolean") {
97
+ return <Badge variant={value ? "success" : "muted"}>{value ? "enabled" : "disabled"}</Badge>;
98
+ }
99
+ if (Array.isArray(value)) {
100
+ if (!value.length) return <span className="text-muted-foreground">None</span>;
101
+ const primitive = value.every((item) => item === null || ["string", "number", "boolean"].includes(typeof item));
102
+ if (primitive) {
103
+ return (
104
+ <span className="flex flex-wrap gap-1">
105
+ {value.slice(0, 5).map((item, index) => <Badge key={`${String(item)}-${index}`} variant="muted">{scalarText(item)}</Badge>)}
106
+ {value.length > 5 ? <Badge variant="muted">+{value.length - 5}</Badge> : null}
107
+ </span>
108
+ );
109
+ }
110
+ return <span className="text-muted-foreground">{fmtNumber(value.length)} records</span>;
111
+ }
112
+ if (isRecord(value)) {
113
+ const keys = Object.keys(value);
114
+ if (!keys.length) return <span className="text-muted-foreground">No fields</span>;
115
+ return <span className="text-muted-foreground">{keys.slice(0, 4).map(titleize).join(", ")}{keys.length > 4 ? ` +${keys.length - 4}` : ""}</span>;
116
+ }
117
+ const text = scalarText(value);
118
+ return <span className="break-words">{text.length > 96 ? shortId(text, 96) : text}</span>;
90
119
  }
91
120
 
92
121
  export function KeyValueList({ data, limit = 8 }: { data: Record<string, unknown>; limit?: number }) {
@@ -97,13 +126,63 @@ export function KeyValueList({ data, limit = 8 }: { data: Record<string, unknown
97
126
  {rows.map(([key, value]) => (
98
127
  <div key={key} className="grid grid-cols-[minmax(9rem,0.5fr)_1fr] gap-3 p-3 text-sm">
99
128
  <span className="font-medium text-muted-foreground">{titleize(key)}</span>
100
- <span className="break-words">{typeof value === "object" ? JSON.stringify(value) : String(value ?? "-")}</span>
129
+ <span className="min-w-0 break-words"><ValuePreview value={value} /></span>
101
130
  </div>
102
131
  ))}
103
132
  </div>
104
133
  );
105
134
  }
106
135
 
136
+ export function StructuredView({
137
+ value,
138
+ titleKey = "title",
139
+ metaKey = "status",
140
+ limit = 8,
141
+ }: {
142
+ value: unknown;
143
+ titleKey?: string;
144
+ metaKey?: string;
145
+ limit?: number;
146
+ }) {
147
+ if (Array.isArray(value)) {
148
+ if (!value.length) return <EmptyState title="No records" detail="The API returned an empty collection." />;
149
+ if (value.every((item) => isRecord(item))) {
150
+ return <EntityList items={value} titleKey={titleKey} metaKey={metaKey} limit={limit} />;
151
+ }
152
+ return (
153
+ <div className="flex flex-wrap gap-1 rounded-md border border-border bg-background p-3">
154
+ {value.slice(0, limit).map((item, index) => <Badge key={`${String(item)}-${index}`} variant="muted">{scalarText(item)}</Badge>)}
155
+ {value.length > limit ? <Badge variant="muted">+{value.length - limit}</Badge> : null}
156
+ </div>
157
+ );
158
+ }
159
+ if (isRecord(value)) return <KeyValueList data={value} limit={limit} />;
160
+ return (
161
+ <div className="rounded-md border border-border bg-background p-3 text-sm">
162
+ <ValuePreview value={value} />
163
+ </div>
164
+ );
165
+ }
166
+
167
+ export function OperationResult({
168
+ result,
169
+ successLabel = "Request completed",
170
+ }: {
171
+ result?: ApiResult<unknown> | null;
172
+ successLabel?: string;
173
+ }) {
174
+ if (!result) return null;
175
+ if (!result.ok) {
176
+ return <EmptyState title="Request unavailable" detail={result.error || <ValuePreview value={result.data} />} />;
177
+ }
178
+ return (
179
+ <div className="space-y-2 rounded-md border border-border bg-background p-3">
180
+ <Badge variant="success">{successLabel}</Badge>
181
+ <StructuredView value={result.data} />
182
+ </div>
183
+ );
184
+ }
185
+
107
186
  export function EntityList({
108
187
  items,
109
188
  titleKey = "title",
@@ -125,8 +204,11 @@ export function EntityList({
125
204
  <div className="font-medium">{String(item[titleKey] || item.name || item.id || `Record ${index + 1}`)}</div>
126
205
  <Badge variant="muted">{String(item[metaKey] || item.status || item.state || "record")}</Badge>
127
206
  </div>
128
- {item.summary || item.description || item.path ? (
129
- <p className="mt-1 text-sm text-muted-foreground">{String(item.summary || item.description || item.path)}</p>
207
+ {item.summary || item.description || item.path || (item.id && item[titleKey] !== item.id) ? (
208
+ <p className="mt-1 text-sm text-muted-foreground">{String(item.summary || item.description || item.path || item.id)}</p>
209
+ ) : null}
210
+ {item.id && item[titleKey] !== item.id ? (
211
+ <div className="mt-1 text-xs text-muted-foreground">{shortId(item.id, 48)}</div>
130
212
  ) : null}
131
213
  </div>
132
214
  ))}
@@ -3,7 +3,7 @@ import { useMutation, useQuery, useQueryClient } from "@tanstack/react-query";
3
3
  import ReactFlow, { Background, Controls, Edge, Node } from "reactflow";
4
4
  import { Bot, GitBranch, PauseCircle, Play, Workflow } from "lucide-react";
5
5
  import { latticeApi } from "@/api/client";
6
- import { ActionButton, DataPanel, EntityList, JsonView, Tabs } from "@/components/primitives";
6
+ import { ActionButton, DataPanel, EntityList, KeyValueList, 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";
@@ -58,6 +58,11 @@ function AgentsPanel() {
58
58
  mutationFn: () => latticeApi.registerAgent({ name: agentName, type: "custom", capabilities: [] }),
59
59
  onSuccess: () => qc.invalidateQueries({ queryKey: ["agentRegistry"] }),
60
60
  });
61
+ const runtimeData = (runtime.data?.data || {}) as Record<string, unknown>;
62
+ const runtimeMeta = (runtimeData.runtime || {}) as Record<string, unknown>;
63
+ const runtimeReady = Boolean(runtimeMeta.ready);
64
+ const runtimeReason = String(runtimeMeta.unavailable_reason || "Load an LLM-backed model before running agents.");
65
+ const canRunAgent = Boolean(goal.trim()) && runtimeReady && !run.isPending;
61
66
  return (
62
67
  <div className="grid gap-4 xl:grid-cols-[0.9fr_1.1fr]">
63
68
  <Card>
@@ -67,12 +72,20 @@ function AgentsPanel() {
67
72
  </CardHeader>
68
73
  <CardContent className="space-y-3">
69
74
  <Textarea value={goal} onChange={(e) => setGoal(e.target.value)} placeholder="Describe the objective..." />
70
- <Button disabled={!goal.trim() || run.isPending} onClick={() => run.mutate()}><Play className="h-4 w-4" /> Run planner/executor/reviewer</Button>
71
- {run.data ? <JsonView value={run.data.data || run.data.error} /> : null}
75
+ {!runtimeReady ? <Badge variant="warning">{runtimeReason}</Badge> : null}
76
+ <Button
77
+ className="w-full"
78
+ variant={runtimeReady ? "default" : "outline"}
79
+ disabled={!canRunAgent}
80
+ onClick={() => run.mutate()}
81
+ >
82
+ <Play className="h-4 w-4" /> {runtimeReady ? "Run pipeline" : "Agent execution unavailable"}
83
+ </Button>
84
+ {run.data ? <OperationResult result={run.data} successLabel="Agent run request completed" /> : null}
72
85
  </CardContent>
73
86
  </Card>
74
87
  <DataPanel title="Runtime status" result={runtime.data}>
75
- {(data) => <JsonView value={data} />}
88
+ {(data) => <StructuredView value={data} />}
76
89
  </DataPanel>
77
90
  <DataPanel title="Agent registry" result={registry.data}>
78
91
  {(data) => (
@@ -86,7 +99,7 @@ function AgentsPanel() {
86
99
  )}
87
100
  </DataPanel>
88
101
  <DataPanel title="Agent capabilities" result={caps.data}>
89
- {(data) => <JsonView value={data} />}
102
+ {(data) => <StructuredView value={data} />}
90
103
  </DataPanel>
91
104
  </div>
92
105
  );
@@ -116,7 +129,9 @@ function RunsPanel() {
116
129
  <div key={token} className="flex flex-wrap items-center justify-between gap-3 rounded-md border border-border p-3">
117
130
  <div>
118
131
  <div className="font-medium">{shortId(token, 16)}</div>
119
- <div className="text-sm text-muted-foreground">{JSON.stringify(value)}</div>
132
+ <div className="mt-2">
133
+ <KeyValueList data={(value || {}) as Record<string, unknown>} limit={5} />
134
+ </div>
120
135
  </div>
121
136
  <div className="flex gap-2">
122
137
  <ActionButton label="Approve" action={() => latticeApi.approvePermission(token)} invalidate={["permissions"]} />
@@ -162,8 +177,26 @@ function RunList({ runs, kind }: { runs: Array<Record<string, unknown>>; kind: "
162
177
  }
163
178
 
164
179
  function WorkflowsPanel() {
180
+ const qc = useQueryClient();
165
181
  const defs = useQuery({ queryKey: ["workflowDefinitions"], queryFn: latticeApi.workflowDefinitions });
166
182
  const triggers = useQuery({ queryKey: ["workflowTriggers"], queryFn: latticeApi.workflowTriggers });
183
+ const [name, setName] = React.useState("Manual workflow");
184
+ const [importText, setImportText] = React.useState("");
185
+ const create = useMutation({
186
+ mutationFn: () => latticeApi.createWorkflow({
187
+ name: name.trim() || "Manual workflow",
188
+ nodes: manualWorkflowNodes(),
189
+ metadata: { created_from: "desktop-act-ui" },
190
+ }),
191
+ onSuccess: () => qc.invalidateQueries({ queryKey: ["workflowDefinitions"] }),
192
+ });
193
+ const importWorkflow = useMutation({
194
+ mutationFn: () => latticeApi.importWorkflow(JSON.parse(importText) as Record<string, unknown>),
195
+ onSuccess: () => {
196
+ setImportText("");
197
+ qc.invalidateQueries({ queryKey: ["workflowDefinitions"] });
198
+ },
199
+ });
167
200
  const workflows = asArray<Record<string, unknown>>((defs.data?.data as Record<string, unknown>)?.workflows);
168
201
  const nodes: Node[] = workflows.slice(0, 12).map((workflow, index) => ({
169
202
  id: String(workflow.id || workflow.workflow_id || index),
@@ -189,7 +222,17 @@ function WorkflowsPanel() {
189
222
  </Card>
190
223
  <DataPanel title="Definitions" result={defs.data}>
191
224
  {() => (
192
- <div className="space-y-2">
225
+ <div className="space-y-3">
226
+ <div className="grid gap-2 rounded-md border border-border p-3">
227
+ <div className="flex flex-wrap gap-2">
228
+ <Input value={name} onChange={(event) => setName(event.target.value)} placeholder="Workflow name" />
229
+ <Button disabled={create.isPending} onClick={() => create.mutate()}>Create</Button>
230
+ </div>
231
+ <Textarea value={importText} onChange={(event) => setImportText(event.target.value)} placeholder="Paste exported workflow JSON" />
232
+ <Button variant="outline" disabled={!importText.trim() || importWorkflow.isPending} onClick={() => importWorkflow.mutate()}>Import</Button>
233
+ {create.data ? <OperationResult result={create.data} successLabel="Workflow created" /> : null}
234
+ {importWorkflow.data ? <OperationResult result={importWorkflow.data} successLabel="Workflow imported" /> : null}
235
+ </div>
193
236
  {workflows.length ? workflows.map((workflow) => {
194
237
  const id = String(workflow.id || workflow.workflow_id);
195
238
  return (
@@ -197,6 +240,7 @@ function WorkflowsPanel() {
197
240
  <div className="font-medium">{String(workflow.name || id)}</div>
198
241
  <div className="mt-2 flex gap-2">
199
242
  <ActionButton label="Run" action={() => latticeApi.runWorkflow(id)} invalidate={["workflowRuns"]} />
243
+ <ActionButton label="Export" action={() => latticeApi.exportWorkflow(id)} />
200
244
  </div>
201
245
  </div>
202
246
  );
@@ -205,12 +249,31 @@ function WorkflowsPanel() {
205
249
  )}
206
250
  </DataPanel>
207
251
  <DataPanel title="Trigger configuration" result={triggers.data} className="xl:col-span-2">
208
- {(data) => <JsonView value={data} />}
252
+ {(data) => <StructuredView value={data} />}
209
253
  </DataPanel>
210
254
  </div>
211
255
  );
212
256
  }
213
257
 
258
+ function manualWorkflowNodes(): Array<Record<string, unknown>> {
259
+ return [
260
+ {
261
+ id: "trigger",
262
+ type: "trigger",
263
+ name: "Manual start",
264
+ config: { trigger: "manual" },
265
+ next: "output",
266
+ },
267
+ {
268
+ id: "output",
269
+ type: "output",
270
+ name: "Output",
271
+ config: { value: "Workflow completed" },
272
+ next: null,
273
+ },
274
+ ];
275
+ }
276
+
214
277
  function HooksPanel() {
215
278
  const hooks = useQuery({ queryKey: ["hooks"], queryFn: latticeApi.hooks });
216
279
  const runs = useQuery({ queryKey: ["hookRuns"], queryFn: latticeApi.hookRuns });
@@ -239,7 +302,7 @@ function ToolsPanel() {
239
302
  const tools = useQuery({ queryKey: ["toolPermissions"], queryFn: latticeApi.toolPermissions });
240
303
  return (
241
304
  <DataPanel title="Tool governance" result={tools.data}>
242
- {(data) => <JsonView value={data} />}
305
+ {(data) => <EntityList items={(data as Record<string, unknown>).permissions || data} titleKey="tool" metaKey="risk" />}
243
306
  </DataPanel>
244
307
  );
245
308
  }
@@ -2,7 +2,7 @@ import * as React from "react";
2
2
  import { useMutation, useQuery, useQueryClient } from "@tanstack/react-query";
3
3
  import { ImagePlus, MessageSquare, Send, Trash2 } from "lucide-react";
4
4
  import { latticeApi } from "@/api/client";
5
- import { DataPanel, EmptyState, EntityList, JsonView, SourceBadge } from "@/components/primitives";
5
+ import { DataPanel, EmptyState, EntityList, SourceBadge, StructuredView } from "@/components/primitives";
6
6
  import { Badge } from "@/components/ui/badge";
7
7
  import { Button } from "@/components/ui/button";
8
8
  import { Card, CardContent, CardDescription, CardHeader, CardTitle } from "@/components/ui/card";
@@ -193,7 +193,7 @@ function ContextPreview({ question, trace }: { question: string; trace: unknown
193
193
  <CardTitle>Why this context</CardTitle>
194
194
  <CardDescription>Trace emitted by `/chat` when the backend includes it.</CardDescription>
195
195
  </CardHeader>
196
- <CardContent>{trace ? <JsonView value={trace} /> : <EmptyState title="No trace yet" />}</CardContent>
196
+ <CardContent>{trace ? <StructuredView value={trace} /> : <EmptyState title="No trace yet" />}</CardContent>
197
197
  </Card>
198
198
  </>
199
199
  );