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,115 @@
1
+ from __future__ import annotations
2
+
3
+ import os
4
+
5
+ import boto3
6
+ from botocore.config import Config
7
+
8
+ from aethergraph.contracts.storage.blob_store import BlobStore
9
+ from aethergraph.services.artifacts.utils import to_thread
10
+
11
+ """
12
+ S3BlobStore: BlobStore implementation using AWS S3 as the backend.
13
+ NOTE: This is a stub implementation; not fully tested.
14
+ """
15
+
16
+
17
+ class S3BlobStore(BlobStore):
18
+ def __init__(self, bucket: str, prefix: str = ""):
19
+ self.bucket = bucket
20
+ self.prefix = prefix.strip("/")
21
+
22
+ # Create S3 client with default config; can be customized as needed
23
+ self.s3_client = boto3.client(
24
+ "s3", config=Config(signature_version="s3v4", max_pool_connections=50)
25
+ )
26
+
27
+ @property
28
+ def base_uri(self) -> str:
29
+ if self.prefix:
30
+ return f"s3://{self.bucket}/{self.prefix}"
31
+ return f"s3://{self.bucket}"
32
+
33
+ def _resolve_key(self, key: str | None, ext: str | None) -> str:
34
+ if key is None:
35
+ import uuid
36
+
37
+ key = uuid.uuid4().hex + (ext or "")
38
+ if self.prefix:
39
+ return f"{self.prefix}/{key.lstrip('/')}"
40
+ return key.lstrip("/")
41
+
42
+ async def put_bytes(
43
+ self,
44
+ data: bytes,
45
+ *,
46
+ key: str | None = None,
47
+ ext: str | None = None,
48
+ mime: str | None = None,
49
+ keep_source: bool = False, # added for interface consistency; not used here
50
+ ) -> str:
51
+ key = self._resolve_key(key, ext)
52
+
53
+ def _upload():
54
+ extra = {}
55
+ if mime:
56
+ extra["ContentType"] = mime
57
+ self._client.put_object(
58
+ Bucket=self.bucket,
59
+ Key=key,
60
+ Body=data,
61
+ **extra,
62
+ )
63
+ return f"s3://{self.bucket}/{key}"
64
+
65
+ return await to_thread(_upload)
66
+
67
+ async def put_file(
68
+ self,
69
+ path: str,
70
+ *,
71
+ key: str | None = None,
72
+ mime: str | None = None,
73
+ ) -> str:
74
+ ext = os.path.splitext(path)[1]
75
+ key = self._resolve_key(key, ext)
76
+
77
+ def _upload_file():
78
+ extra = {}
79
+ if mime:
80
+ extra["ContentType"] = mime
81
+ self._client.upload_file(
82
+ Filename=os.path.abspath(path),
83
+ Bucket=self.bucket,
84
+ Key=key,
85
+ ExtraArgs=extra or None,
86
+ )
87
+ return f"s3://{self.bucket}/{key}"
88
+
89
+ return await to_thread(_upload_file)
90
+
91
+ async def load_bytes(self, uri: str) -> bytes:
92
+ # assume s3://bucket/prefix...
93
+ # you can parse, or just compute key from uri by stripping base_uri
94
+ if uri.startswith("s3://"):
95
+ _, rest = uri.split("://", 1)
96
+ bucket, key = rest.split("/", 1)
97
+ else:
98
+ bucket = self.bucket
99
+ key = uri
100
+
101
+ def _download():
102
+ obj = self._client.get_object(Bucket=bucket, Key=key)
103
+ return obj["Body"].read()
104
+
105
+ return await to_thread(_download)
106
+
107
+ async def load_text(
108
+ self,
109
+ uri: str,
110
+ *,
111
+ encoding: str = "utf-8",
112
+ errors: str = "strict",
113
+ ) -> str:
114
+ data = await self.load_bytes(uri)
115
+ return data.decode(encoding, errors)
@@ -0,0 +1,283 @@
1
+ from __future__ import annotations
2
+
3
+ import asyncio
4
+ from dataclasses import asdict
5
+ from datetime import datetime, timezone
6
+ import hashlib
7
+ import hmac
8
+ import json
9
+ import logging
10
+ import os
11
+ from pathlib import Path
12
+ import re
13
+ import threading
14
+ from typing import Any
15
+
16
+ from aethergraph.services.continuations.continuation import Continuation, Correlator
17
+
18
+
19
+ class FSContinuationStore: # implements AsyncContinuationStore
20
+ def __init__(self, root: str | Path, secret: bytes):
21
+ self.root = Path(root)
22
+ self.secret = secret
23
+ self._lock = threading.RLock()
24
+
25
+ # ---------- helpers ----------
26
+ def _cont_path(self, run_id: str, node_id: str) -> Path:
27
+ return self.root / "runs" / run_id / "nodes" / node_id / "continuation.json"
28
+
29
+ def _token_idx_path(self, token: str) -> Path:
30
+ return self.root / "index" / "tokens" / f"{token}.json"
31
+
32
+ def _rev_idx_path(self, token: str) -> Path:
33
+ return self.root / "index" / "rev" / f"{token}.json"
34
+
35
+ def _safe(self, s: str) -> str:
36
+ s = (s or "").replace("\\", "_").replace("/", "_").replace(":", "_")
37
+ s = re.sub(r"[^A-Za-z0-9._@-]", "_", s)
38
+ s = re.sub(r"_+", "_", s).strip("._ ") or "x"
39
+ if len(s) > 100:
40
+ h = hashlib.sha1(s.encode()).hexdigest()[:8]
41
+ s = f"{s[:92]}_{h}"
42
+ return s
43
+
44
+ def _corr_dir(self, scheme: str, channel: str, thread: str, message: str) -> Path:
45
+ return (
46
+ self.root
47
+ / "index"
48
+ / "corr"
49
+ / self._safe(scheme)
50
+ / self._safe(channel)
51
+ / self._safe(thread)
52
+ / self._safe(message)
53
+ )
54
+
55
+ def _corr_tokens_path(self, scheme: str, channel: str, thread: str, message: str) -> Path:
56
+ return self._corr_dir(scheme, channel, thread, message) / "tokens.json"
57
+
58
+ def _hmac(self, *parts: str) -> str:
59
+ h = hmac.new(self.secret, digestmod=hashlib.sha256)
60
+ for p in parts:
61
+ h.update(p.encode("utf-8"))
62
+ return h.hexdigest()
63
+
64
+ async def mint_token(self, run_id: str, node_id: str, attempts: int) -> str:
65
+ return self._hmac(run_id, node_id, str(attempts), os.urandom(8).hex())
66
+
67
+ # ---------- core ----------
68
+ async def save(self, cont: Continuation) -> None:
69
+ def _write():
70
+ path = self._cont_path(cont.run_id, cont.node_id)
71
+ path.parent.mkdir(parents=True, exist_ok=True)
72
+ payload = cont.to_dict() if hasattr(cont, "to_dict") else asdict(cont)
73
+ for k in ("deadline", "next_wakeup_at", "created_at"):
74
+ v = payload.get(k)
75
+ if isinstance(v, datetime):
76
+ payload[k] = v.astimezone(timezone.utc).isoformat()
77
+ tmp = path.with_suffix(".tmp")
78
+ tmp.write_text(json.dumps(payload, indent=2), encoding="utf-8")
79
+ tmp.replace(path)
80
+ self._write_token_index(cont.run_id, cont.node_id, cont.token)
81
+
82
+ await asyncio.to_thread(_write)
83
+
84
+ async def get(self, run_id: str, node_id: str) -> Continuation | None:
85
+ def _read():
86
+ path = self._cont_path(run_id, node_id)
87
+ if not path.exists():
88
+ return None
89
+ raw = json.loads(path.read_text(encoding="utf-8"))
90
+ for k in ("deadline", "next_wakeup_at", "created_at"):
91
+ if raw.get(k):
92
+ raw[k] = datetime.fromisoformat(raw[k])
93
+ raw["closed"] = bool(raw.get("closed", False))
94
+ return Continuation(**raw)
95
+
96
+ return await asyncio.to_thread(_read)
97
+
98
+ async def list_cont_by_run(self, run_id: str) -> list[Continuation]:
99
+ def _list():
100
+ out = []
101
+ run_path = self.root / "runs" / run_id / "nodes"
102
+ if not run_path.exists():
103
+ return out
104
+ for node_dir in run_path.iterdir():
105
+ cont_path = node_dir / "continuation.json"
106
+ if cont_path.exists():
107
+ raw = json.loads(cont_path.read_text(encoding="utf-8"))
108
+ for k in ("deadline", "next_wakeup_at", "created_at"):
109
+ if raw.get(k):
110
+ raw[k] = datetime.fromisoformat(raw[k])
111
+ raw["closed"] = bool(raw.get("closed", False))
112
+ out.append(Continuation(**raw))
113
+ return out
114
+
115
+ return await asyncio.to_thread(_list)
116
+
117
+ async def delete(self, run_id: str, node_id: str) -> None:
118
+ def _del():
119
+ p = self._cont_path(run_id, node_id)
120
+ if p.exists():
121
+ p.unlink()
122
+
123
+ await asyncio.to_thread(_del)
124
+
125
+ # ---------- token helpers ----------
126
+ def _write_token_index(self, run_id: str, node_id: str, token: str) -> None:
127
+ with self._lock:
128
+ p = self._token_idx_path(token)
129
+ p.parent.mkdir(parents=True, exist_ok=True)
130
+ p.write_text(
131
+ json.dumps({"run_id": run_id, "node_id": node_id}, indent=2), encoding="utf-8"
132
+ )
133
+
134
+ async def get_by_token(self, token: str) -> Continuation | None:
135
+ def _lookup():
136
+ p = self._token_idx_path(token)
137
+ if not p.exists():
138
+ return None
139
+ ref = json.loads(p.read_text(encoding="utf-8"))
140
+ return ref["run_id"], ref["node_id"]
141
+
142
+ ref = await asyncio.to_thread(_lookup)
143
+ if not ref:
144
+ return None
145
+ run_id, node_id = ref
146
+ return await self.get(run_id, node_id)
147
+
148
+ async def mark_closed(self, token: str) -> None:
149
+ c = await self.get_by_token(token)
150
+ if not c:
151
+ return
152
+ if not c.closed:
153
+ c.closed = True
154
+ await self.save(c)
155
+
156
+ async def verify_token(self, run_id: str, node_id: str, token: str) -> bool:
157
+ c = await self.get(run_id, node_id)
158
+ return bool(c and hmac.compare_digest(token, c.token))
159
+
160
+ # ---------- correlators ----------
161
+ async def bind_correlator(self, *, token: str, corr: Correlator) -> None:
162
+ def _bind():
163
+ scheme, channel, thread, message = corr.key()
164
+ tokens_path = self._corr_tokens_path(scheme, channel, thread, message)
165
+ tokens_path.parent.mkdir(parents=True, exist_ok=True)
166
+ toks: list[str] = []
167
+ if tokens_path.exists():
168
+ try:
169
+ toks = json.loads(tokens_path.read_text(encoding="utf-8"))
170
+ except Exception:
171
+ toks = []
172
+ if token not in toks:
173
+ toks.append(token)
174
+ tokens_path.write_text(json.dumps(toks, indent=2), encoding="utf-8")
175
+ # reverse index
176
+ r = self._rev_idx_path(token)
177
+ r.parent.mkdir(parents=True, exist_ok=True)
178
+ paths = []
179
+ if r.exists():
180
+ try:
181
+ paths = json.loads(r.read_text(encoding="utf-8"))
182
+ except Exception:
183
+ paths = []
184
+ key_path = str(tokens_path.relative_to(self.root))
185
+ if key_path not in paths:
186
+ paths.append(key_path)
187
+ r.write_text(json.dumps(paths, indent=2), encoding="utf-8")
188
+
189
+ await asyncio.to_thread(_bind)
190
+
191
+ async def find_by_correlator(self, *, corr: Correlator) -> Continuation | None:
192
+ def _read_toks():
193
+ scheme, channel, thread, message = corr.key()
194
+ p = self._corr_tokens_path(scheme, channel, thread, message)
195
+ if not p.exists():
196
+ return []
197
+ try:
198
+ return json.loads(p.read_text(encoding="utf-8")) or []
199
+ except Exception:
200
+ return []
201
+
202
+ toks = await asyncio.to_thread(_read_toks)
203
+ for tok in reversed(toks):
204
+ c = await self.get_by_token(tok)
205
+ if c and not c.closed:
206
+ if c.deadline and datetime.now(timezone.utc) > c.deadline.astimezone(timezone.utc):
207
+ continue
208
+ return c
209
+ return None
210
+
211
+ async def last_open(self, *, channel: str, kind: str) -> Continuation | None:
212
+ # Optional slow scan (dev only)
213
+ def _scan():
214
+ waits = []
215
+ runs_path = self.root / "runs"
216
+ if not runs_path.exists():
217
+ return waits
218
+ for run_dir in runs_path.iterdir():
219
+ nodes_dir = run_dir / "nodes"
220
+ if not nodes_dir.exists():
221
+ continue
222
+ for node_dir in nodes_dir.iterdir():
223
+ cont_path = node_dir / "continuation.json"
224
+ if cont_path.exists():
225
+ waits.append(json.loads(cont_path.read_text(encoding="utf-8")))
226
+ return waits
227
+
228
+ waits = await asyncio.to_thread(_scan)
229
+ for raw in reversed(waits):
230
+ if raw.get("closed"):
231
+ continue
232
+ if raw.get("channel") == channel and raw.get("kind") == kind:
233
+ for k in ("deadline", "next_wakeup_at", "created_at"):
234
+ if raw.get(k):
235
+ raw[k] = datetime.fromisoformat(raw[k])
236
+ return Continuation(**raw)
237
+ return None
238
+
239
+ async def list_waits(self) -> list[dict[str, Any]]:
240
+ def _scan():
241
+ out = []
242
+ runs_path = self.root / "runs"
243
+ if not runs_path.exists():
244
+ return out
245
+ for run_dir in runs_path.iterdir():
246
+ nodes_dir = run_dir / "nodes"
247
+ if not nodes_dir.exists():
248
+ continue
249
+ for node_dir in nodes_dir.iterdir():
250
+ cont_path = node_dir / "continuation.json"
251
+ if cont_path.exists():
252
+ out.append(json.loads(cont_path.read_text(encoding="utf-8")))
253
+ return out
254
+
255
+ return await asyncio.to_thread(_scan)
256
+
257
+ async def clear(self) -> None:
258
+ def _clear():
259
+ for sub in ("runs", "index"):
260
+ p = self.root / sub
261
+ if p.exists():
262
+ for root, dirs, files in os.walk(p, topdown=False):
263
+ for f in files:
264
+ try:
265
+ os.remove(Path(root) / f)
266
+ except Exception:
267
+ logger = logging.getLogger(
268
+ "aethergraph.services.continuations.stores.fs_store"
269
+ )
270
+ logger.warning("Failed to remove file: %s", Path(root) / f)
271
+ for d in dirs:
272
+ try:
273
+ os.rmdir(Path(root) / d)
274
+ except Exception:
275
+ logger = logging.getLogger(
276
+ "aethergraph.services.continuations.stores.fs_store"
277
+ )
278
+ logger.warning("Failed to remove dir: %s", Path(root) / d)
279
+
280
+ await asyncio.to_thread(_clear)
281
+
282
+ async def alias_for(self, token: str) -> str | None:
283
+ return token[:24]
@@ -0,0 +1,146 @@
1
+ from dataclasses import asdict
2
+ from datetime import datetime, timezone
3
+ import hashlib
4
+ import hmac
5
+ import os
6
+ import threading
7
+ from typing import Any
8
+
9
+ from aethergraph.contracts.services.continuations import AsyncContinuationStore
10
+ from aethergraph.services.continuations.continuation import Continuation, Correlator
11
+
12
+
13
+ class InMemoryContinuationStore(AsyncContinuationStore):
14
+ """
15
+ Process-local, in-memory continuation store.
16
+
17
+ - Thread-safe via RLock (main loop + sidecar can share it).
18
+ - No persistence beyond process lifetime.
19
+
20
+ NOTE: suitable for testing or single-process deployments only.
21
+ """
22
+
23
+ def __init__(self, *, secret: bytes):
24
+ self._secret = secret
25
+ self._by_id: dict[tuple[str, str], Continuation] = {}
26
+ self._by_token: dict[str, tuple[str, str]] = {}
27
+ self._corr_tokens: dict[str, list[str]] = {}
28
+ self._lock = threading.RLock()
29
+
30
+ # --- helpers ---
31
+
32
+ def _key(self, run_id: str, node_id: str) -> tuple[str, str]:
33
+ return (run_id, node_id)
34
+
35
+ def _corr_key(self, corr: Correlator) -> str:
36
+ scheme, channel, thread, message = corr.key()
37
+ return f"{scheme}:{channel}:{thread}:{message}"
38
+
39
+ def _hmac(self, *parts: str) -> str:
40
+ h = hmac.new(self._secret, digestmod=hashlib.sha256)
41
+ for p in parts:
42
+ h.update(p.encode("utf-8"))
43
+ return h.hexdigest()
44
+
45
+ async def mint_token(self, run_id: str, node_id: str, attempts: int) -> str:
46
+ return self._hmac(run_id, node_id, str(attempts), os.urandom(8).hex())
47
+
48
+ # --- core ---
49
+
50
+ async def save(self, cont: Continuation) -> None:
51
+ with self._lock:
52
+ self._by_id[self._key(cont.run_id, cont.node_id)] = cont
53
+ self._by_token[cont.token] = (cont.run_id, cont.node_id)
54
+
55
+ async def get(self, run_id: str, node_id: str) -> Continuation | None:
56
+ with self._lock:
57
+ return self._by_id.get(self._key(run_id, node_id))
58
+
59
+ async def delete(self, run_id: str, node_id: str) -> None:
60
+ with self._lock:
61
+ key = self._key(run_id, node_id)
62
+ cont = self._by_id.pop(key, None)
63
+ if cont:
64
+ self._by_token.pop(cont.token, None)
65
+
66
+ async def list_cont_by_run(self, run_id: str) -> list[Continuation]:
67
+ with self._lock:
68
+ return [c for (r, _), c in self._by_id.items() if r == run_id]
69
+
70
+ # --- token ---
71
+
72
+ async def get_by_token(self, token: str) -> Continuation | None:
73
+ with self._lock:
74
+ ref = self._by_token.get(token)
75
+ if not ref:
76
+ return None
77
+ run_id, node_id = ref
78
+ return self._by_id.get(self._key(run_id, node_id))
79
+
80
+ async def mark_closed(self, token: str) -> None:
81
+ c = await self.get_by_token(token)
82
+ if not c:
83
+ return
84
+ if not c.closed:
85
+ c.closed = True
86
+ await self.save(c)
87
+
88
+ async def verify_token(self, run_id: str, node_id: str, token: str) -> bool:
89
+ c = await self.get(run_id, node_id)
90
+ return bool(c and hmac.compare_digest(token, c.token))
91
+
92
+ # --- correlators ---
93
+
94
+ async def bind_correlator(self, *, token: str, corr: Correlator) -> None:
95
+ key = self._corr_key(corr)
96
+ with self._lock:
97
+ toks = self._corr_tokens.setdefault(key, [])
98
+ if token not in toks:
99
+ toks.append(token)
100
+
101
+ async def find_by_correlator(self, *, corr: Correlator) -> Continuation | None:
102
+ key = self._corr_key(corr)
103
+ with self._lock:
104
+ toks = list(self._corr_tokens.get(key, []))
105
+
106
+ now = datetime.now(timezone.utc)
107
+ for tok in reversed(toks):
108
+ c = await self.get_by_token(tok)
109
+ if not c or c.closed:
110
+ continue
111
+ if c.deadline and now > c.deadline.astimezone(timezone.utc):
112
+ continue
113
+ return c
114
+ return None
115
+
116
+ # --- scans ---
117
+
118
+ async def last_open(self, *, channel: str, kind: str) -> Continuation | None:
119
+ with self._lock:
120
+ candidates = [
121
+ c
122
+ for c in self._by_id.values()
123
+ if not c.closed and c.channel == channel and c.kind == kind
124
+ ]
125
+ if not candidates:
126
+ return None
127
+
128
+ # pick most recent created_at
129
+ def _ts(c: Continuation) -> float:
130
+ dt = c.created_at or datetime.min.replace(tzinfo=timezone.utc)
131
+ return dt.timestamp()
132
+
133
+ return max(candidates, key=_ts)
134
+
135
+ async def list_waits(self) -> list[dict[str, Any]]:
136
+ with self._lock:
137
+ return [asdict(c) for c in self._by_id.values()]
138
+
139
+ async def clear(self) -> None:
140
+ with self._lock:
141
+ self._by_id.clear()
142
+ self._by_token.clear()
143
+ self._corr_tokens.clear()
144
+
145
+ async def alias_for(self, token: str) -> str | None:
146
+ return token[:24]