ltcai 6.1.0 → 6.3.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 (70) hide show
  1. package/README.md +36 -38
  2. package/docs/CHANGELOG.md +75 -1
  3. package/frontend/src/App.tsx +3 -1286
  4. package/frontend/src/components/LanguageSwitcher.tsx +23 -0
  5. package/frontend/src/components/ProductFlow.tsx +32 -669
  6. package/frontend/src/components/onboarding/AnalysisScreen.tsx +127 -0
  7. package/frontend/src/components/onboarding/DownloadConsentPanel.tsx +29 -0
  8. package/frontend/src/components/onboarding/InstallScreen.tsx +208 -0
  9. package/frontend/src/components/onboarding/LanguageChooser.tsx +23 -0
  10. package/frontend/src/components/onboarding/LoginScreen.tsx +131 -0
  11. package/frontend/src/components/onboarding/ProductFlowScreens.tsx +13 -0
  12. package/frontend/src/components/onboarding/RecommendationScreen.tsx +59 -0
  13. package/frontend/src/components/onboarding/recommendationModel.ts +132 -0
  14. package/frontend/src/features/admin/AdminConsole.tsx +294 -0
  15. package/frontend/src/features/brain/BrainCarePanel.tsx +254 -0
  16. package/frontend/src/features/brain/BrainComposer.tsx +66 -0
  17. package/frontend/src/features/brain/BrainConversation.tsx +131 -0
  18. package/frontend/src/features/brain/BrainGraphLayer.tsx +132 -0
  19. package/frontend/src/features/brain/BrainHome.tsx +232 -0
  20. package/frontend/src/features/brain/BrainMemoryLayer.tsx +38 -0
  21. package/frontend/src/features/brain/BrainOverviewPanel.tsx +74 -0
  22. package/frontend/src/features/brain/BrainRelationshipLayer.tsx +43 -0
  23. package/frontend/src/features/brain/DepthEmergence.tsx +53 -0
  24. package/frontend/src/features/brain/brainData.ts +98 -0
  25. package/frontend/src/features/brain/graphLayout.ts +26 -0
  26. package/frontend/src/features/brain/types.ts +44 -0
  27. package/frontend/src/features/review/ReviewCard.tsx +3 -1
  28. package/frontend/src/features/review/ReviewInbox.tsx +11 -1
  29. package/frontend/src/i18n.ts +290 -0
  30. package/frontend/src/pages/Brain.tsx +63 -5
  31. package/frontend/src/pages/Capture.tsx +102 -13
  32. package/frontend/src/pages/Library.tsx +72 -6
  33. package/frontend/src/styles.css +220 -0
  34. package/lattice_brain/__init__.py +1 -1
  35. package/lattice_brain/runtime/multi_agent.py +1 -1
  36. package/latticeai/__init__.py +1 -1
  37. package/latticeai/api/tools.py +50 -23
  38. package/latticeai/app_factory.py +59 -76
  39. package/latticeai/cli/entrypoint.py +283 -0
  40. package/latticeai/core/marketplace.py +1 -1
  41. package/latticeai/core/workspace_os.py +1 -1
  42. package/latticeai/integrations/__init__.py +0 -0
  43. package/latticeai/integrations/telegram_bot.py +1009 -0
  44. package/latticeai/runtime/chat_wiring.py +119 -0
  45. package/latticeai/runtime/lifespan_runtime.py +1 -1
  46. package/latticeai/runtime/model_wiring.py +40 -0
  47. package/latticeai/runtime/platform_runtime_wiring.py +86 -0
  48. package/latticeai/runtime/review_wiring.py +37 -0
  49. package/latticeai/runtime/router_registration.py +49 -30
  50. package/latticeai/runtime/tail_wiring.py +21 -0
  51. package/latticeai/services/p_reinforce.py +258 -0
  52. package/latticeai/services/router_context.py +52 -0
  53. package/ltcai_cli.py +7 -279
  54. package/p_reinforce.py +4 -255
  55. package/package.json +3 -2
  56. package/scripts/check_i18n_literals.mjs +52 -0
  57. package/scripts/release_smoke.py +133 -0
  58. package/scripts/wheel_smoke.py +4 -0
  59. package/src-tauri/Cargo.lock +1 -1
  60. package/src-tauri/Cargo.toml +1 -1
  61. package/src-tauri/tauri.conf.json +1 -1
  62. package/static/app/asset-manifest.json +5 -5
  63. package/static/app/assets/index-D76dWuQk.js +16 -0
  64. package/static/app/assets/index-D76dWuQk.js.map +1 -0
  65. package/static/app/assets/index-Div5vMlq.css +2 -0
  66. package/static/app/index.html +2 -2
  67. package/telegram_bot.py +9 -1002
  68. package/static/app/assets/index-B744yblP.css +0 -2
  69. package/static/app/assets/index-DYaUKNfl.js +0 -16
  70. package/static/app/assets/index-DYaUKNfl.js.map +0 -1
