aethergraph 0.1.0a1__py3-none-any.whl → 0.1.0a2__py3-none-any.whl

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 (267) hide show
  1. aethergraph/__init__.py +4 -10
  2. aethergraph/__main__.py +293 -0
  3. aethergraph/api/v1/__init__.py +0 -0
  4. aethergraph/api/v1/agents.py +46 -0
  5. aethergraph/api/v1/apps.py +70 -0
  6. aethergraph/api/v1/artifacts.py +415 -0
  7. aethergraph/api/v1/channels.py +89 -0
  8. aethergraph/api/v1/deps.py +168 -0
  9. aethergraph/api/v1/graphs.py +259 -0
  10. aethergraph/api/v1/identity.py +25 -0
  11. aethergraph/api/v1/memory.py +353 -0
  12. aethergraph/api/v1/misc.py +47 -0
  13. aethergraph/api/v1/pagination.py +29 -0
  14. aethergraph/api/v1/runs.py +568 -0
  15. aethergraph/api/v1/schemas.py +535 -0
  16. aethergraph/api/v1/session.py +323 -0
  17. aethergraph/api/v1/stats.py +201 -0
  18. aethergraph/api/v1/viz.py +152 -0
  19. aethergraph/config/config.py +22 -0
  20. aethergraph/config/loader.py +3 -2
  21. aethergraph/config/storage.py +209 -0
  22. aethergraph/contracts/__init__.py +0 -0
  23. aethergraph/contracts/services/__init__.py +0 -0
  24. aethergraph/contracts/services/artifacts.py +27 -14
  25. aethergraph/contracts/services/memory.py +45 -17
  26. aethergraph/contracts/services/metering.py +129 -0
  27. aethergraph/contracts/services/runs.py +50 -0
  28. aethergraph/contracts/services/sessions.py +87 -0
  29. aethergraph/contracts/services/state_stores.py +3 -0
  30. aethergraph/contracts/services/viz.py +44 -0
  31. aethergraph/contracts/storage/artifact_index.py +88 -0
  32. aethergraph/contracts/storage/artifact_store.py +99 -0
  33. aethergraph/contracts/storage/async_kv.py +34 -0
  34. aethergraph/contracts/storage/blob_store.py +50 -0
  35. aethergraph/contracts/storage/doc_store.py +35 -0
  36. aethergraph/contracts/storage/event_log.py +31 -0
  37. aethergraph/contracts/storage/vector_index.py +48 -0
  38. aethergraph/core/__init__.py +0 -0
  39. aethergraph/core/execution/forward_scheduler.py +13 -2
  40. aethergraph/core/execution/global_scheduler.py +21 -15
  41. aethergraph/core/execution/step_forward.py +10 -1
  42. aethergraph/core/graph/__init__.py +0 -0
  43. aethergraph/core/graph/graph_builder.py +8 -4
  44. aethergraph/core/graph/graph_fn.py +156 -15
  45. aethergraph/core/graph/graph_spec.py +8 -0
  46. aethergraph/core/graph/graphify.py +146 -27
  47. aethergraph/core/graph/node_spec.py +0 -2
  48. aethergraph/core/graph/node_state.py +3 -0
  49. aethergraph/core/graph/task_graph.py +39 -1
  50. aethergraph/core/runtime/__init__.py +0 -0
  51. aethergraph/core/runtime/ad_hoc_context.py +64 -4
  52. aethergraph/core/runtime/base_service.py +28 -4
  53. aethergraph/core/runtime/execution_context.py +13 -15
  54. aethergraph/core/runtime/graph_runner.py +222 -37
  55. aethergraph/core/runtime/node_context.py +510 -6
  56. aethergraph/core/runtime/node_services.py +12 -5
  57. aethergraph/core/runtime/recovery.py +15 -1
  58. aethergraph/core/runtime/run_manager.py +783 -0
  59. aethergraph/core/runtime/run_manager_local.py +204 -0
  60. aethergraph/core/runtime/run_registration.py +2 -2
  61. aethergraph/core/runtime/run_types.py +89 -0
  62. aethergraph/core/runtime/runtime_env.py +136 -7
  63. aethergraph/core/runtime/runtime_metering.py +71 -0
  64. aethergraph/core/runtime/runtime_registry.py +36 -13
  65. aethergraph/core/runtime/runtime_services.py +194 -6
  66. aethergraph/core/tools/builtins/toolset.py +1 -1
  67. aethergraph/core/tools/toolkit.py +5 -0
  68. aethergraph/plugins/agents/default_chat_agent copy.py +90 -0
  69. aethergraph/plugins/agents/default_chat_agent.py +171 -0
  70. aethergraph/plugins/agents/shared.py +81 -0
  71. aethergraph/plugins/channel/adapters/webui.py +112 -112
  72. aethergraph/plugins/channel/routes/webui_routes.py +367 -102
  73. aethergraph/plugins/channel/utils/slack_utils.py +115 -59
  74. aethergraph/plugins/channel/utils/telegram_utils.py +88 -47
  75. aethergraph/plugins/channel/websockets/weibui_ws.py +172 -0
  76. aethergraph/runtime/__init__.py +15 -0
  77. aethergraph/server/app_factory.py +190 -34
  78. aethergraph/server/clients/channel_client.py +202 -0
  79. aethergraph/server/http/channel_http_routes.py +116 -0
  80. aethergraph/server/http/channel_ws_routers.py +45 -0
  81. aethergraph/server/loading.py +117 -0
  82. aethergraph/server/server.py +131 -0
  83. aethergraph/server/server_state.py +240 -0
  84. aethergraph/server/start.py +227 -66
  85. aethergraph/server/ui_static/assets/KaTeX_AMS-Regular-BQhdFMY1.woff2 +0 -0
  86. aethergraph/server/ui_static/assets/KaTeX_AMS-Regular-DMm9YOAa.woff +0 -0
  87. aethergraph/server/ui_static/assets/KaTeX_AMS-Regular-DRggAlZN.ttf +0 -0
  88. aethergraph/server/ui_static/assets/KaTeX_Caligraphic-Bold-ATXxdsX0.ttf +0 -0
  89. aethergraph/server/ui_static/assets/KaTeX_Caligraphic-Bold-BEiXGLvX.woff +0 -0
  90. aethergraph/server/ui_static/assets/KaTeX_Caligraphic-Bold-Dq_IR9rO.woff2 +0 -0
  91. aethergraph/server/ui_static/assets/KaTeX_Caligraphic-Regular-CTRA-rTL.woff +0 -0
  92. aethergraph/server/ui_static/assets/KaTeX_Caligraphic-Regular-Di6jR-x-.woff2 +0 -0
  93. aethergraph/server/ui_static/assets/KaTeX_Caligraphic-Regular-wX97UBjC.ttf +0 -0
  94. aethergraph/server/ui_static/assets/KaTeX_Fraktur-Bold-BdnERNNW.ttf +0 -0
  95. aethergraph/server/ui_static/assets/KaTeX_Fraktur-Bold-BsDP51OF.woff +0 -0
  96. aethergraph/server/ui_static/assets/KaTeX_Fraktur-Bold-CL6g_b3V.woff2 +0 -0
  97. aethergraph/server/ui_static/assets/KaTeX_Fraktur-Regular-CB_wures.ttf +0 -0
  98. aethergraph/server/ui_static/assets/KaTeX_Fraktur-Regular-CTYiF6lA.woff2 +0 -0
  99. aethergraph/server/ui_static/assets/KaTeX_Fraktur-Regular-Dxdc4cR9.woff +0 -0
  100. aethergraph/server/ui_static/assets/KaTeX_Main-Bold-Cx986IdX.woff2 +0 -0
  101. aethergraph/server/ui_static/assets/KaTeX_Main-Bold-Jm3AIy58.woff +0 -0
  102. aethergraph/server/ui_static/assets/KaTeX_Main-Bold-waoOVXN0.ttf +0 -0
  103. aethergraph/server/ui_static/assets/KaTeX_Main-BoldItalic-DxDJ3AOS.woff2 +0 -0
  104. aethergraph/server/ui_static/assets/KaTeX_Main-BoldItalic-DzxPMmG6.ttf +0 -0
  105. aethergraph/server/ui_static/assets/KaTeX_Main-BoldItalic-SpSLRI95.woff +0 -0
  106. aethergraph/server/ui_static/assets/KaTeX_Main-Italic-3WenGoN9.ttf +0 -0
  107. aethergraph/server/ui_static/assets/KaTeX_Main-Italic-BMLOBm91.woff +0 -0
  108. aethergraph/server/ui_static/assets/KaTeX_Main-Italic-NWA7e6Wa.woff2 +0 -0
  109. aethergraph/server/ui_static/assets/KaTeX_Main-Regular-B22Nviop.woff2 +0 -0
  110. aethergraph/server/ui_static/assets/KaTeX_Main-Regular-Dr94JaBh.woff +0 -0
  111. aethergraph/server/ui_static/assets/KaTeX_Main-Regular-ypZvNtVU.ttf +0 -0
  112. aethergraph/server/ui_static/assets/KaTeX_Math-BoldItalic-B3XSjfu4.ttf +0 -0
  113. aethergraph/server/ui_static/assets/KaTeX_Math-BoldItalic-CZnvNsCZ.woff2 +0 -0
  114. aethergraph/server/ui_static/assets/KaTeX_Math-BoldItalic-iY-2wyZ7.woff +0 -0
  115. aethergraph/server/ui_static/assets/KaTeX_Math-Italic-DA0__PXp.woff +0 -0
  116. aethergraph/server/ui_static/assets/KaTeX_Math-Italic-flOr_0UB.ttf +0 -0
  117. aethergraph/server/ui_static/assets/KaTeX_Math-Italic-t53AETM-.woff2 +0 -0
  118. aethergraph/server/ui_static/assets/KaTeX_SansSerif-Bold-CFMepnvq.ttf +0 -0
  119. aethergraph/server/ui_static/assets/KaTeX_SansSerif-Bold-D1sUS0GD.woff2 +0 -0
  120. aethergraph/server/ui_static/assets/KaTeX_SansSerif-Bold-DbIhKOiC.woff +0 -0
  121. aethergraph/server/ui_static/assets/KaTeX_SansSerif-Italic-C3H0VqGB.woff2 +0 -0
  122. aethergraph/server/ui_static/assets/KaTeX_SansSerif-Italic-DN2j7dab.woff +0 -0
  123. aethergraph/server/ui_static/assets/KaTeX_SansSerif-Italic-YYjJ1zSn.ttf +0 -0
  124. aethergraph/server/ui_static/assets/KaTeX_SansSerif-Regular-BNo7hRIc.ttf +0 -0
  125. aethergraph/server/ui_static/assets/KaTeX_SansSerif-Regular-CS6fqUqJ.woff +0 -0
  126. aethergraph/server/ui_static/assets/KaTeX_SansSerif-Regular-DDBCnlJ7.woff2 +0 -0
  127. aethergraph/server/ui_static/assets/KaTeX_Script-Regular-C5JkGWo-.ttf +0 -0
  128. aethergraph/server/ui_static/assets/KaTeX_Script-Regular-D3wIWfF6.woff2 +0 -0
  129. aethergraph/server/ui_static/assets/KaTeX_Script-Regular-D5yQViql.woff +0 -0
  130. aethergraph/server/ui_static/assets/KaTeX_Size1-Regular-C195tn64.woff +0 -0
  131. aethergraph/server/ui_static/assets/KaTeX_Size1-Regular-Dbsnue_I.ttf +0 -0
  132. aethergraph/server/ui_static/assets/KaTeX_Size1-Regular-mCD8mA8B.woff2 +0 -0
  133. aethergraph/server/ui_static/assets/KaTeX_Size2-Regular-B7gKUWhC.ttf +0 -0
  134. aethergraph/server/ui_static/assets/KaTeX_Size2-Regular-Dy4dx90m.woff2 +0 -0
  135. aethergraph/server/ui_static/assets/KaTeX_Size2-Regular-oD1tc_U0.woff +0 -0
  136. aethergraph/server/ui_static/assets/KaTeX_Size3-Regular-CTq5MqoE.woff +0 -0
  137. aethergraph/server/ui_static/assets/KaTeX_Size3-Regular-DgpXs0kz.ttf +0 -0
  138. aethergraph/server/ui_static/assets/KaTeX_Size4-Regular-BF-4gkZK.woff +0 -0
  139. aethergraph/server/ui_static/assets/KaTeX_Size4-Regular-DWFBv043.ttf +0 -0
  140. aethergraph/server/ui_static/assets/KaTeX_Size4-Regular-Dl5lxZxV.woff2 +0 -0
  141. aethergraph/server/ui_static/assets/KaTeX_Typewriter-Regular-C0xS9mPB.woff +0 -0
  142. aethergraph/server/ui_static/assets/KaTeX_Typewriter-Regular-CO6r4hn1.woff2 +0 -0
  143. aethergraph/server/ui_static/assets/KaTeX_Typewriter-Regular-D3Ib7_Hf.ttf +0 -0
  144. aethergraph/server/ui_static/assets/index-BR5GtXcZ.css +1 -0
  145. aethergraph/server/ui_static/assets/index-CQ0HZZ83.js +400 -0
  146. aethergraph/server/ui_static/index.html +15 -0
  147. aethergraph/server/ui_static/logo.png +0 -0
  148. aethergraph/services/artifacts/__init__.py +0 -0
  149. aethergraph/services/artifacts/facade.py +1239 -132
  150. aethergraph/services/auth/{dev.py → authn.py} +0 -8
  151. aethergraph/services/auth/authz.py +100 -0
  152. aethergraph/services/channel/__init__.py +0 -0
  153. aethergraph/services/channel/channel_bus.py +19 -1
  154. aethergraph/services/channel/factory.py +13 -1
  155. aethergraph/services/channel/ingress.py +311 -0
  156. aethergraph/services/channel/queue_adapter.py +75 -0
  157. aethergraph/services/channel/session.py +502 -19
  158. aethergraph/services/container/default_container.py +122 -43
  159. aethergraph/services/continuations/continuation.py +6 -0
  160. aethergraph/services/continuations/stores/fs_store.py +19 -0
  161. aethergraph/services/eventhub/event_hub.py +76 -0
  162. aethergraph/services/kv/__init__.py +0 -0
  163. aethergraph/services/kv/ephemeral.py +244 -0
  164. aethergraph/services/llm/__init__.py +0 -0
  165. aethergraph/services/llm/generic_client copy.py +691 -0
  166. aethergraph/services/llm/generic_client.py +1288 -187
  167. aethergraph/services/llm/providers.py +3 -1
  168. aethergraph/services/llm/types.py +47 -0
  169. aethergraph/services/llm/utils.py +284 -0
  170. aethergraph/services/logger/std.py +3 -0
  171. aethergraph/services/mcp/__init__.py +9 -0
  172. aethergraph/services/mcp/http_client.py +38 -0
  173. aethergraph/services/mcp/service.py +225 -1
  174. aethergraph/services/mcp/stdio_client.py +41 -6
  175. aethergraph/services/mcp/ws_client.py +44 -2
  176. aethergraph/services/memory/__init__.py +0 -0
  177. aethergraph/services/memory/distillers/llm_long_term.py +234 -0
  178. aethergraph/services/memory/distillers/llm_meta_summary.py +398 -0
  179. aethergraph/services/memory/distillers/long_term.py +225 -0
  180. aethergraph/services/memory/facade/__init__.py +3 -0
  181. aethergraph/services/memory/facade/chat.py +440 -0
  182. aethergraph/services/memory/facade/core.py +447 -0
  183. aethergraph/services/memory/facade/distillation.py +424 -0
  184. aethergraph/services/memory/facade/rag.py +410 -0
  185. aethergraph/services/memory/facade/results.py +315 -0
  186. aethergraph/services/memory/facade/retrieval.py +139 -0
  187. aethergraph/services/memory/facade/types.py +77 -0
  188. aethergraph/services/memory/facade/utils.py +43 -0
  189. aethergraph/services/memory/facade_dep.py +1539 -0
  190. aethergraph/services/memory/factory.py +9 -3
  191. aethergraph/services/memory/utils.py +10 -0
  192. aethergraph/services/metering/eventlog_metering.py +470 -0
  193. aethergraph/services/metering/noop.py +25 -4
  194. aethergraph/services/rag/__init__.py +0 -0
  195. aethergraph/services/rag/facade.py +279 -23
  196. aethergraph/services/rag/index_factory.py +2 -2
  197. aethergraph/services/rag/node_rag.py +317 -0
  198. aethergraph/services/rate_limit/inmem_rate_limit.py +24 -0
  199. aethergraph/services/registry/__init__.py +0 -0
  200. aethergraph/services/registry/agent_app_meta.py +419 -0
  201. aethergraph/services/registry/registry_key.py +1 -1
  202. aethergraph/services/registry/unified_registry.py +74 -6
  203. aethergraph/services/scope/scope.py +159 -0
  204. aethergraph/services/scope/scope_factory.py +164 -0
  205. aethergraph/services/state_stores/serialize.py +5 -0
  206. aethergraph/services/state_stores/utils.py +2 -1
  207. aethergraph/services/viz/__init__.py +0 -0
  208. aethergraph/services/viz/facade.py +413 -0
  209. aethergraph/services/viz/viz_service.py +69 -0
  210. aethergraph/storage/artifacts/artifact_index_jsonl.py +180 -0
  211. aethergraph/storage/artifacts/artifact_index_sqlite.py +426 -0
  212. aethergraph/storage/artifacts/cas_store.py +422 -0
  213. aethergraph/storage/artifacts/fs_cas.py +18 -0
  214. aethergraph/storage/artifacts/s3_cas.py +14 -0
  215. aethergraph/storage/artifacts/utils.py +124 -0
  216. aethergraph/storage/blob/fs_blob.py +86 -0
  217. aethergraph/storage/blob/s3_blob.py +115 -0
  218. aethergraph/storage/continuation_store/fs_cont.py +283 -0
  219. aethergraph/storage/continuation_store/inmem_cont.py +146 -0
  220. aethergraph/storage/continuation_store/kvdoc_cont.py +261 -0
  221. aethergraph/storage/docstore/fs_doc.py +63 -0
  222. aethergraph/storage/docstore/sqlite_doc.py +31 -0
  223. aethergraph/storage/docstore/sqlite_doc_sync.py +90 -0
  224. aethergraph/storage/eventlog/fs_event.py +136 -0
  225. aethergraph/storage/eventlog/sqlite_event.py +47 -0
  226. aethergraph/storage/eventlog/sqlite_event_sync.py +178 -0
  227. aethergraph/storage/factory.py +432 -0
  228. aethergraph/storage/fs_utils.py +28 -0
  229. aethergraph/storage/graph_state_store/state_store.py +64 -0
  230. aethergraph/storage/kv/inmem_kv.py +103 -0
  231. aethergraph/storage/kv/layered_kv.py +52 -0
  232. aethergraph/storage/kv/sqlite_kv.py +39 -0
  233. aethergraph/storage/kv/sqlite_kv_sync.py +98 -0
  234. aethergraph/storage/memory/event_persist.py +68 -0
  235. aethergraph/storage/memory/fs_persist.py +118 -0
  236. aethergraph/{services/memory/hotlog_kv.py → storage/memory/hotlog.py} +8 -2
  237. aethergraph/{services → storage}/memory/indices.py +31 -7
  238. aethergraph/storage/metering/meter_event.py +55 -0
  239. aethergraph/storage/runs/doc_store.py +280 -0
  240. aethergraph/storage/runs/inmen_store.py +82 -0
  241. aethergraph/storage/runs/sqlite_run_store.py +403 -0
  242. aethergraph/storage/sessions/doc_store.py +183 -0
  243. aethergraph/storage/sessions/inmem_store.py +110 -0
  244. aethergraph/storage/sessions/sqlite_session_store.py +399 -0
  245. aethergraph/storage/vector_index/chroma_index.py +138 -0
  246. aethergraph/storage/vector_index/faiss_index.py +179 -0
  247. aethergraph/storage/vector_index/sqlite_index.py +187 -0
  248. {aethergraph-0.1.0a1.dist-info → aethergraph-0.1.0a2.dist-info}/METADATA +138 -31
  249. aethergraph-0.1.0a2.dist-info/RECORD +356 -0
  250. aethergraph-0.1.0a2.dist-info/entry_points.txt +3 -0
  251. aethergraph/services/artifacts/factory.py +0 -35
  252. aethergraph/services/artifacts/fs_store.py +0 -656
  253. aethergraph/services/artifacts/jsonl_index.py +0 -123
  254. aethergraph/services/artifacts/sqlite_index.py +0 -209
  255. aethergraph/services/memory/distillers/episode.py +0 -116
  256. aethergraph/services/memory/distillers/rolling.py +0 -74
  257. aethergraph/services/memory/facade.py +0 -633
  258. aethergraph/services/memory/persist_fs.py +0 -40
  259. aethergraph/services/rag/index/base.py +0 -27
  260. aethergraph/services/rag/index/faiss_index.py +0 -121
  261. aethergraph/services/rag/index/sqlite_index.py +0 -134
  262. aethergraph-0.1.0a1.dist-info/RECORD +0 -182
  263. aethergraph-0.1.0a1.dist-info/entry_points.txt +0 -2
  264. {aethergraph-0.1.0a1.dist-info → aethergraph-0.1.0a2.dist-info}/WHEEL +0 -0
  265. {aethergraph-0.1.0a1.dist-info → aethergraph-0.1.0a2.dist-info}/licenses/LICENSE +0 -0
  266. {aethergraph-0.1.0a1.dist-info → aethergraph-0.1.0a2.dist-info}/licenses/NOTICE +0 -0
  267. {aethergraph-0.1.0a1.dist-info → aethergraph-0.1.0a2.dist-info}/top_level.txt +0 -0
