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
@@ -35,6 +35,41 @@ class ChannelSession:
35
35
  def _node_id(self):
36
36
  return self.ctx.node_id
37
37
 
38
+ @property
39
+ def _session_id(self):
40
+ return self.ctx.session_id
41
+
42
+ def _inject_context_meta(self, meta: dict[str, Any] | None = None) -> dict[str, Any]:
43
+ """
44
+ Merge caller-provided meta with context-derived metadata
45
+ (run_id, session_id, agent_id, app_id, graph_id, node_id).
46
+
47
+ Caller-supplied keys win; we only fill in defaults.
48
+ """
49
+ base: dict[str, Any] = dict(meta or {})
50
+ ctx = self.ctx
51
+
52
+ # Use setdefault so explicit meta wins.
53
+ if getattr(ctx, "run_id", None) is not None:
54
+ base.setdefault("run_id", ctx.run_id)
55
+
56
+ if getattr(ctx, "graph_id", None) is not None:
57
+ base.setdefault("graph_id", ctx.graph_id)
58
+
59
+ if getattr(ctx, "node_id", None) is not None:
60
+ base.setdefault("node_id", ctx.node_id)
61
+
62
+ if getattr(ctx, "session_id", None) is not None:
63
+ base.setdefault("session_id", ctx.session_id)
64
+
65
+ if getattr(ctx, "agent_id", None) is not None:
66
+ base.setdefault("agent_id", ctx.agent_id)
67
+
68
+ if getattr(ctx, "app_id", None) is not None:
69
+ base.setdefault("app_id", ctx.app_id)
70
+
71
+ return base
72
+
38
73
  def _resolve_default_key(self) -> str:
39
74
  """Unified default resolver (bus default → console)."""
40
75
  return self._bus.get_default_channel_key() or "console:stdin"
@@ -71,14 +106,98 @@ class ChannelSession:
71
106
 
72
107
  # -------- send --------
73
108
  async def send(self, event: OutEvent, *, channel: str | None = None):
109
+ """
110
+ Send a single outbound event to the configured channel.
111
+
112
+ This method ensures the event is associated with the correct channel,
113
+ merges context-derived metadata, and publishes the event via the channel bus.
114
+ This is the core low-level send method; higher-level convenience methods
115
+ (e.g., `send_text`, `send_rich`, etc.) build on top of this and are recommended
116
+ for common use cases.
117
+
118
+ Examples:
119
+ Basic usage to send a pre-constructed event:
120
+ ```python
121
+
122
+ event = OutEvent(type="agent.message", text="Hello!", channel=None)
123
+ await context.channel().send(event)
124
+ ```
125
+
126
+ Sending to a specific channel:
127
+ ```python
128
+ await context.channel().send(event, channel="web:chat")
129
+ ```
130
+
131
+ Args:
132
+ event: The `OutEvent` instance to send. If `event.channel` is not set,
133
+ it will be resolved automatically.
134
+ channel: Optional explicit channel key to override the default or event's channel.
135
+
136
+ Returns:
137
+ None
138
+
139
+ Notes:
140
+ for AG WebUI, you can set meta with
141
+ ```python
142
+ {
143
+ "agent_id": "agent-123",
144
+ "name": "Analyst",
145
+ }
146
+ ```
147
+ to override the sender's display name and avatar in the chat.
148
+ """
74
149
  event = self._ensure_channel(event, channel=channel)
150
+
151
+ # merge context meta
152
+ event.meta = self._inject_context_meta(event.meta)
75
153
  await self._bus.publish(event)
76
154
 
77
155
  async def send_text(
78
156
  self, text: str, *, meta: dict[str, Any] | None = None, channel: str | None = None
79
157
  ):