@@ -1,6 +1,6 @@
1
1
  import * as React from "react";
2
2
  import { useMutation, useQuery, useQueryClient } from "@tanstack/react-query";
3
- import { FolderPlus, Globe2, HardDrive, Upload } from "lucide-react";
3
+ import { AlertCircle, CheckCircle2, FolderPlus, Globe2, HardDrive, Loader2, RotateCcw, Upload } from "lucide-react";
4
4
  import { latticeApi } from "@/api/client";
5
5
  import { ActionButton, DataPanel, EntityList, OperationResult, StructuredView, Tabs } from "@/components/primitives";
6
6
  import { Button } from "@/components/ui/button";
@@ -41,10 +41,20 @@ export function CapturePage({ initialTab }: { initialTab?: string }) {
41
41
  function FilesPanel() {
42
42
  const qc = useQueryClient();
43
43
  const docs = useQuery({ queryKey: ["documents"], queryFn: () => latticeApi.documents(200) });
44
+ const [queue, setQueue] = React.useState<UploadQueueItem[]>([]);
44
45
  const upload = useMutation({
45
- mutationFn: (files: FileList) => Promise.all(Array.from(files).map((file) => latticeApi.uploadDocument(file))),
46
- onSuccess: () => qc.invalidateQueries({ queryKey: ["documents"] }),
46
+ mutationFn: (files: File[]) => uploadFiles(files, setQueue),
47
+ onSuccess: () => {
48
+ void qc.invalidateQueries({ queryKey: ["documents"] });
49
+ void qc.invalidateQueries({ queryKey: ["graphStats"] });
50
+ void qc.invalidateQueries({ queryKey: ["memoryManager"] });
51
+ },
47
52
  });
53
+ const beginUpload = React.useCallback((files: FileList | File[]) => {
54
+ const nextFiles = Array.from(files);
55
+ if (!nextFiles.length) return;
56
+ upload.mutate(nextFiles);
57
+ }, [upload]);
48
58
  return (
49
59
  <div className="grid gap-4 xl:grid-cols-[0.75fr_1.25fr]">
50
60
  <Card>
@@ -53,26 +63,105 @@ function FilesPanel() {
53
63
  <CardDescription>Choose files and Lattice will prepare them for search and memory.</CardDescription>
54
64
  </CardHeader>
55
65
  <CardContent>
56
- <label className="flex min-h-56 cursor-pointer flex-col items-center justify-center gap-3 rounded-lg border border-dashed border-border bg-muted/30 p-6 text-center transition hover:bg-muted/50">
66
+ <label
67
+ className="flex min-h-56 cursor-pointer flex-col items-center justify-center gap-3 rounded-lg border border-dashed border-border bg-muted/30 p-6 text-center transition hover:bg-muted/50"
68
+ onDragOver={(event) => event.preventDefault()}
69
+ onDrop={(event) => {
70
+ event.preventDefault();
71
+ beginUpload(event.dataTransfer.files);
72
+ }}
73
+ >
57
74
  <Upload className="h-7 w-7 text-primary" />
58
- <span className="text-lg font-semibold">Choose files</span>
59
- <span className="max-w-sm text-sm leading-6 text-muted-foreground">PDF, Office files, notes, markdown, text, and spreadsheets are all welcome.</span>
60
- <input type="file" multiple className="sr-only" onChange={(e) => e.target.files && upload.mutate(e.target.files)} />
75
+ <span className="text-lg font-semibold">Drop files or choose documents</span>
76
+ <span className="max-w-sm text-sm leading-6 text-muted-foreground">Each file is queued, parsed, written to Memory, and linked into the Brain graph with source metadata.</span>
77
+ <input type="file" multiple className="sr-only" onChange={(e) => e.target.files && beginUpload(e.target.files)} />
61
78
  </label>
62
- {upload.data ? (
63
- <div className="mt-3 space-y-2">
64
- {upload.data.map((item, index) => <OperationResult key={index} result={item} successLabel="Upload completed" />)}
65
- </div>
66
- ) : null}
79
+ <DocumentUploadQueue queue={queue} onRetry={(file) => beginUpload([file])} />
67
80
  </CardContent>
68
81
  </Card>
69
82
  <DataPanel title="Uploaded documents" result={docs.data}>
70
- {(data) => <EntityList items={(data as Record<string, unknown>).documents || data} titleKey="filename" metaKey="ingest_state" limit={12} />}
83
+ {(data) => (
84
+ <div className="space-y-3">
85
+ <EntityList items={(data as Record<string, unknown>).documents || data} titleKey="filename" metaKey="ingest_state" limit={12} />
86
+ <div className="rounded-md border border-border bg-background/55 p-3 text-sm text-muted-foreground">
87
+ Completed uploads appear here after they enter Memory. Graph links may continue building briefly after parsing finishes.
88
+ </div>
89
+ </div>
90
+ )}
71
91
  </DataPanel>
72
92
  </div>
73
93
  );
74
94
  }
75
95
 
96
+ type UploadQueueItem = {
97
+ id: string;
98
+ file: File;
99
+ name: string;
100
+ size: number;
101
+ status: "queued" | "uploading" | "done" | "failed";
102
+ result?: Awaited<ReturnType<typeof latticeApi.uploadDocument>>;
103
+ };
104
+
105
+ async function uploadFiles(files: File[], setQueue: React.Dispatch<React.SetStateAction<UploadQueueItem[]>>) {
106
+ const rows = files.map((file) => ({
107
+ id: `${file.name}-${file.size}-${file.lastModified}-${Date.now()}`,
108
+ file,
109
+ name: file.name,
110
+ size: file.size,
111
+ status: "queued" as const,
112
+ }));
113
+ setQueue((items) => [...rows, ...items].slice(0, 12));
114
+ const results = [];
115
+ for (const row of rows) {
116
+ setQueue((items) => items.map((item) => item.id === row.id ? { ...item, status: "uploading" } : item));
117
+ const result = await latticeApi.uploadDocument(row.file);
118
+ results.push(result);
119
+ setQueue((items) => items.map((item) => item.id === row.id ? { ...item, status: result.ok ? "done" : "failed", result } : item));
120
+ }
121
+ return results;
122
+ }
123
+
124
+ function DocumentUploadQueue({ queue, onRetry }: { queue: UploadQueueItem[]; onRetry: (file: File) => void }) {
125
+ if (!queue.length) return null;
126
+ return (
127
+ <div className="mt-4 space-y-2">
128
+ {queue.map((item) => {
129
+ const detail = uploadResultDetail(item);
130
+ return (
131
+ <div key={item.id} className="rounded-md border border-border bg-background/55 p-3 text-sm">
132
+ <div className="flex flex-wrap items-start justify-between gap-3">
133
+ <div>
134
+ <div className="flex items-center gap-2 font-medium">
135
+ {item.status === "done" ? <CheckCircle2 className="h-4 w-4 text-emerald-400" /> : item.status === "failed" ? <AlertCircle className="h-4 w-4 text-amber-400" /> : <Loader2 className="h-4 w-4 animate-spin text-primary" />}
136
+ {item.name}
137
+ </div>
138
+ <div className="mt-1 text-xs text-muted-foreground">
139
+ {Math.max(1, Math.round(item.size / 1024))} KB · {detail}
140
+ </div>
141
+ </div>
142
+ {item.status === "failed" ? (
143
+ <Button size="sm" variant="outline" onClick={() => onRetry(item.file)}>
144
+ <RotateCcw className="h-3.5 w-3.5" /> Retry
145
+ </Button>
146
+ ) : null}
147
+ </div>
148
+ {item.result ? <OperationResult result={item.result} successLabel="Entered Brain and graph queue" /> : null}
149
+ </div>
150
+ );
151
+ })}
152
+ </div>
153
+ );
154
+ }
155
+
156
+ function uploadResultDetail(item: UploadQueueItem) {
157
+ if (item.status === "queued") return "Waiting in the ingest queue";
158
+ if (item.status === "uploading") return "Parsing and sending to Memory";
159
+ if (!item.result?.ok) return item.result?.error || "Ingest failed before it entered the Brain";
160
+ const data = item.result.data || {};
161
+ const node = String(data.node_id || data.graph_node || data.provenance_id || "");
162
+ return node ? `Captured with source metadata · ${node}` : "Captured with source metadata";
163
+ }
164
+
76
165
  function LocalPanel() {
77
166
  const qc = useQueryClient();
78
167
  const [path, setPath] = React.useState("");
@@ -109,12 +109,20 @@ function ModelsPanel() {
109
109
  const profile = (data as Record<string, unknown>).profile as Record<string, unknown> | undefined;
110
110
  return (
111
111
  <div className="space-y-4">
112
- <StatGrid stats={[
113
- { label: "Computer", value: profile?.os ? `${String(profile.os)} ${String(profile.arch || "")}` : "detected" },
114
- { label: "Memory", value: recommendation?.ram_gb ? `${String(recommendation.ram_gb)} GB` : "checking" },
115
- { label: "Top pick", value: topPick?.name || topPick?.id || "choose below" },
116
- { label: "Current", value: current?.name || currentId || "none" },
117
- ]} />
112
+ <ModelRuntimeSummary
113
+ profile={profile || {}}
114
+ recommendation={recommendation || {}}
115
+ current={current}
116
+ currentId={currentId}
117
+ latestProgress={latestProgress}
118
+ lastResult={lastResult}
119
+ onReload={() => {
120
+ if (current) void prepareModel(String(current.recommended_load_id || current.id || currentId), String(current.recommended_engine || current.engine || "local_mlx"), consent);
121
+ }}
122
+ onUnload={() => {
123
+ if (currentId) void latticeApi.unloadModel(currentId).then(() => qc.invalidateQueries({ queryKey: ["models"] }));
124
+ }}
125
+ />
118
126
  <div className="grid gap-2 md:grid-cols-3 xl:grid-cols-6">
119
127
  {[
120
128
  ["Environment Analysis", true, Cpu],
@@ -296,6 +304,64 @@ function AlternativeModels({ compatibility }: { compatibility: Record<string, un
296
304
  );
297
305
  }
298
306
 
307
+ function ModelRuntimeSummary({
308
+ profile,
309
+ recommendation,
310
+ current,
311
+ currentId,
312
+ latestProgress,
313
+ lastResult,
314
+ onReload,
315
+ onUnload,
316
+ }: {
317
+ profile: Record<string, unknown>;
318
+ recommendation: Record<string, unknown>;
319
+ current?: Record<string, unknown>;
320
+ currentId: string;
321
+ latestProgress: Record<string, unknown> | null;
322
+ lastResult: Record<string, unknown> | null;
323
+ onReload: () => void;
324
+ onUnload: () => void;
325
+ }) {
326
+ const loadedName = String(current?.name || current?.id || currentId || "");
327
+ const engine = String(current?.engine || current?.recommended_engine || latestProgress?.engine || "local_mlx");
328
+ const cachePath = String(current?.local_path || current?.storage_location || recommendation.cache_path || recommendation.storage_location || "~/.cache/huggingface / ~/.latticeai/models");
329
+ const progressStage = String(latestProgress?.stage || lastResult?.stage || (loadedName ? "ready" : "idle"));
330
+ return (
331
+ <div className="space-y-3">
332
+ <StatGrid stats={[
333
+ { label: "Computer", value: profile?.os ? `${String(profile.os)} ${String(profile.arch || "")}` : "detected" },
334
+ { label: "Engine", value: engine },
335
+ { label: "Loaded model", value: loadedName || "No local model loaded" },
336
+ { label: "Runtime state", value: progressStage },
337
+ ]} />
338
+ <div className="rounded-lg border border-border bg-background/55 p-3 text-sm">
339
+ <div className="flex flex-wrap items-start justify-between gap-3">
340
+ <div>
341
+ <div className="font-medium">{loadedName ? "Local model runtime is available" : "No model is loaded yet"}</div>
342
+ <div className="mt-1 text-muted-foreground">Cache/storage: {cachePath}</div>
343
+ </div>
344
+ <div className="flex flex-wrap gap-2">
345
+ <Button variant="outline" size="sm" disabled={!loadedName} onClick={onReload}>Reload</Button>
346
+ <Button variant="outline" size="sm" disabled={!loadedName} onClick={onUnload}>Unload</Button>
347
+ </div>
348
+ </div>
349
+ {latestProgress ? (
350
+ <div className="mt-3">
351
+ <div className="flex justify-between text-xs text-muted-foreground">
352
+ <span>{String(latestProgress.message || "Preparing model")}</span>
353
+ <span>{Number(latestProgress.percent || 0)}%</span>
354
+ </div>
355
+ <div className="mt-1 h-2 overflow-hidden rounded-full bg-muted">
356
+ <div className="h-full bg-primary" style={{ width: `${Number(latestProgress.percent || 8)}%` }} />
357
+ </div>
358
+ </div>
359
+ ) : null}
360
+ </div>
361
+ </div>
362
+ );
363
+ }
364
+
299
365
  function ModelRecovery({ error }: { error: Record<string, unknown> }) {
300
366
  const guidance = asArray<string>(error.recovery_guidance);
301
367
  return (
@@ -2982,6 +2982,58 @@ body {
2982
2982
  box-shadow: 0 0 0 1px hsl(var(--brain-core) / 0.25);
2983
2983
  }
2984
2984
 
2985
+ .ritual-form {
2986
+ max-width: 420px;
2987
+ margin: 0 auto;
2988
+ }
2989
+
2990
+ .ritual-field-stack {
2991
+ display: grid;
2992
+ gap: 0.85rem;
2993
+ }
2994
+
2995
+ .ritual-field-label {
2996
+ margin-bottom: 0.25rem;
2997
+ color: hsl(var(--fg-muted));
2998
+ font-size: 0.75rem;
2999
+ font-weight: 760;
3000
+ letter-spacing: 0;
3001
+ text-transform: uppercase;
3002
+ }
3003
+
3004
+ .ritual-error {
3005
+ margin-top: 0.85rem;
3006
+ padding: 0.6rem 0.85rem;
3007
+ border: 1px solid hsl(var(--destructive) / 0.4);
3008
+ border-radius: 8px;
3009
+ background: hsl(var(--destructive) / 0.12);
3010
+ font-size: 0.9rem;
3011
+ }
3012
+
3013
+ .ritual-full-button,
3014
+ .ritual-primary-model-button,
3015
+ .ritual-model-card {
3016
+ width: 100%;
3017
+ }
3018
+
3019
+ .ritual-full-button {
3020
+ margin-top: 1rem;
3021
+ }
3022
+
3023
+ .ritual-note,
3024
+ .ritual-local-note {
3025
+ color: hsl(var(--fg-muted));
3026
+ font-size: 0.75rem;
3027
+ }
3028
+
3029
+ .ritual-note {
3030
+ margin-top: 0.6rem;
3031
+ }
3032
+
3033
+ .ritual-local-note {
3034
+ margin-top: 0.9rem;
3035
+ }
3036
+
2985
3037
  .ritual-fact-grid {
2986
3038
  display: grid;
2987
3039
  grid-template-columns: repeat(auto-fit, minmax(140px, 1fr));
@@ -3011,6 +3063,59 @@ body {
3011
3063
  color: hsl(var(--fg));
3012
3064
  }
3013
3065
 
3066
+ .ritual-fact-detail,
3067
+ .ritual-muted-text,
3068
+ .ritual-muted-hint,
3069
+ .ritual-status-card {
3070
+ color: hsl(var(--fg-muted));
3071
+ }
3072
+
3073
+ .ritual-fact-detail {
3074
+ margin-top: 0.2rem;
3075
+ font-size: 0.8rem;
3076
+ }
3077
+
3078
+ .ritual-analysis-card,
3079
+ .ritual-centered-actions {
3080
+ margin-top: 1rem;
3081
+ }
3082
+
3083
+ .ritual-centered-actions {
3084
+ display: flex;
3085
+ justify-content: center;
3086
+ }
3087
+
3088
+ .ritual-inline-row {
3089
+ display: flex;
3090
+ align-items: center;
3091
+ gap: 0.6rem;
3092
+ }
3093
+
3094
+ .ritual-core-icon {
3095
+ color: hsl(var(--brain-core));
3096
+ }
3097
+
3098
+ .ritual-strong-text {
3099
+ font-weight: 620;
3100
+ }
3101
+
3102
+ .ritual-muted-text {
3103
+ font-size: 0.9rem;
3104
+ }
3105
+
3106
+ .ritual-wide-button {
3107
+ min-width: 260px;
3108
+ }
3109
+
3110
+ .ritual-model-list {
3111
+ max-width: 560px;
3112
+ margin: 0 auto;
3113
+ }
3114
+
3115
+ .ritual-primary-model-button {
3116
+ margin-bottom: 0.85rem;
3117
+ }
3118
+
3014
3119
  .ritual-model-card {
3015
3120
  text-align: left;
3016
3121
  padding: 1.15rem 1.35rem;
@@ -3046,6 +3151,80 @@ body {
3046
3151
  margin-top: 0.25rem;
3047
3152
  }
3048
3153
 
3154
+ .ritual-model-warning {
3155
+ margin-top: 0.35rem;
3156
+ color: hsl(var(--destructive));
3157
+ font-size: 0.85rem;
3158
+ }
3159
+
3160
+ .ritual-action-row,
3161
+ .ritual-button-row {
3162
+ display: flex;
3163
+ align-items: center;
3164
+ justify-content: center;
3165
+ gap: 0.75rem;
3166
+ flex-wrap: wrap;
3167
+ }
3168
+
3169
+ .ritual-action-row {
3170
+ margin-top: 1.1rem;
3171
+ }
3172
+
3173
+ .ritual-muted-hint {
3174
+ font-size: 0.82rem;
3175
+ }
3176
+
3177
+ .ritual-install-brain {
3178
+ margin: 0.6rem auto 1rem;
3179
+ }
3180
+
3181
+ .ritual-consent-panel {
3182
+ display: grid;
3183
+ grid-template-columns: minmax(0, 1fr) minmax(220px, 0.9fr);
3184
+ gap: 1rem;
3185
+ max-width: 620px;
3186
+ margin: 0 auto 1rem;
3187
+ padding: 1rem;
3188
+ border: 1px solid hsl(var(--border) / 0.6);
3189
+ border-radius: 8px;
3190
+ background: hsl(var(--bg) / 0.48);
3191
+ text-align: left;
3192
+ }
3193
+
3194
+ .ritual-consent-panel strong {
3195
+ display: block;
3196
+ margin-bottom: 0.25rem;
3197
+ }
3198
+
3199
+ .ritual-consent-panel p {
3200
+ margin: 0;
3201
+ color: hsl(var(--fg-muted));
3202
+ font-size: 0.9rem;
3203
+ }
3204
+
3205
+ .ritual-consent-panel dl {
3206
+ display: grid;
3207
+ gap: 0.45rem;
3208
+ margin: 0;
3209
+ }
3210
+
3211
+ .ritual-consent-panel dl div {
3212
+ display: grid;
3213
+ grid-template-columns: 110px minmax(0, 1fr);
3214
+ gap: 0.5rem;
3215
+ }
3216
+
3217
+ .ritual-consent-panel dt {
3218
+ color: hsl(var(--fg-muted));
3219
+ font-size: 0.78rem;
3220
+ }
3221
+
3222
+ .ritual-consent-panel dd {
3223
+ margin: 0;
3224
+ overflow-wrap: anywhere;
3225
+ font-size: 0.86rem;
3226
+ }
3227
+
3049
3228
  .ritual-progress {
3050
3229
  margin: 1.5rem 0;
3051
3230
  }
@@ -3077,6 +3256,11 @@ body {
3077
3256
  color: hsl(var(--fg));
3078
3257
  }
3079
3258
 
3259
+ .ritual-stage-icon {
3260
+ width: 15px;
3261
+ height: 15px;
3262
+ }
3263
+
3080
3264
  .ritual-bar {
3081
3265
  height: 6px;
3082
3266
  background: hsl(var(--border) / 0.6);
@@ -3092,12 +3276,48 @@ body {
3092
3276
  transition: width 280ms ease;
3093
3277
  }
3094
3278
 
3279
+ .ritual-bar-fill.progress-0 { width: 4%; }
3280
+ .ritual-bar-fill.progress-10 { width: 10%; }
3281
+ .ritual-bar-fill.progress-20 { width: 20%; }
3282
+ .ritual-bar-fill.progress-30 { width: 30%; }
3283
+ .ritual-bar-fill.progress-40 { width: 40%; }
3284
+ .ritual-bar-fill.progress-50 { width: 50%; }
3285
+ .ritual-bar-fill.progress-60 { width: 60%; }
3286
+ .ritual-bar-fill.progress-70 { width: 70%; }
3287
+ .ritual-bar-fill.progress-80 { width: 80%; }
3288
+ .ritual-bar-fill.progress-90 { width: 90%; }
3289
+ .ritual-bar-fill.progress-100 { width: 100%; }
3290
+
3095
3291
  .ritual-status {
3096
3292
  font-size: 0.95rem;
3097
3293
  color: hsl(var(--fg-muted));
3098
3294
  margin: 0.8rem 0;
3099
3295
  }
3100
3296
 
3297
+ .ritual-status-card {
3298
+ max-width: 540px;
3299
+ margin: 0.8rem auto 0;
3300
+ font-size: 0.86rem;
3301
+ }
3302
+
3303
+ .ritual-error-card {
3304
+ border-color: hsl(var(--destructive) / 0.45);
3305
+ background: hsl(var(--destructive) / 0.07);
3306
+ }
3307
+
3308
+ .ritual-install-error {
3309
+ margin-bottom: 1rem;
3310
+ }
3311
+
3312
+ .ritual-error-detail {
3313
+ margin-top: 0.5rem;
3314
+ font-size: 0.85rem;
3315
+ }
3316
+
3317
+ .ritual-button-row {
3318
+ margin-top: 1rem;
3319
+ }
3320
+
3101
3321
  .ritual-success {
3102
3322
  text-align: center;
3103
3323
  padding: 1.5rem;
@@ -26,7 +26,7 @@ from .storage import (
26
26
  storage_from_env,
27
27
  )
28
28
 
29
- __version__ = "6.1.0"
29
+ __version__ = "6.3.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.1.0"
22
+ MULTI_AGENT_VERSION = "6.3.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.1.0"
3
+ __version__ = "6.3.0"
@@ -26,7 +26,8 @@ from latticeai.services.tool_dispatch import (
26
26
  get_tool_permission,
27
27
  list_tool_permissions,
28
28
  )
29
- from p_reinforce import BRAIN_DIR
29
+ from latticeai.services.router_context import ToolRouterContext
30
+ from latticeai.services.p_reinforce import BRAIN_DIR
30
31
  from tools import (
31
32
  AGENT_ROOT,
32
33
  ToolError,
@@ -177,30 +178,56 @@ class ToolGitShowRequest(BaseModel):
177
178
 
178
179
  def create_tools_router(
179
180
  *,
180
- config,
181
- ingestion_pipeline,
182
- data_dir: Path,
183
- static_dir: Path,
184
- model_router,
185
- require_user,
186
- require_admin,
187
- get_current_user,
188
- clear_history,
189
- append_audit_event,
190
- enforce_rate_limit,
191
- bytes_match_extension,
192
- classify_sensitive_message,
193
- save_to_history,
194
- enable_graph: bool,
195
- knowledge_graph,
196
- require_graph,
197
- local_kg_watcher,
198
- load_mcp_installs,
199
- recommend_mcps,
200
- install_mcp,
201
- mcp_public_item,
181
+ tool_context: ToolRouterContext | None = None,
182
+ config=None,
183
+ ingestion_pipeline=None,
184
+ data_dir: Path | None = None,
185
+ static_dir: Path | None = None,
186
+ model_router=None,
187
+ require_user=None,
188
+ require_admin=None,
189
+ get_current_user=None,
190
+ clear_history=None,
191
+ append_audit_event=None,
192
+ enforce_rate_limit=None,
193
+ bytes_match_extension=None,
194
+ classify_sensitive_message=None,
195
+ save_to_history=None,
196
+ enable_graph: bool | None = None,
197
+ knowledge_graph=None,
198
+ require_graph=None,
199
+ local_kg_watcher=None,
200
+ load_mcp_installs=None,
201
+ recommend_mcps=None,
202
+ install_mcp=None,
203
+ mcp_public_item=None,
202
204
  hooks=None,
203
205
  ) -> APIRouter:
206
+ if tool_context is not None:
207
+ config = tool_context.config
208
+ ingestion_pipeline = tool_context.ingestion_pipeline
209
+ data_dir = tool_context.data_dir
210
+ static_dir = tool_context.static_dir
211
+ model_router = tool_context.model_router
212
+ require_user = tool_context.require_user
213
+ require_admin = tool_context.require_admin
214
+ get_current_user = tool_context.get_current_user
215
+ clear_history = tool_context.clear_history
216
+ append_audit_event = tool_context.append_audit_event
217
+ enforce_rate_limit = tool_context.enforce_rate_limit
218
+ bytes_match_extension = tool_context.bytes_match_extension
219
+ classify_sensitive_message = tool_context.classify_sensitive_message
220
+ save_to_history = tool_context.save_to_history
221
+ enable_graph = tool_context.enable_graph
222
+ knowledge_graph = tool_context.knowledge_graph
223
+ require_graph = tool_context.require_graph
224
+ local_kg_watcher = tool_context.local_kg_watcher
225
+ load_mcp_installs = tool_context.load_mcp_installs
226
+ recommend_mcps = tool_context.recommend_mcps
227
+ install_mcp = tool_context.install_mcp
228
+ mcp_public_item = tool_context.mcp_public_item
229
+ hooks = tool_context.hooks
230
+
204
231
  api_router = APIRouter()
205
232
  HOOKS = hooks
206
233
  CONFIG = config