@@ -0,0 +1,103 @@
1
+ from __future__ import annotations
2
+
3
+ from dataclasses import dataclass
4
+ import threading
5
+ import time
6
+ from typing import Any
7
+
8
+ from aethergraph.contracts.storage.async_kv import AsyncKV
9
+
10
+
11
+ @dataclass
12
+ class KVEntry:
13
+ value: Any
14
+ expire_at: float | None = None
15
+
16
+
17
+ class InMemoryKV(AsyncKV):
18
+ """
19
+ Simple in-memory KV.
20
+
21
+ - Process-local, not shared across processes.
22
+ - Thread-safe via RLock (sidecar + main thread can share safely).
23
+ - TTL managed best-effort on access / purge.
24
+ """
25
+
26
+ def __init__(self, *, prefix: str = ""):
27
+ self._data: dict[str, Any] = {}
28
+ self._expires_at: dict[str, float | None] = {}
29
+ self._lock = threading.RLock()
30
+ self._prefix = prefix
31
+
32
+ async def get(self, key: str, default: Any = None) -> Any:
33
+ now = time.time()
34
+ with self._lock:
35
+ if key not in self._data:
36
+ return default
37
+ exp = self._expires_at.get(key)
38
+ if exp is not None and exp < now:
39
+ # expired
40
+ self._data.pop(key, None)
41
+ self._expires_at.pop(key, None)
42
+ return default
43
+ return self._data[key]
44
+
45
+ async def set(self, key: str, value: Any, *, ttl_s: int | None = None) -> None:
46
+ with self._lock:
47
+ self._data[key] = value
48
+ self._expires_at[key] = time.time() + ttl_s if ttl_s is not None else None
49
+
50
+ async def delete(self, key: str) -> None:
51
+ with self._lock:
52
+ self._data.pop(key, None)
53
+ self._expires_at.pop(key, None)
54
+
55
+ async def mget(self, keys: list[str]) -> list[Any]:
56
+ # reuse get() so TTL is respected
57
+ return [await self.get(k) for k in keys]
58
+
59
+ async def mset(self, kv: dict[str, Any], *, ttl_s: int | None = None) -> None:
60
+ for k, v in kv.items():
61
+ await self.set(k, v, ttl_s=ttl_s)
62
+
63
+ async def expire(self, key: str, ttl_s: int) -> None:
64
+ with self._lock:
65
+ if key in self._data:
66
+ self._expires_at[key] = time.time() + ttl_s
67
+
68
+ async def purge_expired(self, limit: int = 1000) -> int:
69
+ now = time.time()
70
+ removed = 0
71
+ with self._lock:
72
+ for k in list(self._data.keys()):
73
+ if removed >= limit:
74
+ break
75
+ exp = self._expires_at.get(k)
76
+ if exp is not None and exp < now:
77
+ self._data.pop(k, None)
78
+ self._expires_at.pop(k, None)
79
+ removed += 1
80
+ return removed
81
+
82
+ # Helper to prefix keys
83
+ def _k(self, k: str) -> str:
84
+ return f"{self._prefix}{k}" if self._prefix else k
85
+
86
+ async def list_append_unique(
87
+ self, key: str, items: list[dict], *, id_key: str = "id", ttl_s: int | None = None
88
+ ) -> list[dict]:
89
+ """Append items to a list at `key`, ensuring uniqueness based on `id_key`."""
90
+ k = self._k(key)
91
+ with self._lock:
92
+ cur = list(self._data.get(k, KVEntry([])).value or [])
93
+ seen = {x.get(id_key) for x in cur if isinstance(x, dict)}
94
+ cur.extend([x for x in items if isinstance(x, dict) and x.get(id_key) not in seen])
95
+ self._data[k] = KVEntry(value=cur, expire_at=(time.time() + ttl_s) if ttl_s else None)
96
+ return cur
97
+
98
+ async def list_pop_all(self, key: str) -> list:
99
+ """Pop and return all items from the list at `key`."""
100
+ k = self._k(key)
101
+ with self._lock:
102
+ e = self._data.pop(k, None)
103
+ return list(e.value) if e and isinstance(e.value, list) else []
@@ -0,0 +1,52 @@
1
+ # storage/kv/layered_kv.py
2
+ from __future__ import annotations
3
+
4
+ from typing import Any
5
+
6
+ from aethergraph.contracts.storage.async_kv import AsyncKV
7
+
8
+
9
+ class LayeredKV(AsyncKV):
10
+ """
11
+ Read-through / write-through KV:
12
+
13
+ - hot: typically InMemoryKV
14
+ - cold: persistent KV (SqliteKV, RedisKV, etc.)
15
+ """
16
+
17
+ def __init__(self, hot: AsyncKV, cold: AsyncKV):
18
+ self.hot = hot
19
+ self.cold = cold
20
+
21
+ async def get(self, key: str, default: Any = None) -> Any:
22
+ v = await self.hot.get(key, default=None)
23
+ if v is not None:
24
+ return v
25
+ v = await self.cold.get(key, default=default)
26
+ if v is not None:
27
+ await self.hot.set(key, v)
28
+ return v
29
+
30
+ async def set(self, key: str, value: Any, *, ttl_s: int | None = None) -> None:
31
+ await self.cold.set(key, value, ttl_s=ttl_s)
32
+ await self.hot.set(key, value, ttl_s=ttl_s)
33
+
34
+ async def delete(self, key: str) -> None:
35
+ await self.cold.delete(key)
36
+ await self.hot.delete(key)
37
+
38
+ async def mget(self, keys: list[str]) -> list[Any]:
39
+ return [await self.get(k) for k in keys]
40
+
41
+ async def mset(self, kv: dict[str, Any], *, ttl_s: int | None = None) -> None:
42
+ for k, v in kv.items():
43
+ await self.set(k, v, ttl_s=ttl_s)
44
+
45
+ async def expire(self, key: str, ttl_s: int) -> None:
46
+ await self.cold.expire(key, ttl_s)
47
+ await self.hot.expire(key, ttl_s)
48
+
49
+ async def purge_expired(self, limit: int = 1000) -> int:
50
+ n_cold = await self.cold.purge_expired(limit)
51
+ n_hot = await self.hot.purge_expired(limit)
52
+ return n_cold + n_hot
@@ -0,0 +1,39 @@
1
+ from __future__ import annotations
2
+
3
+ import asyncio
4
+ from typing import Any
5
+
6
+ from aethergraph.contracts.storage.async_kv import AsyncKV
7
+
8
+ from .sqlite_kv_sync import SQLiteKVSync
9
+
10
+
11
+ class SqliteKV(AsyncKV):
12
+ """
13
+ Async KV on top of SQLiteKVSync via asyncio.to_thread.
14
+ Safe across threads (RLock in sync core).
15
+ """
16
+
17
+ def __init__(self, path: str, *, prefix: str = ""):
18
+ self._sync = SQLiteKVSync(path, prefix=prefix)
19
+
20
+ async def get(self, key: str, default: Any = None) -> Any:
21
+ return await asyncio.to_thread(self._sync.get, key, default)
22
+
23
+ async def set(self, key: str, value: Any, *, ttl_s: int | None = None) -> None:
24
+ await asyncio.to_thread(self._sync.set, key, value, ttl_s)
25
+
26
+ async def delete(self, key: str) -> None:
27
+ await asyncio.to_thread(self._sync.delete, key)
28
+
29
+ async def mget(self, keys: list[str]) -> list[Any]:
30
+ return await asyncio.to_thread(self._sync.mget, keys)
31
+
32
+ async def mset(self, kv: dict[str, Any], *, ttl_s: int | None = None) -> None:
33
+ await asyncio.to_thread(self._sync.mset, kv, ttl_s)
34
+
35
+ async def expire(self, key: str, ttl_s: int) -> None:
36
+ await asyncio.to_thread(self._sync.expire, key, ttl_s)
37
+
38
+ async def purge_expired(self, limit: int = 1000) -> int:
39
+ return await asyncio.to_thread(self._sync.purge_expired, limit)
@@ -0,0 +1,98 @@
1
+ from __future__ import annotations
2
+
3
+ import json
4
+ import os
5
+ import sqlite3
6
+ import threading
7
+ import time
8
+ from typing import Any
9
+
10
+ """
11
+ SQLite Key-Value Store with TTL (synchronous). Only used by async wrapper SQLiteKV.
12
+ """
13
+
14
+
15
+ class SQLiteKVSync:
16
+ """
17
+ Durable KV with TTL (JSON values), thread-safe via RLock.
18
+ """
19
+
20
+ def __init__(self, path: str, *, prefix: str = ""):
21
+ os.makedirs(os.path.dirname(path), exist_ok=True)
22
+ self._db = sqlite3.connect(path, check_same_thread=False, isolation_level=None)
23
+ self._db.execute("PRAGMA journal_mode=WAL;")
24
+ self._db.execute("PRAGMA synchronous=NORMAL;")
25
+ self._db.execute(
26
+ """
27
+ CREATE TABLE IF NOT EXISTS kv (
28
+ k TEXT PRIMARY KEY,
29
+ v TEXT,
30
+ expire_at REAL
31
+ )
32
+ """
33
+ )
34
+ self._db.execute("CREATE INDEX IF NOT EXISTS kv_exp_idx ON kv(expire_at);")
35
+ self._lock = threading.RLock()
36
+ self._prefix = prefix
37
+
38
+ def _k(self, k: str) -> str:
39
+ return f"{self._prefix}{k}" if self._prefix else k
40
+
41
+ def get(self, key: str, default: Any = None) -> Any:
42
+ k = self._k(key)
43
+ with self._lock:
44
+ row = self._db.execute("SELECT v, expire_at FROM kv WHERE k=?", (k,)).fetchone()
45
+ if not row:
46
+ return default
47
+ v_txt, exp = row
48
+ if exp and exp < time.time():
49
+ self._db.execute("DELETE FROM kv WHERE k=?", (k,))
50
+ return default
51
+ try:
52
+ return json.loads(v_txt)
53
+ except Exception:
54
+ return default
55
+
56
+ def set(self, key: str, value: Any, ttl_s: int | None = None) -> None:
57
+ k = self._k(key)
58
+ exp = time.time() + ttl_s if ttl_s is not None else None
59
+ v_txt = json.dumps(value, ensure_ascii=False)
60
+ with self._lock:
61
+ self._db.execute(
62
+ """
63
+ INSERT INTO kv (k, v, expire_at) VALUES (?, ?, ?)
64
+ ON CONFLICT(k) DO UPDATE SET v=excluded.v, expire_at=excluded.expire_at
65
+ """,
66
+ (k, v_txt, exp),
67
+ )
68
+
69
+ def delete(self, key: str) -> None:
70
+ k = self._k(key)
71
+ with self._lock:
72
+ self._db.execute("DELETE FROM kv WHERE k=?", (k,))
73
+
74
+ def mget(self, keys: list[str]) -> list[Any]:
75
+ return [self.get(k) for k in keys]
76
+
77
+ def mset(self, kv: dict[str, Any], ttl_s: int | None = None) -> None:
78
+ for k, v in kv.items():
79
+ self.set(k, v, ttl_s=ttl_s)
80
+
81
+ def expire(self, key: str, ttl_s: int) -> None:
82
+ k = self._k(key)
83
+ exp = time.time() + ttl_s
84
+ with self._lock:
85
+ self._db.execute("UPDATE kv SET expire_at=? WHERE k=?", (exp, k))
86
+
87
+ def purge_expired(self, limit: int = 1000) -> int:
88
+ now = time.time()
89
+ with self._lock:
90
+ rows = self._db.execute(
91
+ "SELECT k FROM kv WHERE expire_at IS NOT NULL AND expire_at < ? LIMIT ?",
92
+ (now, limit),
93
+ ).fetchall()
94
+ keys = [r[0] for r in rows]
95
+ if not keys:
96
+ return 0
97
+ self._db.executemany("DELETE FROM kv WHERE k=?", [(k,) for k in keys])
98
+ return len(keys)
@@ -0,0 +1,68 @@
1
+ from __future__ import annotations
2
+
3
+ from dataclasses import asdict
4
+ import hashlib
5
+ from typing import Any
6
+
7
+ from aethergraph.contracts.services.memory import Event, Persistence
8
+ from aethergraph.contracts.storage.doc_store import DocStore
9
+ from aethergraph.contracts.storage.event_log import EventLog
10
+
11
+
12
+ class EventLogPersistence(Persistence):
13
+ """
14
+ Persistence built on top of generic EventLog + DocStore.
15
+
16
+ - append_event: logs Event rows into EventLog with scope_id=run_id, kind="memory".
17
+ - save_json / load_json: store arbitrary JSON in DocStore using memdoc:// URIs.
18
+ """
19
+
20
+ def __init__(
21
+ self,
22
+ *,
23
+ log: EventLog,
24
+ docs: DocStore,
25
+ uri_prefix: str = "memdoc://",
26
+ ):
27
+ self._log = log
28
+ self._docs = docs
29
+ self._prefix = uri_prefix
30
+
31
+ # --------- helpers ---------
32
+ def _doc_id_from_uri(self, uri: str) -> str:
33
+ """
34
+ Accepts:
35
+ - memdoc://<id> -> <id>
36
+ - anything-else -> hashed to a stable doc_id.
37
+ """
38
+ if uri.startswith(self._prefix):
39
+ return uri[len(self._prefix) :]
40
+ # fallback: hash to avoid weird chars
41
+ h = hashlib.sha1(uri.encode("utf-8")).hexdigest()
42
+ return f"memdoc/{h}"
43
+
44
+ def _uri_from_doc_id(self, doc_id: str) -> str:
45
+ if doc_id.startswith("memdoc://"):
46
+ return doc_id
47
+ return f"{self._prefix}{doc_id}"
48
+
49
+ # --------- API ---------
50
+ async def append_event(self, run_id: str, evt: Event) -> None:
51
+ payload = asdict(evt)
52
+ payload.setdefault("scope_id", run_id)
53
+ payload.setdefault("kind", "memory")
54
+ # you can add tags like ["mem"] if useful
55
+ await self._log.append(payload)
56
+
57
+ async def save_json(self, uri: str, obj: dict[str, Any]) -> str:
58
+ doc_id = self._doc_id_from_uri(uri)
59
+ # Let DocStore own where/how it writes
60
+ await self._docs.put(doc_id, obj)
61
+ return self._uri_from_doc_id(doc_id)
62
+
63
+ async def load_json(self, uri: str) -> dict[str, Any]:
64
+ doc_id = self._doc_id_from_uri(uri)
65
+ doc = await self._docs.get(doc_id)
66
+ if doc is None:
67
+ raise FileNotFoundError(f"Memory JSON not found for URI: {uri}")
68
+ return doc
@@ -0,0 +1,118 @@
1
+ from __future__ import annotations
2
+
3
+ import asyncio
4
+ from dataclasses import asdict
5
+ import json
6
+ import os
7
+ from pathlib import Path
8
+ import threading
9
+ import time
10
+ from typing import Any
11
+
12
+ from aethergraph.contracts.services.memory import Event, Persistence
13
+
14
+
15
+ class FSPersistence(Persistence):
16
+ """
17
+ File-system based persistence for memory events + JSON blobs.
18
+
19
+ - Events are written to:
20
+ <base_dir>/mem/<run_id>/events/YYYY-MM-DD.jsonl
21
+
22
+ - JSON docs are read/written via file:// URIs:
23
+ file://relative/path.json -> <base_dir>/relative/path.json
24
+ file:///abs/path.json -> /abs/path.json (not under base_dir)
25
+ """
26
+
27
+ def __init__(self, *, base_dir: str):
28
+ self.base_dir = Path(base_dir).resolve()
29
+ self._lock = threading.RLock()
30
+
31
+ # ---------- Event log (append-only JSONL) ----------
32
+
33
+ async def append_event(self, run_id: str, evt: Event) -> None:
34
+ day = time.strftime("%Y-%m-%d", time.gmtime())
35
+ path = self.base_dir / "mem" / run_id / "events" / f"{day}.jsonl"
36
+
37
+ def _write() -> None:
38
+ path.parent.mkdir(parents=True, exist_ok=True)
39
+ raw = asdict(evt)
40
+ # Drop None values but keep [] / {} / 0.
41
+ data = {k: v for k, v in raw.items() if v is not None}
42
+ line = json.dumps(data, ensure_ascii=False) + "\n"
43
+ with self._lock, path.open("a", encoding="utf-8") as f:
44
+ f.write(line)
45
+
46
+ await asyncio.to_thread(_write)
47
+
48
+ # ---------- JSON blob helpers (file:// URIs) ----------
49
+
50
+ def _uri_to_path(self, uri: str) -> Path:
51
+ """
52
+ Convert a file:// URI into a local Path, resolving *relative* paths
53
+ against self.base_dir. Works cross-platform.
54
+ """
55
+ if not uri.startswith("file://"):
56
+ raise ValueError(f"FSPersistence only supports file:// URIs, got {uri!r}")
57
+
58
+ raw = uri[len("file://") :]
59
+
60
+ # Windows: normalize file:///C:/... -> C:/...
61
+ if (
62
+ os.name == "nt"
63
+ and raw.startswith("/")
64
+ and len(raw) > 2
65
+ and raw[1].isalpha()
66
+ and raw[2] == ":"
67
+ ):
68
+ raw = raw[1:]
69
+
70
+ p = Path(raw)
71
+
72
+ # Relative paths are resolved under base_dir
73
+ if not p.is_absolute():
74
+ p = self.base_dir / p
75
+
76
+ return p
77
+
78
+ def _path_to_uri(self, path: Path) -> str:
79
+ """
80
+ Convert a local Path to canonical file:// URI with forward slashes.
81
+ """
82
+ p = path.resolve()
83
+ s = p.as_posix()
84
+
85
+ # Ensure absolute paths appear as file:///... (add leading slash on Windows)
86
+ if p.is_absolute() and not s.startswith("/"):
87
+ s = "/" + s
88
+
89
+ return f"file://{s}"
90
+
91
+ async def save_json(self, uri: str, obj: dict[str, Any]) -> str:
92
+ """
93
+ Save JSON to the location specified by a file:// URI.
94
+ Returns the canonical file:// URI of the saved file.
95
+ """
96
+ path = self._uri_to_path(uri)
97
+
98
+ def _write() -> None:
99
+ path.parent.mkdir(parents=True, exist_ok=True)
100
+ tmp = path.with_suffix(path.suffix + ".tmp")
101
+ with self._lock, tmp.open("w", encoding="utf-8") as f:
102
+ json.dump(obj, f, ensure_ascii=False, indent=2)
103
+ os.replace(tmp, path)
104
+
105
+ await asyncio.to_thread(_write)
106
+ return self._path_to_uri(path)
107
+
108
+ async def load_json(self, uri: str) -> dict[str, Any]:
109
+ """
110
+ Inverse of save_json: load JSON from a file:// URI.
111
+ """
112
+ path = self._uri_to_path(uri)
113
+
114
+ def _read() -> dict[str, Any]:
115
+ with self._lock, path.open("r", encoding="utf-8") as f:
116
+ return json.load(f)
117
+
118
+ return await asyncio.to_thread(_read)
@@ -1,6 +1,8 @@
1
1
  from aethergraph.contracts.services.kv import AsyncKV
