ltcai 8.7.0 → 8.9.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 +38 -26
- package/auto_setup.py +73 -8
- package/docs/CHANGELOG.md +63 -0
- package/docs/CODE_REVIEW_2026-07-06.md +764 -0
- package/docs/COMMUNITY_AND_PLUGINS.md +4 -3
- package/docs/DEVELOPMENT.md +10 -10
- package/docs/LEGACY_COMPATIBILITY.md +33 -14
- package/docs/ONBOARDING.md +2 -2
- package/docs/PRODUCT_DIRECTION_REVIEW.md +3 -2
- package/docs/TRUST_MODEL.md +5 -1
- package/docs/WHY_LATTICE.md +4 -3
- package/docs/architecture.md +4 -0
- package/docs/kg-schema.md +1 -1
- package/kg_schema.py +1 -1
- package/knowledge_graph.py +2 -2
- package/lattice_brain/__init__.py +1 -1
- package/lattice_brain/conversations.py +156 -21
- package/lattice_brain/core.py +1 -1
- package/lattice_brain/graph/_kg_common.py +12 -34
- package/lattice_brain/graph/json_utils.py +25 -0
- package/lattice_brain/graph/retrieval.py +66 -27
- package/lattice_brain/graph/runtime.py +16 -0
- package/lattice_brain/ingestion.py +35 -20
- package/lattice_brain/runtime/agent_runtime.py +37 -3
- package/lattice_brain/runtime/multi_agent.py +1 -1
- package/latticeai/__init__.py +1 -1
- package/latticeai/api/chat.py +31 -14
- package/latticeai/api/mcp.py +3 -2
- package/latticeai/api/models.py +4 -1
- package/latticeai/api/permissions.py +69 -30
- package/latticeai/api/setup.py +17 -2
- package/latticeai/api/tools.py +104 -62
- package/latticeai/app_factory.py +93 -10
- package/latticeai/core/agent.py +25 -7
- package/latticeai/core/legacy_compatibility.py +36 -25
- package/latticeai/core/marketplace.py +1 -1
- package/latticeai/core/sessions.py +11 -3
- package/latticeai/core/tool_registry.py +15 -4
- package/latticeai/core/workspace_os.py +1 -1
- package/latticeai/runtime/bootstrap.py +1 -1
- package/latticeai/runtime/persistence_runtime.py +1 -1
- package/latticeai/runtime/platform_services_runtime.py +1 -1
- package/latticeai/services/app_context.py +1 -0
- package/latticeai/services/architecture_readiness.py +2 -2
- package/latticeai/services/memory_service.py +38 -7
- package/latticeai/services/model_engines.py +79 -12
- package/latticeai/services/model_runtime.py +24 -4
- package/latticeai/services/process_audit.py +208 -0
- package/latticeai/services/product_readiness.py +11 -11
- package/latticeai/services/search_service.py +106 -30
- package/latticeai/services/tool_dispatch.py +66 -0
- package/latticeai/services/workspace_service.py +15 -0
- package/package.json +1 -1
- package/scripts/check_i18n_literals.mjs +20 -8
- package/scripts/i18n_literal_allowlist.json +34 -0
- package/scripts/lint_frontend.mjs +6 -2
- package/setup_wizard.py +185 -19
- 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 +11 -11
- package/static/app/assets/{Act-_U7mhXir.js → Act-fZokUnC0.js} +1 -1
- package/static/app/assets/{Brain-BxyTHZ21.js → Brain-DtyuWubr.js} +2 -2
- package/static/app/assets/{Capture-DyDKWNh9.js → Capture-D5KV3Cu7.js} +1 -1
- package/static/app/assets/{Library-DJ8KioFM.js → Library-C9kyFkSt.js} +1 -1
- package/static/app/assets/{System-C0FIb3OO.js → System-VbChmX7r.js} +1 -1
- package/static/app/assets/index-DCh5AoXt.css +2 -0
- package/static/app/assets/index-DPdcPoF0.js +17 -0
- package/static/app/assets/{primitives-BywkNS3f.js → primitives-DFeanEV6.js} +1 -1
- package/static/app/assets/{textarea-CQ61Rycp.js → textarea-CD8UNKIy.js} +1 -1
- package/static/app/index.html +2 -2
- package/static/sw.js +1 -1
- package/lattice_brain/_kg_common.py +0 -11
- package/lattice_brain/discovery.py +0 -11
- package/lattice_brain/documents.py +0 -11
- package/lattice_brain/identity.py +0 -11
- package/lattice_brain/ingest.py +0 -11
- package/lattice_brain/network.py +0 -11
- package/lattice_brain/projection.py +0 -11
- package/lattice_brain/provenance.py +0 -11
- package/lattice_brain/retrieval.py +0 -11
- package/lattice_brain/schema.py +0 -11
- package/lattice_brain/store.py +0 -11
- package/lattice_brain/write_master.py +0 -11
- package/latticeai/brain/__init__.py +0 -36
- package/latticeai/brain/_kg_common.py +0 -17
- package/latticeai/brain/context.py +0 -17
- package/latticeai/brain/conversations.py +0 -17
- package/latticeai/brain/discovery.py +0 -17
- package/latticeai/brain/documents.py +0 -17
- package/latticeai/brain/identity.py +0 -17
- package/latticeai/brain/ingest.py +0 -17
- package/latticeai/brain/memory.py +0 -17
- package/latticeai/brain/network.py +0 -17
- package/latticeai/brain/projection.py +0 -17
- package/latticeai/brain/provenance.py +0 -17
- package/latticeai/brain/retrieval.py +0 -17
- package/latticeai/brain/schema.py +0 -17
- package/latticeai/brain/store.py +0 -17
- package/latticeai/brain/write_master.py +0 -17
- package/latticeai/services/agent_runtime.py +0 -11
- package/static/app/assets/index-Bh7IIlyY.js +0 -16
- package/static/app/assets/index-_M5aCv21.css +0 -2
package/latticeai/app_factory.py
CHANGED
|
@@ -15,6 +15,7 @@ lazily via module ``__getattr__`` for backwards compatibility.
|
|
|
15
15
|
from __future__ import annotations
|
|
16
16
|
|
|
17
17
|
import threading
|
|
18
|
+
from dataclasses import dataclass
|
|
18
19
|
from typing import TYPE_CHECKING, Any, Dict, List, Optional
|
|
19
20
|
|
|
20
21
|
from latticeai.runtime.app_context_runtime import build_app_context
|
|
@@ -94,7 +95,7 @@ def _build(config: "Optional[Config]" = None) -> Dict[str, Any]:
|
|
|
94
95
|
from pydantic import BaseModel
|
|
95
96
|
|
|
96
97
|
from latticeai.models.router import LLMRouter, normalize_branding
|
|
97
|
-
from lattice_brain.
|
|
98
|
+
from lattice_brain.graph.runtime import set_llm_router
|
|
98
99
|
from lattice_brain.graph.schema import set_embed_dim
|
|
99
100
|
from latticeai.core.security import (
|
|
100
101
|
hash_password,
|
|
@@ -234,6 +235,8 @@ def _build(config: "Optional[Config]" = None) -> Dict[str, Any]:
|
|
|
234
235
|
ENABLE_GRAPH = _config_runtime["ENABLE_GRAPH"]
|
|
235
236
|
AUTOLOAD_MODELS = _config_runtime["AUTOLOAD_MODELS"]
|
|
236
237
|
MODEL_IDLE_UNLOAD_SECONDS = _config_runtime["MODEL_IDLE_UNLOAD_SECONDS"]
|
|
238
|
+
ALLOW_MODEL_DOWNLOADS = _config_runtime["ALLOW_MODEL_DOWNLOADS"]
|
|
239
|
+
MODEL_DOWNLOAD_TIMEOUT = _config_runtime["MODEL_DOWNLOAD_TIMEOUT"]
|
|
237
240
|
ALLOW_LOCAL_MODELS = _config_runtime["ALLOW_LOCAL_MODELS"]
|
|
238
241
|
REQUIRE_AUTH = _config_runtime["REQUIRE_AUTH"]
|
|
239
242
|
ALLOW_PLAINTEXT_API_KEYS = _config_runtime["ALLOW_PLAINTEXT_API_KEYS"]
|
|
@@ -636,9 +639,33 @@ def _build(config: "Optional[Config]" = None) -> Dict[str, Any]:
|
|
|
636
639
|
get_audit_log = _audit_rt["get_audit_log"]
|
|
637
640
|
append_audit_event = _audit_rt["append_audit_event"]
|
|
638
641
|
|
|
639
|
-
def
|
|
642
|
+
def _history_allowed_workspaces_for(user_email: Optional[str]):
|
|
643
|
+
if not REQUIRE_AUTH or not user_email:
|
|
644
|
+
return None
|
|
645
|
+
try:
|
|
646
|
+
return set(WORKSPACE_SERVICE.readable_workspaces(user_email))
|
|
647
|
+
except Exception as exc:
|
|
648
|
+
logging.warning("history workspace scope resolution failed for %s: %s", user_email, exc)
|
|
649
|
+
return set()
|
|
650
|
+
|
|
651
|
+
def _history_include_legacy_global(user_email: Optional[str]) -> bool:
|
|
652
|
+
return not REQUIRE_AUTH or not user_email
|
|
653
|
+
|
|
654
|
+
def get_history(
|
|
655
|
+
user_email: Optional[str] = None,
|
|
656
|
+
allowed_workspaces=None,
|
|
657
|
+
include_legacy_global: Optional[bool] = None,
|
|
658
|
+
):
|
|
640
659
|
try:
|
|
641
|
-
|
|
660
|
+
if allowed_workspaces is None and user_email:
|
|
661
|
+
allowed_workspaces = _history_allowed_workspaces_for(user_email)
|
|
662
|
+
if include_legacy_global is None:
|
|
663
|
+
include_legacy_global = _history_include_legacy_global(user_email)
|
|
664
|
+
return CONVERSATIONS.history(
|
|
665
|
+
user_email=user_email,
|
|
666
|
+
allowed_workspaces=allowed_workspaces,
|
|
667
|
+
include_legacy_global=include_legacy_global,
|
|
668
|
+
)
|
|
642
669
|
except Exception as e:
|
|
643
670
|
logging.warning("get_history failed: %s", e)
|
|
644
671
|
return []
|
|
@@ -683,17 +710,59 @@ def _build(config: "Optional[Config]" = None) -> Dict[str, Any]:
|
|
|
683
710
|
|
|
684
711
|
return sorted((conversations[key] for key in order), key=lambda item: item.get("updated_at") or "", reverse=True)
|
|
685
712
|
|
|
686
|
-
def get_conversation_messages(
|
|
687
|
-
|
|
713
|
+
def get_conversation_messages(
|
|
714
|
+
conversation_id: str,
|
|
715
|
+
*,
|
|
716
|
+
user_email: Optional[str] = None,
|
|
717
|
+
allowed_workspaces=None,
|
|
718
|
+
include_legacy_global: Optional[bool] = None,
|
|
719
|
+
) -> List[Dict]:
|
|
720
|
+
history = get_history(
|
|
721
|
+
user_email=user_email,
|
|
722
|
+
allowed_workspaces=allowed_workspaces,
|
|
723
|
+
include_legacy_global=include_legacy_global,
|
|
724
|
+
)
|
|
688
725
|
if conversation_id == "legacy-previous-history":
|
|
689
726
|
return [item for item in history if not item.get("conversation_id")]
|
|
690
727
|
return [item for item in history if item.get("conversation_id") == conversation_id]
|
|
691
728
|
|
|
692
|
-
def clear_history(
|
|
693
|
-
|
|
729
|
+
def clear_history(
|
|
730
|
+
keep_last: int = 0,
|
|
731
|
+
*,
|
|
732
|
+
user_email: Optional[str] = None,
|
|
733
|
+
allowed_workspaces=None,
|
|
734
|
+
include_legacy_global: Optional[bool] = None,
|
|
735
|
+
) -> Dict:
|
|
736
|
+
if allowed_workspaces is None and user_email:
|
|
737
|
+
allowed_workspaces = _history_allowed_workspaces_for(user_email)
|
|
738
|
+
if include_legacy_global is None:
|
|
739
|
+
include_legacy_global = _history_include_legacy_global(user_email)
|
|
740
|
+
return CONVERSATIONS.clear_all(
|
|
741
|
+
keep_last=keep_last,
|
|
742
|
+
user_email=user_email,
|
|
743
|
+
allowed_workspaces=allowed_workspaces,
|
|
744
|
+
include_legacy_global=include_legacy_global,
|
|
745
|
+
)
|
|
694
746
|
|
|
695
|
-
def clear_conversation(
|
|
696
|
-
|
|
747
|
+
def clear_conversation(
|
|
748
|
+
conversation_id: str,
|
|
749
|
+
started_at: Optional[str] = None,
|
|
750
|
+
*,
|
|
751
|
+
user_email: Optional[str] = None,
|
|
752
|
+
allowed_workspaces=None,
|
|
753
|
+
include_legacy_global: Optional[bool] = None,
|
|
754
|
+
) -> Dict:
|
|
755
|
+
if allowed_workspaces is None and user_email:
|
|
756
|
+
allowed_workspaces = _history_allowed_workspaces_for(user_email)
|
|
757
|
+
if include_legacy_global is None:
|
|
758
|
+
include_legacy_global = _history_include_legacy_global(user_email)
|
|
759
|
+
return CONVERSATIONS.clear_conversation(
|
|
760
|
+
conversation_id,
|
|
761
|
+
started_at=started_at,
|
|
762
|
+
user_email=user_email,
|
|
763
|
+
allowed_workspaces=allowed_workspaces,
|
|
764
|
+
include_legacy_global=include_legacy_global,
|
|
765
|
+
)
|
|
697
766
|
|
|
698
767
|
_access_runtime = build_access_runtime(
|
|
699
768
|
config=CONFIG,
|
|
@@ -942,6 +1011,8 @@ def _build(config: "Optional[Config]" = None) -> Dict[str, Any]:
|
|
|
942
1011
|
ENABLE_GRAPH=ENABLE_GRAPH,
|
|
943
1012
|
AUTOLOAD_MODELS=AUTOLOAD_MODELS,
|
|
944
1013
|
MODEL_IDLE_UNLOAD_SECONDS=MODEL_IDLE_UNLOAD_SECONDS,
|
|
1014
|
+
ALLOW_MODEL_DOWNLOADS=ALLOW_MODEL_DOWNLOADS,
|
|
1015
|
+
MODEL_DOWNLOAD_TIMEOUT=MODEL_DOWNLOAD_TIMEOUT,
|
|
945
1016
|
ALLOW_LOCAL_MODELS=ALLOW_LOCAL_MODELS,
|
|
946
1017
|
REQUIRE_AUTH=REQUIRE_AUTH,
|
|
947
1018
|
INVITE_GATE_ENABLED=INVITE_GATE_ENABLED,
|
|
@@ -1136,6 +1207,7 @@ def _build(config: "Optional[Config]" = None) -> Dict[str, Any]:
|
|
|
1136
1207
|
load_users=load_users,
|
|
1137
1208
|
get_user_role=get_user_role,
|
|
1138
1209
|
enforce_rate_limit=enforce_rate_limit,
|
|
1210
|
+
allowed_workspaces_for=_history_allowed_workspaces_for,
|
|
1139
1211
|
append_audit_event=append_audit_event,
|
|
1140
1212
|
get_audit_log=get_audit_log,
|
|
1141
1213
|
get_history=get_history,
|
|
@@ -1383,6 +1455,16 @@ def _build(config: "Optional[Config]" = None) -> Dict[str, Any]:
|
|
|
1383
1455
|
return dict(locals())
|
|
1384
1456
|
|
|
1385
1457
|
|
|
1458
|
+
@dataclass(frozen=True)
|
|
1459
|
+
class LegacyRuntimeNamespace:
|
|
1460
|
+
"""Compatibility adapter for the historical module-level runtime surface."""
|
|
1461
|
+
|
|
1462
|
+
namespace: Dict[str, Any]
|
|
1463
|
+
|
|
1464
|
+
def bind(self, runtime: "AppRuntime") -> None:
|
|
1465
|
+
runtime.__dict__.update(self.namespace)
|
|
1466
|
+
|
|
1467
|
+
|
|
1386
1468
|
class AppRuntime:
|
|
1387
1469
|
"""The constructed application namespace.
|
|
1388
1470
|
|
|
@@ -1391,7 +1473,8 @@ class AppRuntime:
|
|
|
1391
1473
|
"""
|
|
1392
1474
|
|
|
1393
1475
|
def __init__(self, namespace: Dict[str, Any]) -> None:
|
|
1394
|
-
self.
|
|
1476
|
+
self._legacy_namespace = LegacyRuntimeNamespace(namespace)
|
|
1477
|
+
self._legacy_namespace.bind(self)
|
|
1395
1478
|
|
|
1396
1479
|
|
|
1397
1480
|
_runtime_lock = threading.RLock()
|
package/latticeai/core/agent.py
CHANGED
|
@@ -52,7 +52,7 @@ class AgentRunContext:
|
|
|
52
52
|
"""Mutable state carrier passed through all agent phases."""
|
|
53
53
|
__slots__ = ("state", "plan", "transcript", "retry_count",
|
|
54
54
|
"state_history", "corrections", "final_message", "rollback_log",
|
|
55
|
-
"executing_model", "reviewing_model")
|
|
55
|
+
"executing_model", "reviewing_model", "approved_by_human")
|
|
56
56
|
|
|
57
57
|
def __init__(self) -> None:
|
|
58
58
|
self.state: AgentState = AgentState.IDLE
|
|
@@ -65,6 +65,7 @@ class AgentRunContext:
|
|
|
65
65
|
self.rollback_log: list = []
|
|
66
66
|
self.executing_model: Optional[str] = None
|
|
67
67
|
self.reviewing_model: Optional[str] = None
|
|
68
|
+
self.approved_by_human: bool = False
|
|
68
69
|
|
|
69
70
|
|
|
70
71
|
def extract_action(raw: str) -> Dict:
|
|
@@ -207,7 +208,7 @@ class SingleAgentRuntime:
|
|
|
207
208
|
ctx.state = AgentState.WAITING_APPROVAL
|
|
208
209
|
|
|
209
210
|
# ── APPROVAL ─────────────────────────────────────────────────────
|
|
210
|
-
def approve(self, ctx: AgentRunContext, current_user: str) -> None:
|
|
211
|
+
def approve(self, ctx: AgentRunContext, current_user: str, *, approved_by_human: bool = False) -> None:
|
|
211
212
|
"""APPROVAL: Check governance, log decision, auto-approve (future: UI prompt)."""
|
|
212
213
|
d = self.deps
|
|
213
214
|
auto_approve_tools = {name for name, p in d.tool_governance.items() if p["auto_approve"]}
|
|
@@ -219,12 +220,22 @@ class SingleAgentRuntime:
|
|
|
219
220
|
"state": AgentState.WAITING_APPROVAL.value,
|
|
220
221
|
"requires_approval": requires,
|
|
221
222
|
"non_auto_approve_steps": non_auto,
|
|
222
|
-
"decision": "auto_approved",
|
|
223
|
+
"decision": "human_approved" if requires and approved_by_human else ("blocked_pending_approval" if requires else "auto_approved"),
|
|
223
224
|
})
|
|
224
225
|
d.audit(
|
|
225
226
|
"agent_approval", user_email=current_user,
|
|
226
|
-
requires_approval=requires,
|
|
227
|
+
requires_approval=requires,
|
|
228
|
+
non_auto_steps=non_auto,
|
|
229
|
+
decision="human_approved" if requires and approved_by_human else ("blocked_pending_approval" if requires else "auto_approved"),
|
|
227
230
|
)
|
|
231
|
+
if requires and not approved_by_human:
|
|
232
|
+
ctx.final_message = (
|
|
233
|
+
"이 작업에는 명시 승인이 필요한 도구가 포함되어 있어 자동 실행을 중단했습니다. "
|
|
234
|
+
"human_in_loop 승인 흐름으로 다시 실행해 주세요."
|
|
235
|
+
)
|
|
236
|
+
ctx.state = AgentState.FAILED
|
|
237
|
+
return
|
|
238
|
+
ctx.approved_by_human = bool(approved_by_human)
|
|
228
239
|
ctx.state = AgentState.EXECUTING
|
|
229
240
|
|
|
230
241
|
# ── EXECUTE ──────────────────────────────────────────────────────
|
|
@@ -317,7 +328,7 @@ class SingleAgentRuntime:
|
|
|
317
328
|
)
|
|
318
329
|
continue
|
|
319
330
|
|
|
320
|
-
if not policy["auto_approve"]:
|
|
331
|
+
if not policy["auto_approve"] and not ctx.approved_by_human:
|
|
321
332
|
d.audit(
|
|
322
333
|
"agent_exec", user_email=current_user, source=getattr(req, "source", None) or "agent",
|
|
323
334
|
state=AgentState.EXECUTING.value, action=name, risk=risk,
|
|
@@ -326,6 +337,13 @@ class SingleAgentRuntime:
|
|
|
326
337
|
rollback=policy["rollback"],
|
|
327
338
|
args={k: v for k, v in args.items() if k != "content"},
|
|
328
339
|
)
|
|
340
|
+
ctx.transcript.append({
|
|
341
|
+
"state": AgentState.EXECUTING.value, "action": name,
|
|
342
|
+
"thoughts": thoughts, "args": args, "risk": risk,
|
|
343
|
+
"governance": dict(policy),
|
|
344
|
+
"error": f"BLOCKED: action '{name}' requires explicit approval.",
|
|
345
|
+
})
|
|
346
|
+
continue
|
|
329
347
|
|
|
330
348
|
try:
|
|
331
349
|
d.check_role(name, current_user)
|
|
@@ -422,8 +440,8 @@ class SingleAgentRuntime:
|
|
|
422
440
|
if gov.get("rollback") != "git":
|
|
423
441
|
continue
|
|
424
442
|
result = step.get("result", {})
|
|
425
|
-
if not
|
|
426
|
-
|
|
443
|
+
if not isinstance(result, dict):
|
|
444
|
+
result = {}
|
|
427
445
|
path = result.get("path") or (step.get("args") or {}).get("path", "")
|
|
428
446
|
if not path:
|
|
429
447
|
continue
|
|
@@ -14,7 +14,7 @@ from pathlib import Path
|
|
|
14
14
|
from typing import Any, Dict, List
|
|
15
15
|
|
|
16
16
|
|
|
17
|
-
LEGACY_COMPATIBILITY_VERSION = "8.
|
|
17
|
+
LEGACY_COMPATIBILITY_VERSION = "8.9.0"
|
|
18
18
|
|
|
19
19
|
|
|
20
20
|
@dataclass(frozen=True)
|
|
@@ -43,7 +43,7 @@ LEGACY_SHIMS: List[LegacyShim] = [
|
|
|
43
43
|
LegacyShim(
|
|
44
44
|
path="knowledge_graph.py",
|
|
45
45
|
owner="lattice_brain.graph",
|
|
46
|
-
replacement="from lattice_brain.store import KnowledgeGraphStore",
|
|
46
|
+
replacement="from lattice_brain.graph.store import KnowledgeGraphStore",
|
|
47
47
|
reason="Historical scripts imported the graph store from the repo root.",
|
|
48
48
|
removal_phase="major-release-after-8.x",
|
|
49
49
|
),
|
|
@@ -57,7 +57,7 @@ LEGACY_SHIMS: List[LegacyShim] = [
|
|
|
57
57
|
LegacyShim(
|
|
58
58
|
path="kg_schema.py",
|
|
59
59
|
owner="lattice_brain.graph.schema",
|
|
60
|
-
replacement="from lattice_brain.schema import ...",
|
|
60
|
+
replacement="from lattice_brain.graph.schema import ...",
|
|
61
61
|
reason="Historical graph schema tests and tools referenced root schema symbols.",
|
|
62
62
|
removal_phase="major-release-after-8.x",
|
|
63
63
|
),
|
|
@@ -96,53 +96,59 @@ LEGACY_SHIMS: List[LegacyShim] = [
|
|
|
96
96
|
reason="Local folder ingestion integrations used the root local knowledge API.",
|
|
97
97
|
removal_phase="requires-api-route-migration",
|
|
98
98
|
),
|
|
99
|
+
]
|
|
100
|
+
|
|
101
|
+
# Shim layers that have completed their compatibility window and were
|
|
102
|
+
# physically deleted. Kept in the report so consumers (docs, tests, release
|
|
103
|
+
# notes) can tell "removed on purpose" apart from "missing by accident".
|
|
104
|
+
# Removing the internal-only layers is the first step of extracting
|
|
105
|
+
# ``lattice_brain`` as a standalone Brain Core package: the package now has
|
|
106
|
+
# exactly one import surface (``lattice_brain.*`` physical paths).
|
|
107
|
+
REMOVED_SHIMS: List[LegacyShim] = [
|
|
99
108
|
LegacyShim(
|
|
100
109
|
path="lattice_brain/store.py",
|
|
101
110
|
owner="lattice_brain.graph.store",
|
|
102
111
|
replacement="from lattice_brain.graph.store import KnowledgeGraphStore",
|
|
103
|
-
reason="Pre-graph-package
|
|
104
|
-
removal_phase="
|
|
112
|
+
reason="Pre-graph-package flat modules were internal-only; removed in 8.8.0.",
|
|
113
|
+
removal_phase="removed-8.8.0",
|
|
105
114
|
layer="brain-flat",
|
|
115
|
+
status="removed",
|
|
106
116
|
),
|
|
107
117
|
LegacyShim(
|
|
108
118
|
path="lattice_brain/ingest.py",
|
|
109
119
|
owner="lattice_brain.graph.ingest",
|
|
110
120
|
replacement="from lattice_brain.graph.ingest import KnowledgeGraphIngestMixin",
|
|
111
|
-
reason="Pre-graph-package
|
|
112
|
-
removal_phase="
|
|
121
|
+
reason="Pre-graph-package flat modules were internal-only; removed in 8.8.0.",
|
|
122
|
+
removal_phase="removed-8.8.0",
|
|
113
123
|
layer="brain-flat",
|
|
124
|
+
status="removed",
|
|
114
125
|
),
|
|
115
126
|
LegacyShim(
|
|
116
127
|
path="lattice_brain/retrieval.py",
|
|
117
128
|
owner="lattice_brain.graph.retrieval",
|
|
118
129
|
replacement="from lattice_brain.graph.retrieval import KnowledgeGraphRetrievalMixin",
|
|
119
|
-
reason="Pre-graph-package
|
|
120
|
-
removal_phase="
|
|
130
|
+
reason="Pre-graph-package flat modules were internal-only; removed in 8.8.0.",
|
|
131
|
+
removal_phase="removed-8.8.0",
|
|
121
132
|
layer="brain-flat",
|
|
133
|
+
status="removed",
|
|
122
134
|
),
|
|
123
135
|
LegacyShim(
|
|
124
|
-
path="latticeai/brain/
|
|
125
|
-
owner="lattice_brain
|
|
126
|
-
replacement="
|
|
127
|
-
reason="The deprecated latticeai.brain namespace
|
|
128
|
-
removal_phase="
|
|
129
|
-
layer="deprecated-namespace",
|
|
130
|
-
),
|
|
131
|
-
LegacyShim(
|
|
132
|
-
path="latticeai/brain/ingest.py",
|
|
133
|
-
owner="lattice_brain.graph.ingest",
|
|
134
|
-
replacement="from lattice_brain.graph.ingest import KnowledgeGraphIngestMixin",
|
|
135
|
-
reason="The deprecated latticeai.brain namespace remains for package users.",
|
|
136
|
-
removal_phase="major-release-after-8.x",
|
|
136
|
+
path="latticeai/brain/",
|
|
137
|
+
owner="lattice_brain",
|
|
138
|
+
replacement="import lattice_brain",
|
|
139
|
+
reason="The deprecated latticeai.brain namespace completed its window; removed in 8.8.0.",
|
|
140
|
+
removal_phase="removed-8.8.0",
|
|
137
141
|
layer="deprecated-namespace",
|
|
142
|
+
status="removed",
|
|
138
143
|
),
|
|
139
144
|
LegacyShim(
|
|
140
145
|
path="latticeai/services/agent_runtime.py",
|
|
141
146
|
owner="lattice_brain.runtime.agent_runtime",
|
|
142
147
|
replacement="from lattice_brain.runtime.agent_runtime import AgentRuntime",
|
|
143
|
-
reason="
|
|
144
|
-
removal_phase="
|
|
148
|
+
reason="The service-layer alias completed its window; removed in 8.8.0.",
|
|
149
|
+
removal_phase="removed-8.8.0",
|
|
145
150
|
layer="service-alias",
|
|
151
|
+
status="removed",
|
|
146
152
|
),
|
|
147
153
|
]
|
|
148
154
|
|
|
@@ -152,23 +158,28 @@ def legacy_shim_report(root: Path | None = None) -> Dict[str, Any]:
|
|
|
152
158
|
root = Path(__file__).resolve().parents[2]
|
|
153
159
|
entries = [shim.as_dict() for shim in LEGACY_SHIMS]
|
|
154
160
|
missing = [shim.path for shim in LEGACY_SHIMS if not (root / shim.path).exists()]
|
|
161
|
+
lingering = [shim.path for shim in REMOVED_SHIMS if (root / shim.path).exists()]
|
|
155
162
|
phases = sorted({shim.removal_phase for shim in LEGACY_SHIMS})
|
|
156
163
|
layers = sorted({shim.layer for shim in LEGACY_SHIMS})
|
|
157
164
|
return {
|
|
158
165
|
"schema_version": "legacy-compatibility/v1",
|
|
159
166
|
"version_target": LEGACY_COMPATIBILITY_VERSION,
|
|
160
|
-
"status": "managed" if not missing else "incomplete",
|
|
167
|
+
"status": "managed" if not missing and not lingering else "incomplete",
|
|
161
168
|
"remaining_count": len(LEGACY_SHIMS),
|
|
169
|
+
"removed_count": len(REMOVED_SHIMS),
|
|
162
170
|
"missing": missing,
|
|
171
|
+
"lingering": lingering,
|
|
163
172
|
"removal_phases": phases,
|
|
164
173
|
"layers": layers,
|
|
165
174
|
"shims": entries,
|
|
175
|
+
"removed": [shim.as_dict() for shim in REMOVED_SHIMS],
|
|
166
176
|
}
|
|
167
177
|
|
|
168
178
|
|
|
169
179
|
__all__ = [
|
|
170
180
|
"LEGACY_COMPATIBILITY_VERSION",
|
|
171
181
|
"LEGACY_SHIMS",
|
|
182
|
+
"REMOVED_SHIMS",
|
|
172
183
|
"LegacyShim",
|
|
173
184
|
"legacy_shim_report",
|
|
174
185
|
]
|
|
@@ -90,8 +90,16 @@ def _entry_created_at(entry: tuple) -> float:
|
|
|
90
90
|
|
|
91
91
|
|
|
92
92
|
class SessionStore:
|
|
93
|
-
def __init__(
|
|
93
|
+
def __init__(
|
|
94
|
+
self,
|
|
95
|
+
data_dir: Optional[Path] = None,
|
|
96
|
+
*,
|
|
97
|
+
ttl_seconds: int = SESSION_TTL,
|
|
98
|
+
refresh_threshold_seconds: int = SESSION_REFRESH_THRESHOLD,
|
|
99
|
+
):
|
|
94
100
|
self._data_dir = data_dir
|
|
101
|
+
self._ttl_seconds = int(ttl_seconds or SESSION_TTL)
|
|
102
|
+
self._refresh_threshold_seconds = int(refresh_threshold_seconds or SESSION_REFRESH_THRESHOLD)
|
|
95
103
|
self._sessions: Dict[str, tuple] = load_sessions(data_dir)
|
|
96
104
|
|
|
97
105
|
def create(self, subject: str, *, email: Optional[str] = None) -> str:
|
|
@@ -117,11 +125,11 @@ class SessionStore:
|
|
|
117
125
|
if entry is None:
|
|
118
126
|
return None
|
|
119
127
|
created_at = _entry_created_at(entry)
|
|
120
|
-
if now - created_at >
|
|
128
|
+
if now - created_at > self._ttl_seconds:
|
|
121
129
|
self._sessions.pop(key, None)
|
|
122
130
|
persist_sessions(self._sessions, self._data_dir)
|
|
123
131
|
return None
|
|
124
|
-
if now - created_at >
|
|
132
|
+
if now - created_at > self._refresh_threshold_seconds:
|
|
125
133
|
refreshed = (_entry_subject(entry), now, _entry_email(entry))
|
|
126
134
|
self._sessions[key] = refreshed
|
|
127
135
|
persist_sessions(self._sessions, self._data_dir)
|
|
@@ -109,6 +109,13 @@ def _w(sandbox: str = "workspace", rollback: str = "none") -> ToolPolicy:
|
|
|
109
109
|
)
|
|
110
110
|
|
|
111
111
|
|
|
112
|
+
def _wa(sandbox: str = "workspace", rollback: str = "none") -> ToolPolicy:
|
|
113
|
+
return ToolPolicy(
|
|
114
|
+
risk="write", destructive=False, shell=False, network=False,
|
|
115
|
+
auto_approve=True, sandbox=sandbox, rollback=rollback,
|
|
116
|
+
)
|
|
117
|
+
|
|
118
|
+
|
|
112
119
|
def _e(sandbox: str = "workspace", rollback: str = "none") -> ToolPolicy:
|
|
113
120
|
return ToolPolicy(
|
|
114
121
|
risk="exec", destructive=False, shell=True, network=False,
|
|
@@ -137,6 +144,7 @@ TOOL_GOVERNANCE: Dict[str, ToolPolicy] = {
|
|
|
137
144
|
"search_files": _r(),
|
|
138
145
|
"grep": _r(),
|
|
139
146
|
"inspect_html": _r(),
|
|
147
|
+
"preview_url": _r(),
|
|
140
148
|
"todo_read": _r(),
|
|
141
149
|
"local_list": _r(sandbox="home"),
|
|
142
150
|
"local_read": _r(sandbox="home"),
|
|
@@ -161,7 +169,6 @@ TOOL_GOVERNANCE: Dict[str, ToolPolicy] = {
|
|
|
161
169
|
"create_xlsx": _w(),
|
|
162
170
|
"create_pptx": _w(),
|
|
163
171
|
"create_pdf": _w(),
|
|
164
|
-
"preview_url": _w(),
|
|
165
172
|
"todo_write": _w(),
|
|
166
173
|
"knowledge_save": _w(sandbox="home"),
|
|
167
174
|
"obsidian_save": _w(sandbox="home"),
|
|
@@ -325,9 +332,13 @@ class ToolRegistry:
|
|
|
325
332
|
|
|
326
333
|
def policy_for(self, action_name: str, args: Optional[dict] = None) -> ToolPolicy:
|
|
327
334
|
policy = self.governance.get(action_name, self.default_policy)
|
|
328
|
-
if action_name
|
|
329
|
-
path = str((args or {}).get("path", ""))
|
|
330
|
-
|
|
335
|
+
if action_name in {"local_write", "write_file", "edit_file"}:
|
|
336
|
+
path = str((args or {}).get("path", "")).replace("\\", "/")
|
|
337
|
+
for prefix in self.local_write_blocked_prefixes:
|
|
338
|
+
normalized_prefix = str(prefix).rstrip("/")
|
|
339
|
+
blocked = path == normalized_prefix or path.startswith(f"{normalized_prefix}/")
|
|
340
|
+
if not blocked:
|
|
341
|
+
continue
|
|
331
342
|
return ToolPolicy(
|
|
332
343
|
risk="destructive", destructive=True, shell=False, network=False,
|
|
333
344
|
auto_approve=False, sandbox="system", rollback="none",
|
|
@@ -49,7 +49,7 @@ __all__ = [
|
|
|
49
49
|
"remove_skill_directory",
|
|
50
50
|
]
|
|
51
51
|
|
|
52
|
-
WORKSPACE_OS_VERSION = "8.
|
|
52
|
+
WORKSPACE_OS_VERSION = "8.9.0"
|
|
53
53
|
|
|
54
54
|
# Workspace types separate single-user Personal workspaces from shared
|
|
55
55
|
# Organization workspaces. Both keep the same local-first JSON store; the type
|
|
@@ -24,7 +24,7 @@ def build_session_runtime(
|
|
|
24
24
|
|
|
25
25
|
from latticeai.core.sessions import SessionStore
|
|
26
26
|
|
|
27
|
-
session_store = SessionStore()
|
|
27
|
+
session_store = SessionStore(ttl_seconds=ttl_seconds)
|
|
28
28
|
|
|
29
29
|
def create_session(email: str) -> str:
|
|
30
30
|
return session_store.create(user_id_resolver(email) or email, email=email)
|
|
@@ -27,7 +27,7 @@ def build_persistence_runtime(
|
|
|
27
27
|
import os
|
|
28
28
|
from pathlib import Path
|
|
29
29
|
|
|
30
|
-
from lattice_brain.identity import DeviceIdentity
|
|
30
|
+
from lattice_brain.graph.identity import DeviceIdentity
|
|
31
31
|
from lattice_brain.ingestion import IngestionPipeline
|
|
32
32
|
from lattice_brain.portability import KGPortabilityService
|
|
33
33
|
from latticeai.core.agent_registry import AgentRegistry
|
|
@@ -30,7 +30,7 @@ def build_brain_network(
|
|
|
30
30
|
) -> Any:
|
|
31
31
|
"""Construct peer sync/network service for brain portability routes."""
|
|
32
32
|
|
|
33
|
-
from lattice_brain.network import BrainNetwork
|
|
33
|
+
from lattice_brain.graph.network import BrainNetwork
|
|
34
34
|
|
|
35
35
|
return BrainNetwork(
|
|
36
36
|
identity=identity,
|
|
@@ -47,6 +47,7 @@ class AppContext:
|
|
|
47
47
|
load_users: Optional[Callable[[], dict]] = None
|
|
48
48
|
get_user_role: Optional[Callable[..., str]] = None
|
|
49
49
|
enforce_rate_limit: Optional[Callable[..., None]] = None
|
|
50
|
+
allowed_workspaces_for: Optional[Callable[..., Any]] = None
|
|
50
51
|
|
|
51
52
|
# ── audit / history callables ─────────────────────────────────────────
|
|
52
53
|
append_audit_event: Optional[Callable[..., None]] = None
|
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
"""Machine-checkable architecture readiness gates for release work.
|
|
2
2
|
|
|
3
|
-
8.
|
|
3
|
+
8.9.0 keeps the major architecture priorities under an explicit release
|
|
4
4
|
contract while product maturity work reduces visible beta seams. AgentRuntime, ToolRegistry,
|
|
5
5
|
central Config, decomposed server runtime, and Knowledge Graph stabilization
|
|
6
6
|
must remain discoverable, ordered, and backed by tests before the release can be
|
|
@@ -17,7 +17,7 @@ from typing import Any, Dict, List
|
|
|
17
17
|
from latticeai.core.legacy_compatibility import legacy_shim_report
|
|
18
18
|
|
|
19
19
|
|
|
20
|
-
ARCHITECTURE_VERSION_TARGET = "8.
|
|
20
|
+
ARCHITECTURE_VERSION_TARGET = "8.9.0"
|
|
21
21
|
|
|
22
22
|
PREFERRED_REFACTORING_ORDER = [
|
|
23
23
|
"agent-runtime",
|
|
@@ -533,6 +533,10 @@ class MemoryService:
|
|
|
533
533
|
"title": item.get("title"),
|
|
534
534
|
"snippet": item.get("snippet"),
|
|
535
535
|
"score": item.get("score", 0),
|
|
536
|
+
# Evidence explainability: why this item was recalled, so the
|
|
537
|
+
# citation UI can show the matched terms instead of a bare score.
|
|
538
|
+
"matched_terms": item.get("matched_terms") or [],
|
|
539
|
+
"confidence": item.get("confidence") or "low",
|
|
536
540
|
}
|
|
537
541
|
for item in recall.get("results", [])[: max(1, min(limit, 8))]
|
|
538
542
|
]
|
|
@@ -616,15 +620,17 @@ class MemoryService:
|
|
|
616
620
|
q = str(query or "").strip()
|
|
617
621
|
query_tokens = [tok for tok in q.lower().split() if tok]
|
|
618
622
|
|
|
619
|
-
def
|
|
623
|
+
def _matched_terms(*texts: Any) -> List[str]:
|
|
624
|
+
haystack = " ".join(str(t or "") for t in texts).lower()
|
|
625
|
+
return [tok for tok in query_tokens if tok in haystack]
|
|
626
|
+
|
|
627
|
+
def _lexical_score(matched: List[str]) -> float:
|
|
620
628
|
# Honest, comparable relevance: fraction of query tokens present.
|
|
621
629
|
# Both tiers share this scorer so the cross-tier ranking is real,
|
|
622
630
|
# not an artifact of per-tier constants.
|
|
623
631
|
if not query_tokens:
|
|
624
632
|
return 0.0
|
|
625
|
-
|
|
626
|
-
hits = sum(1 for tok in query_tokens if tok in haystack)
|
|
627
|
-
return round(hits / len(query_tokens), 4)
|
|
633
|
+
return round(len(matched) / len(query_tokens), 4)
|
|
628
634
|
|
|
629
635
|
results: List[Dict[str, Any]] = []
|
|
630
636
|
|
|
@@ -633,13 +639,15 @@ class MemoryService:
|
|
|
633
639
|
except Exception:
|
|
634
640
|
mem = []
|
|
635
641
|
for m in mem:
|
|
642
|
+
matched = _matched_terms(m.get("content"), " ".join(m.get("tags") or []), m.get("kind"))
|
|
636
643
|
results.append({
|
|
637
644
|
"source": "workspace",
|
|
638
645
|
"id": m.get("id"),
|
|
639
646
|
"title": (m.get("kind") or "memory"),
|
|
640
647
|
"snippet": str(m.get("content") or "")[:240],
|
|
641
648
|
"kind": m.get("kind"),
|
|
642
|
-
"score": _lexical_score(
|
|
649
|
+
"score": _lexical_score(matched),
|
|
650
|
+
"matched_terms": matched,
|
|
643
651
|
"tags": m.get("tags") or [],
|
|
644
652
|
})
|
|
645
653
|
|
|
@@ -650,17 +658,40 @@ class MemoryService:
|
|
|
650
658
|
except Exception:
|
|
651
659
|
hits = []
|
|
652
660
|
for hit in hits[:limit]:
|
|
661
|
+
matched = _matched_terms(hit.get("title"), hit.get("name"), hit.get("summary"), hit.get("content"))
|
|
653
662
|
results.append({
|
|
654
663
|
"source": "graph",
|
|
655
664
|
"id": hit.get("id") or hit.get("node_id"),
|
|
656
665
|
"title": hit.get("title") or hit.get("name") or "node",
|
|
657
666
|
"snippet": str(hit.get("summary") or hit.get("content") or "")[:240],
|
|
658
667
|
"kind": hit.get("type") or "node",
|
|
659
|
-
"score": _lexical_score(
|
|
668
|
+
"score": _lexical_score(matched),
|
|
669
|
+
"matched_terms": matched,
|
|
660
670
|
})
|
|
661
671
|
|
|
672
|
+
# Quality gate: when at least one result carries real lexical evidence,
|
|
673
|
+
# zero-score rows are noise relative to it and are dropped. When nothing
|
|
674
|
+
# scores (e.g. tokenization mismatch), everything is kept so the tiers'
|
|
675
|
+
# own search filters still decide — the gate never empties a recall.
|
|
676
|
+
candidates = len(results)
|
|
677
|
+
if query_tokens and any(r.get("score", 0) > 0 for r in results):
|
|
678
|
+
results = [r for r in results if r.get("score", 0) > 0]
|
|
679
|
+
for r in results:
|
|
680
|
+
r["confidence"] = "high" if r.get("score", 0) >= 0.65 else "medium" if r.get("score", 0) >= 0.3 else "low"
|
|
681
|
+
|
|
662
682
|
results.sort(key=lambda r: r.get("score", 0), reverse=True)
|
|
663
|
-
return {
|
|
683
|
+
return {
|
|
684
|
+
"query": q,
|
|
685
|
+
"results": results[: max(1, min(limit, 100))],
|
|
686
|
+
"count": len(results),
|
|
687
|
+
"source": "live",
|
|
688
|
+
"quality_gate": {
|
|
689
|
+
"candidates": candidates,
|
|
690
|
+
"passed": len(results),
|
|
691
|
+
"filtered": candidates - len(results),
|
|
692
|
+
"gate": "lexical-evidence/v1",
|
|
693
|
+
},
|
|
694
|
+
}
|
|
664
695
|
|
|
665
696
|
# ── inspect a single tier ─────────────────────────────────────────────
|
|
666
697
|
def inspect(self, source: str, *, user_email: Optional[str] = None, workspace_id: Optional[str] = None, limit: int = 50) -> Dict[str, Any]:
|