158
+ """
159
+ Send a plain text message to the configured channel.
160
+
161
+ This method constructs a normalized outbound event, merges context-derived metadata,
162
+ and dispatches the message via the channel bus.
163
+
164
+ Examples:
165
+ Basic usage to send a text message:
166
+ ```python
167
+ await context.channel().send_text("Hello, world!")
168
+ ```
169
+
170
+ Sending with additional metadata and to a specific channel:
171
+ ```python
172
+ await context.channel().send_text(
173
+ "Status update.",
174
+ meta={"priority": "high"},
175
+ channel="web:chat"
176
+ )
177
+ ```
178
+
179
+ Args:
180
+ text: The primary text content to send.
181
+ meta: Optional dictionary of metadata to include with the event.
182
+ channel: Optional explicit channel key to override the default or session-bound channel.
183
+
184
+ Returns:
185
+ None
186
+
187
+ Notes:
188
+ for AG WebUI, you can set meta with
189
+ ```python
190
+ {
191
+ "agent_id": "agent-123",
192
+ "name": "Analyst",
193
+ }
194
+ ```
195
+ """
80
196
  event = OutEvent(
81
- type="agent.message", channel=self._resolve_key(channel), text=text, meta=meta or {}
197
+ type="agent.message",
198
+ channel=self._resolve_key(channel),
199
+ text=text,
200
+ meta=self._inject_context_meta(meta),
82
201
  )
83
202
  await self._bus.publish(event)
84
203
 
@@ -90,13 +209,57 @@ class ChannelSession:
90
209
  meta: dict[str, Any] | None = None,
91
210
  channel: str | None = None,
92
211
  ):
212
+ """
213
+ Send a rich message to the configured channel.
214
+
215
+ This method constructs and dispatches an outbound event that can include both plain text and
216
+ structured rich content (such as cards, tables, or custom payloads). Context-derived metadata
217
+ is automatically merged, and the event is published via the channel bus.
218
+
219
+ Examples:
220
+ Basic usage to send a rich message:
221
+ ```python
222
+ await context.channel().send_rich(
223
+ text="Here are your results:",
224
+ rich={"table": {"rows": [["A", 1], ["B", 2]]}}
225
+ )
226
+ ```
227
+
228
+ Sending with additional metadata and to a specific channel:
229
+ ```python
230
+ await context.channel().send_rich(
231
+ text="Task completed.",
232
+ rich={"status": "success"},
233
+ meta={"priority": "high"},
234
+ channel="web:chat"
235
+ )
236
+ ```
237
+
238
+ Args:
239
+ text: The primary text content to send (optional).
240
+ rich: A dictionary containing structured rich content to include with the message.
241
+ meta: Optional dictionary of metadata to include with the event.
242
+ channel: Optional explicit channel key to override the default or session-bound channel.
243
+
244
+ Returns:
245
+ None
246
+
247
+ Notes:
248
+ for AG WebUI, you can set meta with
249
+ ```python
250
+ {
251
+ "agent_id": "agent-123",
252
+ "name": "Analyst",
253
+ }
254
+ ```
255
+ """
93
256
  await self._bus.publish(
94
257
  OutEvent(
95
258
  type="agent.message",
96
259
  channel=self._resolve_key(channel),
97
260
  text=text,
98
261
  rich=rich,
99
- meta=meta or {},
262
+ meta=self._inject_context_meta(meta),
100
263
  )
101
264
  )
102
265
 
@@ -108,12 +271,51 @@ class ChannelSession:
108
271
  title: str | None = None,
109
272
  channel: str | None = None,
110
273
  ):
