ltcai 2.0.0 → 2.1.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.
@@ -32,7 +32,7 @@ from datetime import datetime
32
32
  from typing import Any, AsyncIterator, Dict, List, Optional, Set
33
33
 
34
34
 
35
- REALTIME_VERSION = "2.0.0"
35
+ REALTIME_VERSION = "2.1.0"
36
36
  _FEED_LIMIT = 200
37
37
  _QUEUE_MAX = 100
38
38
 
@@ -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.0.0"
31
+ WORKFLOW_ENGINE_VERSION = "2.1.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.0.0"
21
+ WORKSPACE_OS_VERSION = "2.1.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 (v2.0) receives all workspace activity without per-call wiring.
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": self._resolve_scope(workspace_id, state),
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.record_timeline_event("agent", "agent_run", {"run_id": run["id"], "agent_id": agent_id, "status": status})
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
- # v2.0 Workflow Designer stores a typed-node graph alongside the legacy
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": self._resolve_scope(workspace_id, state),
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.record_timeline_event("workflow", "workflow_run", {"run_id": run["id"], "workflow_id": workflow_id, "status": status})
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 (v2.0) — mirrors the skill registry contract.
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
  # ------------------------------------------------------------------
@@ -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.0 Agentic Workspace Platform layers ───────────────────────────────────
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.0 Realtime bus: constructed first so the store can fan every timeline
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.0 Plugin SDK registry (extends skills; discovers plugins/<id>/plugin.json)
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.0 Agentic Workspace Platform: cross-system wiring ─────────────────────
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,
@@ -1,6 +1,6 @@
1
- """v2.0 Agentic Workspace Platform runtime — cross-system wiring.
1
+ """v2 Agentic Workspace Platform runtime — cross-system wiring.
2
2
 
3
- This is the single place the four v2.0 subsystems (Plugin SDK, Workflow
3
+ This is the single place the v2 subsystems (Plugin SDK, Workflow
4
4
  Designer, Multi-Agent Runtime, Realtime) connect to one another and to the
5
5
  workspace. Keeping it out of ``server_app`` honours the AGENTS.md preference for
6
6
  small, composable modules and keeps the wiring independently testable.
@@ -132,7 +132,7 @@ class PlatformRuntime:
132
132
  plugin_id = cfg.get("plugin_id") or cfg.get("plugin") or ""
133
133
  action = cfg.get("action") or "run_skill"
134
134
  result = self.registry.execute_action(
135
- plugin_id, action, cfg.get("args") or {}, runners=self.plugin_capability_runners(user, scope)
135
+ plugin_id, action, cfg.get("args") or {}, runners=self.plugin_capability_runners(user, scope), workspace_id=scope
136
136
  )
137
137
  return result.as_dict()
138
138
  return runner
@@ -169,7 +169,7 @@ class PlatformRuntime:
169
169
  def run_agent(self, goal, user, scope, *, with_workflow: bool, roles=None, inputs=None) -> Dict[str, Any]:
170
170
  role_runner = default_role_runner(
171
171
  workflow_runner=(lambda wf_ref, ctx: self.run_workflow_by_id(wf_ref, user, scope, with_agent=False, inputs=ctx.inputs)) if with_workflow else None,
172
- plugin_runner=lambda pid, ctx: self.registry.execute_action(pid, "run_skill", {}, runners=self.plugin_capability_runners(user, scope)).as_dict(),
172
+ plugin_runner=lambda pid, ctx: self.registry.execute_action(pid, "run_skill", {}, runners=self.plugin_capability_runners(user, scope), workspace_id=scope).as_dict(),
173
173
  context_provider=self._context_provider(user, scope),
174
174
  )
175
175
  result = MultiAgentOrchestrator(role_runner=role_runner).run(
@@ -178,6 +178,10 @@ class PlatformRuntime:
178
178
  run = self.store.record_agent_run(
179
179
  agent_id=result.agent_id, status=result.status, input_text=goal,
180
180
  output_text=result.output, timeline=result.timeline, relationships=[],
181
+ handoffs=result.handoffs, context_packets=result.context_packets,
182
+ plan=result.plan, plan_review=result.plan_review,
183
+ review_history=result.review_history, retry_history=result.retry_history,
184
+ memory_snapshots=result.memory_snapshots,
181
185
  user_email=user, graph=self.workspace_graph(), workspace_id=scope,
182
186
  )
183
187
  return {"agent_run_id": run["id"], "status": result.status, "output": result.output}
@@ -195,6 +199,6 @@ class PlatformRuntime:
195
199
  def build_orchestrator(self, user, scope) -> MultiAgentOrchestrator:
196
200
  return MultiAgentOrchestrator(role_runner=default_role_runner(
197
201
  workflow_runner=lambda wf_ref, ctx: self.run_workflow_by_id(wf_ref, user, scope, with_agent=False, inputs=ctx.inputs),
198
- plugin_runner=lambda pid, ctx: self.registry.execute_action(pid, "run_skill", {}, runners=self.plugin_capability_runners(user, scope)).as_dict(),
202
+ plugin_runner=lambda pid, ctx: self.registry.execute_action(pid, "run_skill", {}, runners=self.plugin_capability_runners(user, scope), workspace_id=scope).as_dict(),
199
203
  context_provider=self._context_provider(user, scope),
200
204
  ))
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "ltcai",
3
- "version": "2.0.0",
3
+ "version": "2.1.0",
4
4
  "description": "Lattice AI Workspace OS for local-first graph, memory, agent, workflow, and skill operations",
5
5
  "homepage": "https://github.com/TaeSooPark-PTS/LatticeAI#readme",
6
6
  "repository": {
@@ -19,7 +19,7 @@
19
19
  "dev": "python3 ltcai_cli.py --reload",
20
20
  "build": "npm run build:python",
21
21
  "build:python": "python3 -m build",
22
- "check:python": "python3 -m py_compile ltcai_cli.py server.py latticeai/server_app.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/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/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",
22
+ "check:python": "python3 -m py_compile ltcai_cli.py server.py latticeai/server_app.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/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
23
  "test": "python3 -m pytest tests/ -v",
24
24
  "test:unit": "python3 -m pytest tests/unit/ -v",
25
25
  "test:integration": "python3 -m pytest tests/integration/ -v",