ltcai 5.2.0 → 5.4.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.
- package/README.md +144 -164
- package/docs/CHANGELOG.md +63 -0
- package/docs/DEVELOPMENT.md +99 -0
- package/docs/LEGACY_COMPATIBILITY.md +55 -0
- package/docs/WHY_LATTICE.md +4 -3
- package/frontend/src/App.tsx +8 -2
- package/frontend/src/api/client.ts +2 -0
- package/frontend/src/components/FirstRunGuide.tsx +5 -5
- package/frontend/src/components/ProductFlow.tsx +1 -1
- package/frontend/src/i18n.ts +40 -40
- package/frontend/src/pages/Act.tsx +82 -1
- package/frontend/src/pages/Library.tsx +18 -6
- package/lattice_brain/__init__.py +1 -1
- package/lattice_brain/archive.py +12 -0
- package/lattice_brain/portability.py +14 -0
- package/lattice_brain/runtime/__init__.py +53 -0
- package/lattice_brain/runtime/agent_runtime.py +7 -0
- package/lattice_brain/runtime/hooks.py +6 -0
- package/lattice_brain/runtime/multi_agent.py +7 -2
- package/latticeai/__init__.py +1 -1
- package/latticeai/api/workflow_designer.py +60 -0
- package/latticeai/app_factory.py +5 -78
- package/latticeai/core/marketplace.py +1 -1
- package/latticeai/core/workspace_os.py +1 -1
- package/latticeai/runtime/__init__.py +2 -0
- package/latticeai/runtime/brain_runtime.py +41 -0
- package/latticeai/runtime/config_runtime.py +36 -0
- package/latticeai/runtime/security_runtime.py +27 -0
- package/latticeai/services/brain_automation.py +214 -0
- package/latticeai/services/model_capability_registry.py +2 -3
- package/latticeai/services/triggers.py +61 -4
- package/package.json +2 -2
- package/scripts/verify_hf_model_registry.py +1 -3
- package/src-tauri/Cargo.lock +1 -1
- package/src-tauri/Cargo.toml +1 -1
- package/src-tauri/tauri.conf.json +1 -1
- package/static/app/asset-manifest.json +5 -5
- package/static/app/assets/index-C7vzwUjU.js +16 -0
- package/static/app/assets/index-C7vzwUjU.js.map +1 -0
- package/static/app/assets/index-HN4f2wbe.css +2 -0
- package/static/app/index.html +2 -2
- package/static/app/assets/index-CQmHhk8Q.css +0 -2
- package/static/app/assets/index-DsnfomFs.js +0 -16
- package/static/app/assets/index-DsnfomFs.js.map +0 -1
|
@@ -20,11 +20,17 @@ from __future__ import annotations
|
|
|
20
20
|
|
|
21
21
|
import json
|
|
22
22
|
import logging
|
|
23
|
+
import os
|
|
23
24
|
import threading
|
|
24
25
|
import time
|
|
25
26
|
from pathlib import Path
|
|
26
27
|
from typing import Any, Callable, Dict, List, Optional
|
|
27
28
|
|
|
29
|
+
try:
|
|
30
|
+
from zoneinfo import ZoneInfo
|
|
31
|
+
except Exception: # pragma: no cover
|
|
32
|
+
ZoneInfo = None # type: ignore
|
|
33
|
+
|
|
28
34
|
DEFAULT_TICK_SECONDS = 5.0
|
|
29
35
|
MIN_INTERVAL_SECONDS = 60
|
|
30
36
|
|
|
@@ -51,6 +57,15 @@ class TriggerService:
|
|
|
51
57
|
self._stop_event = threading.Event()
|
|
52
58
|
self._thread: Optional[threading.Thread] = None
|
|
53
59
|
self._lock = threading.Lock()
|
|
60
|
+
# LATTICE_TZ: wall-clock / display 용. interval 계산은 여전히 unix seconds (duration 기반, drift 방지).
|
|
61
|
+
# describe()와 이벤트에 tz 정보 노출. calendar "daily at HH:MM" semantics 는 추후 cron 확장 시 사용.
|
|
62
|
+
self._tz_name = os.environ.get("LATTICE_TZ") or "UTC"
|
|
63
|
+
self._tz = None
|
|
64
|
+
if ZoneInfo is not None:
|
|
65
|
+
try:
|
|
66
|
+
self._tz = ZoneInfo(self._tz_name)
|
|
67
|
+
except Exception:
|
|
68
|
+
self._tz = ZoneInfo("UTC") if ZoneInfo else None
|
|
54
69
|
|
|
55
70
|
# ── durable state ──────────────────────────────────────────────────────
|
|
56
71
|
def _load_state(self) -> Dict[str, Any]:
|
|
@@ -86,27 +101,37 @@ class TriggerService:
|
|
|
86
101
|
continue
|
|
87
102
|
cfg = node.get("config") or {}
|
|
88
103
|
kind = str(cfg.get("trigger") or "manual")
|
|
104
|
+
if cfg.get("enabled") is False:
|
|
105
|
+
continue
|
|
89
106
|
if kind in ("interval", "brain_event"):
|
|
90
107
|
found.append({"workflow": wf, "node": node, "kind": kind, "config": cfg})
|
|
91
108
|
return found
|
|
92
109
|
|
|
93
110
|
def describe(self) -> Dict[str, Any]:
|
|
94
|
-
"""Honest status surface: what is armed, when it last fired/skipped.
|
|
111
|
+
"""Honest status surface: what is armed, when it last fired/skipped.
|
|
112
|
+
Includes LATTICE_TZ, per-trigger status (armed|degraded), consecutive_failures.
|
|
113
|
+
"""
|
|
95
114
|
state = self._load_state()
|
|
96
115
|
armed = []
|
|
97
116
|
for item in self._triggered_workflows():
|
|
98
117
|
wf_id = item["workflow"].get("id")
|
|
118
|
+
entry = state.get(wf_id) or {}
|
|
119
|
+
fails = int(entry.get("consecutive_failures", 0))
|
|
120
|
+
status = "degraded" if fails >= 3 else "armed"
|
|
99
121
|
armed.append({
|
|
100
122
|
"workflow_id": wf_id,
|
|
101
123
|
"name": item["workflow"].get("name"),
|
|
102
124
|
"kind": item["kind"],
|
|
103
125
|
"config": {k: v for k, v in item["config"].items() if k != "trigger"},
|
|
104
|
-
"last_fired_at":
|
|
105
|
-
"
|
|
126
|
+
"last_fired_at": entry.get("last_fired_at"),
|
|
127
|
+
"status": status,
|
|
128
|
+
"consecutive_failures": fails,
|
|
129
|
+
"recent_events": entry.get("events", [])[-5:],
|
|
106
130
|
})
|
|
107
131
|
return {
|
|
108
132
|
"running": bool(self._thread and self._thread.is_alive()),
|
|
109
133
|
"tick_seconds": self._tick,
|
|
134
|
+
"tz": self._tz_name,
|
|
110
135
|
"armed": armed,
|
|
111
136
|
}
|
|
112
137
|
|
|
@@ -133,6 +158,7 @@ class TriggerService:
|
|
|
133
158
|
skipped += missed
|
|
134
159
|
# Reset the cadence from now — no catch-up storm.
|
|
135
160
|
entry["last_fired_at"] = now if last is not None else entry.get("last_fired_at")
|
|
161
|
+
entry["last_attempt_at"] = now
|
|
136
162
|
self._save_state(state)
|
|
137
163
|
return skipped
|
|
138
164
|
|
|
@@ -152,9 +178,16 @@ class TriggerService:
|
|
|
152
178
|
if last is None:
|
|
153
179
|
# First sighting arms the schedule; it fires one interval later.
|
|
154
180
|
entry["last_fired_at"] = now
|
|
181
|
+
entry["last_attempt_at"] = now
|
|
155
182
|
continue
|
|
156
183
|
if now - float(last) < interval:
|
|
157
184
|
continue
|
|
185
|
+
# Dedup guard (edge case): short cooldown + last_attempt prevents rapid re-fire on
|
|
186
|
+
# clock skew, tick jitter, or restart races. Interval 자체 + 이 가드로 중복 실행 방지.
|
|
187
|
+
last_attempt = float(entry.get("last_attempt_at") or 0)
|
|
188
|
+
if now - last_attempt < 10:
|
|
189
|
+
continue
|
|
190
|
+
entry["last_attempt_at"] = now
|
|
158
191
|
entry["last_fired_at"] = now
|
|
159
192
|
self._record_event(state, wf_id, {"type": "fired", "trigger": "interval"})
|
|
160
193
|
fired += 1
|
|
@@ -181,7 +214,14 @@ class TriggerService:
|
|
|
181
214
|
if wanted and wanted != source_type:
|
|
182
215
|
continue
|
|
183
216
|
wf_id = item["workflow"].get("id")
|
|
184
|
-
state.setdefault(wf_id, {})
|
|
217
|
+
entry = state.setdefault(wf_id, {})
|
|
218
|
+
now = self._clock()
|
|
219
|
+
# Dedup guard for brain_event too (rapid ingest burst 등).
|
|
220
|
+
last_attempt = float(entry.get("last_attempt_at") or 0)
|
|
221
|
+
if now - last_attempt < 5:
|
|
222
|
+
continue
|
|
223
|
+
entry["last_attempt_at"] = now
|
|
224
|
+
entry["last_fired_at"] = now
|
|
185
225
|
self._record_event(state, wf_id, {
|
|
186
226
|
"type": "fired", "trigger": "brain_event", "source_type": source_type,
|
|
187
227
|
})
|
|
@@ -211,11 +251,28 @@ class TriggerService:
|
|
|
211
251
|
def _run():
|
|
212
252
|
try:
|
|
213
253
|
self._run_workflow(workflow_id, {"__trigger__": trigger_info})
|
|
254
|
+
self._record_fire_outcome(workflow_id, ok=True)
|
|
214
255
|
except Exception as exc:
|
|
215
256
|
logging.warning("trigger run failed for %s: %s", workflow_id, exc)
|
|
257
|
+
self._record_fire_outcome(workflow_id, ok=False, detail=str(exc))
|
|
216
258
|
|
|
217
259
|
threading.Thread(target=_run, name=f"trigger-{workflow_id}", daemon=True).start()
|
|
218
260
|
|
|
261
|
+
def _record_fire_outcome(self, wf_id: str, *, ok: bool, detail: str = "") -> None:
|
|
262
|
+
"""Track consecutive launch failures for degraded status in describe().
|
|
263
|
+
(Deep execution failures are visible via workflow run records; 여기서는 scheduler fire 자체 실패를 카운트.)
|
|
264
|
+
"""
|
|
265
|
+
with self._lock:
|
|
266
|
+
state = self._load_state()
|
|
267
|
+
entry = state.setdefault(wf_id, {})
|
|
268
|
+
if ok:
|
|
269
|
+
entry["consecutive_failures"] = 0
|
|
270
|
+
else:
|
|
271
|
+
fails = int(entry.get("consecutive_failures", 0)) + 1
|
|
272
|
+
entry["consecutive_failures"] = fails
|
|
273
|
+
self._record_event(state, wf_id, {"type": "failed", "detail": detail[:200]})
|
|
274
|
+
self._save_state(state)
|
|
275
|
+
|
|
219
276
|
def start(self) -> None:
|
|
220
277
|
if self._thread and self._thread.is_alive():
|
|
221
278
|
return
|
package/package.json
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "ltcai",
|
|
3
|
-
"version": "5.
|
|
4
|
-
"description": "Lattice AI — local-first
|
|
3
|
+
"version": "5.4.0",
|
|
4
|
+
"description": "Lattice AI — local-first Digital Brain that keeps your knowledge durable across any AI model.",
|
|
5
5
|
"homepage": "https://github.com/TaeSooPark-PTS/LatticeAI#readme",
|
|
6
6
|
"repository": {
|
|
7
7
|
"type": "git",
|
|
@@ -26,7 +26,6 @@ from __future__ import annotations
|
|
|
26
26
|
|
|
27
27
|
import argparse
|
|
28
28
|
import json
|
|
29
|
-
import os
|
|
30
29
|
import sys
|
|
31
30
|
import time
|
|
32
31
|
import urllib.error
|
|
@@ -43,7 +42,6 @@ try:
|
|
|
43
42
|
from latticeai.services.model_capability_registry import (
|
|
44
43
|
get_all_capabilities,
|
|
45
44
|
ModelCapability,
|
|
46
|
-
VerificationStatus,
|
|
47
45
|
)
|
|
48
46
|
except Exception as e:
|
|
49
47
|
print("ERROR: Could not import model_capability_registry:", e)
|
|
@@ -206,7 +204,7 @@ def main() -> int:
|
|
|
206
204
|
args = parser.parse_args()
|
|
207
205
|
|
|
208
206
|
caps = get_all_capabilities()
|
|
209
|
-
print(
|
|
207
|
+
print("Lattice AI 5.2.0 HF Model Registry Verifier")
|
|
210
208
|
print(f"Capabilities in registry: {len(caps)}")
|
|
211
209
|
print(f"Time: {datetime.now(timezone.utc).isoformat()}")
|
|
212
210
|
print("-" * 88)
|
package/src-tauri/Cargo.lock
CHANGED
package/src-tauri/Cargo.toml
CHANGED
|
@@ -1,13 +1,13 @@
|
|
|
1
1
|
{
|
|
2
|
-
"version": "5.
|
|
2
|
+
"version": "5.4.0",
|
|
3
3
|
"generated_at": "vite",
|
|
4
4
|
"entrypoints": {
|
|
5
5
|
"app": "/static/app/index.html"
|
|
6
6
|
},
|
|
7
7
|
"assets": {
|
|
8
8
|
"../node_modules/@tauri-apps/api/core.js": "/static/app/assets/core-CwxXejkd.js",
|
|
9
|
-
"index.html": "/static/app/assets/index-
|
|
10
|
-
"assets/index-
|
|
9
|
+
"index.html": "/static/app/assets/index-C7vzwUjU.js",
|
|
10
|
+
"assets/index-HN4f2wbe.css": "/static/app/assets/index-HN4f2wbe.css"
|
|
11
11
|
},
|
|
12
12
|
"vite": {
|
|
13
13
|
"../node_modules/@tauri-apps/api/core.js": {
|
|
@@ -17,7 +17,7 @@
|
|
|
17
17
|
"isDynamicEntry": true
|
|
18
18
|
},
|
|
19
19
|
"index.html": {
|
|
20
|
-
"file": "assets/index-
|
|
20
|
+
"file": "assets/index-C7vzwUjU.js",
|
|
21
21
|
"name": "index",
|
|
22
22
|
"src": "index.html",
|
|
23
23
|
"isEntry": true,
|
|
@@ -25,7 +25,7 @@
|
|
|
25
25
|
"../node_modules/@tauri-apps/api/core.js"
|
|
26
26
|
],
|
|
27
27
|
"css": [
|
|
28
|
-
"assets/index-
|
|
28
|
+
"assets/index-HN4f2wbe.css"
|
|
29
29
|
]
|
|
30
30
|
}
|
|
31
31
|
}
|