274
+ """
275
+ Send an image message to the configured channel.
276
+
277
+ This method constructs and dispatches an outbound event containing image metadata,
278
+ including the image URL, alternative text, and an optional title. Context-derived
279
+ metadata is automatically merged, and the event is published via the channel bus.
280
+
281
+ Examples:
282
+ Basic usage to send an image:
283
+ ```python
284
+ await context.channel().send_image(
285
+ url="https://example.com/image.png",
286
+ alt="Sample image"
287
+ )
288
+ ```
289
+
290
+ Sending with a custom title and to a specific channel:
291
+ ```python
292
+ await context.channel().send_image(
293
+ url="https://example.com/photo.jpg",
294
+ alt="User profile photo",
295
+ title="Profile",
296
+ channel="web:chat"
297
+ )
298
+ ```
299
+
300
+ Args:
301
+ url: The URL of the image to send. If None, an empty string is used.
302
+ alt: Alternative text describing the image (for accessibility).
303
+ title: Optional title to display with the image.
304
+ channel: Optional explicit channel key to override the default or session-bound channel.
305
+
306
+ Returns:
307
+ None
308
+
309
+ Notes:
310
+ The capability to render images depends on the client adapter.
311
+ """
111
312
  await self._bus.publish(
112
313
  OutEvent(
113
314
  type="agent.message",
114
315
  channel=self._resolve_key(channel),
115
316
  text=title or alt,
116
317
  image={"url": url or "", "alt": alt, "title": title or ""},
318
+ meta=self._inject_context_meta(None),
117
319
  )
118
320
  )
119
321
 
@@ -126,13 +328,59 @@ class ChannelSession:
126
328
  title: str | None = None,
127
329
  channel: str | None = None,
128
330
  ):
331
+ """
332
+ Send a file to the configured channel in a normalized format.
333
+
334
+ This method constructs and dispatches an outbound event containing file metadata,
335
+ including the file URL, raw bytes, filename, and an optional title. Context-derived
336
+ metadata is automatically merged, and the event is published via the channel bus.
337
+
338
+ Examples:
339
+ Basic usage to send a file by URL:
340
+ ```python
341
+ await context.channel().send_file(
342
+ url="https://example.com/report.pdf",
343
+ filename="report.pdf",
344
+ title="Monthly Report"
345
+ )
346
+ ```
347
+
348
+ Sending a file from bytes:
349
+ ```python
350
+ await context.channel().send_file(
351
+ file_bytes=b"binarydata...",
352
+ filename="data.bin",
353
+ title="Raw Data"
354
+ )
355
+ ```
356
+
357
+ Args:
358
+ url: The URL of the file to send. If None, only file_bytes will be used.
359
+ file_bytes: Optional raw bytes of the file to send.
360
+ filename: The display name of the file (defaults to "file.bin").
361
+ title: Optional title to display with the file.
362
+ channel: Optional explicit channel key to override the default or session-bound channel.
363
+
364
+ Returns:
365
+ None
366
+
367
+ Notes:
368
+ The capability to handle file uploads depends on the client adapter.
369
+ If both `url` and `file_bytes` are provided, both will be included in the event.
370
+ """
129
371
  file = {"filename": filename}
130
372
  if url:
131
373
  file["url"] = url
132
374
  if file_bytes is not None:
133
375
  file["bytes"] = file_bytes
134
376
  await self._bus.publish(
135
- OutEvent(type="file.upload", channel=self._resolve_key(channel), text=title, file=file)
377
+ OutEvent(
378
+ type="file.upload",
379
+ channel=self._resolve_key(channel),
380
+ text=title,
381
+ file=file,
382
+ meta=self._inject_context_meta(None),
383
+ )
136
384
  )
137
385
 
138
386
  async def send_buttons(
@@ -143,13 +391,47 @@ class ChannelSession:
143
391
  meta: dict[str, Any] | None = None,
144
392
  channel: str | None = None,
145
393
  ):
