ltcai 3.1.0 → 3.2.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 (44) hide show
  1. package/README.md +34 -8
  2. package/docs/CHANGELOG.md +53 -0
  3. package/docs/V3_2_AUDIT.md +82 -0
  4. package/docs/V3_FRONTEND.md +1 -1
  5. package/docs/architecture.md +6 -0
  6. package/latticeai/__init__.py +1 -1
  7. package/latticeai/api/agent_registry.py +103 -0
  8. package/latticeai/api/hooks.py +113 -0
  9. package/latticeai/api/marketplace.py +13 -0
  10. package/latticeai/api/memory.py +109 -0
  11. package/latticeai/core/agent_registry.py +234 -0
  12. package/latticeai/core/hooks.py +284 -0
  13. package/latticeai/core/marketplace.py +87 -2
  14. package/latticeai/core/multi_agent.py +1 -1
  15. package/latticeai/core/workspace_os.py +1 -1
  16. package/latticeai/server_app.py +41 -0
  17. package/latticeai/services/memory_service.py +324 -0
  18. package/package.json +2 -2
  19. package/scripts/build_v3_assets.mjs +1 -1
  20. package/static/v3/asset-manifest.json +16 -8
  21. package/static/v3/js/{app.46fb61d9.js → app.a5adc0f3.js} +1 -1
  22. package/static/v3/js/core/{api.22a41d42.js → api.603b978f.js} +64 -0
  23. package/static/v3/js/core/api.js +64 -0
  24. package/static/v3/js/core/{routes.f935dd50.js → routes.07ad6696.js} +11 -0
  25. package/static/v3/js/core/routes.js +11 -0
  26. package/static/v3/js/core/{shell.1b6199d6.js → shell.ea0b9ae5.js} +2 -2
  27. package/static/v3/js/views/{agents.14e48bdd.js → agents.c373d48c.js} +100 -0
  28. package/static/v3/js/views/agents.js +100 -0
  29. package/static/v3/js/views/hooks.f3edebca.js +99 -0
  30. package/static/v3/js/views/hooks.js +99 -0
  31. package/static/v3/js/views/marketplace.ab0583d4.js +141 -0
  32. package/static/v3/js/views/marketplace.js +141 -0
  33. package/static/v3/js/views/mcp.99b5c6a7.js +114 -0
  34. package/static/v3/js/views/mcp.js +114 -0
  35. package/static/v3/js/views/memory.d2ed7a7c.js +146 -0
  36. package/static/v3/js/views/memory.js +146 -0
  37. package/static/v3/js/views/planning.9ac3e313.js +153 -0
  38. package/static/v3/js/views/planning.js +153 -0
  39. package/static/v3/js/views/skills.c6c2f965.js +109 -0
  40. package/static/v3/js/views/skills.js +109 -0
  41. package/static/v3/js/views/tools.e4f11276.js +108 -0
  42. package/static/v3/js/views/tools.js +108 -0
  43. package/static/v3/js/views/workflows.26c57290.js +128 -0
  44. package/static/v3/js/views/workflows.js +128 -0
@@ -113,6 +113,12 @@ from latticeai.api.tools import create_tools_router
113
113
  from latticeai.api.static_routes import create_static_routes_router
114
114
  from latticeai.api.garden import create_garden_router
115
115
  from latticeai.api.setup import create_setup_router