2
2
  from aethergraph.contracts.services.memory import Event, HotLog
3
3
 
4
+ # No specific backend is required; we use AsyncKV for storage.
5
+
4
6
 
5
7
  def kv_hot_key(run_id: str) -> str:
6
8
  return f"mem:{run_id}:hot"
@@ -13,13 +15,17 @@ class KVHotLog(HotLog):
13
15
  async def append(self, run_id: str, evt: Event, *, ttl_s: int, limit: int) -> None:
14
16
  key = kv_hot_key(run_id)
15
17
  buf = list((await self.kv.get(key, default=[])) or [])
16
- buf.append(evt.__dict__) # store as dict for JSON serializability
18
+ buf.append(evt.__dict__) # store as dict for JSON-ability
17
19
  if len(buf) > limit:
18
20
  buf = buf[-limit:]
19
21
  await self.kv.set(key, buf, ttl_s=ttl_s)
20
22
 
21
23
  async def recent(
22
- self, run_id: str, *, kinds: list[str] | None = None, limit: int = 50
24
+ self,
25
+ run_id: str,
26
+ *,
27
+ kinds: list[str] | None = None,
28
+ limit: int = 50,
23
29
  ) -> list[Event]:
24
30
  buf = (await self.kv.get(kv_hot_key(run_id), default=[])) or []