394
+ """
395
+ Send a message with interactive buttons to the configured channel.
396
+
397
+ This method constructs and dispatches an outbound event containing a text prompt and a list of interactive buttons. Context-derived metadata is automatically merged, and the event is published via the channel bus.
398
+
399
+ Examples:
400
+ Basic usage to send a button prompt:
401
+ ```python
402
+ from aethergraph import Button
403
+ await context.channel().send_buttons(
404
+ "Choose an option:",
405
+ [Button(label="Yes", value="yes"), Button(label="No", value="no")]
406
+ )
407
+ ```
408
+
409
+ Sending with additional metadata and to a specific channel:
410
+ ```python
411
+ await context.channel().send_buttons(
412
+ "Select your role:",
413
+ [Button(label="Admin", value="admin"), Button(label="User", value="user")],
414
+ meta={"priority": "high"},
415
+ channel="web:chat"
416
+ )
417
+ ```
418
+
419
+ Args:
420
+ text: The primary text content to display above the buttons.
421
+ buttons: A list of `Button` objects representing the interactive options.
422
+ meta: Optional dictionary of metadata to include with the event.
423
+ channel: Optional explicit channel key to override the default or session-bound channel.
424
+
425
+ Returns:
426
+ None
427
+ """
146
428
  await self._bus.publish(
147
429
  OutEvent(
148
430
  type="link.buttons",
149
431
  channel=self._resolve_key(channel),
150
432
  text=text,
151
433
  buttons=buttons,
152
- meta=meta or {},
434
+ meta=self._inject_context_meta(meta),
153
435
  )
154
436
  )
155
437
 
@@ -163,12 +445,10 @@ class ChannelSession:
163
445
  timeout_s: int,
164
446
  ) -> dict:
165
447
  ch_key = self._resolve_key(channel)
166
-
167
448
  # 1) Create continuation (with audit/security)
168
449
  cont = await self.ctx.create_continuation(
169
450
  channel=ch_key, kind=kind, payload=payload, deadline_s=timeout_s
170
451
  )
171
-
172
452
  # 2) PREPARE the wait future BEFORE notifying (prevents race)
173
453
  fut = self.ctx.prepare_wait_for_resume(cont.token)
174
454
 
@@ -225,6 +505,36 @@ class ChannelSession:
225
505
  silent: bool = False, # kept for back-compat; same behavior as before
226
506
  channel: str | None = None,
227
507
  ) -> str:
508
+ """
509
+ Prompt the user for a text response in a normalized format.
510
+
511
+ This method sends a prompt to the configured channel, waits for a user reply, and returns the text input.
512
+ It automatically handles context metadata, timeout, and channel resolution.
513
+
514
+ Examples:
515
+ Basic usage to prompt for user input:
516
+ ```python
517
+ reply = await context.channel().ask_text("What is your name?")
518
+ ```
519
+
520
+ Prompting with a custom timeout and silent mode:
521
+ ```python
522
+ reply = await context.channel().ask_text(
523
+ "Enter your feedback.",
524
+ timeout_s=120,
525
+ silent=True
526
+ )
527
+ ```
528
+
529
+ Args:
530
+ prompt: The text prompt to display to the user. If None, a generic prompt may be shown.
531
+ timeout_s: Maximum time in seconds to wait for a response (default: 3600).
532
+ silent: If True, suppresses prompt display in some adapters (back-compat; default: False).
533
+ channel: Optional explicit channel key to override the default or session-bound channel.
534
+
535
+ Returns:
536
+ str: The user's text response, or an empty string if no input was received.
537
+ """
228
538
  payload = await self._ask_core(
229
539
  kind="user_input",
230
540
  payload={"prompt": prompt, "_silent": silent},
@@ -234,6 +544,33 @@ class ChannelSession:
234
544
  return str(payload.get("text", ""))
235
545
 
236
546
  async def wait_text(self, *, timeout_s: int = 3600, channel: str | None = None) -> str:
547
+ """
548
+ Wait for a single text response from the user in a normalized format.
549
+
550
+ This method prompts the user for input (with no explicit prompt), waits for a reply,
551
+ and returns the text. It automatically handles context metadata, timeout, and channel resolution.
552
+
553
+ Examples:
554
+ Basic usage to wait for user input:
555
+ ```python
556
+ reply = await context.channel().wait_text()
557
+ ```
558
+
559
+ Waiting with a custom timeout and specific channel:
560
+ ```python
561
+ reply = await context.channel().wait_text(
562
+ timeout_s=120,
563
+ channel="web:chat"
564
+ )
565
+ ```
566
+
567
+ Args:
568
+ timeout_s: Maximum time in seconds to wait for a response (default: 3600).
569
+ channel: Optional explicit channel key to override the default or session-bound channel.
570
+
571
+ Returns:
572
+ str: The user's text response, or an empty string if no input was received.
573
+ """
237
574
  # Alias for ask_text(prompt=None) but keeps existing signature
238
575
  return await self.ask_text(prompt=None, timeout_s=timeout_s, silent=True, channel=channel)
239
576
 
@@ -245,6 +582,44 @@ class ChannelSession:
245
582
  timeout_s: int = 3600,
246
583
  channel: str | None = None,
247
584
  ) -> dict[str, Any]:
