aethergraph 0.1.0a1__py3-none-any.whl → 0.1.0a3__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 +296 -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 +196 -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.0a3.dist-info}/METADATA +138 -31
  249. aethergraph-0.1.0a3.dist-info/RECORD +356 -0
  250. aethergraph-0.1.0a3.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.0a3.dist-info}/WHEEL +0 -0
  265. {aethergraph-0.1.0a1.dist-info → aethergraph-0.1.0a3.dist-info}/licenses/LICENSE +0 -0
  266. {aethergraph-0.1.0a1.dist-info → aethergraph-0.1.0a3.dist-info}/licenses/NOTICE +0 -0
  267. {aethergraph-0.1.0a1.dist-info → aethergraph-0.1.0a3.dist-info}/top_level.txt +0 -0
@@ -0,0 +1,261 @@
1
+ from __future__ import annotations
2
+
3
+ from dataclasses import asdict
4
+ from datetime import datetime, timezone
5
+ import hashlib
6
+ import hmac
7
+ import os
8
+ from typing import Any
9
+
10
+ from aethergraph.contracts.services.continuations import AsyncContinuationStore
11
+ from aethergraph.contracts.services.kv import AsyncKV
12
+ from aethergraph.contracts.storage.doc_store import DocStore
13
+ from aethergraph.contracts.storage.event_log import EventLog
14
+ from aethergraph.services.continuations.continuation import Continuation, Correlator
15
+
16
+
17
+ class KVDocContinuationStore(AsyncContinuationStore):
18
+ """
19
+ Continuation store backed by:
20
+ - DocStore: main continuation document (one per (run_id, node_id))
21
+ - AsyncKV: token and correlator indices
22
+ - EventLog: (optional) audit trail of continuation events
23
+ """
24
+
25
+ def __init__(
26
+ self,
27
+ *,
28
+ doc_store: DocStore,
29
+ kv: AsyncKV,
30
+ event_log: EventLog | None = None,
31
+ secret: bytes,
32
+ namespace: str = "cont",
33
+ ):
34
+ self._docs = doc_store
35
+ self._kv = kv
36
+ self._log = event_log
37
+ self._secret = secret
38
+ self._ns = namespace.rstrip("/") # namespace prefix for KV keys
39
+
40
+ # ---------- key helpers ----------
41
+ def _cont_id(self, run_id: str, node_id: str) -> str:
42
+ # one doc per continuation
43
+ return f"{self._ns}/runs/{run_id}/nodes/{node_id}"
44
+
45
+ def _token_key(self, token: str) -> str:
46
+ return f"{self._ns}:token:{token}"
47
+
48
+ def _corr_key(self, corr: Correlator) -> str:
49
+ scheme, channel, thread, message = corr.key()
50
+ return f"{self._ns}:corr:{scheme}:{channel}:{thread}:{message}"
51
+
52
+ # ---------- token helpers ----------
53
+ def _hmac(self, *parts: str) -> str:
54
+ h = hmac.new(self._secret, digestmod=hashlib.sha256)
55
+ for p in parts:
56
+ h.update(p.encode("utf-8"))
57
+ return h.hexdigest()
58
+
59
+ async def mint_token(self, run_id: str, node_id: str, attempts: int) -> str:
60
+ token = self._hmac(run_id, node_id, str(attempts), os.urandom(8).hex())
61
+ return token
62
+
63
+ # ---------- main methods ----------
64
+ async def save(self, cont: Continuation) -> None:
65
+ payload = cont.to_dict() if hasattr(cont, "to_dict") else asdict(cont)
66
+
67
+ # Normalize datetime fields to ISO format
68
+ for k in ("deadline", "next_wakeup_at", "created_at"):
69
+ v = payload.get(k)
70
+ if isinstance(v, datetime):
71
+ payload[k] = v.astimezone(timezone.utc).isoformat()
72
+
73
+ doc_id = self._cont_id(cont.run_id, cont.node_id)
74
+ await self._docs.put(doc_id, payload)
75
+
76
+ # token -> (run_id, node_id)
77
+ await self._kv.set(
78
+ self._token_key(cont.token),
79
+ {"run_id": cont.run_id, "node_id": cont.node_id},
80
+ )
81
+
82
+ if self._log is not None:
83
+ evt = {
84
+ "scope_id": cont.run_id,
85
+ "kind": "continuation.save",
86
+ "ts": datetime.now(timezone.utc).isoformat(),
87
+ "tags": [cont.channel or "", cont.kind or ""],
88
+ "payload": payload,
89
+ }
90
+ await self._log.append(evt)
91
+
92
+ async def _doc_to_cont(self, data: dict[str, Any] | None) -> Continuation | None:
93
+ if data is None:
94
+ return None
95
+
96
+ for k in ("deadline", "next_wakeup_at", "created_at"):
97
+ v = data.get(k)
98
+ if isinstance(v, str):
99
+ data[k] = datetime.fromisoformat(v)
100
+
101
+ data["closed"] = bool(data.get("closed", False))
102
+ return Continuation(**data)
103
+
104
+ async def get(self, run_id: str, node_id: str) -> Continuation | None:
105
+ doc_id = self._cont_id(run_id, node_id)
106
+ data = await self._docs.get(doc_id)
107
+ return await self._doc_to_cont(data)
108
+
109
+ async def list_cont_by_run(self, run_id: str) -> list[Continuation]:
110
+ prefix = f"{self._ns}/runs/{run_id}/nodes/"
111
+ ids = await self._docs.list()
112
+ out: list[Continuation] = []
113
+ for doc_id in ids:
114
+ if not doc_id.startswith(prefix):
115
+ continue
116
+ data = await self._docs.get(doc_id)
117
+ cont = await self._doc_to_cont(data)
118
+ if cont is not None:
119
+ out.append(cont)
120
+ return out
121
+
122
+ async def delete(self, run_id: str, node_id: str) -> None:
123
+ cont = await self.get(run_id, node_id)
124
+ doc_id = self._cont_id(run_id, node_id)
125
+ await self._docs.delete(doc_id)
126
+ if cont:
127
+ # best-effort delete of token mapping
128
+ await self._kv.delete(self._token_key(cont.token))
129
+
130
+ # ---------- token methods ----------
131
+ async def get_by_token(self, token: str) -> Continuation | None:
132
+ ref = await self._kv.get(self._token_key(token), default=None)
133
+ if not ref:
134
+ return None
135
+ run_id = ref.get("run_id")
136
+ node_id = ref.get("node_id")
137
+ if not run_id or not node_id:
138
+ return None
139
+ return await self.get(run_id, node_id)
140
+
141
+ async def mark_closed(self, token: str) -> None:
142
+ cont = await self.get_by_token(token)
143
+ if not cont:
144
+ return
145
+ if not cont.closed:
146
+ cont.closed = True
147
+ await self.save(cont)
148
+
149
+ async def verify_token(self, run_id: str, node_id: str, token: str) -> bool:
150
+ cont = await self.get(run_id, node_id)
151
+ return bool(cont and hmac.compare_digest(token, cont.token))
152
+
153
+ # ---------- correlator methods ----------
154
+ async def bind_correlator(self, *, token: str, corr: str) -> None:
155
+ key = self._corr_key(corr)
156
+ toks: list[str] = await self._kv.get(key, default=[]) or []
157
+ if token not in toks:
158
+ toks.append(token)
159
+ await self._kv.set(key, toks)
160
+
161
+ # TODO: consider reverse mapping: token -> correlators for easier cleanup
162
+ # for now we don't need that
163
+
164
+ async def find_by_correlator(self, *, corr: Correlator) -> list[Continuation] | None:
165
+ """
166
+ Find all continuations matching the correlator. Following the method:
167
+ - get all tokens for the correlator
168
+ - fetch each continuation by token
169
+ - filter out closed or expired continuations
170
+ - return the first valid continuation found (or None)
171
+ """
172
+ key = self._corr_key(corr)
173
+ toks: list[str] = await self._kv.get(key, default=[]) or []
174
+ from datetime import datetime as dt, timezone as tz
175
+
176
+ for tok in reversed(toks):
177
+ cont = await self.get_by_token(tok)
178
+ if not cont or cont.closed:
179
+ continue
180
+ if cont.deadline and dt.now(tz.utc) > cont.deadline.astimezone(tz.utc):
181
+ continue
182
+ return cont
183
+ return None
184
+
185
+ # ---------- scans ----------
186
+ async def last_open(self, *, channel: str, kind: str) -> Continuation | None:
187
+ """
188
+ Slow scan (like FS version) – OK for dev scale.
189
+ We scan all docs and pick the most recent created_at with matching channel/kind.
190
+ NOTE: this is only for debugging and development purposes. Do not use in production!
191
+ """
192
+ ids = await self._docs.list()
193
+ best: Continuation | None = None
194
+ best_ts: float | None = None
195
+
196
+ for doc_id in ids:
197
+ if not doc_id.startswith(f"{self._ns}/runs/"):
198
+ continue
199
+ data = await self._docs.get(doc_id)
200
+ cont = await self._doc_to_cont(data)
201
+ if not cont or cont.closed:
202
+ continue
203
+ if cont.channel != channel or cont.kind != kind:
204
+ continue
205
+ created = cont.created_at or datetime.min.replace(tzinf=timezone.utc)
206
+ ts = created.timestamp()
207
+ if best_ts is None or ts > best_ts:
208
+ best = cont
209
+ best_ts = ts
210
+ return best
211
+
212
+ async def list_waits(self) -> list[dict[str, Any]]:
213
+ """
214
+ Return all continuations as dicts (similar to FS version).
215
+ Caller can filter for waits if needed.
216
+ """
217
+ ids = await self._docs.list()
218
+ out: list[dict[str, Any]] = []
219
+ for doc_id in ids:
220
+ if not doc_id.startswith(f"{self._ns}/runs/"):
221
+ continue
222
+ data = await self._docs.get(doc_id)
223
+ if data:
224
+ out.append(data)
225
+ return out
226
+
227
+ async def clear(self) -> None:
228
+ """
229
+ Best-effort clear of continuation documents.
230
+
231
+ NOTE: KV indexes may remain unless we have a scan-capable KV implementation.
232
+ They are harmless: get_by_token() will just return None if the doc is gone.
233
+ """
234
+ # 1) DocStore cleanup
235
+ ids = await self._docs.list()
236
+ for doc_id in ids:
237
+ if doc_id.startswith(f"{self._ns}/runs/"):
238
+ await self._docs.delete(doc_id)
239
+
240
+ # 2) Best-effort KV cleanup if scan_keys is available
241
+ # TODO: implement KV indexes if we have scan capability
242
+ scan = getattr(self._kv, "scan_keys", None)
243
+ if callable(scan):
244
+ token_prefix = f"{self._ns}:token:"
245
+ corr_prefix = f"{self._ns}:corr:"
246
+
247
+ for pfx in (token_prefix, corr_prefix):
248
+ try:
249
+ keys: list[str] = await scan(pfx) # type: ignore[call-arg]
250
+ except Exception:
251
+ # If a backend throws here, we still consider clear() successful
252
+ continue
253
+ for k in keys:
254
+ try:
255
+ await self._kv.delete(k)
256
+ except Exception:
257
+ # swallow individual key errors; we're best-effort here
258
+ continue
259
+
260
+ async def alias_for(self, token: str) -> str | None:
261
+ return token[:24]
@@ -0,0 +1,63 @@
1
+ import asyncio
2
+ import json
3
+ import os
4
+ from pathlib import Path
5
+ import threading
6
+ from typing import Any
7
+
8
+ from aethergraph.contracts.storage.doc_store import DocStore
9
+
10
+
11
+ class FSDocStore(DocStore):
12
+ def __init__(self, root: str):
13
+ self.root = Path(root)
14
+ self.root.mkdir(parents=True, exist_ok=True)
15
+ self._lock = threading.Lock()
16
+
17
+ def _path_for(self, doc_id: str) -> Path:
18
+ p = self.root / f"{doc_id}.json"
19
+ p.parent.mkdir(parents=True, exist_ok=True)
20
+ return p
21
+
22
+ async def put(self, doc_id: str, doc: dict[str, Any]) -> None:
23
+ path = self._path_for(doc_id)
24
+
25
+ def _write():
26
+ tmp = path.with_suffix(path.suffix + ".tmp")
27
+ with self._lock, tmp.open("w", encoding="utf-8") as f:
28
+ json.dump(doc, f, ensure_ascii=False, indent=2)
29
+ os.replace(tmp, path)
30
+
31
+ await asyncio.to_thread(_write)
32
+
33
+ async def get(self, doc_id: str) -> dict[str, Any] | None:
34
+ path = self._path_for(doc_id)
35
+
36
+ def _read():
37
+ if not path.exists():
38
+ return None
39
+ with self._lock, path.open("r", encoding="utf-8") as f:
40
+ return json.load(f)
41
+
42
+ return await asyncio.to_thread(_read)
43
+
44
+ async def delete(self, doc_id):
45
+ path = self._path_for(doc_id)
46
+
47
+ def _delete():
48
+ if path.exists():
49
+ with self._lock:
50
+ path.unlink()
51
+
52
+ await asyncio.to_thread(_delete)
53
+
54
+ async def list(self) -> list[str]:
55
+ def _list():
56
+ out = []
57
+ for p in self.root.rglob("*.json"):
58
+ rel = p.relative_to(self.root)
59
+ doc_id = str(rel.with_suffix("").as_posix())
60
+ out.append(doc_id)
61
+ return out
62
+
63
+ return await asyncio.to_thread(_list)
@@ -0,0 +1,31 @@
1
+ from __future__ import annotations
2
+
3
+ import asyncio
4
+ from typing import Any
5
+
6
+ from aethergraph.contracts.storage.doc_store import DocStore
7
+
8
+ from .sqlite_doc_sync import SQLiteDocStoreSync
9
+
10
+
11
+ class SqliteDocStore(DocStore):
12
+ """
13
+ Async DocStore implemented on top of SQLiteDocStoreSync via asyncio.to_thread.
14
+
15
+ Safe to use from multiple threads (sidecar + main loop) due to RLock in sync core.
16
+ """
17
+
18
+ def __init__(self, path: str):
19
+ self._sync = SQLiteDocStoreSync(path)
20
+
21
+ async def put(self, doc_id: str, doc: dict[str, Any]) -> None:
22
+ await asyncio.to_thread(self._sync.put, doc_id, doc)
23
+
24
+ async def get(self, doc_id: str) -> dict[str, Any] | None:
25
+ return await asyncio.to_thread(self._sync.get, doc_id)
26
+
27
+ async def delete(self, doc_id: str) -> None:
28
+ await asyncio.to_thread(self._sync.delete, doc_id)
29
+
30
+ async def list(self) -> list[str]:
31
+ return await asyncio.to_thread(self._sync.list)
@@ -0,0 +1,90 @@
1
+ from __future__ import annotations
2
+
3
+ import json
4
+ from pathlib import Path
5
+ import sqlite3
6
+ import threading
7
+ import time
8
+ from typing import Any
9
+
10
+ """
11
+ This is not used in the main codebase; only used by async wrapper SqliteDocStore.
12
+ """
13
+
14
+
15
+ class SQLiteDocStoreSync:
16
+ """
17
+ Durable document store on SQLite.
18
+
19
+ - Single connection per instance.
20
+ - Thread-safe via RLock.
21
+ - Values are JSON-serialized dicts.
22
+ """
23
+
24
+ def __init__(self, path: str):
25
+ path_obj = Path(path)
26
+ path_obj.parent.mkdir(parents=True, exist_ok=True)
27
+
28
+ self._db = sqlite3.connect(
29
+ str(path_obj),
30
+ check_same_thread=False, # allow multi-thread access (guarded by RLock)
31
+ isolation_level=None, # autocommit
32
+ )
33
+ self._db.execute("PRAGMA journal_mode=WAL;")
34
+ self._db.execute("PRAGMA synchronous=NORMAL;")
35
+ self._db.execute(
36
+ """
37
+ CREATE TABLE IF NOT EXISTS docs (
38
+ doc_id TEXT PRIMARY KEY,
39
+ data_json TEXT NOT NULL,
40
+ updated_at REAL NOT NULL
41
+ )
42
+ """
43
+ )
44
+ self._lock = threading.RLock()
45
+
46
+ def put(self, doc_id: str, doc: dict[str, Any]) -> None:
47
+ payload = json.dumps(doc, ensure_ascii=False)
48
+ now = time.time()
49
+ # TEMP: tiny backoff to avoid rare SQLite stalls under continuations.
50
+ # This is a hacky workaround.
51
+ # It happens when following conditions align:
52
+ # 1) continuation store using sqlite doc to save
53
+ # 2) the continuation is created under if/else or for loop nodes
54
+ # NOTE: this bug only appears in SQLite and not in other DBs.
55
+ # Remove once we move continuations to Postgres or refactor SQLite usage.
56
+ time.sleep(0.01)
57
+ with self._lock:
58
+ try:
59
+ self._db.execute(
60
+ """
61
+ INSERT INTO docs (doc_id, data_json, updated_at)
62
+ VALUES (?, ?, ?)
63
+ ON CONFLICT(doc_id) DO UPDATE SET
64
+ data_json = excluded.data_json,
65
+ updated_at = excluded.updated_at
66
+ """,
67
+ (doc_id, payload, now),
68
+ )
69
+ except sqlite3.Error as e:
70
+ print("🍓 SQLiteDocStoreSync ERROR during put:", doc_id, repr(e))
71
+ raise
72
+
73
+ def get(self, doc_id: str) -> dict[str, Any] | None:
74
+ with self._lock:
75
+ row = self._db.execute(
76
+ "SELECT data_json FROM docs WHERE doc_id = ?",
77
+ (doc_id,),
78
+ ).fetchone()
79
+ if not row:
80
+ return None
81
+ return json.loads(row[0])
82
+
83
+ def delete(self, doc_id: str) -> None:
84
+ with self._lock:
85
+ self._db.execute("DELETE FROM docs WHERE doc_id = ?", (doc_id,))
86
+
87
+ def list(self) -> list[str]:
88
+ with self._lock:
89
+ rows = self._db.execute("SELECT doc_id FROM docs").fetchall()
90
+ return [r[0] for r in rows]
@@ -0,0 +1,136 @@
1
+ import asyncio
2
+ from datetime import datetime, timezone
3
+ import json
4
+ from pathlib import Path
5
+ import threading
6
+ import time
7
+
8
+ from aethergraph.contracts.storage.event_log import EventLog
9
+
10
+
11
+ def _to_ts_float(v) -> float | None:
12
+ """
13
+ Normalize event ts field to a float UNIX timestamp.
14
+
15
+ Supports:
16
+ - float / int already
17
+ - ISO 8601 string, e.g. '2025-11-27T19:48:09.758687+00:00'
18
+ - ISO with 'Z' suffix, e.g. '2025-11-27T19:48:09Z'
19
+ """
20
+ if v is None:
21
+ return None
22
+ if isinstance(v, int | float):
23
+ return float(v)
24
+ if isinstance(v, str):
25
+ try:
26
+ s = v.replace("Z", "+00:00") if v.endswith("Z") else v
27
+ dt = datetime.fromisoformat(s)
28
+ if dt.tzinfo is None:
29
+ dt = dt.replace(tzinfo=timezone.utc)
30
+ return dt.timestamp()
31
+ except Exception:
32
+ return None
33
+ if isinstance(v, datetime):
34
+ if v.tzinfo is None:
35
+ v = v.replace(tzinfo=timezone.utc)
36
+ return v.timestamp()
37
+ return None
38
+
39
+
40
+ class FSEventLog(EventLog):
41
+ def __init__(self, root: str):
42
+ self.root = Path(root)
43
+ self.root.mkdir(parents=True, exist_ok=True)
44
+ self._lock = threading.Lock()
45
+ self._log_path = self.root / "events.jsonl"
46
+
47
+ async def append(self, evt: dict) -> None:
48
+ def _write():
49
+ self._log_path.parent.mkdir(parents=True, exist_ok=True)
50
+ row = evt.copy()
51
+
52
+ # Normalize ts to a float UNIX timestamp
53
+ ts = _to_ts_float(row.get("ts"))
54
+ if ts is None:
55
+ ts = time.time()
56
+ row["ts"] = ts
57
+
58
+ with self._lock, self._log_path.open("a", encoding="utf-8") as f:
59
+ f.write(json.dumps(row, ensure_ascii=False) + "\n")
60
+
61
+ await asyncio.to_thread(_write)
62
+
63
+ async def query(
64
+ self,
65
+ *,
66
+ scope_id: str | None = None,
67
+ since: datetime | None = None,
68
+ until: datetime | None = None,
69
+ kinds: list[str] | None = None,
70
+ limit: int | None = None,
71
+ tags: list[str] | None = None,
72
+ offset: int = 0,
73
+ user_id: str | None = None,
74
+ org_id: str | None = None,
75
+ ) -> list[dict]:
76
+ """
77
+ FSEventLog reads the single events.jsonl file linearly, applies
78
+ all filters (scope_id, time window, kinds, tags, tenant) in Python,
79
+ and then slices via offset + limit.
80
+
81
+ This is fine for dev/demo / low event volumes. For production,
82
+ prefer SQLiteEventLog or a DB-backed implementation.
83
+ """
84
+ if not self._log_path.exists():
85
+ return []
86
+
87
+ def _read() -> list[dict]:
88
+ out: list[dict] = []
89
+ t_min = since.timestamp() if since else None
90
+ t_max = until.timestamp() if until else None
91
+
92
+ # If we want to early-break, we need enough rows to cover offset+limit.
93
+ needed = None
94
+ if limit is not None:
95
+ needed = (offset or 0) + limit
96
+
97
+ with self._lock, self._log_path.open("r", encoding="utf-8") as f:
98
+ for line in f:
99
+ if not line.strip():
100
+ continue
101
+ row = json.loads(line)
102
+
103
+ ts_val = _to_ts_float(row.get("ts"))
104
+
105
+ if t_min is not None and ts_val is not None and ts_val < t_min:
106
+ continue
107
+ if t_max is not None and ts_val is not None and ts_val > t_max:
108
+ continue
109
+ if scope_id is not None and row.get("scope_id") != scope_id:
110
+ continue
111
+ if kinds is not None and row.get("kind") not in kinds:
112
+ continue
113
+ if tags is not None:
114
+ row_tags = set(row.get("tags", []))
115
+ if not row_tags.issuperset(tags):
116
+ continue
117
+ if user_id is not None and row.get("user_id") != user_id:
118
+ continue
119
+ if org_id is not None and row.get("org_id") != org_id:
120
+ continue
121
+
122
+ out.append(row)
123
+
124
+ # Only break early when we've collected enough to satisfy offset+limit
125
+ if needed is not None and len(out) >= needed:
126
+ break
127
+
128
+ # Apply offset/limit on the filtered rows
129
+ if offset > 0:
130
+ out = out[offset:]
131
+ if limit is not None:
132
+ out = out[:limit]
133
+
134
+ return out
135
+
136
+ return await asyncio.to_thread(_read)
@@ -0,0 +1,47 @@
1
+ # storage/events/sqlite_event_log.py
2
+ from __future__ import annotations
3
+
4
+ import asyncio
5
+ from datetime import datetime
6
+
7
+ from aethergraph.contracts.storage.event_log import EventLog
8
+
9
+ from .sqlite_event_sync import SQLiteEventLogSync
10
+
11
+
12
+ class SqliteEventLog(EventLog):
13
+ """
14
+ Async EventLog wrapper around SQLiteEventLogSync via asyncio.to_thread.
15
+ """
16
+
17
+ def __init__(self, path: str):
18
+ self._sync = SQLiteEventLogSync(path)
19
+
20
+ async def append(self, evt: dict) -> None:
21
+ await asyncio.to_thread(self._sync.append, evt)
22
+
23
+ async def query(
24
+ self,
25
+ *,
26
+ scope_id: str | None = None,
27
+ since: datetime | None = None,
28
+ until: datetime | None = None,
29
+ kinds: list[str] | None = None,
30
+ limit: int | None = None,
31
+ tags: list[str] | None = None,
32
+ offset: int = 0,
33
+ user_id: str | None = None,
34
+ org_id: str | None = None,
35
+ ) -> list[dict]:
36
+ return await asyncio.to_thread(
37
+ self._sync.query,
38
+ scope_id=scope_id,
39
+ since=since,
40
+ until=until,
41
+ kinds=kinds,
42
+ limit=limit,
43
+ tags=tags,
44
+ offset=offset,
45
+ user_id=user_id,
46
+ org_id=org_id,
47
+ )