25
31
  if kinds:
@@ -1,19 +1,22 @@
1
+ from __future__ import annotations
2
+
1
3
  from typing import Any
2
4
 
3
- from aethergraph.contracts.services.kv import AsyncKV
4
5
  from aethergraph.contracts.services.memory import Event, Indices
6
+ from aethergraph.contracts.storage.async_kv import AsyncKV
5
7
 
6
8
 
7
9
  def idx_by_ref_kind(run_id: str) -> str:
8
- return f"mem:{run_id}:idx2:ref_kind"
10
+ return f"mem:{run_id}:idx:ref_kind"
9
11
 
10
12
 
11
13
  def idx_by_name(run_id: str) -> str:
12
- return f"mem:{run_id}:idx2:name"
14
+ return f"mem:{run_id}:idx:name"
13
15
 
14
16
 
15
17
  def idx_by_topic(run_id: str) -> str:
16
- return f"mem:{run_id}:idx2:topic"
18
+ # topic = tool / agent / flow name
19
+ return f"mem:{run_id}:idx:topic"
17
20
 
18
21
 
19
22
  class KVIndices(Indices):
@@ -29,25 +32,39 @@ class KVIndices(Indices):
29
32
  by_name = (await self.kv.get(idx_by_name(run_id), {})) or {}