585
+ """
586
+ Prompt the user for approval or rejection in a normalized format.
587
+
588
+ This method sends an approval prompt with customizable options (buttons) to the configured channel,
589
+ waits for the user's selection, and returns a normalized result indicating approval status and choice.
590
+ Context metadata, timeout, and channel resolution are handled automatically.
591
+
592
+ Examples:
593
+ Basic usage to prompt for approval:
594
+ ```python
595
+ result = await context.channel().ask_approval("Do you approve this action?")
596
+ # result: { "approved": True/False, "choice": "Approve"/"Reject" }
597
+ ```
598
+
599
+ Prompting with custom options and timeout:
600
+ ```python
601
+ result = await context.channel().ask_approval(
602
+ "Proceed with deployment?",
603
+ options=["Yes", "No", "Defer"],
604
+ timeout_s=120
605
+ )
606
+ ```
607
+
608
+ Args:
609
+ prompt: The text prompt to display to the user.
610
+ options: Iterable of button labels for user choices (defaults to "Approve" and "Reject").
611
+ timeout_s: Maximum time in seconds to wait for a response (default: 3600).
612
+ channel: Optional explicit channel key to override the default or session-bound channel.
613
+
614
+ Returns:
615
+ dict: A dictionary containing:
616
+ - "approved": bool indicating if the __first__ option was selected (True if approved, False otherwise).
617
+ - "choice": The label of the button selected by the user (str or None).
618
+
619
+ Warning:
620
+ The returned "choices" are determined by the external adapter and may vary. To be robust, make sure
621
+ to use `choices.lower()` and strip whitespace when comparing.
622
+ """
248
623
  payload = await self._ask_core(
249
624
  kind="approval",
250
625
  payload={"prompt": {"title": prompt, "buttons": list(options)}},
@@ -252,10 +627,9 @@ class ChannelSession:
252
627
  timeout_s=timeout_s,
253
628
  )
254
629
  choice = payload.get("choice")
255
-
256
630
  # Normalize return
257
631
  # 1) If adapter explicitly sets approved, trust it
258
- buttons = list(options) # just plan list, not Button objects
632
+ buttons = list(options) # just plain list, not Button objects
259
633
  # 2) Fallback: derive from choice + options
260
634
  if choice is None or not buttons:
261
635
  approved = False
@@ -271,20 +645,54 @@ class ChannelSession:
271
645
 
272
646
  async def ask_files(
273
647
  self,
274
- *,
275
648
  prompt: str,
649
+ *,
276
650
  accept: list[str] | None = None,
277
651
  multiple: bool = True,
278
652
  timeout_s: int = 3600,
279
653
  channel: str | None = None,
280
654
  ) -> dict:
