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,29 @@
1
+ from fastapi import HTTPException
2
+
3
+
4
+ def decode_cursor(cursor: str | None) -> int:
5
+ """
6
+ Turn an opaque cursor string into an integer offset.
7
+
8
+ For now, cursor is just the stringified offest. Later we will
9
+ switch to base64 JSON or keyset pagination without changing
10
+ the endpoints
11
+ """
12
+ if not cursor:
13
+ return 0
14
+
15
+ try:
16
+ return int(cursor)
17
+ except ValueError as e:
18
+ raise HTTPException(status_code=400, detail="Invalid cursor") from e
19
+
20
+
21
+ def encode_cursor(offset: int) -> str:
22
+ """
23
+ Turn an integer offset into an opaque cursor string.
24
+
25
+ For now, cursor is just the stringified offest. Later we will
26
+ switch to base64 JSON or keyset pagination without changing
27
+ the endpoints
28
+ """
29
+ return str(offset)
@@ -0,0 +1,568 @@
1
+ # /runs
2
+
3
+ from datetime import datetime, timezone
4
+ from typing import Any
5
+
6
+ from fastapi import APIRouter, Depends, HTTPException, Query, Request
7
+
8
+ from aethergraph.api.v1.pagination import decode_cursor, encode_cursor
9
+ from aethergraph.core.runtime.run_manager import RunManager
10
+ from aethergraph.core.runtime.run_types import RunImportance, RunOrigin, RunVisibility
11
+ from aethergraph.core.runtime.runtime_registry import current_registry
12
+ from aethergraph.core.runtime.runtime_services import current_services
13
+
14
+ from .deps import RequestIdentity, enforce_run_rate_limits, get_identity, require_runs_execute
15
+ from .schemas import (
16
+ NodeSnapshot,
17
+ RunChannelEvent,
18
+ RunCreateRequest,
19
+ RunCreateResponse,
20
+ RunListResponse,
21
+ RunSnapshot,
22
+ RunStatus,
23
+ RunSummary,
24
+ )
25
+
26
+ router = APIRouter(tags=["runs"])
27
+
28
+
29
+ @router.post(
30
+ "/graphs/{graph_id}/runs",
31
+ response_model=RunCreateResponse,
32
+ dependencies=[Depends(enforce_run_rate_limits)], # noqa: B008
33
+ )
34
+ async def create_run(
35
+ graph_id: str,
36
+ body: RunCreateRequest,
37
+ identity: RequestIdentity = Depends(require_runs_execute), # noqa: B008
38
+ ) -> RunCreateResponse:
39
+ container = current_services()
40
+ rm: RunManager = getattr(container, "run_manager", None)
41
+ if rm is None:
42
+ raise HTTPException(status_code=503, detail="Run manager not configured")
43
+
44
+ app_vis = None
45
+ app_imp = None
46
+ reg = getattr(container, "registry", None) or current_registry()
47
+ if body.app_id and reg is not None:
48
+ app_meta = reg.get_meta(nspace="app", name=body.app_id)
49
+ if app_meta:
50
+ app_vis = app_meta.get("run_visibility")
51
+ app_imp = app_meta.get("run_importance")
52
+ app_vis = RunVisibility(app_vis) if app_vis else None
53
+ app_imp = RunImportance(app_imp) if app_imp else None
54
+
55
+ record = await rm.submit_run(
56
+ graph_id=graph_id,
57
+ inputs=body.inputs or {},
58
+ run_id=body.run_id,
59
+ tags=body.tags,
60
+ identity=identity,
61
+ origin=body.origin or RunOrigin.app,
62
+ visibility=body.visibility or app_vis or RunVisibility.normal,
63
+ importance=body.importance or app_imp or RunImportance.normal,
64
+ agent_id=body.agent_id or None,
65
+ app_id=body.app_id or None,
66
+ )
67
+
68
+ return RunCreateResponse(
69
+ run_id=record.run_id,
70
+ graph_id=record.graph_id,
71
+ status=record.status, # typically "running"
72
+ outputs=None,
73
+ has_waits=False, # for now, we don't expose waits on submit
74
+ continuations=[],
75
+ started_at=record.started_at,
76
+ finished_at=record.finished_at,
77
+ )
78
+
79
+
80
+ def _extract_app_id_from_tags(tags: list[str]) -> str | None:
81
+ # This is a convention: look for first tag that is not a client/flow tag
82
+ # and return it as app_id
83
+ # NOTE: this is not robust; in real usage, app_id should be stored in RunRecord.meta
84
+ # Only for demo purposes
85
+ for t in tags:
86
+ # skip client / flow tags
87
+ if t.startswith("client:") or t.startswith("flow:"):
88
+ continue
89
+ return t
90
+ return None
91
+
92
+
93
+ @router.get("/runs", response_model=RunListResponse)
94
+ async def list_runs(
95
+ graph_id: str | None = Query(None), # noqa: B008
96
+ status: RunStatus | None = Query(None), # noqa: B008
97
+ flow_id: str | None = Query(None), # noqa: B008
98
+ cursor: str | None = Query(None), # noqa: B008
99
+ limit: int = Query(20, ge=1, le=100), # noqa: B008
100
+ identity: RequestIdentity = Depends(get_identity), # noqa: B008
101
+ ) -> RunListResponse:
102
+ """
103
+ List recent runs, optionally filterable by graph_id, status, flow_id.
104
+
105
+ Tenant scoping:
106
+ - cloud/demo: filtered by identity.user_id/org_id at the RunStore level.
107
+ - local: currently returns all runs.
108
+ """
109
+ container = current_services()
110
+ rm = getattr(container, "run_manager", None)
111
+ if rm is None:
112
+ raise HTTPException(status_code=503, detail="Run manager not configured")
113
+
114
+ offset = decode_cursor(cursor)
115
+
116
+ # Enforce identity for cloud/demo (guest demo etc.)
117
+ if identity.mode in ("cloud", "demo") and identity.user_id is None:
118
+ raise HTTPException(status_code=403, detail="User identity required")
119
+
120
+ records = await rm.list_records(
121
+ graph_id=graph_id,
122
+ status=status,
123
+ flow_id=flow_id,
124
+ user_id=identity.user_id if identity.mode in ("cloud", "demo") else None,
125
+ org_id=identity.org_id if identity.mode in ("cloud", "demo") else None,
126
+ limit=limit,
127
+ offset=offset,
128
+ )
129
+
130
+ # Still apply UI visibility policy in Python (this is cheap)
131
+ records = [
132
+ rec
133
+ for rec in records
134
+ if rec.visibility == RunVisibility.normal and rec.importance == RunImportance.normal
135
+ ]
136
+
137
+ reg = getattr(container, "registry", None) or current_registry()
138
+ summaries: list[RunSummary] = []
139
+
140
+ for rec in records:
141
+ # Graph metadata logic as before
142
+ flow_meta_id: str | None = None
143
+ entrypoint = False
144
+ if reg is not None:
145
+ if rec.kind == "taskgraph":
146
+ meta = reg.get_meta(nspace="graph", name=rec.graph_id, version=None) or {}
147
+ elif rec.kind == "graphfn":
148
+ meta = reg.get_meta(nspace="graphfn", name=rec.graph_id, version=None) or {}
149
+ else:
150
+ meta = {}
151
+ flow_meta_id = meta.get("flow_id")
152
+ entrypoint = bool(meta.get("entrypoint", False))
153
+
154
+ effective_flow_id = rec.meta.get("flow_id") or flow_meta_id
155
+
156
+ app_id = rec.app_id
157
+ app_name = rec.meta.get("app_name")
158
+
159
+ summaries.append(
160
+ RunSummary(
161
+ run_id=rec.run_id,
162
+ graph_id=rec.graph_id,
163
+ status=rec.status,
164
+ started_at=rec.started_at,
165
+ finished_at=rec.finished_at,
166
+ tags=rec.tags,
167
+ user_id=rec.user_id,
168
+ org_id=rec.org_id,
169
+ session_id=rec.session_id or None,
170
+ graph_kind=rec.kind,
171
+ flow_id=effective_flow_id,
172
+ entrypoint=entrypoint,
173
+ meta=rec.meta or {},
174
+ app_id=app_id,
175
+ app_name=app_name,
176
+ agent_id=rec.meta.get("agent_id") or None,
177
+ origin=rec.origin,
178
+ visibility=rec.visibility,
179
+ importance=rec.importance,
180
+ artifact_count=rec.get("artifact_count"),
181
+ last_artifact_at=rec.get("last_artifact_at"),
182
+ )
183
+ )
184
+
185
+ next_cursor = encode_cursor(offset + limit) if len(records) == limit else None
186
+ return RunListResponse(runs=summaries, next_cursor=next_cursor)
187
+
188
+
189
+ @router.get("/runs/{run_id}", response_model=RunSummary)
190
+ async def get_run(
191
+ run_id: str,
192
+ identity: RequestIdentity = Depends(get_identity), # noqa: B008
193
+ ) -> RunSummary:
194
+ """
195
+ Get high-level summary for a run from RunStore.
196
+
197
+ NOTE: `client_id` is a demo-only soft guard. If provided, we'll 404
198
+ runs that are not tagged with `client:<client_id>`.
199
+ """
200
+ container = current_services()
201
+ rm = getattr(container, "run_manager", None)
202
+ if rm is None:
203
+ raise HTTPException(status_code=503, detail="Run manager not configured")
204
+
205
+ rec = await rm.get_record(run_id)
206
+ if rec is None:
207
+ raise HTTPException(status_code=404, detail="Run not found")
208
+
209
+ if identity.mode in ("cloud", "demo"):
210
+ user, _ = identity.user_id, identity.org_id
211
+ if user is not None:
212
+ if rec.user_id != user:
213
+ raise HTTPException(status_code=404, detail="Run not found")
214
+ else:
215
+ raise HTTPException(status_code=403, detail="User identity required")
216
+
217
+ reg = getattr(container, "registry", None) or current_registry()
218
+ flow_id: str | None = None
219
+ entrypoint = False
220
+
221
+ if reg is not None:
222
+ if rec.kind == "taskgraph":
223
+ meta = reg.get_meta(nspace="graph", name=rec.graph_id, version=None) or {}
224
+ elif rec.kind == "graphfn":
225
+ meta = reg.get_meta(nspace="graphfn", name=rec.graph_id, version=None) or {}
226
+ else:
227
+ meta = {}
228
+
229
+ flow_id = meta.get("flow_id")
230
+ entrypoint = bool(meta.get("entrypoint", False))
231
+
232
+ app_id = rec.app_id or rec.meta.get("app_id") or _extract_app_id_from_tags(rec.tags)
233
+ app_name = rec.meta.get("app_name")
234
+ agent_id = rec.agent_id or rec.meta.get("agent_id")
235
+
236
+ return RunSummary(
237
+ run_id=rec.run_id,
238
+ graph_id=rec.graph_id,
239
+ status=rec.status,
240
+ started_at=rec.started_at,
241
+ finished_at=rec.finished_at,
242
+ tags=rec.tags,
243
+ user_id=rec.user_id,
244
+ org_id=rec.org_id,
245
+ graph_kind=rec.kind,
246
+ flow_id=flow_id,
247
+ entrypoint=entrypoint,
248
+ meta=rec.meta or {},
249
+ app_id=app_id,
250
+ app_name=app_name,
251
+ agent_id=agent_id,
252
+ session_id=rec.session_id or None,
253
+ origin=rec.origin,
254
+ visibility=rec.visibility,
255
+ importance=rec.importance,
256
+ artifact_count=rec.get("artifact_count"),
257
+ last_artifact_at=rec.get("last_artifact_at"),
258
+ )
259
+
260
+
261
+ @router.post("/runs/{run_id}/cancel")
262
+ async def cancel_run(
263
+ run_id: str,
264
+ identity: RequestIdentity = Depends(get_identity), # noqa: B008
265
+ ) -> dict:
266
+ """
267
+ Request run cancellation.
268
+
269
+ TODO:
270
+ - Call runtime/cancellation mechanism.
271
+ """
272
+ container = current_services()
273
+ rm = getattr(container, "run_manager", None)
274
+ if rm is None:
275
+ raise HTTPException(status_code=503, detail="Run manager not configured")
276
+ await rm.cancel_run(run_id)
277
+ return {"run_id": run_id, "status": "cancellation_requested"}
278
+
279
+
280
+ def _coerce_ts_to_dt(value: Any) -> datetime | None:
281
+ """
282
+ Accepts:
283
+ - None
284
+ - datetime
285
+ - float / int epoch seconds
286
+ - ISO8601 string
287
+ Returns timezone-aware UTC datetime or None.
288
+ """
289
+ if value is None:
290
+ return None
291
+ if isinstance(value, datetime):
292
+ # Ensure it's tz-aware; default to UTC if naive.
293
+ if value.tzinfo is None:
294
+ return value.replace(tzinfo=timezone.utc)
295
+ return value
296
+
297
+ # Epoch seconds (int/float)
298
+ if isinstance(value, int | float):
299
+ try:
300
+ return datetime.fromtimestamp(float(value), tz=timezone.utc)
301
+ except Exception:
302
+ return None
303
+
304
+ # ISO string
305
+ if isinstance(value, str):
306
+ try:
307
+ dt = datetime.fromisoformat(value)
308
+ if dt.tzinfo is None:
309
+ dt = dt.replace(tzinfo=timezone.utc)
310
+ return dt
311
+ except Exception:
312
+ return None
313
+
314
+ return None
315
+
316
+
317
+ def _coerce_node_status(value: Any, fallback: RunStatus) -> RunStatus:
318
+ """
319
+ Try to convert arbitrary value to RunStatus, else use fallback.
320
+ """
321
+ if isinstance(value, RunStatus):
322
+ return value
323
+ if isinstance(value, str):
324
+ try:
325
+ if value == "DONE":
326
+ return RunStatus.succeeded
327
+ if value == "FAILED":
328
+ return RunStatus.failed
329
+ if value == "CANCELLED":
330
+ return RunStatus.canceled
331
+ if value == "PENDING":
332
+ return RunStatus.pending
333
+ return RunStatus(value)
334
+ except ValueError:
335
+ # maybe uppercased, etc.
336
+ try:
337
+ return RunStatus(value.lower())
338
+ except Exception:
339
+ pass
340
+ return fallback
341
+
342
+
343
+ @router.get("/runs/{run_id}/snapshot", response_model=RunSnapshot)
344
+ async def get_run_snapshot(
345
+ run_id: str,
346
+ identity: RequestIdentity = Depends(get_identity), # noqa: B008
347
+ ) -> RunSnapshot:
348
+ """
349
+ Run snapshot for a single graph within this run.
350
+
351
+ - Uses RunRecord for run-level status.
352
+ - Uses registry metadata for graph_kind, flow_id, entrypoint.
353
+ - Uses state_store (if present) for node-level state.
354
+ - Falls back to TaskGraphSpec or a single pseudo-node.
355
+ """
356
+ container = current_services()
357
+
358
+ rm = getattr(container, "run_manager", None)
359
+ if rm is None:
360
+ raise HTTPException(status_code=503, detail="Run manager not configured")
361
+
362
+ state_store = getattr(container, "state_store", None)
363
+
364
+ rec = await rm.get_record(run_id)
365
+ if rec is None:
366
+ raise HTTPException(status_code=404, detail="Run not found")
367
+
368
+ graph_id = rec.graph_id
369
+ graph_kind = rec.kind
370
+
371
+ # --- Graph metadata from registry ---
372
+ reg = getattr(container, "registry", None) or current_registry()
373
+
374
+ flow_id: str | None = None
375
+ entrypoint = False
376
+ meta = {}
377
+
378
+ if reg is not None:
379
+ if graph_kind == "taskgraph":
380
+ meta = reg.get_meta(nspace="graph", name=graph_id, version=None) or {}
381
+ elif graph_kind == "graphfn":
382
+ meta = reg.get_meta(nspace="graphfn", name=graph_id, version=None) or {}
383
+
384
+ flow_id = meta.get("flow_id")
385
+ entrypoint = bool(meta.get("entrypoint", False))
386
+
387
+ # --- Load static TaskGraph spec if it exists ---
388
+ spec = None
389
+ if reg is not None:
390
+ try:
391
+ graph_obj = reg.get_graph(name=graph_id, version=None)
392
+ spec = getattr(graph_obj, "spec", None)
393
+ except KeyError:
394
+ spec = None
395
+
396
+ # --- Load latest GraphSnapshot (if we have a state store) ---
397
+ snap = None
398
+ if state_store is not None:
399
+ snap = await state_store.load_latest_snapshot(run_id)
400
+
401
+ nodes_state: dict[str, dict[str, Any]] = {}
402
+ snapshot_edges: list[dict[str, str]] = []
403
+
404
+ if snap is not None and isinstance(snap.state, dict):
405
+ raw_nodes = snap.state.get("nodes") or snap.state.get("node_state") or {}
406
+ if isinstance(raw_nodes, dict):
407
+ nodes_state = {str(k): (v or {}) for k, v in raw_nodes.items()}
408
+
409
+ raw_edges = snap.state.get("edges") or []
410
+ if isinstance(raw_edges, list):
411
+ snapshot_edges = [
412
+ {"source": e.get("from"), "target": e.get("to")}
413
+ for e in raw_edges
414
+ if isinstance(e, dict) and "from" in e and "to" in e
415
+ ]
416
+
417
+ # --- Build edges ---
418
+ edges: list[dict[str, str]] = []
419
+
420
+ if snapshot_edges:
421
+ edges = snapshot_edges
422
+ elif spec is not None and getattr(spec, "nodes", None):
423
+ edge_set: set[tuple[str, str]] = set()
424
+ for node_id, node_spec in spec.nodes.items():
425
+ for dep_id in getattr(node_spec, "dependencies", []):
426
+ edge_set.add((str(dep_id), str(node_id)))
427
+ edges = [{"source": src, "target": dst} for (src, dst) in sorted(edge_set)]
428
+
429
+ nodes: list[NodeSnapshot] = []
430
+
431
+ # --- Case 1: TaskGraph with spec (static graph) ---
432
+ if spec is not None and getattr(spec, "nodes", None):
433
+ for node_id, node_spec in spec.nodes.items():
434
+ node_id_str = str(node_id)
435
+ st = nodes_state.get(node_id_str, {})
436
+
437
+ node_status = _coerce_node_status(st.get("status"), fallback=rec.status)
438
+ started_at = _coerce_ts_to_dt(st.get("started_at"))
439
+ finished_at = _coerce_ts_to_dt(st.get("finished_at"))
440
+ outputs = st.get("outputs")
441
+ error = st.get("error")
442
+
443
+ nodes.append(
444
+ NodeSnapshot(
445
+ node_id=node_id_str,
446
+ tool_name=getattr(node_spec, "tool_name", None),
447
+ status=node_status,
448
+ started_at=started_at,
449
+ finished_at=finished_at,
450
+ outputs=outputs,
451
+ error=error,
452
+ )
453
+ )
454
+
455
+ return RunSnapshot(
456
+ run_id=rec.run_id,
457
+ graph_id=graph_id,
458
+ nodes=nodes,
459
+ edges=edges,
460
+ graph_kind=graph_kind,
461
+ flow_id=flow_id,
462
+ entrypoint=entrypoint,
463
+ )
464
+
465
+ # --- Case 2: no spec, but snapshot has nodes (graphfn / dynamic) ---
466
+ if nodes_state:
467
+ for node_id, st in nodes_state.items():
468
+ node_status = _coerce_node_status(st.get("status"), fallback=rec.status)
469
+ started_at = _coerce_ts_to_dt(st.get("started_at"))
470
+ finished_at = _coerce_ts_to_dt(st.get("finished_at"))
471
+ outputs = st.get("outputs")
472
+ error = st.get("error")
473
+
474
+ nodes.append(
475
+ NodeSnapshot(
476
+ node_id=str(node_id),
477
+ tool_name=st.get("tool_name"),
478
+ status=node_status,
479
+ started_at=started_at,
480
+ finished_at=finished_at,
481
+ outputs=outputs,
482
+ error=error,
483
+ )
484
+ )
485
+
486
+ return RunSnapshot(
487
+ run_id=rec.run_id,
488
+ graph_id=graph_id,
489
+ nodes=nodes,
490
+ edges=edges,
491
+ graph_kind=graph_kind,
492
+ flow_id=flow_id,
493
+ entrypoint=entrypoint,
494
+ )
495
+
496
+ # --- Case 3: no spec, no snapshot → single pseudo-node, each node is the graph itself---
497
+ node = NodeSnapshot(
498
+ node_id=graph_id,
499
+ tool_name=None,
500
+ status=rec.status,
501
+ started_at=rec.started_at,
502
+ finished_at=rec.finished_at,
503
+ outputs=None,
504
+ error=rec.error,
505
+ )
506
+ return RunSnapshot(
507
+ run_id=rec.run_id,
508
+ graph_id=graph_id,
509
+ nodes=[node],
510
+ edges=[],
511
+ graph_kind=graph_kind,
512
+ flow_id=flow_id,
513
+ entrypoint=entrypoint,
514
+ )
515
+
516
+
517
+ @router.get("/runs/{run_id}/channel/events", response_model=list[RunChannelEvent])
518
+ async def get_run_channel_events(
519
+ run_id: str,
520
+ request: Request,
521
+ since_ts: float | None = None,
522
+ identity: RequestIdentity = Depends(get_identity), # noqa: B008
523
+ ):
524
+ """
525
+ Fetch normalized UI channel events for a run.
526
+
527
+ - Optionally enforces a demo-only `client_id` filter by checking the run's tags.
528
+ - Frontend can poll with `since_ts` for incremental updates.
529
+ """
530
+ container = request.app.state.container
531
+ event_log = getattr(container, "eventlog", None)
532
+ rm = getattr(container, "run_manager", None)
533
+
534
+ if event_log is None or rm is None:
535
+ raise HTTPException(status_code=503, detail="Event log or run manager not configured")
536
+
537
+ # --- Build the time filter ---
538
+ since_dt: datetime | None = None
539
+ if since_ts is not None:
540
+ since_dt = datetime.fromtimestamp(since_ts, tz=timezone.utc)
541
+
542
+ # Query only this run's channel events
543
+ events = await event_log.query(
544
+ scope_id=run_id,
545
+ since=since_dt,
546
+ kinds=["run_channel"],
547
+ limit=200,
548
+ )
549
+
550
+ out: list[RunChannelEvent] = []
551
+ for e in events:
552
+ payload = e.get("payload", {})
553
+
554
+ ev = RunChannelEvent(
555
+ id=e.get("id"),
556
+ run_id=e.get("scope_id") or run_id,
557
+ type=payload.get("type") or "agent.message",
558
+ text=payload.get("text"),
559
+ buttons=payload.get("buttons") or [],
560
+ file=payload.get("file"),
561
+ meta=payload.get("meta") or {},
562
+ ts=e.get("ts"),
563
+ )
564
+ out.append(ev)
565
+
566
+ # Sort ascending by ts for stable UI
567
+ out.sort(key=lambda ev: ev.ts)
568
+ return out