116
+ from latticeai.api.hooks import create_hooks_router
117
+ from latticeai.core.hooks import HooksRegistry
118
+ from latticeai.api.agent_registry import create_agent_registry_router
119
+ from latticeai.core.agent_registry import AgentRegistry
120
+ from latticeai.api.memory import create_memory_router
121
+ from latticeai.services.memory_service import MemoryService
116
122
  from latticeai.services.tool_dispatch import (
117
123
  LOCAL_WRITE_BLOCKED_PREFIXES as _LOCAL_WRITE_BLOCKED_PREFIXES,
118
124
  TOOL_GOVERNANCE,
@@ -122,6 +128,7 @@ from latticeai.services.tool_dispatch import (
122
128
  configure_tool_dispatch,
123
129
  get_tool_permission,
124
130
  list_tool_permissions,
131
+ tool_response as _tool_response,
125
132
  )
126
133
  from latticeai.core.tool_registry import TOOL_CATALOG_BRIEF as _TOOL_CATALOG_BRIEF
127
134
  from mcp_registry import (
@@ -291,6 +298,19 @@ WORKSPACE_SERVICE = WorkspaceService(WORKSPACE_OS)
291
298
  PLUGINS_DIR = Path(os.getenv("LATTICEAI_PLUGINS_DIR") or (BASE_DIR / "plugins"))
292
299
  PLUGIN_REGISTRY = PluginRegistry(PLUGINS_DIR, store=WORKSPACE_OS)
293
300
  TEMPLATE_CATALOG = TemplateCatalog()
301
+ # ── v3.2 platform registries: lifecycle hooks + agent registry, persisted under
302
+ # DATA_DIR so the /app Hooks and Agent Registry views read/write real state.
303
+ HOOKS_REGISTRY = HooksRegistry(DATA_DIR / "hooks.json")
304
+ AGENT_REGISTRY = AgentRegistry(DATA_DIR / "agent_registry.json")
305
+ # Unified long-term memory platform fronting workspace memories, agent
306
+ # snapshots, conversation history, and the KG graph/vector index.
307
+ MEMORY_SERVICE = MemoryService(
308
+ store=WORKSPACE_OS,
309
+ data_dir=DATA_DIR,
310
+ knowledge_graph=KNOWLEDGE_GRAPH,
311
+ enable_graph=ENABLE_GRAPH,
312
+ history_file=HISTORY_FILE,
313
+ )
294
314
 
295
315
  def _require_graph():
296
316
  if not ENABLE_GRAPH or KNOWLEDGE_GRAPH is None:
@@ -1439,6 +1459,27 @@ app.include_router(create_tools_router(
1439
1459
  mcp_public_item=mcp_public_item,
1440
1460
  ))
1441
1461
 
1462
+ app.include_router(create_hooks_router(
1463
+ registry=HOOKS_REGISTRY,
1464
+ require_user=require_user,
1465
+ append_audit_event=append_audit_event,
1466
+ ))
1467
+
1468
+ app.include_router(create_agent_registry_router(
1469
+ registry=AGENT_REGISTRY,
1470
+ require_user=require_user,
1471
+ append_audit_event=append_audit_event,
1472
+ ))
1473
+
1474
+ app.include_router(create_memory_router(
1475
+ service=MEMORY_SERVICE,
1476
+ require_user=require_user,
1477
+ get_current_user=get_current_user,
1478
+ gate_read=PLATFORM.gate_read,
1479
+ gate_write=PLATFORM.gate_write,
1480
+ append_audit_event=append_audit_event,
1481
+ ))
1482
+
1442
1483
  app.include_router(create_garden_router(gardener=gardener, require_user=require_user))
1443
1484
  app.include_router(create_setup_router(model_router=router, require_user=require_user))
1444
1485
 
@@ -0,0 +1,324 @@
1
+ """Long-Term Memory platform + Memory Manager (v3.2.0).
2
+
3
+ Parts 7, 8 and 13. Lattice AI already persists memory in several real stores;
4
+ before this service they were unrelated. ``MemoryService`` unifies them behind
5
+ one façade and adds a Memory Manager that reports usage / sources / health /
6
+ size / type and supports recall / inspect / prune / compact / rebuild / clear.
7
+
8
+ Memory tiers and their real backing store (nothing is fabricated — a tier with
9
+ no backing reports ``unavailable``):
10
+
11
+ * **workspace** — personal workspace memories (``WorkspaceOS`` memories)
12
+ * **project** — memories scoped to a non-personal (organization) workspace
13
+ * **agent** — agent memory snapshots captured during runs
14
+ * **conversation** — chat history conversations
15
+ * **graph** — Knowledge Graph nodes (entities + relations)
16
+ * **vector** — local embedding vector index
17
+
18
+ The service never invents counts or health: every number is read from the
19
+ underlying store, and missing stores surface as ``unavailable``.
20
+ """
21
+
22
+ from __future__ import annotations
23
+
24
+ import json
25
+ from datetime import datetime
26
+ from pathlib import Path
27
+ from typing import Any, Callable, Dict, List, Optional
28
+
29
+ # Personal workspace memory kinds (from WorkspaceOS.MEMORY_KINDS).
30
+ WORKSPACE_KINDS = (
31
+ "short_term",
32
+ "workspace",
33
+ "preferences",
34
+ "decisions",
35
+ "working_style",
36
+ "frequently_used_tools",
37
+ "long_term",
38
+ )
39
+
40
+ TIERS = ("workspace", "project", "agent", "conversation", "graph", "vector")
41
+
42
+
43
+ def _now() -> str:
44
+ return datetime.now().isoformat(timespec="seconds")
45
+
46
+
47
+ def _file_size(path: Path) -> int:
48
+ try:
49
+ return path.stat().st_size if path.exists() else 0
50
+ except Exception:
51
+ return 0
52
+
53
+
54
+ class MemoryService:
55
+ def __init__(
56
+ self,
57
+ *,
58
+ store: Any,
59
+ data_dir: Path,
60
+ knowledge_graph: Any = None,
61
+ enable_graph: bool = True,
62
+ history_file: Optional[Path] = None,
63
+ ):
64
+ self._store = store
65
+ self._kg = knowledge_graph
66
+ self._enable_graph = bool(enable_graph and knowledge_graph is not None)
67
+ self._data_dir = Path(data_dir)
68
+ self._history_file = Path(history_file) if history_file else (self._data_dir / "chat_history.json")
69
+
70
+ # ── helpers over the underlying stores ────────────────────────────────
71
+ def _workspace_memories(self, *, user_email: Optional[str], workspace_id: Optional[str]) -> List[Dict[str, Any]]:
72
+ try:
73
+ return list(self._store.list_memories(user_email=user_email, workspace_id=workspace_id).get("memories", []))
74
+ except Exception:
75
+ return []
76
+
77
+ def _all_memories(self) -> List[Dict[str, Any]]:
78
+ try:
79
+ return list(self._store.list_memories().get("memories", []))
80
+ except Exception:
81
+ return []
82
+
83
+ def _snapshots(self, *, workspace_id: Optional[str]) -> List[Dict[str, Any]]:
84
+ try:
85
+ return list(self._store.list_memory_snapshots(workspace_id=workspace_id, limit=200).get("snapshots", []))
86
+ except Exception:
87
+ return []
88
+
89
+ def _conversations(self) -> List[Dict[str, Any]]:
90
+ if not self._history_file.exists():
91
+ return []
92
+ try:
93
+ with open(self._history_file, "r", encoding="utf-8") as fh:
94
+ data = json.load(fh)
95
+ except Exception:
96
+ return []
97
+ if isinstance(data, dict):
98
+ convs = data.get("conversations")
99
+ if isinstance(convs, list):
100
+ return convs
101
+ return [{"id": k, **(v if isinstance(v, dict) else {"messages": v})} for k, v in data.items()]
102
+ if isinstance(data, list):
103
+ return data
104
+ return []
105
+
106
+ def _kg_stats(self) -> Optional[Dict[str, Any]]:
107
+ if not self._enable_graph:
108
+ return None
109
+ try:
110
+ return self._kg.stats()
111
+ except Exception:
112
+ return None
113
+
114
+ def _kg_index(self) -> Optional[Dict[str, Any]]:
115
+ if not self._enable_graph:
116
+ return None
117
+ try:
118
+ return self._kg.index_status()
119
+ except Exception:
120
+ return None
121
+
122
+ # ── Memory Manager: sources / usage / health ──────────────────────────
123
+ def manager(self, *, user_email: Optional[str] = None, workspace_id: Optional[str] = None) -> Dict[str, Any]:
124
+ ws_mem = self._workspace_memories(user_email=user_email, workspace_id="personal")
125
+ project_mem = [m for m in self._all_memories() if (m.get("workspace_id") or "personal") != "personal"]
126
+ snaps = self._snapshots(workspace_id=workspace_id)
127
+ convs = self._conversations()
128
+ kg_stats = self._kg_stats()
129
+ kg_index = self._kg_index()
130
+
131
+ ws_bytes = _file_size(self._data_dir / "workspace_os.json")
132
+ kg_bytes = _file_size(self._data_dir / "knowledge_graph.sqlite")
133
+ conv_bytes = _file_size(self._history_file)
134
+
135
+ node_total = sum((kg_stats or {}).get("nodes", {}).values()) if kg_stats else None
136
+ edge_total = sum((kg_stats or {}).get("edges", {}).values()) if kg_stats else None
137
+ vector_total = None
138
+ if kg_index and isinstance(kg_index.get("vector_counts"), dict):
139
+ vector_total = sum(kg_index["vector_counts"].values())
140
+ elif kg_index:
141
+ vector_total = kg_index.get("indexed") or kg_index.get("ready")
142
+
143
+ sources = [
144
+ {
145
+ "id": "workspace", "type": "workspace", "label": "Workspace Memory",
146
+ "count": len(ws_mem), "size_bytes": ws_bytes if ws_mem else 0,
147
+ "health": "ok", "detail": "Personal workspace knowledge, by kind.",
148
+ },
149
+ {
150
+ "id": "project", "type": "project", "label": "Project Memory",
151
+ "count": len(project_mem), "size_bytes": 0,
152
+ "health": "ok", "detail": "Memory scoped to organization workspaces.",
153
+ },
154
+ {
155
+ "id": "agent", "type": "agent", "label": "Agent Memory",
156
+ "count": len(snaps), "size_bytes": 0,
157
+ "health": "ok", "detail": "Per-run agent memory snapshots.",
158
+ },
159
+ {
160
+ "id": "conversation", "type": "conversation", "label": "Conversation Memory",
161
+ "count": len(convs), "size_bytes": conv_bytes,
162
+ "health": "ok" if self._history_file.exists() else "empty",
163
+ "detail": "Historical interaction memory from chat.",
164
+ },
165
+ {
166
+ "id": "graph", "type": "graph", "label": "Graph Memory",
167
+ "count": node_total, "size_bytes": kg_bytes,
168
+ "health": "ok" if kg_stats else "unavailable",
169
+ "detail": "Knowledge Graph entities and relations." if kg_stats else "Knowledge graph disabled or unavailable.",
170
+ "edges": edge_total,
171
+ },
172
+ {
173
+ "id": "vector", "type": "vector", "label": "Vector Memory",
174
+ "count": vector_total, "size_bytes": 0,
175
+ "health": "ok" if kg_index else "unavailable",
176
+ "detail": "Local embedding vector index." if kg_index else "Vector index unavailable.",
177
+ },
178
+ ]
179
+ total_items = sum((s["count"] or 0) for s in sources)
180
+ total_bytes = ws_bytes + kg_bytes + conv_bytes
181
+ healthy = sum(1 for s in sources if s["health"] == "ok")
182
+ overall = "ok" if healthy >= 4 else "degraded" if healthy >= 1 else "unavailable"
183
+ return {
184
+ "sources": sources,
185
+ "tiers": list(TIERS),
186
+ "usage": {"total_items": total_items, "total_bytes": total_bytes, "sources": len(sources)},
187
+ "health": overall,
188
+ "graph_enabled": self._enable_graph,
189
+ "generated_at": _now(),
190
+ }
191
+
192
+ def tiers(self) -> Dict[str, Any]:
193
+ return {"tiers": list(TIERS), "workspace_kinds": list(WORKSPACE_KINDS)}
194
+
195
+ # ── recall (unified retrieval over the memory tiers) ───────────────────
196
+ def recall(
197
+ self,
198
+ query: str,
199
+ *,
200
+ user_email: Optional[str] = None,
201
+ workspace_id: Optional[str] = None,
202
+ limit: int = 20,
203
+ ) -> Dict[str, Any]:
204
+ q = str(query or "").strip()
205
+ results: List[Dict[str, Any]] = []
206
+
207
+ try:
208
+ mem = self._store.search_memories(q, user_email=user_email, limit=limit, workspace_id=workspace_id).get("memories", [])
209
+ except Exception:
210
+ mem = []
211
+ for m in mem:
212
+ results.append({
213
+ "source": "workspace",
214
+ "id": m.get("id"),
215
+ "title": (m.get("kind") or "memory"),
216
+ "snippet": str(m.get("content") or "")[:240],
217
+ "kind": m.get("kind"),
218
+ "score": 0.6,
219
+ "tags": m.get("tags") or [],
220
+ })
221
+
222
+ if self._enable_graph and q:
223
+ try:
224
+ hits = self._kg.search(q, limit).get("results", [])
225
+ except Exception:
226
+ hits = []
227
+ for hsit in hits[:limit]:
228
+ results.append({
229
+ "source": "graph",
230
+ "id": hsit.get("id") or hsit.get("node_id"),
231
+ "title": hsit.get("title") or hsit.get("name") or "node",
232
+ "snippet": str(hsit.get("summary") or hsit.get("content") or "")[:240],
233
+ "kind": hsit.get("type") or "node",
234
+ "score": float(hsit.get("score") or 0.5),
235
+ })
236
+
237
+ results.sort(key=lambda r: r.get("score", 0), reverse=True)
238
+ return {"query": q, "results": results[: max(1, min(limit, 100))], "count": len(results), "source": "live"}
239
+
240
+ # ── inspect a single tier ─────────────────────────────────────────────
241
+ def inspect(self, source: str, *, user_email: Optional[str] = None, workspace_id: Optional[str] = None, limit: int = 50) -> Dict[str, Any]:
242
+ if source == "workspace":
243
+ items = self._workspace_memories(user_email=user_email, workspace_id="personal")[:limit]
244
+ return {"source": source, "items": items, "count": len(items)}
245
+ if source == "project":
246
+ items = [m for m in self._all_memories() if (m.get("workspace_id") or "personal") != "personal"][:limit]
247
+ return {"source": source, "items": items, "count": len(items)}
248
+ if source == "agent":
249
+ items = self._snapshots(workspace_id=workspace_id)[:limit]
250
+ return {"source": source, "items": items, "count": len(items)}
251
+ if source == "conversation":
252
+ convs = self._conversations()
253
+ items = [{"id": c.get("id"), "title": c.get("title") or c.get("id"), "messages": len(c.get("messages") or [])} for c in convs[:limit]]
254
+ return {"source": source, "items": items, "count": len(convs)}
255
+ if source == "graph":
256
+ return {"source": source, "stats": self._kg_stats() or {}, "available": bool(self._kg_stats())}
257
+ if source == "vector":
258
+ return {"source": source, "index": self._kg_index() or {}, "available": bool(self._kg_index())}
259
+ raise KeyError(source)
260
+
261
+ # ── mutating operations ───────────────────────────────────────────────
262
+ def prune(self, *, ids: Optional[List[str]] = None, kind: Optional[str] = None, user_email: Optional[str] = None) -> Dict[str, Any]:
263
+ removed: List[str] = []
264
+ target_ids = list(ids or [])
265
+ if kind:
266
+ for m in self._workspace_memories(user_email=user_email, workspace_id=None):
267
+ if m.get("kind") == kind and m.get("id"):
268
+ target_ids.append(m["id"])
269
+ for mid in target_ids:
270
+ try:
271
+ self._store.delete_memory(mid)
272
+ removed.append(mid)
273
+ except Exception:
274
+ continue
275
+ return {"removed": removed, "count": len(removed)}
276
+
277
+ def compact(self, *, user_email: Optional[str] = None) -> Dict[str, Any]:
278
+ """Dedupe workspace memories with identical (kind, content)."""
279
+ seen: set = set()
280
+ removed: List[str] = []
281
+ # Oldest first so the first occurrence (oldest) is kept.
282
+ memories = list(reversed(self._workspace_memories(user_email=user_email, workspace_id=None)))
283
+ for m in memories:
284
+ key = (m.get("kind"), str(m.get("content") or "").strip())
285
+ if key in seen:
286
+ if m.get("id"):
287
+ try:
288
+ self._store.delete_memory(m["id"])
289
+ removed.append(m["id"])
290
+ except Exception:
291
+ continue
292
+ else:
293
+ seen.add(key)
294
+ return {"compacted": len(removed), "removed": removed, "remaining": len(seen)}
295
+
296
+ def rebuild(self, target: str = "vector") -> Dict[str, Any]:
297
+ if target in {"vector", "index", "vector_index"}:
298
+ if not self._enable_graph:
299
+ return {"status": "unavailable", "detail": "Knowledge graph / vector index disabled."}
300
+ try:
301
+ result = self._kg.rebuild_vector_index()
302
+ return {"status": "ok", "target": "vector_index", "result": result}
303
+ except Exception as exc:
304
+ return {"status": "error", "detail": str(exc)}
305
+ return {"status": "error", "detail": f"Unknown rebuild target: {target}"}
306
+
307
+ def clear(self, *, scope: str, confirm: bool = False, user_email: Optional[str] = None) -> Dict[str, Any]:
308
+ if not confirm:
309
+ raise ValueError("clear requires confirm=true")
310
+ if scope in WORKSPACE_KINDS:
311
+ result = self.prune(kind=scope, user_email=user_email)
312
+ return {"cleared": scope, **result}
313
+ if scope == "workspace":
314
+ ids = [m["id"] for m in self._workspace_memories(user_email=user_email, workspace_id=None) if m.get("id")]
315
+ result = self.prune(ids=ids, user_email=user_email)
316
+ return {"cleared": "workspace", **result}
317
+ if scope == "graph":
318
+ if not self._enable_graph:
319
+ return {"status": "unavailable", "detail": "Knowledge graph disabled."}
320
+ try:
321
+ return {"cleared": "graph", "result": self._kg.clear_all()}
322
+ except Exception as exc:
323
+ return {"status": "error", "detail": str(exc)}
324
+ raise ValueError(f"unsupported clear scope: {scope}")
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "ltcai",
3
- "version": "3.1.0",
3
+ "version": "3.2.0",
4
4
  "description": "Lattice AI v3 local-first AI workspace platform with knowledge graph, vector index, hybrid search, agents, and workspace modes.",
5
5
  "homepage": "https://github.com/TaeSooPark-PTS/LatticeAI#readme",
6
6
  "repository": {
@@ -20,7 +20,7 @@
20
20
  "build": "npm run build:assets && npm run build:python",
21
21
  "build:assets": "node scripts/build_v3_assets.mjs",
22
22
  "build:python": "python3 -m build",
23
- "check:python": "python3 -m py_compile ltcai_cli.py server.py latticeai/server_app.py latticeai/api/auth.py latticeai/api/chat.py latticeai/api/computer_use.py latticeai/api/deps.py latticeai/api/garden.py latticeai/api/local_files.py latticeai/api/permissions.py latticeai/api/setup.py latticeai/api/static_routes.py latticeai/api/tools.py latticeai/api/plugins.py latticeai/api/workflow_designer.py latticeai/api/agents.py latticeai/api/realtime.py latticeai/api/marketplace.py latticeai/api/search.py latticeai/services/search_service.py latticeai/core/local_embeddings.py latticeai/core/embedding_providers.py latticeai/services/agent_runtime.py latticeai/core/config.py latticeai/api/admin.py latticeai/services/app_context.py latticeai/services/model_runtime.py latticeai/services/model_catalog.py latticeai/services/model_recommendation.py latticeai/services/tool_dispatch.py latticeai/services/upload_service.py latticeai/core/tool_registry.py latticeai/core/enterprise.py latticeai/core/enterprise_admin.py latticeai/core/agent_prompts.py latticeai/core/workspace_os.py latticeai/core/plugins.py latticeai/core/marketplace.py latticeai/core/workflow_engine.py latticeai/core/multi_agent.py latticeai/core/realtime.py knowledge_graph.py knowledge_graph_api.py local_knowledge_api.py llm_router.py p_reinforce.py telegram_bot.py tools.py codex_telegram_bot.py",
23
+ "check:python": "python3 -m py_compile ltcai_cli.py server.py latticeai/server_app.py latticeai/api/auth.py latticeai/api/chat.py latticeai/api/computer_use.py latticeai/api/deps.py latticeai/api/garden.py latticeai/api/local_files.py latticeai/api/permissions.py latticeai/api/setup.py latticeai/api/static_routes.py latticeai/api/tools.py latticeai/api/plugins.py latticeai/api/workflow_designer.py latticeai/api/agents.py latticeai/api/realtime.py latticeai/api/marketplace.py latticeai/api/search.py latticeai/services/search_service.py latticeai/core/local_embeddings.py latticeai/core/embedding_providers.py latticeai/services/agent_runtime.py latticeai/core/config.py latticeai/api/admin.py latticeai/services/app_context.py latticeai/services/model_runtime.py latticeai/services/model_catalog.py latticeai/services/model_recommendation.py latticeai/services/tool_dispatch.py latticeai/services/upload_service.py latticeai/core/tool_registry.py latticeai/core/enterprise.py latticeai/core/enterprise_admin.py latticeai/core/agent_prompts.py latticeai/core/workspace_os.py latticeai/core/plugins.py latticeai/core/marketplace.py latticeai/core/workflow_engine.py latticeai/core/multi_agent.py latticeai/core/realtime.py latticeai/api/mcp.py latticeai/core/hooks.py latticeai/api/hooks.py latticeai/core/agent_registry.py latticeai/api/agent_registry.py latticeai/services/memory_service.py latticeai/api/memory.py knowledge_graph.py knowledge_graph_api.py local_knowledge_api.py llm_router.py p_reinforce.py telegram_bot.py tools.py codex_telegram_bot.py",
24
24
  "lint": "node --check static/scripts/account.js && node --check static/scripts/admin.js && node --check static/scripts/chat.js && node --check static/scripts/graph.js && node --check static/scripts/platform.js && node --check static/scripts/ux.js && node --check static/scripts/workspace.js && node --check tests/visual/mock_server.cjs && node --check tests/visual/v3.spec.js && npm run lint:v3",
25
25
  "lint:v3": "node scripts/lint_v3.mjs",
26
26
  "typecheck": "cd vscode-extension && npm run build",
@@ -151,7 +151,7 @@ for (const mod of modules.values()) {
151
151
  }
152
152
 
153
153
  const manifest = {
154
- version: "3.1.0",
154
+ version: "3.2.0",
155
155
  generated_at: "deterministic",
156
156
  entrypoints: {
157
157
  app: assets[entry],
@@ -1,8 +1,8 @@
1
1
  {
2
- "version": "3.1.0",
2
+ "version": "3.2.0",
3
3
  "generated_at": "deterministic",
4
4
  "entrypoints": {
5
- "app": "/static/v3/js/app.46fb61d9.js",
5
+ "app": "/static/v3/js/app.a5adc0f3.js",
6
6
  "styles": [
7
7
  "/static/css/tokens.5a595671.css",
8
8
  "/static/v3/css/lattice.tokens.c597ff81.css",
@@ -19,13 +19,13 @@
19
19
  "static/v3/css/lattice.components.css": "/static/v3/css/lattice.components.011e988b.css",
20
20
  "static/v3/css/lattice.shell.css": "/static/v3/css/lattice.shell.4920f42d.css",
21
21
  "static/v3/css/lattice.views.css": "/static/v3/css/lattice.views.3ee19d4e.css",
22
- "static/v3/js/app.js": "/static/v3/js/app.46fb61d9.js",
23
- "static/v3/js/core/api.js": "/static/v3/js/core/api.22a41d42.js",
22
+ "static/v3/js/app.js": "/static/v3/js/app.a5adc0f3.js",
23
+ "static/v3/js/core/api.js": "/static/v3/js/core/api.603b978f.js",
24
24
  "static/v3/js/core/components.js": "/static/v3/js/core/components.4c83e0a9.js",
25
25
  "static/v3/js/core/dom.js": "/static/v3/js/core/dom.a2773eb0.js",
26
26
  "static/v3/js/core/router.js": "/static/v3/js/core/router.584570f2.js",
27
- "static/v3/js/core/routes.js": "/static/v3/js/core/routes.f935dd50.js",
28
- "static/v3/js/core/shell.js": "/static/v3/js/core/shell.1b6199d6.js",
27
+ "static/v3/js/core/routes.js": "/static/v3/js/core/routes.07ad6696.js",
28
+ "static/v3/js/core/shell.js": "/static/v3/js/core/shell.ea0b9ae5.js",
29
29
  "static/v3/js/core/store.js": "/static/v3/js/core/store.34ebd5e6.js",
30
30
  "static/v3/js/views/admin-audit.js": "/static/v3/js/views/admin-audit.660a1fb1.js",
31
31
  "static/v3/js/views/admin-permissions.js": "/static/v3/js/views/admin-permissions.a7ae5f09.js",
@@ -33,15 +33,23 @@
33
33
  "static/v3/js/views/admin-private-vpc.js": "/static/v3/js/views/admin-private-vpc.7d342d36.js",
34
34
  "static/v3/js/views/admin-security.js": "/static/v3/js/views/admin-security.07c66b72.js",
35
35
  "static/v3/js/views/admin-users.js": "/static/v3/js/views/admin-users.03bac88c.js",
36
- "static/v3/js/views/agents.js": "/static/v3/js/views/agents.14e48bdd.js",
36
+ "static/v3/js/views/agents.js": "/static/v3/js/views/agents.c373d48c.js",
37
37
  "static/v3/js/views/chat.js": "/static/v3/js/views/chat.718144ce.js",
38
38
  "static/v3/js/views/files.js": "/static/v3/js/views/files.4935197e.js",
39
39
  "static/v3/js/views/home.js": "/static/v3/js/views/home.cdde3b32.js",
40
+ "static/v3/js/views/hooks.js": "/static/v3/js/views/hooks.f3edebca.js",
40
41
  "static/v3/js/views/hybrid-search.js": "/static/v3/js/views/hybrid-search.b22b97e0.js",
41
42
  "static/v3/js/views/knowledge-graph.js": "/static/v3/js/views/knowledge-graph.a14ea7e7.js",
43
+ "static/v3/js/views/marketplace.js": "/static/v3/js/views/marketplace.ab0583d4.js",
44
+ "static/v3/js/views/mcp.js": "/static/v3/js/views/mcp.99b5c6a7.js",
45
+ "static/v3/js/views/memory.js": "/static/v3/js/views/memory.d2ed7a7c.js",
42
46
  "static/v3/js/views/models.js": "/static/v3/js/views/models.a1ffa147.js",
43
47
  "static/v3/js/views/my-computer.js": "/static/v3/js/views/my-computer.1b2ff621.js",
44
48
  "static/v3/js/views/pipeline.js": "/static/v3/js/views/pipeline.c522f1ce.js",
45
- "static/v3/js/views/settings.js": "/static/v3/js/views/settings.4f777210.js"
49
+ "static/v3/js/views/planning.js": "/static/v3/js/views/planning.9ac3e313.js",
50
+ "static/v3/js/views/settings.js": "/static/v3/js/views/settings.4f777210.js",
51
+ "static/v3/js/views/skills.js": "/static/v3/js/views/skills.c6c2f965.js",
52
+ "static/v3/js/views/tools.js": "/static/v3/js/views/tools.e4f11276.js",
53
+ "static/v3/js/views/workflows.js": "/static/v3/js/views/workflows.26c57290.js"
46
54
  }
47
55
  }
@@ -3,7 +3,7 @@
3
3
  * Boots the shell. Views are lazy-loaded by the router (see core/routes.js).
4
4
  * ========================================================================== */
5
5
 
6
- import { boot } from "./core/shell.1b6199d6.js";
6
+ import { boot } from "./core/shell.ea0b9ae5.js";
7
7
 
8
8
  const root = document.getElementById("app");
9
9
  if (root) boot(root);
@@ -321,6 +321,70 @@ export const api = {
321
321
  }
322
322
  return { source: "live", text, trace, model };
323
323
  },
324
+
325
+ /* ── v3.2 platform surfaces (all fallback-safe; never fabricate) ─────── */
326
+
327
+ // Agent Registry (Part 2)
328
+ agentRegistry(type) { return withFallback(`/agents/api/registry${type ? "?type=" + encodeURIComponent(type) : ""}`, {}, { agents: [], counts: {}, types: [] }); },
329
+ agentCapabilities() { return withFallback("/agents/api/registry/capabilities", {}, { capabilities: {} }); },
330
+ registerAgent(body) { return raw("/agents/api/registry", { method: "POST", body }); },
331
+ updateAgent(id, body) { return raw(`/agents/api/registry/${encodeURIComponent(id)}`, { method: "PATCH", body }); },
332
+ removeAgent(id) { return raw(`/agents/api/registry/${encodeURIComponent(id)}`, { method: "DELETE" }); },
333
+ agentRunDetail(runId) { return raw(`/agents/api/runs/${encodeURIComponent(runId)}`); },
334
+ agentRunReplay(runId) { return raw(`/agents/api/runs/${encodeURIComponent(runId)}/replay`); },
335
+ stopAgentRun(runId) { return raw(`/agents/api/runs/${encodeURIComponent(runId)}/stop`, { method: "POST" }); },
336
+
337
+ // Marketplace + Templates (Parts 3, 4)
338
+ templates(kind) { return withFallback(`/marketplace/templates${kind ? "?kind=" + encodeURIComponent(kind) : ""}`, {}, { templates: [], kinds: [] }); },
339
+ templateRegistry() { return withFallback("/marketplace/templates/registry", {}, { registry: [] }); },
340
+ exportTemplate(kind, id) { return raw(`/marketplace/templates/${encodeURIComponent(kind)}/${encodeURIComponent(id)}/export`); },
341
+ importTemplate(data) { return raw("/marketplace/templates/import", { method: "POST", body: { data } }); },
342
+ installTemplate(data) { return raw("/marketplace/templates/install", { method: "POST", body: { data } }); },
343
+ cloneTemplate(kind, id, name) { return raw(`/marketplace/templates/${encodeURIComponent(kind)}/${encodeURIComponent(id)}/clone`, { method: "POST", body: { name } }); },
344
+ pluginsRegistry() { return withFallback("/plugins/registry", {}, { plugins: [] }); },
345
+ pluginsDirectory() { return withFallback("/plugins/directory", {}, { plugins: [], categories: [] }); },
346
+
347
+ // Workflow Agents (Part 5)
348
+ workflowDefinitions() { return withFallback("/workflows/api/definitions", {}, { workflows: [] }); },
349
+ createWorkflow(body) { return raw("/workflows/api/definitions", { method: "POST", body }); },
350
+ runWorkflow(id, body = {}) { return raw(`/workflows/api/definitions/${encodeURIComponent(id)}/run`, { method: "POST", body }); },
351
+ workflowRuns() { return withFallback("/workflows/api/runs", {}, { runs: [] }); },
352
+ workflowReplay(runId) { return raw(`/workflows/api/runs/${encodeURIComponent(runId)}/replay`); },
353
+
354
+ // Long-Term Memory + Memory Manager (Parts 7, 8)
355
+ memoryManager() { return withFallback("/api/memory/manager", {}, { sources: [], tiers: [], usage: {} }); },
356
+ memoryTiers() { return withFallback("/api/memory/tiers", {}, { tiers: [], workspace_kinds: [] }); },
357
+ memoryInspect(source, limit = 50) { return withFallback(`/api/memory/inspect?source=${encodeURIComponent(source)}&limit=${limit}`, {}, { items: [] }); },
358
+ memoryRecall(query, limit = 20) { return raw("/api/memory/recall", { method: "POST", body: { query, limit } }); },
359
+ memoryPrune(body) { return raw("/api/memory/prune", { method: "POST", body }); },
360
+ memoryCompact() { return raw("/api/memory/compact", { method: "POST", body: {} }); },
361
+ memoryRebuild(target = "vector") { return raw("/api/memory/rebuild", { method: "POST", body: { target } }); },
362
+ memoryClear(scope, confirm = true) { return raw("/api/memory/clear", { method: "POST", body: { scope, confirm } }); },
363
+ workspaceMemories(kind) { return withFallback(`/workspace/memories${kind ? "?kind=" + encodeURIComponent(kind) : ""}`, {}, { memories: [] }); },
364
+
365
+ // Skills Registry (Part 9)
366
+ skills() { return withFallback("/workspace/skills", {}, { skills: [] }); },
367
+ skillEnable(skill) { return raw("/workspace/skills/enable", { method: "POST", body: { skill } }); },
368
+ skillDisable(skill) { return raw("/workspace/skills/disable", { method: "POST", body: { skill } }); },
369
+ skillInstall(skill, plugin) { return raw("/workspace/skills/install", { method: "POST", body: { skill, plugin: plugin || "" } }); },
370
+ skillUninstall(skill) { return raw("/workspace/skills/uninstall", { method: "POST", body: { skill } }); },
371
+ skillsMarketplace() { return withFallback("/skills/marketplace", {}, { skills: [], categories: [] }); },
372
+
373
+ // Hooks Registry (Part 10)
374
+ hooks(kind) { return withFallback(`/api/hooks${kind ? "?kind=" + encodeURIComponent(kind) : ""}`, {}, { hooks: [], kinds: [], counts: {} }); },
375
+ hookEnable(hook_id, enabled = true) { return raw("/api/hooks/enable", { method: "POST", body: { hook_id, enabled } }); },
376
+ hookDisable(hook_id) { return raw("/api/hooks/disable", { method: "POST", body: { hook_id, enabled: false } }); },
377
+ hookReorder(kind, ordered_ids) { return raw("/api/hooks/reorder", { method: "POST", body: { kind, ordered_ids } }); },
378
+ hookRegister(body) { return raw("/api/hooks/register", { method: "POST", body }); },
379
+ hookRemove(hook_id) { return raw(`/api/hooks/${encodeURIComponent(hook_id)}`, { method: "DELETE" }); },
380
+
381
+ // Tool Registry + MCP (Parts 11, 12)
382
+ toolPermissions() { return withFallback("/tools/permissions", {}, { permissions: [] }); },
383
+ mcpTools() { return withFallback("/mcp/tools", {}, { tools: [], installed_mcps: [] }); },
384
+ mcpInstalled() { return withFallback("/mcp/installed", {}, { installed: [] }); },
385
+ mcpClaudeServers() { return withFallback("/mcp/claude-code-servers", {}, { servers: [] }); },
386
+ mcpCustom() { return withFallback("/mcp/custom", {}, { custom: [] }); },
387
+ mcpRecommend(query, limit = 6) { return raw("/mcp/recommend", { method: "POST", body: { query, limit } }); },
324
388
  };
325
389
 
326
390
  const sleep = (ms) => new Promise((r) => setTimeout(r, ms));
@@ -321,6 +321,70 @@ export const api = {
321
321
  }
322
322
  return { source: "live", text, trace, model };
323
323
  },
324
+
325
+ /* ── v3.2 platform surfaces (all fallback-safe; never fabricate) ─────── */
326
+
327
+ // Agent Registry (Part 2)
328
+ agentRegistry(type) { return withFallback(`/agents/api/registry${type ? "?type=" + encodeURIComponent(type) : ""}`, {}, { agents: [], counts: {}, types: [] }); },
329
+ agentCapabilities() { return withFallback("/agents/api/registry/capabilities", {}, { capabilities: {} }); },
330
+ registerAgent(body) { return raw("/agents/api/registry", { method: "POST", body }); },
331
+ updateAgent(id, body) { return raw(`/agents/api/registry/${encodeURIComponent(id)}`, { method: "PATCH", body }); },
332
+ removeAgent(id) { return raw(`/agents/api/registry/${encodeURIComponent(id)}`, { method: "DELETE" }); },
333
+ agentRunDetail(runId) { return raw(`/agents/api/runs/${encodeURIComponent(runId)}`); },
334
+ agentRunReplay(runId) { return raw(`/agents/api/runs/${encodeURIComponent(runId)}/replay`); },
335
+ stopAgentRun(runId) { return raw(`/agents/api/runs/${encodeURIComponent(runId)}/stop`, { method: "POST" }); },
336
+
337
+ // Marketplace + Templates (Parts 3, 4)
338
+ templates(kind) { return withFallback(`/marketplace/templates${kind ? "?kind=" + encodeURIComponent(kind) : ""}`, {}, { templates: [], kinds: [] }); },
339
+ templateRegistry() { return withFallback("/marketplace/templates/registry", {}, { registry: [] }); },
340
+ exportTemplate(kind, id) { return raw(`/marketplace/templates/${encodeURIComponent(kind)}/${encodeURIComponent(id)}/export`); },
341
+ importTemplate(data) { return raw("/marketplace/templates/import", { method: "POST", body: { data } }); },
342
+ installTemplate(data) { return raw("/marketplace/templates/install", { method: "POST", body: { data } }); },
343
+ cloneTemplate(kind, id, name) { return raw(`/marketplace/templates/${encodeURIComponent(kind)}/${encodeURIComponent(id)}/clone`, { method: "POST", body: { name } }); },
344
+ pluginsRegistry() { return withFallback("/plugins/registry", {}, { plugins: [] }); },
345
+ pluginsDirectory() { return withFallback("/plugins/directory", {}, { plugins: [], categories: [] }); },
346
+
347
+ // Workflow Agents (Part 5)
348
+ workflowDefinitions() { return withFallback("/workflows/api/definitions", {}, { workflows: [] }); },
349
+ createWorkflow(body) { return raw("/workflows/api/definitions", { method: "POST", body }); },
350
+ runWorkflow(id, body = {}) { return raw(`/workflows/api/definitions/${encodeURIComponent(id)}/run`, { method: "POST", body }); },
351
+ workflowRuns() { return withFallback("/workflows/api/runs", {}, { runs: [] }); },
352
+ workflowReplay(runId) { return raw(`/workflows/api/runs/${encodeURIComponent(runId)}/replay`); },
353
+
354
+ // Long-Term Memory + Memory Manager (Parts 7, 8)
355
+ memoryManager() { return withFallback("/api/memory/manager", {}, { sources: [], tiers: [], usage: {} }); },
356
+ memoryTiers() { return withFallback("/api/memory/tiers", {}, { tiers: [], workspace_kinds: [] }); },
357
+ memoryInspect(source, limit = 50) { return withFallback(`/api/memory/inspect?source=${encodeURIComponent(source)}&limit=${limit}`, {}, { items: [] }); },
358
+ memoryRecall(query, limit = 20) { return raw("/api/memory/recall", { method: "POST", body: { query, limit } }); },
359
+ memoryPrune(body) { return raw("/api/memory/prune", { method: "POST", body }); },
360
+ memoryCompact() { return raw("/api/memory/compact", { method: "POST", body: {} }); },
361
+ memoryRebuild(target = "vector") { return raw("/api/memory/rebuild", { method: "POST", body: { target } }); },
362
+ memoryClear(scope, confirm = true) { return raw("/api/memory/clear", { method: "POST", body: { scope, confirm } }); },
363
+ workspaceMemories(kind) { return withFallback(`/workspace/memories${kind ? "?kind=" + encodeURIComponent(kind) : ""}`, {}, { memories: [] }); },
364
+
365
+ // Skills Registry (Part 9)
366
+ skills() { return withFallback("/workspace/skills", {}, { skills: [] }); },
367
+ skillEnable(skill) { return raw("/workspace/skills/enable", { method: "POST", body: { skill } }); },
368
+ skillDisable(skill) { return raw("/workspace/skills/disable", { method: "POST", body: { skill } }); },
369
+ skillInstall(skill, plugin) { return raw("/workspace/skills/install", { method: "POST", body: { skill, plugin: plugin || "" } }); },
370
+ skillUninstall(skill) { return raw("/workspace/skills/uninstall", { method: "POST", body: { skill } }); },
371
+ skillsMarketplace() { return withFallback("/skills/marketplace", {}, { skills: [], categories: [] }); },
372
+
373
+ // Hooks Registry (Part 10)
374
+ hooks(kind) { return withFallback(`/api/hooks${kind ? "?kind=" + encodeURIComponent(kind) : ""}`, {}, { hooks: [], kinds: [], counts: {} }); },
375
+ hookEnable(hook_id, enabled = true) { return raw("/api/hooks/enable", { method: "POST", body: { hook_id, enabled } }); },
376
+ hookDisable(hook_id) { return raw("/api/hooks/disable", { method: "POST", body: { hook_id, enabled: false } }); },
377
+ hookReorder(kind, ordered_ids) { return raw("/api/hooks/reorder", { method: "POST", body: { kind, ordered_ids } }); },
378
+ hookRegister(body) { return raw("/api/hooks/register", { method: "POST", body }); },
379
+ hookRemove(hook_id) { return raw(`/api/hooks/${encodeURIComponent(hook_id)}`, { method: "DELETE" }); },
380
+
381
+ // Tool Registry + MCP (Parts 11, 12)
382
+ toolPermissions() { return withFallback("/tools/permissions", {}, { permissions: [] }); },
383
+ mcpTools() { return withFallback("/mcp/tools", {}, { tools: [], installed_mcps: [] }); },
384
+ mcpInstalled() { return withFallback("/mcp/installed", {}, { installed: [] }); },
385
+ mcpClaudeServers() { return withFallback("/mcp/claude-code-servers", {}, { servers: [] }); },
386
+ mcpCustom() { return withFallback("/mcp/custom", {}, { custom: [] }); },
387
+ mcpRecommend(query, limit = 6) { return raw("/mcp/recommend", { method: "POST", body: { query, limit } }); },
324
388
  };
325
389
 
326
390
  const sleep = (ms) => new Promise((r) => setTimeout(r, ms));