30
33
  by_topic = (await self.kv.get(idx_by_topic(run_id), {})) or {}
31
34
 
35
+ # 1) Index by output name & ref.kind
32
36
  for v in outs:
33
37
  nm = v.get("name")
34
38
  if not nm:
35
39
  continue
40
+
41
+ # name index
36
42
  by_name[nm] = {
37
43
  "ts": ts,
38
44
  "event_id": eid,
39
45
  "vtype": v.get("vtype"),
40
46
  "value": v.get("value"),
41
47
  }
48
+
49
+ # ref.kind index
42
50
  if v.get("vtype") == "ref" and isinstance(v.get("value"), dict):
43
51
  kind = v["value"].get("kind")
44
52
  uri = v["value"].get("uri")
45
53
  if kind and uri:
46
54
  lst = by_kind.setdefault(kind, [])
47
- lst.append({"ts": ts, "event_id": eid, "name": nm, "uri": uri, "topic": tool})
55
+ lst.append(
56
+ {
57
+ "ts": ts,
58
+ "event_id": eid,
59
+ "name": nm,
60
+ "uri": uri,
61
+ "topic": tool,
62
+ }
63
+ )
48
64
  if len(lst) > 200:
49
65
  del lst[:-200]
50
66
 
67
+ # 2) Index by topic (tool / flow / agent)
51
68
  if tool:
52
69
  last = by_topic.get(tool, {}) or {}
53
70
  last["ts"] = ts
@@ -59,15 +76,22 @@ class KVIndices(Indices):
59
76
  await self.kv.set(idx_by_name(run_id), by_name, ttl_s=self.ttl)
60
77
  await self.kv.set(idx_by_topic(run_id), by_topic, ttl_s=self.ttl)
61
78
 
79
+ # ---------- queries ----------
62
80
  async def last_by_name(self, run_id: str, name: str) -> dict[str, Any] | None:
63
81
  by_name = await self.kv.get(idx_by_name(run_id), {}) or {}
64
82
  return by_name.get(name)
65
83
 
66
84
  async def latest_refs_by_kind(
67
- self, run_id: str, kind: str, *, limit: int = 50
85
+ self,
86
+ run_id: str,
87
+ kind: str,
88
+ *,
89
+ limit: int = 50,
68
90
  ) -> list[dict[str, Any]]:
69
91
  by_kind = await self.kv.get(idx_by_ref_kind(run_id), {}) or {}
70
- return list(reversed((by_kind.get(kind) or [])[-limit:]))
92
+ arr = (by_kind.get(kind) or [])[-limit:]
93
+ # latest first
94
+ return list(reversed(arr))
71
95
 
72
96
  async def last_outputs_by_topic(self, run_id: str, topic: str) -> dict[str, Any] | None:
73
97
  by_topic = await self.kv.get(idx_by_topic(run_id), {}) or {}
@@ -0,0 +1,55 @@
1
+ from dataclasses import dataclass
2
+ from datetime import datetime
3
+ from typing import Any
4
+
5
+ from aethergraph.contracts.services.metering import MeteringStore
6
+ from aethergraph.contracts.storage.event_log import EventLog
7
+
8
+ METER_TAG = "meter" # shared tag for all metering events
9
+
10
+
11
+ @dataclass
12
+ class EventLogMeteringStore(MeteringStore):
13
+ """
14
+ MeteringStore backed by a generic EventLog.
15
+
16
+ Convention:
17
+ - kind: e.g. "meter.llm", "meter.run", "meter.artifact", "meter.event"
18
+ - tags: always includes "meter" so queries don't mix with other app events
19
+ """
20
+
21
+ event_log: EventLog
22
+
23
+ async def append(self, event: dict[str, Any]) -> None:
24
+ # Enforce metering conventions
25
+ kind = event.get("kind")
26
+ if not kind or not kind.startswith("meter."):
27
+ raise ValueError(f"Metering event kind must start with 'meter.': {kind!r}")
28
+
29
+ tags = set(event.get("tags") or [])
30
+ tags.add(METER_TAG)
31
+ event["tags"] = list(tags)
32
+
33
+ await self.event_log.append(event)
34
+
35
+ async def query(
36
+ self,
37
+ *,
38
+ since: datetime | None = None,
39
+ until: datetime | None = None,
40
+ kinds: list[str] | None = None,
41
+ limit: int | None = None,
42
+ user_id: str | None = None,
43
+ org_id: str | None = None,
44
+ ) -> list[dict[str, Any]]:
45
+ # Always filter by meter tag
46
+ return await self.event_log.query(
47
+ scope_id=None,
48
+ since=since,
49
+ until=until,
50
+ kinds=kinds,
51
+ tags=[METER_TAG],
52
+ limit=limit,
53
+ user_id=user_id,
54
+ org_id=org_id,
55
+ )