281
655
  """
282
- Ask for file upload (plus optional text). Returns:
283
- { "text": str, "files": List[FileRef] }
284
- Note: console has no uploads; you’ll get only text there.
285
-
286
- The `accept` list can contain MIME types (e.g., "image/png") or file extensions (e.g., ".png"). This
287
- is a hint to the client UI about what file types to accept. Aethergraph does not enforce file type restrictions.
656
+ Prompt the user to upload one or more files, optionally with a text comment.
657
+
658
+ This method sends a file upload request to the configured channel, allowing the user to select files
659
+ and optionally enter accompanying text. The `accept` parameter provides hints to the client UI about
660
+ which file types are preferred, but is not enforced server-side. The method waits for the user's response
661
+ and returns a normalized result containing both text and file references.
662
+
663
+ Examples:
664
+ Basic usage to prompt for file upload:
665
+ ```python
666
+ result = await context.channel().ask_files(
667
+ prompt="Please upload your report."
668
+ )
669
+ # result: { "text": "...", "files": [FileRef(...), ...] }
670
+ ```
671
+
672
+ Restricting to images and allowing multiple files:
673
+ ```python
674
+ result = await context.channel().ask_files(
675
+ prompt="Upload images for review.",
676
+ accept=["image/png", ".jpg"],
677
+ multiple=True
678
+ )
679
+ ```
680
+
681
+ Args:
682
+ prompt: The text prompt to display to the user above the file picker.
683
+ accept: Optional list of MIME types or file extensions to suggest allowed file types (e.g., "image/png", ".pdf", ".jpg").
684
+ multiple: If True, allows the user to select multiple files (default: True).
685
+ timeout_s: Maximum time in seconds to wait for a response (default: 3600).
686
+ channel: Optional explicit channel key to override the default or session-bound channel.
687
+
688
+ Returns:
689
+ dict: A dictionary containing:
690
+ - "text": str, the user's comment or description (may be empty).
691
+ - "files": List[FileRef], references to the uploaded files (empty if none).
692
+
693
+ Notes:
694
+ On console adapters, file upload is not supported; only text will be returned.
695
+ The `accept` parameter is a UI hint and does not guarantee file type enforcement.
288
696
  """
289
697
  payload = await self._ask_core(
290
698
  kind="user_files",
@@ -317,7 +725,36 @@ class ChannelSession:
317
725
 
318
726
  # ---------- inbox helpers (platform-agnostic) ----------
319
727
  async def get_latest_uploads(self, *, clear: bool = True) -> list[FileRef]:
320
- """Get latest uploaded files in this channel's inbox, optionally clearing them."""
728
+ """
729
+ Retrieve the latest uploaded files from this channel's inbox in a normalized format.
730
+
731
+ This method accesses the ephemeral KV store to fetch file uploads associated with the current channel.
732
+ By default, it clears the inbox after retrieval to prevent duplicate processing. If the KV service
733
+ is unavailable in the context, a RuntimeError is raised. This method allows the fetch the files user
734
+ uploaded __not__ from an ask_files prompt, but from any prior upload event.
735
+
736
+ Examples:
737
+ Basic usage to fetch and clear uploaded files:
738
+ ```python
739
+ files = await context.channel().get_latest_uploads()
740
+ ```
741
+
742
+ Fetching files without clearing the inbox:
743
+ ```python
744
+ files = await context.channel().get_latest_uploads(clear=False)
745
+ ```
746
+
747
+ Args:
748
+ clear: If True (default), removes files from the inbox after retrieval.
749
+ If False, files are returned but remain in the inbox.
750
+
751
+ Returns:
752
+ List[FileRef]: A list of `FileRef` objects representing the uploaded files.
753
+ Returns an empty list if no files are present.
754
+
755
+ Raises:
756
+ RuntimeError: If the ephemeral KV service is not available in the current context.
757
+ """
321
758
  kv = getattr(self.ctx.services, "kv", None)
322
759
  if kv:
323
760
  if clear:
@@ -339,6 +776,9 @@ class ChannelSession:
339
776
  self._channel_key = outer._resolve_key(channel_key)
340
777
  self._upsert_key = f"{outer._run_id}:{outer._node_id}:stream"
341
778
 
779
+ def _inject_context_meta(self, meta: dict[str, Any] | None = None) -> dict[str, Any]:
780
+ return self._outer._inject_context_meta(meta)
781
+
342
782
  def _buf(self):
343
783
  return getattr(self, "__buf", None)
344
784
 
@@ -355,6 +795,7 @@ class ChannelSession:
355
795
  type="agent.stream.start",
356
796
  channel=self._channel_key,
357
797
  upsert_key=self._upsert_key,
798
+ meta=self._inject_context_meta(None),
358
799
  )
359
800
  )
360
801
 
@@ -369,6 +810,7 @@ class ChannelSession:
369
810
  channel=self._channel_key,
