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,159 @@
1
+ from __future__ import annotations
2
+
3
+ from dataclasses import dataclass, field, replace
4
+ from typing import Any
5
+
6
+
7
+ @dataclass(frozen=True)
8
+ class Scope:
9
+ # Tenant / actor
10
+ org_id: str | None = None
11
+ user_id: str | None = None
12
+ client_id: str | None = None
13
+ mode: str | None = None # "cloud", "demo", "local", etc.
14
+
15
+ # App / execution context
16
+ app_id: str | None = None
17
+ session_id: str | None = None
18
+ run_id: str | None = None
19
+ graph_id: str | None = None
20
+ node_id: str | None = None
21
+ flow_id: str | None = None # optional flow ID within a graph -- not implemented yet
22
+
23
+ # Tooling / proveance (optional)
24
+ tool_name: str | None = None
25
+ tool_version: str | None = None
26
+
27
+ # Extra tags
28
+ labels: dict[str, Any] = field(default_factory=dict)
29
+
30
+ # Internal override for memory scope ID
31
+ _memory_scope_id: str | None = None
32
+
33
+ def __item__(self, key: str) -> Any:
34
+ return getattr(self, key)
35
+
36
+ def get(self, key: str, default: Any = None) -> Any:
37
+ return getattr(self, key, default)
38
+
39
+ def artifact_scope_labels(self) -> dict[str, str]:
40
+ """
41
+ Labels to attach to every artifact for this scope.
42
+ These will be mirrored both into Artifact.labels and the index.
43
+ """
44
+ out: dict[str, str] = {}
45
+ if self.org_id:
46
+ out["org_id"] = self.org_id
47
+ if self.user_id:
48
+ out["user_id"] = self.user_id
49
+ if self.client_id:
50
+ out["client_id"] = self.client_id
51
+ if self.app_id:
52
+ out["app_id"] = self.app_id
53
+ if self.session_id:
54
+ out["session_id"] = self.session_id
55
+ if self.run_id:
56
+ out["run_id"] = self.run_id
57
+ if self.graph_id:
58
+ out["graph_id"] = self.graph_id
59
+ if self.node_id:
60
+ out["node_id"] = self.node_id
61
+
62
+ # canonicial scope ids
63
+ if self.session_id:
64
+ out["scope_id"] = self.session_id # session-centric
65
+ elif self.run_id:
66
+ out["scope_id"] = self.run_id # run-centric for non-session runs
67
+ elif self.graph_id:
68
+ out["scope_id"] = f"graph:{self.graph_id}" # graph-centric for non-run artifacts
69
+ elif self.node_id:
70
+ out["scope_id"] = f"node:{self.node_id}" # node-centric for non-graph artifacts
71
+ return out
72
+
73
+ def metering_dimensions(self) -> dict[str, Any]:
74
+ """Dimensions for MeteringService: what to attach to events."""
75
+ out: dict[str, Any] = {}
76
+ if self.user_id:
77
+ out["user_id"] = self.user_id
78
+ if self.org_id:
79
+ out["org_id"] = self.org_id
80
+ if self.client_id:
81
+ out["client_id"] = self.client_id
82
+ if self.app_id:
83
+ out["app_id"] = self.app_id
84
+ if self.session_id:
85
+ out["session_id"] = self.session_id
86
+ if self.run_id:
87
+ out["run_id"] = self.run_id
88
+ if self.graph_id:
89
+ out["graph_id"] = self.graph_id
90
+ if self.node_id:
91
+ out["node_id"] = self.node_id
92
+ if self.flow_id:
93
+ out["flow_id"] = self.flow_id
94
+ return out
95
+
96
+ def with_memory_scope(self, mem_scope_id: str) -> Scope:
97
+ """Return a copy with explicit memory scope override"""
98
+ return replace(self, _memory_scope_id=mem_scope_id)
99
+
100
+ def memory_scope_id(self) -> str:
101
+ """
102
+ Stable key for “memory bucket”.
103
+ Default precedence: explicit override > session > user > run > org > app.
104
+ """
105
+ if self._memory_scope_id:
106
+ return self._memory_scope_id
107
+ if self.session_id:
108
+ return f"session:{self.session_id}"
109
+ if self.user_id:
110
+ return f"user:{self.user_id}"
111
+ if self.run_id:
112
+ return f"run:{self.run_id}"
113
+ if self.org_id:
114
+ return f"org:{self.org_id}"
115
+ if self.app_id:
116
+ return f"app:{self.app_id}"
117
+ return "global"
118
+
119
+ def rag_labels(self, *, scope_id: str | None = None) -> dict[str, Any]:
120
+ """
121
+ Labels that should be stamped on RAG docs/chunks.
122
+ scope_id is usually memory_scope_id (for memory-tied corpora),
123
+ but can be any logical scope key.
124
+ """
125
+ out: dict[str, Any] = {}
126
+ if self.user_id:
127
+ out["user_id"] = self.user_id
128
+ if self.org_id:
129
+ out["org_id"] = self.org_id
130
+ if self.client_id:
131
+ out["client_id"] = self.client_id
132
+ if self.app_id:
133
+ out["app_id"] = self.app_id
134
+ if self.session_id:
135
+ out["session_id"] = self.session_id
136
+ if self.run_id:
137
+ out["run_id"] = self.run_id
138
+ if self.graph_id:
139
+ out["graph_id"] = self.graph_id
140
+ if self.node_id:
141
+ out["node_id"] = self.node_id
142
+ if scope_id:
143
+ out["scope_id"] = scope_id
144
+ return out
145
+
146
+ def rag_filter(self, *, scope_id: str | None = None) -> dict[str, Any]:
147
+ """
148
+ Default filter for RAG search based on identity.
149
+ You can adjust strictness (e.g., ignore run_id for per-user corpora).
150
+ """
151
+ out: dict[str, Any] = {}
152
+ if self.user_id:
153
+ out["user_id"] = self.user_id
154
+ if self.org_id:
155
+ out["org_id"] = self.org_id
156
+ if scope_id:
157
+ out["scope_id"] = scope_id
158
+ # you can choose to include session_id / run_id only for very strict isolation
159
+ return out
@@ -0,0 +1,164 @@
1
+ from dataclasses import dataclass
2
+ from typing import Literal
3
+
4
+ from aethergraph.api.v1.deps import RequestIdentity
5
+ from aethergraph.services.scope.scope import Scope
6
+
7
+
8
+ @dataclass(frozen=True)
9
+ class ScopeFactory:
10
+ default_app_id: str | None = None
11
+
12
+ def base_from_identity(self, identity: RequestIdentity | None) -> Scope:
13
+ """
14
+ Create a base Scope from a RequestIdentity.
15
+ """
16
+ if identity is None:
17
+ return Scope(mode="local")
18
+
19
+ return Scope(
20
+ org_id=identity.org_id,
21
+ user_id=identity.user_id,
22
+ client_id=identity.client_id,
23
+ mode=identity.mode,
24
+ app_id=self.default_app_id,
25
+ )
26
+
27
+ def for_node(
28
+ self,
29
+ *,
30
+ identity: RequestIdentity | None = None,
31
+ run_id: str | None = None,
32
+ graph_id: str | None = None,
33
+ node_id: str | None = None,
34
+ session_id: str | None = None,
35
+ app_id: str | None = None,
36
+ tool_name: str | None = None,
37
+ tool_version: str | None = None,
38
+ ) -> Scope:
39
+ """
40
+ Create a Scope for a specific node execution.
41
+ """
42
+ base = self.base_from_identity(identity)
43
+ return Scope(
44
+ org_id=base.org_id,
45
+ user_id=base.user_id,
46
+ client_id=base.client_id,
47
+ mode=base.mode,
48
+ app_id=app_id or base.app_id,
49
+ session_id=session_id,
50
+ run_id=run_id,
51
+ graph_id=graph_id,
52
+ node_id=node_id,
53
+ tool_name=tool_name,
54
+ tool_version=tool_version,
55
+ )
56
+
57
+ def for_run(
58
+ self,
59
+ *,
60
+ identity: RequestIdentity | None = None,
61
+ run_id: str,
62
+ graph_id: str | None = None,
63
+ session_id: str | None = None,
64
+ flow_id: str | None = None,
65
+ ) -> Scope:
66
+ s = self.base_from_identity(identity)
67
+ s.run_id = run_id
68
+ s.graph_id = graph_id
69
+ s.session_id = session_id
70
+ s.flow_id = flow_id
71
+ return s
72
+
73
+ def for_memory_custom_override(
74
+ self,
75
+ *,
76
+ identity: RequestIdentity | None = None,
77
+ run_id: str,
78
+ graph_id: str | None = None,
79
+ node_id: str | None = None,
80
+ session_id: str | None = None,
81
+ level: Literal["session", "user", "run", "org"] = "session",
82
+ custom_scope_id: str | None = None,
83
+ ):
84
+ """
85
+ Scope for MemoryFacade. level defines how we group memory:
86
+ - "session": per-session (default)
87
+ - "user": across runs/sessions for a given user
88
+ - "run": per-run
89
+ - "org": org-level memory
90
+ """
91
+ s = self.for_node(
92
+ identity=identity,
93
+ run_id=run_id,
94
+ graph_id=graph_id,
95
+ node_id=node_id,
96
+ session_id=session_id,
97
+ )
98
+ if custom_scope_id is not None:
99
+ mem_id = custom_scope_id
100
+ else:
101
+ if level == "session":
102
+ base = session_id or run_id
103
+ mem_id = f"session:{base}"
104
+ elif level == "user":
105
+ u = s.user_id or s.client_id or "anon"
106
+ mem_id = f"user:{u}"
107
+ elif level == "run":
108
+ mem_id = f"run:{run_id}"
109
+ elif level == "org":
110
+ o = s.org_id or "orgless"
111
+ mem_id = f"org:{o}"
112
+ else: # pragma: no cover
113
+ mem_id = f"run:{run_id}"
114
+
115
+ return s.with_memory_scope(mem_id)
116
+
117
+ def for_memory(
118
+ self,
119
+ *,
120
+ identity: RequestIdentity | None = None,
121
+ run_id: str,
122
+ graph_id: str | None = None,
123
+ node_id: str | None = None,
124
+ session_id: str | None = None,
125
+ level: Literal["session", "user", "run", "org"] = "session",
126
+ custom_scope_id: str | None = None,
127
+ ):
128
+ """
129
+ Rule of thumb for memory scope IDs:
130
+ - session-level: "session:{session_id}" or "session:{run_id}" if no session
131
+ - user-level: "user:{user_id}" or "user:{client_id}"
132
+ - run-level: "run:{run_id}"
133
+ - org-level: "org:{org_id}"
134
+ 1) Compute the base ID from the level
135
+ 2) If a custom scope is provided, treat it as a suffix under the root
136
+ e.g. "session:{base}:{custom_scope_id}"
137
+ """
138
+ s = self.for_node(
139
+ identity=identity,
140
+ run_id=run_id,
141
+ graph_id=graph_id,
142
+ node_id=node_id,
143
+ session_id=session_id,
144
+ )
145
+
146
+ # 1) Compute the base ID from the level
147
+ if level == "session":
148
+ base = session_id or run_id
149
+ root = f"session:{base}"
150
+ elif level == "user":
151
+ u = s.user_id or s.client_id or "anon"
152
+ root = f"user:{u}"
153
+ elif level == "run":
154
+ root = f"run:{run_id}"
155
+ elif level == "org":
156
+ o = s.org_id or "orgless"
157
+ root = f"org:{o}"
158
+ else: # pragma: no cover
159
+ root = f"run:{run_id}"
160
+
161
+ # 2) If a custom scope is provided, treat it as a suffix under the root
162
+ mem_id = f"{root}:{custom_scope_id}" if custom_scope_id else root
163
+
164
+ return s.with_memory_scope(mem_id)
@@ -207,6 +207,8 @@ async def state_to_json_safe(
207
207
  for nid, ns in state_obj.nodes.items():
208
208
  status = getattr(ns, "status", None)
209
209
  status_name = getattr(status, "name", status) # Enum.name or string
210
+ started_at = getattr(ns, "started_at", None)
211
+ finished_at = getattr(ns, "finished_at", None)
210
212
  tool_name = getattr(ns, "tool_name", None) or getattr(
211
213
  getattr(ns, "spec", None), "tool_name", None
212
214
  )
@@ -232,7 +234,10 @@ async def state_to_json_safe(
232
234
  "attempts": getattr(ns, "attempts", 0),
233
235
  "next_wakeup_at": getattr(ns, "next_wakeup_at", None),
234
236
  "wait_token": getattr(ns, "wait_token", None),
237
+ "started_at": started_at,
238
+ "finished_at": finished_at,
235
239
  }
240
+
236
241
  if include_wait_spec:
237
242
  ws = getattr(ns, "wait_spec", None)
238
243
  if ws:
@@ -29,7 +29,7 @@ async def snapshot_from_graph(
29
29
  allow_externalize=allow_externalize,
30
30
  include_wait_spec=include_wait_spec,
31
31
  )
32
- return GraphSnapshot(
32
+ snap = GraphSnapshot(
33
33
  run_id=run_id,
34
34
  graph_id=graph_id,
35
35
  rev=rev,
@@ -37,6 +37,7 @@ async def snapshot_from_graph(
37
37
  spec_hash=spec_hash,
38
38
  state=json_state,
39
39
  )
40
+ return snap
40
41
 
41
42
 
42
43
  def _status_to_str(s) -> str:
File without changes