ltcai 2.0.0 → 2.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.
- package/README.md +140 -589
- package/auto_setup.py +17 -17
- package/docs/CHANGELOG.md +99 -0
- package/docs/MULTI_AGENT_RUNTIME.md +23 -5
- package/docs/PLUGIN_SDK.md +21 -8
- package/docs/REALTIME_COLLABORATION.md +19 -6
- package/docs/V2_ARCHITECTURE.md +65 -33
- package/docs/WORKFLOW_DESIGNER.md +18 -8
- package/docs/architecture.md +127 -135
- package/docs/kg-schema.md +3 -3
- package/docs/public-deploy.md +2 -3
- package/knowledge_graph.py +2 -2
- package/latticeai/__init__.py +1 -1
- package/latticeai/api/agents.py +57 -1
- package/latticeai/api/marketplace.py +81 -0
- package/latticeai/api/models.py +8 -0
- package/latticeai/api/plugins.py +1 -1
- package/latticeai/api/realtime.py +1 -1
- package/latticeai/api/workflow_designer.py +10 -1
- package/latticeai/core/config.py +1 -1
- package/latticeai/core/graph_curator.py +2 -2
- package/latticeai/core/marketplace.py +178 -0
- package/latticeai/core/model_compat.py +7 -63
- package/latticeai/core/model_resolution.py +1 -1
- package/latticeai/core/multi_agent.py +359 -68
- package/latticeai/core/plugins.py +29 -13
- package/latticeai/core/realtime.py +1 -1
- package/latticeai/core/workflow_engine.py +1 -1
- package/latticeai/core/workspace_os.py +257 -10
- package/latticeai/server_app.py +17 -5
- package/latticeai/services/model_catalog.py +105 -153
- package/latticeai/services/model_recommendation.py +28 -17
- package/latticeai/services/model_runtime.py +2 -2
- package/latticeai/services/platform_runtime.py +9 -5
- package/llm_router.py +80 -92
- package/ltcai_cli.py +2 -3
- package/package.json +2 -2
- package/static/agents.html +47 -3
- package/static/chat.html +5 -6
- package/static/plugins.html +51 -0
- package/static/scripts/chat.js +34 -36
- package/static/workflows.html +22 -0
- package/static/workspace.html +1 -1
- package/telegram_bot.py +1 -1
|
@@ -28,7 +28,7 @@ from datetime import datetime
|
|
|
28
28
|
from typing import Any, Callable, Dict, List, Optional
|
|
29
29
|
|
|
30
30
|
|
|
31
|
-
WORKFLOW_ENGINE_VERSION = "2.
|
|
31
|
+
WORKFLOW_ENGINE_VERSION = "2.2.0"
|
|
32
32
|
|
|
33
33
|
# The node vocabulary a workflow can be built from. ``trigger`` and ``output``
|
|
34
34
|
# are structural; the rest dispatch to an injected runner of the same family.
|
|
@@ -18,7 +18,7 @@ from pathlib import Path
|
|
|
18
18
|
from typing import Any, Callable, Dict, Iterable, List, Optional
|
|
19
19
|
|
|
20
20
|
|
|
21
|
-
WORKSPACE_OS_VERSION = "2.
|
|
21
|
+
WORKSPACE_OS_VERSION = "2.2.0"
|
|
22
22
|
|
|
23
23
|
# Workspace types separate single-user Personal workspaces from shared
|
|
24
24
|
# Organization workspaces. Both keep the same local-first JSON store; the type
|
|
@@ -51,6 +51,7 @@ WORKSPACE_AREAS = [
|
|
|
51
51
|
"workflow",
|
|
52
52
|
"plugins",
|
|
53
53
|
"skills",
|
|
54
|
+
"marketplace",
|
|
54
55
|
"timeline",
|
|
55
56
|
]
|
|
56
57
|
|
|
@@ -67,6 +68,8 @@ ONBOARDING_STEPS = [
|
|
|
67
68
|
]
|
|
68
69
|
|
|
69
70
|
MEMORY_KINDS = {
|
|
71
|
+
"short_term",
|
|
72
|
+
"workspace",
|
|
70
73
|
"preferences",
|
|
71
74
|
"decisions",
|
|
72
75
|
"working_style",
|
|
@@ -74,6 +77,23 @@ MEMORY_KINDS = {
|
|
|
74
77
|
"long_term",
|
|
75
78
|
}
|
|
76
79
|
|
|
80
|
+
EXECUTION_EVENT_TYPES = {
|
|
81
|
+
"agent_started",
|
|
82
|
+
"handoff_created",
|
|
83
|
+
"handoff_accepted",
|
|
84
|
+
"handoff_completed",
|
|
85
|
+
"review_requested",
|
|
86
|
+
"review_approved",
|
|
87
|
+
"review_rejected",
|
|
88
|
+
"retry_requested",
|
|
89
|
+
"workflow_started",
|
|
90
|
+
"workflow_completed",
|
|
91
|
+
"plugin_started",
|
|
92
|
+
"plugin_completed",
|
|
93
|
+
"execution_failed",
|
|
94
|
+
"execution_cancelled",
|
|
95
|
+
}
|
|
96
|
+
|
|
77
97
|
DEFAULT_AGENTS = [
|
|
78
98
|
{
|
|
79
99
|
"id": "agent:planner",
|
|
@@ -174,7 +194,7 @@ class WorkspaceOSStore:
|
|
|
174
194
|
self.snapshots_dir.mkdir(parents=True, exist_ok=True)
|
|
175
195
|
self.exports_dir.mkdir(parents=True, exist_ok=True)
|
|
176
196
|
# Optional realtime hook: fired on every timeline event so the Realtime
|
|
177
|
-
# bus
|
|
197
|
+
# bus receives all workspace activity without per-call wiring.
|
|
178
198
|
# Defaults to None → zero behavior change for existing callers/tests.
|
|
179
199
|
self.event_sink = event_sink
|
|
180
200
|
|
|
@@ -282,6 +302,14 @@ class WorkspaceOSStore:
|
|
|
282
302
|
"workflow_designer": True,
|
|
283
303
|
"multi_agent_runtime": True,
|
|
284
304
|
"realtime_collaboration": True,
|
|
305
|
+
"agent_handoff": True,
|
|
306
|
+
"agent_context_packets": True,
|
|
307
|
+
"review_retry_loops": True,
|
|
308
|
+
"timeline_replay": True,
|
|
309
|
+
"agent_memory": True,
|
|
310
|
+
"agent_planning": True,
|
|
311
|
+
"marketplace_foundation": True,
|
|
312
|
+
"realtime_execution_observability": True,
|
|
285
313
|
},
|
|
286
314
|
"onboarding": {
|
|
287
315
|
"completed": False,
|
|
@@ -300,12 +328,15 @@ class WorkspaceOSStore:
|
|
|
300
328
|
"snapshots": [],
|
|
301
329
|
"traces": [],
|
|
302
330
|
"memories": [],
|
|
331
|
+
"memory_snapshots": [],
|
|
303
332
|
"agents": list(DEFAULT_AGENTS),
|
|
304
333
|
"agent_runs": [],
|
|
334
|
+
"handoffs": [],
|
|
305
335
|
"workflows": [],
|
|
306
336
|
"workflow_runs": [],
|
|
307
337
|
"skill_registry": {},
|
|
308
338
|
"plugin_registry": {},
|
|
339
|
+
"template_registry": {},
|
|
309
340
|
"computer_memory": {
|
|
310
341
|
"enabled": False,
|
|
311
342
|
"approved": False,
|
|
@@ -361,6 +392,38 @@ class WorkspaceOSStore:
|
|
|
361
392
|
pass
|
|
362
393
|
return event
|
|
363
394
|
|
|
395
|
+
def _emit_execution_event(
|
|
396
|
+
self,
|
|
397
|
+
*,
|
|
398
|
+
area: str,
|
|
399
|
+
event_type: str,
|
|
400
|
+
payload: Dict[str, Any],
|
|
401
|
+
workspace_id: Optional[str],
|
|
402
|
+
) -> None:
|
|
403
|
+
"""Best-effort execution observability event for the realtime feed."""
|
|
404
|
+
if event_type not in EXECUTION_EVENT_TYPES:
|
|
405
|
+
return
|
|
406
|
+
try:
|
|
407
|
+
self.record_timeline_event(area, event_type, payload, workspace_id=workspace_id)
|
|
408
|
+
except Exception:
|
|
409
|
+
pass
|
|
410
|
+
|
|
411
|
+
def _emit_replayable_timeline_events(
|
|
412
|
+
self,
|
|
413
|
+
*,
|
|
414
|
+
area: str,
|
|
415
|
+
run_id: str,
|
|
416
|
+
timeline: List[Dict[str, Any]],
|
|
417
|
+
workspace_id: Optional[str],
|
|
418
|
+
) -> None:
|
|
419
|
+
for index, item in enumerate(timeline or []):
|
|
420
|
+
event_type = item.get("event") or item.get("event_type")
|
|
421
|
+
if event_type in EXECUTION_EVENT_TYPES:
|
|
422
|
+
payload = {k: v for k, v in item.items() if k not in {"context_packet"}}
|
|
423
|
+
payload["run_id"] = run_id
|
|
424
|
+
payload["timeline_index"] = index
|
|
425
|
+
self._emit_execution_event(area=area, event_type=event_type, payload=payload, workspace_id=workspace_id)
|
|
426
|
+
|
|
364
427
|
def summary(self) -> Dict[str, Any]:
|
|
365
428
|
state = self.load_state()
|
|
366
429
|
return {
|
|
@@ -375,11 +438,14 @@ class WorkspaceOSStore:
|
|
|
375
438
|
"snapshots": len(_listify(state.get("snapshots"))),
|
|
376
439
|
"traces": len(_listify(state.get("traces"))),
|
|
377
440
|
"memories": len(_listify(state.get("memories"))),
|
|
441
|
+
"memory_snapshots": len(_listify(state.get("memory_snapshots"))),
|
|
378
442
|
"agent_runs": len(_listify(state.get("agent_runs"))),
|
|
443
|
+
"handoffs": len(_listify(state.get("handoffs"))),
|
|
379
444
|
"workflows": len(_listify(state.get("workflows"))),
|
|
380
445
|
"workflow_runs": len(_listify(state.get("workflow_runs"))),
|
|
381
446
|
"skills": len(state.get("skill_registry") or {}),
|
|
382
447
|
"plugins": len(state.get("plugin_registry") or {}),
|
|
448
|
+
"templates": len(state.get("template_registry") or {}),
|
|
383
449
|
"timeline": len(_listify(state.get("timeline"))),
|
|
384
450
|
},
|
|
385
451
|
"onboarding": state.get("onboarding"),
|
|
@@ -646,7 +712,9 @@ class WorkspaceOSStore:
|
|
|
646
712
|
public["counts"] = {
|
|
647
713
|
"snapshots": len(self._scoped(_listify(state.get("snapshots")), workspace_id)),
|
|
648
714
|
"memories": len(self._scoped(_listify(state.get("memories")), workspace_id)),
|
|
715
|
+
"memory_snapshots": len(self._scoped(_listify(state.get("memory_snapshots")), workspace_id)),
|
|
649
716
|
"agent_runs": len(self._scoped(_listify(state.get("agent_runs")), workspace_id)),
|
|
717
|
+
"handoffs": len(self._scoped(_listify(state.get("handoffs")), workspace_id)),
|
|
650
718
|
"workflows": len(self._scoped(_listify(state.get("workflows")), workspace_id)),
|
|
651
719
|
"workflow_runs": len(self._scoped(_listify(state.get("workflow_runs")), workspace_id)),
|
|
652
720
|
"traces": len(self._scoped(_listify(state.get("traces")), workspace_id)),
|
|
@@ -1118,7 +1186,7 @@ class WorkspaceOSStore:
|
|
|
1118
1186
|
"content": content,
|
|
1119
1187
|
"user_email": user_email,
|
|
1120
1188
|
"tags": tags or [],
|
|
1121
|
-
"metadata": metadata or {},
|
|
1189
|
+
"metadata": {**(metadata or {}), "memory_scope": kind},
|
|
1122
1190
|
"workspace_id": self._resolve_scope(workspace_id, state) if existing is None else self._record_workspace(record),
|
|
1123
1191
|
"updated_at": now,
|
|
1124
1192
|
})
|
|
@@ -1138,7 +1206,7 @@ class WorkspaceOSStore:
|
|
|
1138
1206
|
memories.append(record)
|
|
1139
1207
|
state["memories"] = memories[-500:]
|
|
1140
1208
|
self.save_state(state)
|
|
1141
|
-
self.record_timeline_event("memory", "memory_upserted", {"memory_id": memory_id, "kind": kind})
|
|
1209
|
+
self.record_timeline_event("memory", "memory_upserted", {"memory_id": memory_id, "kind": kind}, workspace_id=record.get("workspace_id"))
|
|
1142
1210
|
return record
|
|
1143
1211
|
|
|
1144
1212
|
def list_memories(self, user_email: Optional[str] = None, kind: Optional[str] = None, workspace_id: Optional[str] = None) -> Dict[str, Any]:
|
|
@@ -1172,6 +1240,44 @@ class WorkspaceOSStore:
|
|
|
1172
1240
|
self.record_timeline_event("memory", "memory_deleted", {"memory_id": memory_id})
|
|
1173
1241
|
return {"status": "ok", "memory_id": memory_id}
|
|
1174
1242
|
|
|
1243
|
+
def create_memory_snapshot(
|
|
1244
|
+
self,
|
|
1245
|
+
*,
|
|
1246
|
+
label: str = "memory snapshot",
|
|
1247
|
+
user_email: Optional[str] = None,
|
|
1248
|
+
workspace_id: Optional[str] = None,
|
|
1249
|
+
memory_ids: Optional[List[str]] = None,
|
|
1250
|
+
reason: str = "",
|
|
1251
|
+
) -> Dict[str, Any]:
|
|
1252
|
+
"""Persist a replayable point-in-time memory view without mutation."""
|
|
1253
|
+
state = self.load_state()
|
|
1254
|
+
scope = self._resolve_scope(workspace_id, state)
|
|
1255
|
+
memories = self._scoped(_listify(state.get("memories")), scope)
|
|
1256
|
+
if user_email:
|
|
1257
|
+
memories = [item for item in memories if item.get("user_email") in {None, user_email}]
|
|
1258
|
+
if memory_ids:
|
|
1259
|
+
allowed = set(memory_ids)
|
|
1260
|
+
memories = [item for item in memories if item.get("id") in allowed]
|
|
1261
|
+
snapshot = {
|
|
1262
|
+
"id": f"memory-snapshot-{_json_hash([label, scope, memories, _now()])[:16]}",
|
|
1263
|
+
"label": label,
|
|
1264
|
+
"reason": reason,
|
|
1265
|
+
"workspace_id": scope,
|
|
1266
|
+
"user_email": user_email,
|
|
1267
|
+
"memory_count": len(memories),
|
|
1268
|
+
"memories": memories,
|
|
1269
|
+
"created_at": _now(),
|
|
1270
|
+
}
|
|
1271
|
+
state.setdefault("memory_snapshots", []).append(snapshot)
|
|
1272
|
+
state["memory_snapshots"] = state["memory_snapshots"][-200:]
|
|
1273
|
+
self.save_state(state)
|
|
1274
|
+
self.record_timeline_event("memory", "memory_snapshot", {"snapshot_id": snapshot["id"], "memory_count": len(memories)}, workspace_id=scope)
|
|
1275
|
+
return snapshot
|
|
1276
|
+
|
|
1277
|
+
def list_memory_snapshots(self, workspace_id: Optional[str] = None, limit: int = 50) -> Dict[str, Any]:
|
|
1278
|
+
snapshots = self._scoped(_listify(self.load_state().get("memory_snapshots")), workspace_id)
|
|
1279
|
+
return {"snapshots": list(reversed(snapshots[-max(1, min(limit, 200)):]))}
|
|
1280
|
+
|
|
1175
1281
|
# ------------------------------------------------------------------
|
|
1176
1282
|
# Agent and workflow graph
|
|
1177
1283
|
# ------------------------------------------------------------------
|
|
@@ -1191,10 +1297,18 @@ class WorkspaceOSStore:
|
|
|
1191
1297
|
user_email: Optional[str],
|
|
1192
1298
|
timeline: Optional[List[Dict[str, Any]]] = None,
|
|
1193
1299
|
relationships: Optional[List[str]] = None,
|
|
1300
|
+
handoffs: Optional[List[Dict[str, Any]]] = None,
|
|
1301
|
+
context_packets: Optional[List[Dict[str, Any]]] = None,
|
|
1302
|
+
plan: Optional[List[Dict[str, Any]]] = None,
|
|
1303
|
+
plan_review: Optional[Dict[str, Any]] = None,
|
|
1304
|
+
review_history: Optional[List[Dict[str, Any]]] = None,
|
|
1305
|
+
retry_history: Optional[List[Dict[str, Any]]] = None,
|
|
1306
|
+
memory_snapshots: Optional[List[Dict[str, Any]]] = None,
|
|
1194
1307
|
graph: Any = None,
|
|
1195
1308
|
workspace_id: Optional[str] = None,
|
|
1196
1309
|
) -> Dict[str, Any]:
|
|
1197
1310
|
state = self.load_state()
|
|
1311
|
+
resolved_workspace = self._resolve_scope(workspace_id, state)
|
|
1198
1312
|
run = {
|
|
1199
1313
|
"id": f"agent-run-{_json_hash([agent_id, input_text, output_text, _now()])[:16]}",
|
|
1200
1314
|
"agent_id": agent_id,
|
|
@@ -1202,9 +1316,16 @@ class WorkspaceOSStore:
|
|
|
1202
1316
|
"input": input_text,
|
|
1203
1317
|
"output_preview": output_text[:1000],
|
|
1204
1318
|
"user_email": user_email,
|
|
1205
|
-
"workspace_id":
|
|
1319
|
+
"workspace_id": resolved_workspace,
|
|
1206
1320
|
"relationships": relationships or [],
|
|
1207
1321
|
"timeline": timeline or [],
|
|
1322
|
+
"handoffs": handoffs or [],
|
|
1323
|
+
"context_packets": context_packets or [],
|
|
1324
|
+
"plan": plan or [],
|
|
1325
|
+
"plan_review": plan_review or {},
|
|
1326
|
+
"review_history": review_history or [],
|
|
1327
|
+
"retry_history": retry_history or [],
|
|
1328
|
+
"memory_snapshots": memory_snapshots or [],
|
|
1208
1329
|
"created_at": _now(),
|
|
1209
1330
|
}
|
|
1210
1331
|
if graph is not None:
|
|
@@ -1219,12 +1340,37 @@ class WorkspaceOSStore:
|
|
|
1219
1340
|
run["graph_node_id"] = ingested.get("node_id")
|
|
1220
1341
|
except Exception as exc:
|
|
1221
1342
|
run["graph_error"] = str(exc)
|
|
1343
|
+
if handoffs:
|
|
1344
|
+
stored_handoffs = state.setdefault("handoffs", [])
|
|
1345
|
+
for handoff in handoffs:
|
|
1346
|
+
stored = {
|
|
1347
|
+
**handoff,
|
|
1348
|
+
"run_id": run["id"],
|
|
1349
|
+
"workspace_id": resolved_workspace,
|
|
1350
|
+
}
|
|
1351
|
+
stored_handoffs.append(stored)
|
|
1352
|
+
state["handoffs"] = stored_handoffs[-500:]
|
|
1222
1353
|
state.setdefault("agent_runs", []).append(run)
|
|
1223
1354
|
state["agent_runs"] = state["agent_runs"][-300:]
|
|
1224
1355
|
self.save_state(state)
|
|
1225
|
-
self.
|
|
1356
|
+
self._emit_replayable_timeline_events(area="agent", run_id=run["id"], timeline=run["timeline"], workspace_id=resolved_workspace)
|
|
1357
|
+
if status == "failed":
|
|
1358
|
+
self._emit_execution_event(area="agent", event_type="execution_failed", payload={"run_id": run["id"], "agent_id": agent_id, "status": status}, workspace_id=resolved_workspace)
|
|
1359
|
+
self.record_timeline_event("agent", "agent_run", {"run_id": run["id"], "agent_id": agent_id, "status": status}, workspace_id=resolved_workspace)
|
|
1360
|
+
return run
|
|
1361
|
+
|
|
1362
|
+
def get_agent_run(self, run_id: str, workspace_id: Optional[str] = None) -> Dict[str, Any]:
|
|
1363
|
+
run = next((item for item in _listify(self.load_state().get("agent_runs")) if item.get("id") == run_id), None)
|
|
1364
|
+
if not run or (workspace_id and self._record_workspace(run) != str(workspace_id)):
|
|
1365
|
+
raise FileNotFoundError(run_id)
|
|
1226
1366
|
return run
|
|
1227
1367
|
|
|
1368
|
+
def list_handoffs(self, workspace_id: Optional[str] = None, run_id: Optional[str] = None) -> Dict[str, Any]:
|
|
1369
|
+
handoffs = self._scoped(_listify(self.load_state().get("handoffs")), workspace_id)
|
|
1370
|
+
if run_id:
|
|
1371
|
+
handoffs = [item for item in handoffs if item.get("run_id") == run_id]
|
|
1372
|
+
return {"handoffs": list(reversed(handoffs[-200:]))}
|
|
1373
|
+
|
|
1228
1374
|
def create_workflow(
|
|
1229
1375
|
self,
|
|
1230
1376
|
*,
|
|
@@ -1248,7 +1394,7 @@ class WorkspaceOSStore:
|
|
|
1248
1394
|
"created_at": _now(),
|
|
1249
1395
|
"updated_at": _now(),
|
|
1250
1396
|
}
|
|
1251
|
-
#
|
|
1397
|
+
# Workflow Designer stores a typed-node graph alongside the legacy
|
|
1252
1398
|
# ``steps`` list so older history keeps working and new editors get nodes.
|
|
1253
1399
|
if nodes is not None:
|
|
1254
1400
|
workflow["nodes"] = nodes
|
|
@@ -1284,6 +1430,7 @@ class WorkspaceOSStore:
|
|
|
1284
1430
|
) -> Dict[str, Any]:
|
|
1285
1431
|
"""Persist a Workflow Designer execution into local-first run history."""
|
|
1286
1432
|
state = self.load_state()
|
|
1433
|
+
resolved_workspace = self._resolve_scope(workspace_id, state)
|
|
1287
1434
|
run = {
|
|
1288
1435
|
"id": f"workflow-run-{_json_hash([workflow_id, name, status, _now()])[:16]}",
|
|
1289
1436
|
"workflow_id": workflow_id,
|
|
@@ -1292,7 +1439,7 @@ class WorkspaceOSStore:
|
|
|
1292
1439
|
"timeline": timeline or [],
|
|
1293
1440
|
"outputs": outputs or {},
|
|
1294
1441
|
"user_email": user_email,
|
|
1295
|
-
"workspace_id":
|
|
1442
|
+
"workspace_id": resolved_workspace,
|
|
1296
1443
|
"created_at": _now(),
|
|
1297
1444
|
}
|
|
1298
1445
|
if graph is not None:
|
|
@@ -1316,7 +1463,13 @@ class WorkspaceOSStore:
|
|
|
1316
1463
|
wf["updated_at"] = _now()
|
|
1317
1464
|
break
|
|
1318
1465
|
self.save_state(state)
|
|
1319
|
-
self.
|
|
1466
|
+
self._emit_execution_event(area="workflow", event_type="workflow_started", payload={"run_id": run["id"], "workflow_id": workflow_id, "name": name}, workspace_id=resolved_workspace)
|
|
1467
|
+
self._emit_replayable_timeline_events(area="workflow", run_id=run["id"], timeline=run["timeline"], workspace_id=resolved_workspace)
|
|
1468
|
+
if status == "failed":
|
|
1469
|
+
self._emit_execution_event(area="workflow", event_type="execution_failed", payload={"run_id": run["id"], "workflow_id": workflow_id, "status": status}, workspace_id=resolved_workspace)
|
|
1470
|
+
elif status in {"ok", "partial"}:
|
|
1471
|
+
self._emit_execution_event(area="workflow", event_type="workflow_completed", payload={"run_id": run["id"], "workflow_id": workflow_id, "status": status}, workspace_id=resolved_workspace)
|
|
1472
|
+
self.record_timeline_event("workflow", "workflow_run", {"run_id": run["id"], "workflow_id": workflow_id, "status": status}, workspace_id=resolved_workspace)
|
|
1320
1473
|
return run
|
|
1321
1474
|
|
|
1322
1475
|
def list_workflow_runs(self, workflow_id: Optional[str] = None, limit: int = 50, workspace_id: Optional[str] = None) -> Dict[str, Any]:
|
|
@@ -1325,6 +1478,67 @@ class WorkspaceOSStore:
|
|
|
1325
1478
|
runs = [run for run in runs if run.get("workflow_id") == workflow_id]
|
|
1326
1479
|
return {"runs": list(reversed(runs[-max(1, min(limit, 300)):]))}
|
|
1327
1480
|
|
|
1481
|
+
def get_workflow_run(self, run_id: str, workspace_id: Optional[str] = None) -> Dict[str, Any]:
|
|
1482
|
+
run = next((item for item in _listify(self.load_state().get("workflow_runs")) if item.get("id") == run_id), None)
|
|
1483
|
+
if not run or (workspace_id and self._record_workspace(run) != str(workspace_id)):
|
|
1484
|
+
raise FileNotFoundError(run_id)
|
|
1485
|
+
return run
|
|
1486
|
+
|
|
1487
|
+
@staticmethod
|
|
1488
|
+
def _replay_frames(run: Dict[str, Any], *, kind: str) -> List[Dict[str, Any]]:
|
|
1489
|
+
frames = []
|
|
1490
|
+
for index, item in enumerate(run.get("timeline") or []):
|
|
1491
|
+
event = item.get("event") or item.get("event_type") or item.get("type") or "event"
|
|
1492
|
+
actor = (
|
|
1493
|
+
item.get("agent_id")
|
|
1494
|
+
or item.get("role")
|
|
1495
|
+
or item.get("source_agent")
|
|
1496
|
+
or item.get("target_agent")
|
|
1497
|
+
or item.get("node")
|
|
1498
|
+
or kind
|
|
1499
|
+
)
|
|
1500
|
+
result = item.get("result") if "result" in item else item.get("output")
|
|
1501
|
+
decision = item.get("outcome") or item.get("verdict") or item.get("status")
|
|
1502
|
+
frames.append({
|
|
1503
|
+
"index": index,
|
|
1504
|
+
"event": event,
|
|
1505
|
+
"actor": actor,
|
|
1506
|
+
"when": item.get("timestamp") or item.get("started_at") or run.get("created_at"),
|
|
1507
|
+
"why": item.get("reason") or item.get("note") or item.get("name") or "",
|
|
1508
|
+
"input": item.get("context_packet") or item.get("trigger") or run.get("input"),
|
|
1509
|
+
"output": result,
|
|
1510
|
+
"decision": decision,
|
|
1511
|
+
"raw": item,
|
|
1512
|
+
})
|
|
1513
|
+
return frames
|
|
1514
|
+
|
|
1515
|
+
def replay_agent_run(self, run_id: str, workspace_id: Optional[str] = None) -> Dict[str, Any]:
|
|
1516
|
+
run = self.get_agent_run(run_id, workspace_id=workspace_id)
|
|
1517
|
+
return {
|
|
1518
|
+
"kind": "agent",
|
|
1519
|
+
"run_id": run_id,
|
|
1520
|
+
"status": run.get("status"),
|
|
1521
|
+
"workspace_id": self._record_workspace(run),
|
|
1522
|
+
"replayable": True,
|
|
1523
|
+
"frames": self._replay_frames(run, kind="agent"),
|
|
1524
|
+
"handoffs": run.get("handoffs") or [],
|
|
1525
|
+
"context_packets": run.get("context_packets") or [],
|
|
1526
|
+
"review_history": run.get("review_history") or [],
|
|
1527
|
+
"retry_history": run.get("retry_history") or [],
|
|
1528
|
+
}
|
|
1529
|
+
|
|
1530
|
+
def replay_workflow_run(self, run_id: str, workspace_id: Optional[str] = None) -> Dict[str, Any]:
|
|
1531
|
+
run = self.get_workflow_run(run_id, workspace_id=workspace_id)
|
|
1532
|
+
return {
|
|
1533
|
+
"kind": "workflow",
|
|
1534
|
+
"run_id": run_id,
|
|
1535
|
+
"status": run.get("status"),
|
|
1536
|
+
"workspace_id": self._record_workspace(run),
|
|
1537
|
+
"replayable": True,
|
|
1538
|
+
"frames": self._replay_frames(run, kind="workflow"),
|
|
1539
|
+
"outputs": run.get("outputs") or {},
|
|
1540
|
+
}
|
|
1541
|
+
|
|
1328
1542
|
def get_workflow(self, workflow_id: str, workspace_id: Optional[str] = None) -> Dict[str, Any]:
|
|
1329
1543
|
workflow = next((wf for wf in _listify(self.load_state().get("workflows")) if wf.get("id") == workflow_id), None)
|
|
1330
1544
|
if not workflow or (workspace_id and self._record_workspace(workflow) != str(workspace_id)):
|
|
@@ -1603,7 +1817,7 @@ class WorkspaceOSStore:
|
|
|
1603
1817
|
return entry
|
|
1604
1818
|
|
|
1605
1819
|
# ------------------------------------------------------------------
|
|
1606
|
-
# Plugin SDK registry
|
|
1820
|
+
# Plugin SDK registry — mirrors the skill registry contract.
|
|
1607
1821
|
# ------------------------------------------------------------------
|
|
1608
1822
|
|
|
1609
1823
|
def list_plugin_registry(self) -> Dict[str, Any]:
|
|
@@ -1643,6 +1857,39 @@ class WorkspaceOSStore:
|
|
|
1643
1857
|
self.record_timeline_event("plugins", "plugin_uninstalled", {"plugin": plugin_id})
|
|
1644
1858
|
return {"status": "ok", "plugin_id": plugin_id, "registry": entry}
|
|
1645
1859
|
|
|
1860
|
+
# ------------------------------------------------------------------
|
|
1861
|
+
# Marketplace template registry (v2.1 foundation)
|
|
1862
|
+
# ------------------------------------------------------------------
|
|
1863
|
+
|
|
1864
|
+
def list_template_registry(self) -> Dict[str, Any]:
|
|
1865
|
+
return dict(self.load_state().get("template_registry") or {})
|
|
1866
|
+
|
|
1867
|
+
def mark_template_installed(
|
|
1868
|
+
self,
|
|
1869
|
+
*,
|
|
1870
|
+
kind: str,
|
|
1871
|
+
template_id: str,
|
|
1872
|
+
version: str = "1.0.0",
|
|
1873
|
+
metadata: Optional[Dict[str, Any]] = None,
|
|
1874
|
+
workspace_id: Optional[str] = None,
|
|
1875
|
+
) -> Dict[str, Any]:
|
|
1876
|
+
state = self.load_state()
|
|
1877
|
+
scope = self._resolve_scope(workspace_id, state)
|
|
1878
|
+
key = f"{kind}:{template_id}"
|
|
1879
|
+
entry = state.setdefault("template_registry", {}).setdefault(key, {"id": template_id, "kind": kind})
|
|
1880
|
+
entry.update({
|
|
1881
|
+
"id": template_id,
|
|
1882
|
+
"kind": kind,
|
|
1883
|
+
"version": version,
|
|
1884
|
+
"installed": True,
|
|
1885
|
+
"workspace_id": scope,
|
|
1886
|
+
"metadata": metadata or entry.get("metadata") or {},
|
|
1887
|
+
"updated_at": _now(),
|
|
1888
|
+
})
|
|
1889
|
+
self.save_state(state)
|
|
1890
|
+
self.record_timeline_event("marketplace", "template_installed", {"kind": kind, "template_id": template_id}, workspace_id=scope)
|
|
1891
|
+
return entry
|
|
1892
|
+
|
|
1646
1893
|
# ------------------------------------------------------------------
|
|
1647
1894
|
# Audit timeline
|
|
1648
1895
|
# ------------------------------------------------------------------
|
package/latticeai/server_app.py
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
"""
|
|
2
2
|
Lattice AI MLX — Local LLM Bridge Server
|
|
3
|
-
Apple Silicon (M1-M5) 전용 |
|
|
3
|
+
Apple Silicon (M1-M5) 전용 | MLX-VLM 기반
|
|
4
4
|
"""
|
|
5
5
|
|
|
6
6
|
import asyncio
|
|
@@ -93,14 +93,16 @@ from latticeai.services.model_runtime import (
|
|
|
93
93
|
)
|
|
94
94
|
from latticeai.api.workspace import create_workspace_router, _workspace_scope_from_request
|
|
95
95
|
from latticeai.api.health import create_health_router
|
|
96
|
-
# ── v2
|
|
96
|
+
# ── v2 Agentic Workspace Platform layers ─────────────────────────────────────
|
|
97
97
|
from latticeai.core.plugins import PluginRegistry
|
|
98
98
|
from latticeai.core.realtime import RealtimeBus
|
|
99
|
+
from latticeai.core.marketplace import TemplateCatalog
|
|
99
100
|
from latticeai.services.platform_runtime import PlatformRuntime
|
|
100
101
|
from latticeai.api.plugins import create_plugins_router
|
|
101
102
|
from latticeai.api.workflow_designer import create_workflow_designer_router
|
|
102
103
|
from latticeai.api.agents import create_agents_router
|
|
103
104
|
from latticeai.api.realtime import create_realtime_router
|
|
105
|
+
from latticeai.api.marketplace import create_marketplace_router
|
|
104
106
|
from latticeai.api.models import create_models_router
|
|
105
107
|
from latticeai.api.chat import create_chat_router
|
|
106
108
|
from latticeai.api.tools import create_tools_router
|
|
@@ -244,16 +246,17 @@ AUDIT_FILE = DATA_DIR / "audit_log.json"
|
|
|
244
246
|
SSO_FILE = DATA_DIR / "sso_config.json"
|
|
245
247
|
KNOWLEDGE_GRAPH = KnowledgeGraphStore(DATA_DIR / "knowledge_graph.sqlite", DATA_DIR / "knowledge_graph_blobs") if ENABLE_GRAPH else None
|
|
246
248
|
LOCAL_KG_WATCHER = LocalKnowledgeWatcher(lambda: KNOWLEDGE_GRAPH) if ENABLE_GRAPH else None
|
|
247
|
-
# ── v2
|
|
249
|
+
# ── v2 Realtime bus: constructed first so the store can fan every timeline
|
|
248
250
|
# event into the realtime feed via a single additive sink (no per-call wiring).
|
|
249
251
|
REALTIME_BUS = RealtimeBus()
|
|
250
252
|
WORKSPACE_OS = WorkspaceOSStore(DATA_DIR, event_sink=REALTIME_BUS)
|
|
251
253
|
# Service layer (latticeai.services) wraps the store with scope/permission
|
|
252
254
|
# guardrails; routers and the app assembly share this single instance.
|
|
253
255
|
WORKSPACE_SERVICE = WorkspaceService(WORKSPACE_OS)
|
|
254
|
-
# ── v2
|
|
256
|
+
# ── v2 Plugin SDK registry (extends skills; discovers plugins/<id>/plugin.json)
|
|
255
257
|
PLUGINS_DIR = Path(os.getenv("LATTICEAI_PLUGINS_DIR") or (BASE_DIR / "plugins"))
|
|
256
258
|
PLUGIN_REGISTRY = PluginRegistry(PLUGINS_DIR, store=WORKSPACE_OS)
|
|
259
|
+
TEMPLATE_CATALOG = TemplateCatalog()
|
|
257
260
|
|
|
258
261
|
def _require_graph():
|
|
259
262
|
if not ENABLE_GRAPH or KNOWLEDGE_GRAPH is None:
|
|
@@ -1201,7 +1204,7 @@ app.include_router(create_workspace_router(
|
|
|
1201
1204
|
))
|
|
1202
1205
|
|
|
1203
1206
|
|
|
1204
|
-
# ── v2
|
|
1207
|
+
# ── v2 Agentic Workspace Platform: cross-system wiring ───────────────────────
|
|
1205
1208
|
# All cross-subsystem closures live in latticeai.services.platform_runtime to
|
|
1206
1209
|
# keep this assembly file lean; server_app only constructs it and mounts routers.
|
|
1207
1210
|
PLATFORM = PlatformRuntime(
|
|
@@ -1251,6 +1254,15 @@ app.include_router(create_agents_router(
|
|
|
1251
1254
|
static_dir=STATIC_DIR,
|
|
1252
1255
|
))
|
|
1253
1256
|
|
|
1257
|
+
app.include_router(create_marketplace_router(
|
|
1258
|
+
store=WORKSPACE_OS,
|
|
1259
|
+
catalog=TEMPLATE_CATALOG,
|
|
1260
|
+
require_user=require_user,
|
|
1261
|
+
gate_read=PLATFORM.gate_read,
|
|
1262
|
+
gate_write=PLATFORM.gate_write,
|
|
1263
|
+
workspace_graph=_workspace_graph,
|
|
1264
|
+
))
|
|
1265
|
+
|
|
1254
1266
|
app.include_router(create_realtime_router(
|
|
1255
1267
|
bus=REALTIME_BUS,
|
|
1256
1268
|
require_user=require_user,
|