370
811
  text="".join(buf),
371
812
  upsert_key=self._upsert_key,
813
+ meta=self._inject_context_meta(None),
372
814
  )
373
815
  )
374
816
 
@@ -380,19 +822,54 @@ class ChannelSession:
380
822
  channel=self._channel_key,
381
823
  text=full_text,
382
824
  upsert_key=self._upsert_key,
825
+ meta=self._inject_context_meta(None),
383
826
  )
384
827
  )
385
828
  await self._outer._bus.publish(
386
829
  OutEvent(
387
- type="agent.stream.end", channel=self._channel_key, upsert_key=self._upsert_key
830
+ type="agent.stream.end",
831
+ channel=self._channel_key,
832
+ upsert_key=self._upsert_key,
833
+ meta=self._inject_context_meta(None),
388
834
  )
389
835
  )
390
836
 
391
837
  @asynccontextmanager
392
838
  async def stream(self, channel: str | None = None) -> AsyncIterator["_StreamSender"]:
393
839
  """
394
- Back-compat: no arg uses session/default/console.
395
- New: pass a channel key to target a specific channel for this stream.
840
+ Stream a sequence of text deltas to the configured channel in a normalized format.
841
+
842
+ This method provides a context manager for streaming incremental message updates (such as LLM generation)
843
+ to the target channel. It automatically handles context metadata, upsert keys, and dispatches start, update,
844
+ and end events to the channel bus. The caller is responsible for sending deltas and ending the stream.
845
+
846
+ Examples:
847
+ Basic usage to stream LLM output:
848
+ ```python
849
+ async with context.channel().stream() as s:
850
+ await s.delta("Hello, ")
851
+ await s.delta("world!")
852
+ await s.end()
853
+ ```
854
+
855
+ Streaming to a specific channel:
856
+ ```python
857
+ async with context.channel().stream(channel="web:chat") as s:
858
+ await s.delta("Generating results...")
859
+ await s.end(full_text="Results complete.")
860
+ ```
861
+
862
+ Args:
863
+ channel: Optional explicit channel key to target a specific channel for this stream.
864
+ If None, uses the session-bound or default channel.
865
+
866
+ Returns:
867
+ AsyncIterator[_StreamSender]: An async context manager yielding a `_StreamSender` object
868
+ for sending deltas and ending the stream.
869
+
870
+ Notes:
871
+ The caller must explicitly call `end()` to finalize the stream. No auto-end is performed.
872
+ The adapter may have specific behaviors for rendering streamed content (update vs. append).
396
873
  """
397
874
  s = ChannelSession._StreamSender(self, channel_key=channel)
398
875
  try:
@@ -421,6 +898,9 @@ class ChannelSession:
421
898
  self._channel_key = outer._resolve_key(channel_key)
422
899
  self._upsert_key = f"{outer._run_id}:{outer._node_id}:{key_suffix}"
423
900
 
901
+ def _inject_context_meta(self, meta: dict[str, Any] | None = None) -> dict[str, Any]:
902
+ return self._outer._inject_context_meta(meta)
903
+
424
904
  async def start(self, *, subtitle: str | None = None):
425
905
  if not self._started:
426
906
  self._started = True
@@ -435,6 +915,7 @@ class ChannelSession:
435
915
  "total": self._total,
436
916
  "current": self._current,
437
917
  },
918
+ meta=self._inject_context_meta(None),
438
919
  )
439
920
  )
440
921
 
@@ -468,6 +949,7 @@ class ChannelSession:
468
949
  channel=self._channel_key,
469
950
  upsert_key=self._upsert_key,
470
951
  rich=payload,
952
+ meta=self._inject_context_meta(None),
471
953
  )
472
954
  )
473
955
 
@@ -484,6 +966,7 @@ class ChannelSession:
484
966
  "total": self._total,
485
967
  "current": self._total if self._total is not None else None,
486
968
  },
969
+ meta=self._inject_context_meta(None),
487
970
  )
488
971
  )
489
972